1 题目
二叉树的前序遍历(Binary Tree Preorder Traversal)
lintcode:题号——66,难度——easy
2 描述
给出一棵二叉树,返回其节点值的前序遍历。
名词:
遍历
按照一定的顺序对树中所有节点进行访问的过程叫做树的遍历。
前序遍历
在二叉树中,首先访问根结点然后遍历左子树,最后遍历右子树,在遍历左右子树时,仍然采用这种规则,这样的遍历方式叫做二叉树的前序遍历。
序列化规则:
使用大括号包裹节点序列来表示二叉树,首个数据为根节点,后面接着是其左儿子和右儿子节点值,"#"表示不存在该子节点,之后以此类推。
样例1:
输入:二叉树 = {1,2,3}
输出:[1,2,3]
解释:上述{1,2,3}序列按照序列化规则表示的二叉树如下1 / \ 2 3
按照前序遍历规则,先根再左右子树,顺序即为[1,2,3]
样例2:
输入:二叉树 = {1,#,2,3}
输出:[1,2,3]
解释:上述{1,#,2,3}序列按照序列化规则表示的二叉树如下1 / \ # 2 / 3
按照前序遍历规则,先根再左右子树,顺序同样为[1,2,3]
3 解决方案
二叉树的遍历可以使用递归和非递归两种方式:
- 使用递归的方式编写能够减少思维复杂度,但是在二叉树深度很深的情况下,由于编程语言一般会限制递归调用的深度,递归层数太深会导致程序调用的栈溢出。
- 使用非递归的方式比较难于理解,虽然会使用到栈,但只是存储数值,不会产生上述程序调用的栈溢出情况。
3.1 递归算法
程序在执行过程中调用自己,叫做递归。递归通常可以把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序代码就可描述出解题过程所需要的多次重复计算。
递归的三个要素(编程中):
- 递归的定义(递归函数的返回值、参数要如何定义)
- 递归的拆解(递归如何向下拆分)
- 递归的出口(递归的结束条件)
递归又有两种形式,遍历和分治。
3.1.1 遍历法(Traverse)
思路
遍历的整个过程可以看成是为了完成某项大任务,于是领导亲自下基层处理各项小任务,所有工作都“亲历亲为”,参与了所有工作最终完成了大任务。
遍历法的主要递归函数一般不带返回值,会拥有
全局参数
或者贯穿整个递归过程的函数参数
用来处理数据。
源码
C++版本:
/**
* @param root: A Tree
* @return: Preorder in ArrayList which contains node values.
*/
vector<int> preorderTraversal(TreeNode * root) {
// write your code here
vector<int> result;
traversa(root, result);
return result;
}
// 遍历法的递归函数,没有返回值,函数参数result贯穿整个递归过程用来记录遍历的结果
void traversa(TreeNode * curNode, vector<int> & result) // 递归三要素之定义
{
if (curNode == nullptr)
{
return; // 递归三要素之出口
}
result.push_back(curNode->val);
traversa(curNode->left, result); // 递归三要素之拆解,
traversa(curNode->right, result);
}
3.1.2 分治法(Devide And Conquer)
思路
分治法的整个过程可以看成是为了完成某项大人物,于是领导把各项工作分派给各个下属,各个下属又把工作继续细分层层向下分派给基层人员,每一级都只需要获取下级完成的任务结果即可,最终所有层级任务都完成了,大任务也对应完成了。
分治法的主要递归函数一般需要有返回值,用来向上一层提供结果。
源码
C++版本:
/**
* @param root: A Tree
* @return: Preorder in ArrayList which contains node values.
*/
// 由于主函数的形式已经符合分治法需要的形式(具有合适的返回值),直接使用主函数做为递归函数
vector<int> preorderTraversal(TreeNode * root) { //递归三要素之定义
// write your code here
vector<int> result;
if (root == nullptr)
{
return result; // 递归三要素之出口
}
// 获取左子树的遍历结果
vector<int> leftResult = preorderTraversal(root->left); // 递归三要素之拆解
// 获取右子树的遍历结果
vector<int> rightResult = preorderTraversal(root->right);
// 组合左右子树的返回值
result.push_back(root->val);
result.insert(result.end(), leftResult.begin(), leftResult.end());
result.insert(result.end(), rightResult.begin(), rightResult.end());
return result;
}
3.2 非递归算法
3.2.1 二叉树遍历的非递归通用解法
思路
在非递归算法中,需要用到数据结构“栈”来保存二叉树遍历过程中的状态,模拟整个前序遍历的路径。下面提供一种针对二叉树的前、中、后序遍历都比较通用的形式来解决遍历问题。
- 左穿入栈到底,边入栈边解析;(解析:即向结果序列插入节点值)
- 从栈内弹出当前
左子树
;(左支在步骤3已解析完成) - 从栈内弹出当前
根节点
;(根节点在步骤3已解析完成) -
右子树
的根节点入栈,重复3、4、5步骤,直到满足循环结束条件,该二叉树的前序遍历即在结果序列中。
初始条件:
栈为空
、当前指针指向根节点
。
循环结束条件:栈为空
且当前指针为空
。
源码
/**
* @param root: A Tree
* @return: Preorder in ArrayList which contains node values.
*/
vector<int> preorderTraversal(TreeNode * root) {
// write your code here
vector<int> result;
if (root == nullptr)
{
return result;
}
stack<TreeNode *> nodeStack; // 空栈
TreeNode * cur = root; // cur指针指向根节点
while (!nodeStack.empty() || cur != nullptr) // 栈和cur指针都不为空,则继续循环
{
while (cur != nullptr) // while循环完成左穿入栈的步骤
{
result.push_back(cur->val);
nodeStack.push(cur);
cur = cur->left;
}
cur = nodeStack.top();
nodeStack.pop(); // 弹出节点(左子树、根节点)
cur = cur->right; // cur指向右支,目的是让下一次循环时将右支入栈
}
return result;
}
图解
非递归的解法理解起来比较不容易一些,可以边调试边理解,逻辑比较绕。
主要规律就是“左穿入栈解析->弹出已解析的栈顶->压入未解析的右子树”不断重复。
样例:{1,2,3,4,5,6,7}
初始状态
:
1
/ \
2 3
/ \ / \
4 5 6 7
nodeStack——(栈底)空(栈顶)
cur——1
result——空
第一轮while循环
:
图形表示栈nodeStack的状态
1
/
2
/
4 左穿入栈到底,边入栈边解析
nodeStack——(栈底)1,2,4(栈顶)
cur——nullptr,cur此时指向4的左孩子
result——1,2,4(只在解析时才插入新值)
1
/
2 弹出栈顶
nodeStack——(栈底)1,2(栈顶)
cur——nullptr,cur被指向之前的栈顶4,弹出栈顶之后,此时指向4的右孩子
第二轮while循环
:
1
/
2 循环开始时cur指向空,跳过左穿入栈的过程
nodeStack——(栈底)1,2(栈顶)
cur——nullptr,cur指向4的右孩子
1 弹出栈顶
nodeStack——(栈底)1(栈顶)
cur——5,cur被指向之前的栈顶2,弹出栈顶之后,此时指向2的右孩子
第三轮while循环
:
1
/
空 节点2不在栈内
\
5 循环开始时cur指向5,左穿入栈到底,边入栈边解析
nodeStack——(栈底)1,5(栈顶)
cur——nullptr,cur指向5的左孩子
result——1,2,4,5
1
nodeStack——(栈底)1(栈顶)
cur——nullptr,cur被指向之前的栈顶5,弹出栈顶之后,此时指向5的右孩子
第四轮while循环
:
1 循环开始时cur指向空,跳过左穿入栈的过程
nodeStack——(栈底)1(栈顶)
cur——nullptr,cur指向5的右孩子
空 弹出栈顶
nodeStack——(栈底)空(栈顶)
cur——3,cur被指向之前的栈顶1,弹出栈顶之后,此时指向1的右孩子
第五轮while循环
:
空
\
3
/
6 左穿入栈,边入栈边解析
nodeStack——(栈底)3,6(栈顶)
cur——nullptr,cur指向6的左孩子
result——1,2,4,5,3,6
空
\
3 弹出栈顶
nodeStack——(栈底)3(栈顶)
cur——nullptr,cur被指向之前的栈顶6,弹出栈顶之后,此时指向6的右孩子
第六轮while循环
:
空
\
3 循环开始时cur指向空,跳过左穿入栈的过程
nodeStack——(栈底)3(栈顶)
cur——nullptr,cur指向6的右孩子
空 弹出栈顶
nodeStack——(栈底)空(栈顶)
cur——7,cur被指向之前的栈顶3,弹出栈顶之后,此时指向3的右孩子
第七轮while循环
:
空
\
空
\
7 左穿入栈,边入栈边解析
nodeStack——(栈底)7(栈顶)
cur——nullptr,cur指向7的左孩子
result——1,2,4,5,3,6,7
空
\
空 弹出栈顶
nodeStack——(栈底)空(栈顶)
cur——nullptr,cur被指向之前的栈顶7,弹出栈顶之后,此时指向7的右孩子
result——1,2,4,5,3,6,7
此时,nodeStack和cur都为空,满足循环结束条件,跳出循环。
此时的result序列即为该二叉树的前序遍历。
3.2.2 前序遍历的非递归解法二
思路
这个代码和通用解法的不同点有两个:
- 初始条件:
栈初始化包含根节点
、当前指针指向根节点
; - 循环结束条件:
栈为空
;
源码
/**
* @param root: A Tree
* @return: Preorder in ArrayList which contains node values.
*/
vector<int> preorderTraversal(TreeNode * root) {
// write your code here
vector<int> result;
if (root == nullptr)
{
return result;
}
stack<TreeNode *> nodeStack;
nodeStack.push(root); // 初始化时候,根节点入栈
TreeNode * cur = root;
while (!nodeStack.empty()) // 循环只需要判断栈是否为空
{
while (cur != nullptr)
{
result.push_back(cur->val);
nodeStack.push(cur);
cur = cur->left;
}
cur = nodeStack.top();
nodeStack.pop();
cur = cur->right;
}
}
3.2.3 前序遍历的非递归解法三
思路
这个解法比前两种非递归解法要容易理解,但为了统一前、中、后序的遍历方式,建议记非递归的通用解法,按照前序遍历的规则来处理节点。步骤如下:
- 循环前向栈内压入根节点;
- 解析cur,弹出cur;
- 右子树入栈;
- 左子树入栈;
- 重复2、3、4步骤,直到栈空。
初始条件:
栈初始化包含根节点
、当前指针指向空
;
循环结束条件:栈为空
;
源码
/**
* @param root: A Tree
* @return: Preorder in ArrayList which contains node values.
*/
vector<int> preorderTraversal(TreeNode * root){
// write your code here
vector<int> result;
if (root == nullptr)
{
return result;
}
stack<TreeNode *> nodeStack;
nodeStack.push(root); // 循环前压入根节点
TreeNode * cur = nullptr;
while (!nodeStack.empty())
{
cur = nodeStack.top();
result.push_back(cur->val); // 解析栈顶节点
nodeStack.pop(); // 弹出已解析的节点
if (cur->right != nullptr)
{
nodeStack.push(cur->right); // 压入右子树
}
if (cur->left != nullptr)
{
nodeStack.push(cur->left); // 压入左子树
}
}
return result;
}
3.3 时间复杂度
对二叉树的遍历,不管使用什么方式都是将整棵树中的所有节点走一遍,所以算法的时间复杂度都为O(n)。
关于二叉树和分治法的时间复杂度,如果节点操作的的时间复杂度为O(1),总复杂度为O(n):
T(n) = 2 * T(n/2) + O(1)
= 2 * (2*T(n/4) + O(1)) + O(1)
= 4 * T(n/4) + 2 * O(1) + O(1)
= 4 * (2*T(n/8) + O(1)) + O(1)
= 8 * T(n/8) + 4 * O(1) + 2 * O(1) + O(1)
= n * T(n/n) + O(n/2 + n/4 + …… + 1)
= O(n) + O(n)
= O(n)
如果节点操作的的时间复杂度为O(n),总复杂度为O(nlogn):
T(n) = 2 * T(n/2) + O(n)
= 2 * (2*T(n/4) + O(n/2)) + O(n)
= 4 * T(n/4) + 2*O(n/2) + O(n)
= 4 * (2*T(n/8) + O(n/4)) + 2*O(n/2) + O(n)
= 8 * T(n/8) + 4*O(n/4) + 2*O(n/2) + O(n)
= n * T(n/n) + O(n) + O(n) + …… + O(n)
= O(n) + logn*O(n)
= O(nlogn)
3.4 空间复杂度
算法的空间复杂度为O(n)。
4 总结
二叉树的遍历在一般使用递归算法,在二叉树较大的情况下才使用非递归的算法。