最近在学习数据结构,从书上的代码示例中学习到了一种抽象的思考方式,记录一些学习二叉树的感悟

先序遍历

先序遍历相对简单,我一开实现的时候考虑了四种情况

  1. 左孩子为空 && 右孩子为空
    访问根节点,然后出栈
  2. 左孩子不为空 && 右孩子为空
    访问根节点,然后继续访问左孩子
  3. 左孩子为空 && 右孩子不为空
    访问根节点,入栈右孩子,出栈
  4. 左孩子不为空 && 右孩子不为空
    访问根节点,入栈右孩子,继续访问左孩子
template<typename Elemtype>
template<typename Fun>
bool BiTree<Elemtype>::PreOrderTraverse(Fun &visit){
  Stack<BiTNode<Elemtype>*> S;
  InitStack<bool,BiTNode<Elemtype>*>(S);
  BiTNode<Elemtype> *p = root;
  while (p != nullptr)
  {
      visit(p);
      if(p->rChrilde != nullptr)
          Push<bool,BiTNode<Elemtype>*>(S,p->rChrilde);

      if(p->lChrilde != nullptr)
          p = p->lChrilde;
      else
          //如果栈空Pop()返回false,用于判断栈空结束循环
          if(!Pop<bool,BiTNode<Elemtype>*>(S,p))
              p = nullptr;
  }
  return true;
}

这种常规的做法,相信大家都有不同的实现,但是书上的示例(清华大学出版社的 数据结构(c语言版))非常简洁

template<typename Elemtype>
template<typename Fun>
bool BiTree<Elemtype>::PreOrderTraverse(Fun &visit){
Stack<BiTNode<Elemtype>*> S;
InitStack<bool,BiTNode<Elemtype>*>(S);
BiTNode<Elemtype> *p = root;
while (p || !StackEmpty<bool,BiTNode<Elemtype>*>(S)){
    if (p != nullptr){
        //存入栈中
        Push<bool,BiTNode<Elemtype>*>(S,p);
        //访问节点
        visit(p);
        //遍历左子树
        p = p->lChrilde;
     }
    else{
        //退栈
        Pop<bool,BiTNode<Elemtype>*>(S,p);
        //访问右子树
        p = p->rChrilde;
    }
  }
return true;
}

思路很简单,总结来说就是,往左走,走不通就往右走
这样一来,这个问题就被抽象成了两种情况

  1. 当前节点不为空
    往左走
  2. 当前节点为空
    往当前节点的根节点的右孩子走
    这样整个判断情况的简单了非常多,将一个具有四种情况的问题抽象成立两种情况
    这其实也是递归思想变成循环实现

中序遍历

中序遍历跟先序遍历思路几乎一样,只是访问的时间不同而已

template<typename Elemtype>
template<typename Fun>
bool BiTree<Elemtype>::PreOrderTraverse(Fun &visit){
Stack<BiTNode<Elemtype>*> S;
InitStack<bool,BiTNode<Elemtype>*>(S);
BiTNode<Elemtype> *p = root;
while (p || !StackEmpty<bool,BiTNode<Elemtype>*>(S)){
    if (p != nullptr){
        //存入栈中
        Push<bool,BiTNode<Elemtype>*>(S,p);
        //遍历左子树
        p = p->lChrilde;
     }
    else{
        //退栈
        Pop<bool,BiTNode<Elemtype>*>(S,p);
        //访问节点
        visit(p);
        //访问右子树
        p = p->rChrilde;
    }
  }
return true;
}

后序遍历

后序遍历相比之下复杂很多,因为顺序是 左 -> 右 -> 根
因为你在访问左孩子的时候,无法知道右孩子有没有,以及右孩子有没有子节点
我稍微查了一些百度上的实现方式大多如下

第一种思路:对于任一结点P,将其入栈,然后沿其左子树一直往下搜索,直到搜索到没有左孩子的结点,此时该结点出现在栈顶,但是此时不能将其出栈并访问, 因此其右孩子还为被访问。所以接下来按照相同的规则对其右子树进行相同的处理,当访问完其右孩子时,该结点又出现在栈顶,此时可以将其出栈并访问。这样就 保证了正确的访问顺序。可以看出,在这个过程中,每个结点都两次出现在栈顶,只有在第二次出现在栈顶时,才能访问它。因此需要多设置一个变量标识该结点是 否是第一次出现在栈顶。

第二种思路:要保证根结点在左孩子和右孩子访问之后才能访问,因此对于任一结点P,先将其入栈。如果P不存在左孩子和右孩子,则可以直接访问它;或者P存 在左孩子或者右孩子,但是其左孩子和右孩子都已被访问过了,则同样可以直接访问该结点。若非上述两种情况,则将P的右孩子和左孩子依次入栈,这样就保证了 每次取栈顶元素的时候,左孩子在右孩子前面被访问,左孩子和右孩子都在根结点前面被访问。(注意看他的入栈顺序,和上一种方法不一样,不是将左子树全部入栈后再转向右子树。)

但是我想到一种稍微不一样的实现方式
其实将顺序反过来看(因为栈是先进后出)根 -> 右 -> 左
有没有觉得非常眼熟?再换个方式 根 -> 左 -> 右
没错,其实是右孩子优先的 先序遍历,将访问左右孩子的的顺序调换,就可以得到这样的栈
打个比方(灵魂画图)

这样的右孩子优先的 先序遍历的结果为 1 5 7 6 2 4 3
反过来就是 3 4 2 6 7 5 1
这不就是后序遍历的结果了么


template<typename Elemtype>
template<typename Fun>
bool BiTree<Elemtype>::PostOrderTraverse(Fun &visit){
Stack<BiTNode<Elemtype>*> S,tmp;
InitStack<bool,BiTNode<Elemtype>*>(S);
InitStack<bool,BiTNode<Elemtype>*>(tmp);
BiTNode<Elemtype> *p = root;
  while (p || !StackEmpty<bool,BiTNode<Elemtype>*>(S)){
      if (p != nullptr){
          //存入栈中
          Push<bool,BiTNode<Elemtype>*>(S,p);
          Push<bool,BiTNode<Elemtype>*>(tmp,p);
          //遍历右子树
          p = p->rChrilde;
       }
      else{
          //退栈
          Pop<bool,BiTNode<Elemtype>*>(S,p);
          //访问左子树
          p = p->lChrilde;
      }
  }
  while (!StackEmpty<bool,BiTNode<Elemtype>*>(tmp))
  {
      Pop<bool,BiTNode<Elemtype>*>(tmp,p);
      visit(p);
  }

  return true;
}

这样就可以相对简单的实现非递归的后序遍历

总结

感觉所谓算法只是思想的体现,或者说对抽象场景的判断,如果能将问题很好的抽象,那么解决的难度就会降低很多很多

btw:我还在学习c++当中,模板可能学的不是特别好,如果觉得实现复杂了求轻喷。。。

12-17 16:19