写在前面:
- 二叉树的四种遍历方式,leetcode有对应的题目,题号为:94,102,144,145;大家看完可以去拿下这四道题;
- 迭代算法参考学堂在线课程:清华-数据结构-邓俊晖老师;代码略做修改适应leetcode判题系统;
二叉树的先序遍历:
定义:如果二叉树为空,则返回,否则先访问根结点,然后先序遍历左子树,再先序遍历右子树;
递归算法版本:
// 根据定义很容易写出递归版本算法
vector<int> preorderTraversal(TreeNode* root) {
vector<int> res;
helper(root, res);
return res;
}
void helper(TreeNode* root, vector<int> &visited)
{
if(nullptr == root)
return;
visited.push_back(root->val);
helper(root->left, visited);
helper(root->right, visited);
}
根据T(n) = O(1)+T(a)+T(n-a-1) = O(n) (n 为树的结点总数,a为左子树的结点数量),我们可以看出递归算法的时间复杂度为O(n);
在实际运行栈中,每个递归,都对应一个函数栈,每个栈帧都是通用格式,相同的大小;
如果设计为迭代算法,我们的栈帧可以根据具体结点的差异来调节大小,这个和递归的栈帧大小相比是常数意义下的,如果n很大的情况下,这个优化还是很有意义的;
同时迭代算法也能更好的帮我们理解递归算法的本质,所以迭代算法是有必要学习的;
迭代算法版本:
我们注意到递归版本的先序遍历,它的递归函数是在递归实例体的尾部,我们称之为尾递归。尾递归是比较容易改写为迭代算法的,具体代码如下:
vector<int> preorderTraversal(TreeNode* root) {
vector<int> res;
stack<TreeNode*> s;
if(nullptr != root) s.push(root);
while(!s.empty())
{
TreeNode* curnode = s.top();s.pop();res.push_back(curnode->val);
// 注意栈先入后出,需要先访问左子树,那左子树应该随后入栈
if(nullptr != curnode->right) s.push(curnode->right);
if(nullptr != curnode->left) s.push(curnode->left);
}
return res;
}
二叉树的中序遍历:
定义:如果根结点为空,则直接返回,否则中序遍历左子树,然后访问根结点,最终中序遍历右子树;
递归算法版本:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> res;
helper(root, res);
return res;
}
void helper(TreeNode* root, vector<int> &visited)
{
if(nullptr == root)
return;
helper(root->left, visited);
visited.push_back(root->val);
helper(root->right, visited);
}
迭代算法版本:
那么此时,我们能不能想先序遍历的迭代算法一样,写出中序遍历的迭代算法呢
我们发现,只是简单的把visit方法放到中间是不可行的,相当于根结点还是再左子树之前被访问到了;
这个时候,我们是可以体会到,尾递归和非尾递归算法,要替换为迭代版本,难度是不一样的;
// 尝试版本1,失败
vector<int> inorderTraversal(TreeNode* root) {
vector<int> res;
stack<TreeNode*> s;
if(nullptr != root) s.push(root);
while(!s.empty())
{
// 我们发现,只是简单的把visit方法放到中间是不可行的,相当于根结点还是再左子树之前被访问到了
TreeNode* curnode = s.top();s.pop();
if(nullptr != curnode->right) s.push(curnode->right);
res.push_back(curnode->val);
if(nullptr != curnode->left) s.push(curnode->left);
}
return res;
}
再论二叉树的先序迭代算法:
让我们从二叉树的先序遍历开始,重新梳理下有没有更好的办法进行迭代:
对于先序遍历,我们从下面的图中,可以看到从i->d->c->…p->o;访问的时候,这个顺序是从左侧链开始访问的,访问到左侧分支的最后一个结点a,才把控制权交给对应的右侧结点(以右侧结点为根的子树),右侧结点按深度最深的开始访问直到深度最小的以l为根结点的右子树;
我们可以再抽象下,把树想象成下面这样,先序遍历为自上而下对藤上结点的访问,以及随后自下而上对各右子树的遍历;
具体实现思路,我们可以观察到,右子树的从d->d-1…->0,也就是出栈的顺序,那么入栈顺序为0->1…->d,左侧树不入栈,直接访问,代码如下:
void visitAlongLeftBranch(TreeNode* root, vector<int> &visit, stack<TreeNode*> &s)
{
while(nullptr!=root)
{
// 访问当前结点
visit.push_back(root->val);
s.push(root->right); // 左子树入栈
root = root->left; // 沿着左侧链往下
}
}
vector<int> preorderTraversal(TreeNode* root) {
vector<int> res;
stack<TreeNode*> s;
if(nullptr != root) s.push(root);// 先根结点入栈
while(!s.empty())
{
TreeNode* curnode = s.top();s.pop();
// 沿着左侧链访问当前结点,并将右子树入栈
visitAlongLeftBranch(curnode, res, s);
// visitAlongLeftBranch 结束后,以当前结点的左子树就已经访问完了
}
return res;
}
再论二叉树的中序迭代算法:
那么再看下中序遍历,中序遍历有啥特点呢?
先遍历左侧链,找到左侧链的最远端,访问最远端的结点,再将控制权交给最远端的右侧子树,同样访问右侧子树的左侧链,遍历完,再叫给进一步的左侧链结点;这里感觉是需要一个栈,遍历左侧链,需要将左侧链上的结点入栈,出战的时候,需要先访问当前结点,再将当前结点的右子树进行左侧遍历;右子树不用存栈,是因为你栈里已经存了左侧链上的结点,每次出栈,都可以访问对应当前结点的右子树;
// 中序遍历迭代版本算法
void goAlongLeftBranch(TreeNode* root, stack<TreeNode*> &s)
{
while(nullptr!=root)
{
s.push(root); // 左子树入栈
root = root->left; // 沿着左侧链往下
}
}
vector<int> inorderTraversal(TreeNode* root) {
vector<int> res;
stack<TreeNode*> s;
goAlongLeftBranch(root, s); // 先把根结点的左侧链放入栈中
while(!s.empty())
{
// 此时拿到的是最远端的左侧链结点
TreeNode* curnode = s.top();s.pop();
res.push_back(curnode->val);//访问当前结点
if(nullptr != curnode->right)
goAlongLeftBranch(curnode->right, s);// 将控制权交给最远端左侧链结点的右子树
}
return res;
}
二叉树的后序遍历:
定义:如果二叉树为空,则返回,否则后序遍历左子树,接着后序遍历右子树,最后访问根结点;
递归算法版本:
// 后序遍历递归算法
vector<int> postorderTraversal(TreeNode* root) {
vector<int> res;
helper(root, res);
return res;
}
void helper(TreeNode* root, vector<int> &visited)
{
if(nullptr == root)
return;
helper(root->left, visited);
helper(root->right, visited);
visited.push_back(root->val);
}
迭代算法版本:
那迭代算法呢,我们跟着下面的图思考一下:
后序遍历算法,如果大家有不依赖父结点这个属性的版本,欢迎告诉博主,因为有依赖父结点这个属性,相当于已经通过先序遍历重建了一次二叉树,增加上了父结点这个属性,其实是有点走远的,大家可以先关注算法具体本身;
/**
* 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) {}
* };
*/
// 后序遍历迭代算法
struct MyTreeNode
{
int val;
MyTreeNode *parent;
MyTreeNode *left;
MyTreeNode *right;
MyTreeNode() : val(0), parent(nullptr), left(nullptr), right(nullptr) {}
MyTreeNode(int x) : val(x), parent(nullptr), left(nullptr), right(nullptr) {}
MyTreeNode(int x, MyTreeNode *parent, MyTreeNode *left, MyTreeNode *right) : val(x), parent(parent), left(left), right(right) {}
};
// 重建二叉树结点,增加parent结点属性
void rebuildBinaryTree(TreeNode* root,MyTreeNode* myroot)
{
if(nullptr == root) return;
myroot->val = root->val;
if(nullptr != root->left)
{
MyTreeNode* myleft = new MyTreeNode();
myleft->parent = myroot;
myroot->left = myleft;
rebuildBinaryTree(root->left,myleft);
}
if(nullptr != root->right)
{
MyTreeNode* myright = new MyTreeNode();
myright->parent = myroot;
myroot->right = myright;
rebuildBinaryTree(root->right,myright);
}
return;
}
void gotoLeftmostLeaf(stack<MyTreeNode*> &s)
{
while(MyTreeNode* curnode = s.top()) // 自顶而下反复检查栈顶结点
{
if(nullptr != curnode->left) // 尽可能向左遍历
{
if(nullptr != curnode->right) // 如果有右孩子,则右孩子优先入栈
{
s.push(curnode->right);
}
s.push(curnode->left);// 然后转向左孩子
}else // 实不得已。没有左孩子,则右孩子入栈
s.push(curnode->right); // 这里不用对右孩子判空么
}
s.pop();
}
vector<int> postorderTraversal(TreeNode* root)
{
vector<int> res;
MyTreeNode* myroot=nullptr == root?nullptr: new MyTreeNode();
rebuildBinaryTree(root, myroot); // 因为一般的TreeNode没有parent结点,我们就重建一下二叉树结点
stack<MyTreeNode*> s;
MyTreeNode* curnode = myroot;
if(nullptr!=curnode) s.push(curnode);// 根结点首先入栈
while(!s.empty())
{
if(s.top() != curnode->parent) // 如栈顶非curnode的父结点,应该是右侧的兄弟结点
gotoLeftmostLeaf(s);// 则在其左子树中找到最靠左的叶子结点,
curnode = s.top();s.pop(); // 此时栈顶curnode已经是最靠左的叶子结点,
res.push_back(curnode->val);// 访问curnode
}
return res;
}
二叉树的层序遍历:
定义:如果二叉树为空,则直接返回,否则,按照从上至下,从左至右的顺序,依次访问二叉树上的结点;
递归版本算法:
这个递归版本其实类似于树的先序遍历的递归版本,不同的是,遍历的时候,结果根据层数添加到对应层的访问结果中,结点的实际访问顺序同先序遍历的访问顺序;
// 递归版本算法
void helper(vector<vector<int>> &ans, TreeNode *node, int level) {
if (nullptr == node) return;
if (level >= ans.size()) // 当前层大于已添加的层数时
ans.push_back({}); // 新增加一层
ans[level].push_back(node->val);// 将当前节点添加到对应层的访问结果中
helper(ans,node->left,level+1);
helper(ans,node->right,level+1);
}
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> ans;
helper(ans,root,0);
return ans;
}
迭代版本算法:
二叉树的层序遍历,往往和图的广度优先搜索遍历联系在一起,和队列queue有着密切的关系;我们接下来看看代码如何实现:
// 迭代版本算法 version1
vector<vector<int>> levelOrder(TreeNode *root)
{
vector<vector<int>> res;
if (nullptr == root)
return res;
queue<TreeNode *> q; // q始终存放着一层的结点数量
q.push(root); // 根结点先入队
vector<int> levelresult; // 一层结点的遍历结果
while (!q.empty())
{
int currentsize = q.size(); //当前层的结点梳理
for (int i = 0; i < currentsize; i++)
{
TreeNode *curnode = q.front();
q.pop();
levelresult.push_back(curnode->val);
if (nullptr != curnode->left)
q.push(curnode->left);
if (nullptr != curnode->right)
q.push(curnode->right);
}
// 一层结点访问完毕
res.push_back(levelresult); // 保存当前层访问结果到最终结果
levelresult.clear(); //清空当前层的结果
}
return res;
}
// 迭代版本算法 version2
vector<vector<int>> levelOrder(TreeNode *root)
{
vector<vector<int>> result;
if (!root)
return result;
queue<TreeNode *> q;
q.push(root);
q.push(nullptr); // 插入一个nullptr作为一层的分割点
vector<int> cur_vec;
while (!q.empty())
{
TreeNode *t = q.front();
q.pop();
if (t == nullptr) // 访问到分层的一点
{
result.push_back(cur_vec); // 保存当前层访问结果到最终结果
cur_vec.resize(0); //清空当前层的结果
if (q.size() > 0) // 此时队列不为空,说明新的一层的结点已经添加进队列
{
q.push(nullptr); // 增加分层的分割点
}
}
else
{
cur_vec.push_back(t->val);
if (t->left)
q.push(t->left);
if (t->right)
q.push(t->right);
}
}
return result;
}