二叉树遍历算法总结

三类:

  • 栈
    • 系统栈
    • 自己实现的栈
  • 线索:Morris preorder/inorder/postorder traversal
  • 其他edge-crawling方式:Schorr-Waite graph marking algorithm、Lindstrom-Dwyer algorithm
#include <algorithm>
#include <cstdio>
#include <queue>
#include <stack>
using namespace std;

struct Node {
  int key;
  int s; // only used by Schorr-Waite
  Node *l, *r;
  Node(int key, Node *l, Node *r) : key(key), l(l), r(r), s(0) {}
  void visit() const {
    printf("%d\n", key);
  }
};

enum Type { PREORDER, INORDER, POSTORDER };

/*
preorder模拟栈版:栈中存放的节点`x`表示之后待遍历`x`的子树。
*/
void preorder(Node *p)
{
  if (p) {
    p->visit();
    preorder(p->l);
    preorder(p->r);
  }
}

/*
inorder模拟栈版:栈中存放的节点`x`表示`x`待访问,且之后遍历`x`的右子树。
*/
void inorder(Node *p)
{
  if (p) {
    inorder(p->l);
    p->visit();
    inorder(p->r);
  }
}

void postorder(Node *p)
{
  if (p) {
    postorder(p->l);
    postorder(p->r);
    p->visit();
  }
}

void preorder_stack(Node *p)
{
  stack<Node *> s;
  for(;;) {
    while (p) {
      p->visit();
      if (p->r)
        s.push(p->r);
      p = p->l;
    }
    if (s.empty()) break;
    p = s.top();
    s.pop();
  }
}

void inorder_stack(Node *p)
{
  stack<Node *> s;
  for(;;) {
    while (p) {
      s.push(p);
      p = p->l;
    }
    if (s.empty()) break;
    p = s.top();
    s.pop();
    p->visit();
    p = p->r;
  }
}

/*
栈中存放的节点`x`表示两种阶段之一:
1. 当前在访问`x`左子树,之后待遍历`x`的右子树,然后访问`x`。
2. 当前在访问`x`的右子树,之后待访问`x`。

祖先节点进入阶段2的时刻晚于后裔节点。
外层循环开始处即`while (p) {`行,以下两个条件之一满足:
1. `p`非NULL,需要遍历`p`子树,栈上存放`p`的所有祖先。
2. `p`为NULL,栈上是栈顶及栈顶的所有祖先,下一个访问的是栈顶。

遍历一棵子树的方式是沿着左孩子链接下溯并把沿途节点都压栈,直到遇到NULL。
若遇到的最后一个节点`x`有右孩子`y`,则当前指针置为`y`,`x`及`x`的一些连续的祖先进入阶段2;
否则(第二个while循环实现这一复杂逻辑)说明`x`已在阶段2,访问`x`,
同时`x`一些连续的祖先也进入阶段2了,也需要访问并从栈中弹出。
*/
void postorder_stack(Node *p)
{
  stack<Node *> s;
  for(;;) {
    while (p) {
      s.push(p);
      p = p->l;
    }
    while (! s.empty() && s.top()->r == p) {
      p = s.top();
      s.pop();
      p->visit();
    }
    if (s.empty()) break;
    p = s.top()->r;
  }
}

void postorder2_stack(Node *p)
{
  stack<Node *> s;
  for(;;) {
    while (p) {
      if (p->r)
        s.push(p->r);
      s.push(p);
      p = p->l;
    }
    if (s.empty()) break;
    p = s.top();
    s.pop();
    if (! s.empty() && p->r == s.top()) {
      s.pop();
      s.push(p);
      p = p->r;
    } else {
      p->visit();
      p = NULL;
    }
  }
}

/*
按层级顺序访问,使用队列进行BFS
*/
void level_order(Node *p)
{
  queue<Node *> q;
  if (p) {
    q.push(p);
    while (! q.empty()) {
      p = q.front();
      q.pop();
      p->visit();
      if (p->l)
        q.push(p->l);
      if (p->r)
        q.push(p->r);
    }
  }
}

/*
preorder或inorder可以实现O(1)额外空间的遍历,
方法是把右孩子为空的节点的右孩子改造成中序后继的线索指针。
*/
void morris_traversal(Node *p, Type t)
{
  while (p) {
    Node *q = p->l;
    if (q) {
      while (q->r && q->r != p) q = q->r;
      if (q->r == p)
        q->r = NULL;
      else {
        if (t == PREORDER)
          p->visit();
        q->r = p;
        p = p->l;
        continue;
      }
    } else if (t == PREORDER)
      p->visit();
    if (t == INORDER)
      p->visit();
    p = p->r;
  }
}

// morris_postorder_traversal的辅助函数
void reverse_right_chain(Node *x, Node *y)
{
  Node *p = x, *q = x->r, *r;
  while (p != y) {
    r = q->r;
    q->r = p;
    p = q;
    q = r;
  }
}

