二叉树的四种遍历算法_递归和迭代的实现

写在前面:

  • 二叉树的四种遍历方式,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为根结点的右子树;

image-20210514214556437

我们可以再抽象下,把树想象成下面这样,先序遍历为自上而下对藤上结点的访问,以及随后自下而上对各右子树的遍历;

image-20210515215236388

具体实现思路,我们可以观察到,右子树的从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;
}

再论二叉树的中序迭代算法:

那么再看下中序遍历,中序遍历有啥特点呢?

先遍历左侧链,找到左侧链的最远端,访问最远端的结点,再将控制权交给最远端的右侧子树,同样访问右侧子树的左侧链,遍历完,再叫给进一步的左侧链结点;这里感觉是需要一个栈,遍历左侧链,需要将左侧链上的结点入栈,出战的时候,需要先访问当前结点,再将当前结点的右子树进行左侧遍历;右子树不用存栈,是因为你栈里已经存了左侧链上的结点,每次出栈,都可以访问对应当前结点的右子树;

image-20210515001144341

image-20210515001224144

// 中序遍历迭代版本算法
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);
}

迭代算法版本:

那迭代算法呢,我们跟着下面的图思考一下:

image-20210515111523898

image-20210515111033708

image-20210515111103386

后序遍历算法,如果大家有不依赖父结点这个属性的版本,欢迎告诉博主,因为有依赖父结点这个属性,相当于已经通过先序遍历重建了一次二叉树,增加上了父结点这个属性,其实是有点走远的,大家可以先关注算法具体本身;

/**
 * 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;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值