二叉搜索树的遍历
二叉树遍历的内容很多,但是也是最重要的,最需要理解的,很多二叉树的相关算法,只要用活了遍历就没有问题了
前序遍历
对于每一棵树,先遍历其根节点,然后遍历其左子树,最后用同样的方式遍历右子树
递归实现前序遍历的过程太简单了,这里就不放了,直接说明二叉树的非递归的前序遍历实现:
如果不用递归实现前序遍历,那么就必须要用栈了,栈的作用是什么呢?我觉得栈就是记录了你的遍历过程,而且可以告诉你下一次应该回溯到哪个节点,每一次取出栈顶的元素,都说明已经遍历完这个栈顶节点的左子树了。
思路也很简单,就是每一次遇到一个节点就放入栈中,因为是前序遍历,所以先输出根节点,然后遍历左子树,当发现左子树遍历完成后,根据前序遍历的规则,此时需要遍历右子树,所以就取出栈顶节点,并遍历栈顶节点的右子树
bTree* stack[50] = {0};int top = 0;bTree *now = root;while(!isNull(now) || top){ while(!isNull(now)) { stack[top++] = now; std::cout << now->key << std::endl; now = now->left; } now = stack[--top]; now = now->right;}
中序遍历
对于每一棵树,先遍历其左子树,然后遍历其根节点,最后用同样的方式遍历右子树
- 方法1:使用栈:
思路和前序遍历差不多,但是这一次因为是中序遍历,所以每一次取出栈顶节点时,说明左子树遍历完毕,那么此时输出中间节点,并遍历右子树,用栈很容易实现,但是这里可以不用栈(这也是算法导论的一道题)
void inorderWalk(){ bTree* stack[50] = {0}; int top = 0; bTree *now = root; while(!isNull(now) || top) { while(!isNull(now)) { stack[top++] = now; now = now->left; } now = stack[--top]; std::cout << now->key << std::endl; now = now->right; }}
- 方法2,使用一个变量记录当前节点的父亲节点
last就承担了stack的责任,stack其实有用的也是栈顶元素,这里last承担了这个栈顶元素的责任
void inorderWalkNotUseStack(){ bTree *last = 0; bTree *now = root; while(!isNull(now)) { //last永远记录now的父亲,如果now非null,那么父亲也可以通过now->p来得到,但是当now是null时就需要通过last得到 if(!isNull(now)) { //如果now不是null last = now; now = now->left; } else{ //此时说明now是null,那么now可能是父亲的左子节点,也可能是右子节点 //如果now是某个节点的左子节点,那么说明此时应输出now的父亲,然后设置now为父亲的right //如果now是某个节点的右子节点,而且now还是null,那么借助last回溯,完全可以根据last和last的父亲的关系,把now设置为0或者1(不能直接设置为last,否则会重复遍历,其实这里就相当于把last所在的子树设置为null!这样就相当于last所在子树已经被遍历完毕) //为了更好的区分左右节点,我把原来为null的左子节点值设置为0,右子节点值设置为1,这样可以区分是左子节点还是右子节点,如果左右都是null就区分不了是左还是右子节点了 if(now == (bTree *)1) { //说明now是右子节点,那么此时看last即它的父亲是否为root,如果是那么说明遍历完成,如果不是,那么照之前的设置即可 if(last == root) { break; } else{ //last不是root,那么回溯即可 now = last == last->p->left ? 0 : (bTree*) 1; last = last->p; } } else{ std::cout << last->key << std::endl; now = last->right; } } }}
- 方法3:其实中序遍历还有个方法,就是利用中序遍历的结果其实就是排好序的结果,所以我们可以通过先找到最小的节点,然后不断的调用successor()方法不断获得其后继。
//使用minimum函数和successor完成的中序遍历void inorderWalk2(){ bTree *min = iterativeMinimum(root); while(!isNull(min)) { std::cout << min->key << std::endl; min = successor(min); }}
后序遍历
对于每一棵树,先遍历其左子树,再遍历右子树,最后才遍历根节点
后序遍历比前两种遍历方式的麻烦之处在于,后序遍历会两次回溯根节点,因为在遍历完左子树以后,接下来要遍历右子树,而获得右子树必须要通过根节点获得,这是第一次使用根节点,然后右子树遍历完以后,需要遍历根节点了,此时第二次使用根节点。
所以必须要通过某种方式得知当前是第几次遍历这个根节点,有两种方式:
- 第一种是通过设置一个标志位栈和节点栈同步维护
- 另一种方式是我选择的方式,就是第一次遍历一个根节点,一定是从它的左子节点回溯的,第二次,一定是从它的右子节点回溯的,那么如果我能得到当前节点是栈顶节点的左子节点还是右子节点来判断是第几次回溯了。
- 实现的方法就是给二叉树节点增加p属性,便于寻找其父节点。但光是增加p属性还不行,对于节点是null,无法通过p属性获得其父亲,而且当左右子节点都是null时,无法判断当前节点是父亲的左子节点还是右子节点,所以我设定:左子节点是null时,设置为0,右子节点是null时,设置为1,这样就能区分开了
void postorderWalk(){ bTree *stack[50] = {0}; int top = 0; bTree *now = root; while(!isNull(now) || top) { while(!isNull(now)) { stack[top++] = now; now = now->left; } //后序遍历不能直接从栈中取节点,因为后序是先左再右,所以要先找右结点 //所以后序遍历会取两次节点,那么什么时候输出这个节点呢,就要看当前节点和栈顶节点的关系了 //如果当前节点是栈顶节点的左子节点,那么直接找右子节点, //错误的想法: //如果当前节点是栈顶结点的右子节点,那么输出并设置now为0,因为刚取出来的栈顶结点一定是已经遍历完左子节点的 //这样会导致死循环,正确的做法应该是不断地取出栈顶元素,如果说当前结点是栈顶元素的右子节点,那么继续回溯 bTree *p = stack[top - 1]; if(now == 0) { //说明now是栈顶结点的左子节点 now = p->right; } else{ //说明now是栈顶结点的右子节点 bTree *q = now; while(top && q == p->right) { //这里要不断的取出已经遍历完的子树的根节点 top--; std::cout << p->key << std::endl; q = p; p = stack[top - 1]; } if(top == 0) { //如果top是0说明已经遍历完成了 break; } else{ //此时p是栈顶元素,q是p的左子节点,此时设置now为p的右子节点 now = p->right; } } }}
总结
这三种里我觉得中序和后序是最重要的,中序遍历结果是排序的结果,后序遍历的话特点就是左右子节点都遍历完以后才会遍历父节点,所以很多算法会用到后序遍历