/*
增量构造线索进行post-order遍历比较复杂
*/
void morris_postorder_traversal(Node *p)
{
  Node aux;
  aux.l = p;
  aux.r = NULL;
  p = &aux;
  while (p) {
    Node *q = p->l;
    if (q) {
      while (q->r && q->r != p) q = q->r;
      if (q->r == p) {
        reverse_right_chain(p->l, q);
        for (Node *r = q; ; r = r->r) {
          r->visit();
          if (r == p->l) break;
        }
        reverse_right_chain(q, p->l);
        q->r = NULL;
      } else {
        q->r = p;
        p = p->l;
        continue;
      }
    }
    p = p->r;
  }
}

/* Schorr-Waite graph marking algorithm
 *
 * 该算法需要在节点信息里额外维护一个域 s,表示遍历过程中的访问次数,有如下几种取值:
 * 0: 尚未访问
 * 1: 访问过1次(preorder),尚未遍历其左右子树
 * 2: 访问过2次(inorder),已遍历其左子树,尚未遍历其右子树
 * 3: 访问过3次(postorder),已遍历其左右子树
 *
 * 访问到 x 时,其祖先节点及它们的子树遍历进度(在遍历左子树还是右子树)是通过栈维护的。
 * 该算法把祖先节点下溯的指针翻转,再通过设置一个父节点指针 p 以把祖先节点组织为一个链。
 * 通过 s 可以知道各祖先节点当前在遍历左子树还是右子树。
 *
 * 若注释掉代码中的 x->s = 0; 则在算法执行完后遍历到的节点的 s 值为3,可以判断节点是否被访问过。
 *
 * 该算法可用于遍历一般的有向图,也需要注释掉 x->s = 0。带有 q 个指针域的节点将被访问 q+1 次。
 */

void schorr_waite(Node *x, Type t)
{
  if (! x) return;
  Node *y, *p = NULL;
  for(;;) {
    if ((int)t == (int)x->s)
      x->visit();
    if (x->s < 2) {
      x->s++;
      y = x->s == 1 ? x->l : x->r;
      if (y && y->s == 0) {
        (x->s == 1 ? x->l : x->r) = p;
        p = x;
        x = y;
      }
    } else {
      x->s = 0;               // reset to 0
      if (! p) return;
      y = x;
      x = p;
      if (x->s == 1)
        p = x->l, x->l = y;
      else
        p = x->r, x->r = y;
    }
  }
}

// http://www.cs.cornell.edu/courses/cs312/2007fa/lectures/lec21-schorr-waite.pdf
// 下面是一种变体,也可用于一般有向图,每个节点都有两个指针域
// 原始的Schorr-Waite在下溯时把指针和 p 交换,上溯时再交换回来
// 该方法使用四指针置换和三指针置换来统一几种逻辑判断

void schorr_waite_alternative(Node *p, Type t)
{
  Node *q = (Node *)-1;
  while (p != (Node *)-1) {
    if ((int)t == (int)p->s)
      p->visit();
    p->s++;
    if (p->s == 3 || p->l && p->l->s == 0) {
      Node *r = p->l;
      p->l = p->r;
      p->r = q;
      q = p;
      p = r;
    } else {
      Node *r = p->l;
      p->l = p->r;
      p->r = q;
      q = r;
    }
  }
}

// Lindstrom-Dwyer algorithm
// Schorr-Waite的变体,对于二叉树,上面 schorr_waite_alternative 的代码可以进一步简化
// 考察条件 p->s == 3 || p->l && p->l->s == 0
// 若 p->s != 3 则有 1 <= p->s && p->s <= 2
// 此时 p->l 为原节点的左子树或右子树,因为是树,不存在其他节点的指向它们
// 因此 p->l 若非 NULL 则必有 p->l->s != 0
// 该条件只有在 p->l == NULL 时不成立。即使不成立,我们依然可以套用四指针置换
// 但需要把 p(此时为NULL) 设为 q,把 q 设为 NULL
// 之后 s 仅用于判断节点访问次数,不再参与逻辑控制
// 如果不在乎一个节点调用 visit 三次的话,可以省去 s

void lindstrom_dwyer(Node *p)
{
  Node *q = (Node *)-1;
  while (p != (Node *)-1) {
    p->visit();
    Node *r = p->l;
    p->l = p->r;
    p->r = q;
    q = p;
    p = r;
    if (! p) p = q, q = NULL;
  }
}

int main()
{
  Node *a[7];
  for (int i = 7; i--; )
    a[i] = new Node(i, i*2+1 < 7 ? a[i*2+1] : NULL, i*2+2 < 7 ? a[i*2+2] : NULL);
  preorder(a[0]);
  inorder(a[0]);
  postorder(a[0]);
  level_order(a[0]);
  morris_traversal(a[0], PREORDER);
  morris_traversal(a[0], INORDER);
  preorder_stack(a[0]);
  inorder_stack(a[0]);
  postorder_stack(a[0]);
  postorder2_stack(a[0]);
  schorr_waite(a[0], PREORDER);
  schorr_waite(a[0], INORDER);
  schorr_waite(a[0], POSTORDER);
  schorr_waite_alternative(a[0], PREORDER);
  schorr_waite_alternative(a[0], INORDER);
  schorr_waite_alternative(a[0], POSTORDER);
  lindstrom_dwyer(a[0]);
}