参考
代码随想录
一、理论基础
(一)二叉树的种类
满二叉树、完全二叉树、二叉搜索树、平衡二叉搜索树
(二) 二叉树的存储方式
(三)二叉树的遍历方式
(1)深度优先遍历
- 前序遍历(递归法、迭代法)
- 中序遍历(递归法、迭代法)
- 后序遍历(递归法、迭代法)
(2)广度优先遍历
二、二叉树的递归遍历
递归函数实现的一般顺序
(1)确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
(2)确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
(3)确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。
(1)确定递归函数的参数和返回值
遍历是为了按照一定的顺序获取每个节点的值,因此按照常规思路,返回值应该是节点的值,为了获取节点的值当然要传入节点的地址。但是这里是递归调用,所以节点的值不用函数返回,而时存储在传入的vector中,这样最后所有节点的值都存放在vector中。所以函数原型为:
void recursion(TreeNode* cur,vector<int>& result);
(2)确定终止条件
如果当前节点为空,就返回,即
if(cur == nullptr) return;
(3)确定单层递归逻辑
前序遍历的遍历顺序是中左右,即先遍历中间节点,然后再遍历左右节点。代码如下:
result.push_back(cur->vval); //中
recursion(cur->left,result); //左
recursion(cur->right,result); //右
完整的代码实现如下:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
void recursion(TreeNode* cur,vector<int>& result)
{
if(cur == nullptr) return;
result.push_back(cur->val); //中
recursion(cur->left,result); //左
recursion(cur->right,result); //左
}
vector<int> preorderTraversal(TreeNode* root) {
vector<int> result;
recursion(root,result);
return result;
}
};
中序遍历遍历的实现思路和前序遍历一致,只是遍历顺序为左中右。代码如下:
class Solution {
public:
void recursion(TreeNode* cur,vector<int>& result)
{
if(cur == nullptr) return;
recursion(cur->left,result); //左
result.push_back(cur->val); //中
recursion(cur->right,result); //左
}
vector<int> inorderTraversal(TreeNode* root) {
vector<int> result;
recursion(root,result);
return result;
}
};
后序遍历遍历的遍历顺序为左右中。代码如下:
class Solution {
public:
void recursion(TreeNode* cur,vector<int>& result)
{
if(cur == nullptr) return;
recursion(cur->left,result); //左
recursion(cur->right,result); //左
result.push_back(cur->val); //中
}
vector<int> postorderTraversal(TreeNode* root) {
vector<int> result;
recursion(root,result);
return result;
}
};
三、二叉树的迭代遍历
这里说的迭代遍历本质上来看和递归没有区别,都是利用栈来是实现的。递归就是不停调用自己,直到满足条件,每次函数调用都会将局部变量、参数等压入栈中,每次返回的时候又从栈中弹出数据,所以迭代遍历就是模拟压栈和弹栈的过程。对于不同的遍历顺序,压栈和弹栈的顺序是不一样的。
(一)前序遍历
以上图为例,定义一个栈,并把根结点压入栈中,如下图所示:
然后就开始迭代遍历了。每次弹出一个节点,读取该节点的值,然后把其右节点和左节点分别压入栈中,注意要先压入右节点,这样弹栈的时候左节点才会先弹出。
迭代过程为:弹出一个节点,读取其值,若该节点有子节点则将子节点压栈。重复上述过程直到栈空。
迭代遍历的过程如下:
- 弹栈,将子节点压栈
- 弹栈,将子节点压栈
- 弹栈,无子节点,所以不压栈
- 弹栈,无子节点,所以不压栈
- 弹栈,将子节点压栈
- 弹栈,无子节点,所以不压栈
- 弹栈,无子节点,所以不压栈
- 栈空,结束遍历,最后的结果为1,2,4,5,3,6,7
清楚了实现过程之后代码实现就简单了,代码如下:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
if(root == nullptr) return {};
vector<int> result;
stack<TreeNode*> stk;
stk.push(root); //将根结点压栈
while(!stk.empty())
{
TreeNode* cur = stk.top();
stk.pop();
result.push_back(cur->val);
//右子节点先压栈
if(cur->right != nullptr) stk.push(cur->right);
if(cur->left != nullptr) stk.push(cur->left);
}
return result;
}
};
(二)中序遍历
中序遍历的遍历顺序是左中右,因此要一直寻找左节点,直到某一个节点没有左节点再往回处理节点。如果某一个节点没有左节点(或者认为左节点为空),那么就可以按照左中右的顺序开始处理节点了,即应该处理当前节点,然后处理当前节点的右节点。
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
TreeNode* cur = root;
while (cur != NULL || !st.empty()) {
if (cur != NULL) { // 指针来访问节点,访问到最底层
st.push(cur); // 将访问的节点放进栈
cur = cur->left; // 左
} else {
cur = st.top(); // 从栈里弹出的数据,就是要处理的数据(放进result数组里的数据)
st.pop();
result.push_back(cur->val); // 中
cur = cur->right; // 右
}
}
return result;
}
};
(三)后序遍历
后续遍历可以根据前序遍历变化而来,变化过程如上图所示,因此对前序遍历的代码稍作修改即可得到后序遍历。代码如下:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
if(root == nullptr) return {};
vector<int> result;
stack<TreeNode*> stk;
stk.push(root); //将根结点压栈
while(!stk.empty())
{
TreeNode* cur = stk.top();
stk.pop();
result.push_back(cur->val);
//左子节点先压栈
if(cur->left != nullptr) stk.push(cur->left);
if(cur->right != nullptr) stk.push(cur->right);
}
reverse(result.begin(),result.end());
return result;
}
};
三、二叉树的统一迭代遍历
在上面用迭代法遍历二叉树的时候,三种遍历方法的实现过程区别还是挺大的,特别是中序遍历,所以代码随想录给出了一种统一的迭代遍历方法。在用迭代法进行中序遍历的时候,无法同时解决访问节点(遍历节点)和处理节点(将元素放进结果集)不一致的情况。那我们就将访问的节点放入栈中,把要处理的节点也放入栈中但是要做标记。如何标记呢?就是要处理的节点放入栈之后,紧接着放入一个空指针作为标记。 这种方法也可以叫做标记法。
(一)中序遍历
中序遍历的代码如下:
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
if (root != NULL) st.push(root);
while (!st.empty()) {
TreeNode* node = st.top();
if (node != NULL) {
st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
if (node->right) st.push(node->right); // 添加右节点(空节点不入栈)
st.push(node); // 添加中节点
st.push(NULL); // 中节点访问过,但是还没有处理,加入空节点做为标记。
if (node->left) st.push(node->left); // 添加左节点(空节点不入栈)
} else { // 只有遇到空节点的时候,才将下一个节点放进结果集
st.pop(); // 将空节点弹出
node = st.top(); // 重新取出栈中元素
st.pop();
result.push_back(node->val); // 加入到结果集
}
}
return result;
}
以下图为例,对上面的代码的实现过程进行说明:
- 初始化
下面进入while循环体。
- 栈顶元素不为空,弹出栈顶元素,然后再按照右中左的顺序压栈,在中间节点后要加入NULL标记
- 栈顶元素不为空,弹出栈顶元素,然后再按照右中左的顺序压栈,在中间节点后要加入NULL标记
- 栈顶元素不为空,弹出栈顶元素,然后再按照右中左的顺序压栈,在中间节点后要加入NULL标记
- 栈顶元素为空,弹出栈顶的空元素,再弹出栈顶元素进行处理,即读取值
- 栈顶元素为空,弹出栈顶的空元素,再弹出栈顶元素读取值
- 栈顶元素不为空,弹出栈顶元素,然后再按照右中左的顺序压栈,在中间节点后要加入NULL标记
- 栈顶元素为空,弹出栈顶的空元素,再弹出栈顶元素读取值
- 栈顶元素为空,弹出栈顶的空元素,再弹出栈顶元素读取值
- 栈顶元素不为空,弹出栈顶元素,然后再按照右中左的顺序压栈,在中间节点后要加入NULL标记
- 栈顶元素为空,弹出栈顶的空元素,再弹出栈顶元素读取值
- 栈空,最后输出的结果为 4,2,5,1,3
总结一下上面的实现过程:
(1)初始化。定义一个栈,然后将根结点加入栈中。
(2)判断栈是否为空,如是则结束;否则读取栈顶元素,判断栈顶元素是否为空,若是则弹出空元素,再弹出新的栈顶元素放入结果数组中,若栈顶元素非空,则将栈顶元素弹出,然后按照右中左的顺序(弹栈就是左中右)将弹出的栈顶元素的右节点、本身、NULL(作为标记)和左节点压栈。重复步骤(2),直到栈空为之。
个人理解:上面的判断中,很重要的一个点是判断栈顶元素是否为空,原因在与压栈的时候对节点进行了标记,说明该节点是中间节点,如果栈顶元素为空,说明用NULL标记的这个节点的左节点已经处理了或没有左子节点,现在可以处理中间节点了,而如果栈顶元素不为空,说明该节点还不是还有左子节点,因此需要将其子节点继续压栈。假设遍历到某叶子节点,因为它没有子节点,所以按照一定的顺序压栈后,栈顶元素就是NULL,这是关键!!!
(二)前序遍历和后序遍历
上面的是中序遍历的情况,其他两种情调整元素的压栈顺序即可。其余两种情况的代码实现如下:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
if (root != NULL) st.push(root);
while (!st.empty()) {
TreeNode* node = st.top();
if (node != NULL) {
st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
if (node->right) st.push(node->right); //右
if (node->left) st.push(node->left); //左
st.push(node); //中
st.push(NULL); // 中节点访问过,但是还没有处理,加入空节点做为标记。
} else { // 只有遇到空节点的时候,才将下一个节点放进结果集
st.pop(); // 将空节点弹出
node = st.top(); // 重新取出栈中元素
st.pop();
result.push_back(node->val); // 加入到结果集
}
}
return result;
}
};
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
if (root != NULL) st.push(root);
while (!st.empty()) {
TreeNode* node = st.top();
if (node != NULL) {
st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
st.push(node); //中
st.push(NULL); // 中节点访问过,但是还没有处理,加入空节点做为标记。
if (node->right) st.push(node->right); //右
if (node->left) st.push(node->left); //左
} else { // 只有遇到空节点的时候,才将下一个节点放进结果集
st.pop(); // 将空节点弹出
node = st.top(); // 重新取出栈中元素
st.pop();
result.push_back(node->val); // 加入到结果集
}
}
return result;
}
};
可以看出,统一迭代遍历里三种遍历的区别仅仅是压栈顺序不同,比如中序遍历的遍历顺序是左中右,那么压栈应该是右中左,这样弹栈的时候才会符合遍历顺序,其他两种也是如此。
四、今日小结
二叉树基础+二叉树的深度优先遍历,深度优先遍历里有三种方法,每种方法又有两种方法(递归和迭代),用递归法进行遍历,三种遍历顺序的写法很相似,很好理解。而对于迭代法,如果不用统一迭代方法,三种遍历顺序的写法差异较大,特别是中序遍历,用统一迭代法之后,三种遍历顺序的区别也类似于递归法,区别仅在于处理顺序。在统一迭代法里注意压栈和弹栈的顺序是相反的。总的来说,二叉树的深度优先遍历用递归法和统一迭代法就可以了,在看不懂代码的时候画图来描述代码的运行过程,画完图就明白代码的运行过程了。