数据结构与算法-二叉树遍历

树的遍历是当且仅当访问树中每一个节点一次的过程。遍历能够解释为把全部的节点放在一条线上,或者将树线性化。
遍历的定义只指定了一个条件:每一个节点仅访问一次,没有指定这些节点的访问顺序。所以,节点有多少种排列方式,就有多少种遍历方法。对于有n个节点的树,共有n!个不一样的遍历方式。然而大多数遍历方式是混乱的,很难从中找到规律,所以实现这样的遍历缺少广泛性。通过前人总结,这里介绍两种遍历方式,即广度优先遍历与深度优先遍历。
  • 广度优先遍历
广度优先遍历是从根节点开始,向下逐层访问每一个节点。当使用队列时,这种遍历方式的实现至关直接。假设从上到下、从左到右进行广度优先遍历。在访问了一个节点以后,它的子节点就放到队列的末尾,而后访问队列头部的节点。对于层次为n的节点,它的子节点位于第n+1层,若是将该节点的全部子节点都放到队列的末尾,那么,这些节点将在第n层的全部节点都访问以后再访问。
代码实现以下:
//广度优先遍历
void breadthFirst(Node* &bt){
	queue<Node*> que;
	Node* BT = bt;
	que.push(BT);
	while (!que.empty()){
		BT = que.front();
		que.pop();
		printf("%c", BT->data);
		if (BT->lchild)
			que.push(BT->lchild);
		if (BT->rchild)
			que.push(BT->rchild);
	}
}复制代码
广度优先遍历的逻辑比较简单,这里就不过多介绍了。
  • 深度优先遍历
树的深度优先遍历至关有趣。深度优先遍历将尽量的向下进行,在遇到第一个转折点时,向左或向右一步,而后再尽量的向下发展。这一过程一直重复,直至访问了全部节点为止。然而,这必定义并无清楚的指明何时访问节点:在沿着树向下进行以前仍是在折返以后呢?所以,深度优先遍历有着几种变种。
假如如今有个简单的二叉树:
咱们从左到右进行深度优先遍历:
你会发现其实每一个节点遍历到了3次:
红色的是第一次遍历到的次序,蓝色的是第二次遍历到的次序,黑色的是第三次遍历到的次序。这其实就是二叉树从左到右的前序、中序、后序遍历,从右到左也有三种遍历,和上面遍历的相似。前辈们对这三种遍历方式进一步总结,将深度遍历分解成3个任务:
  • V-访问节点
  • L-遍历左子树
  • R-遍历右子树
前序遍历实际上是VLR,中序遍历是LVR,后序遍历是LRV
咱们来讨论深度遍历的实现方法,毫无疑问,递归是很是简单而且容易理解的方式,代码以下:
//从左向右前中后递归遍历
void leftPreOrder(Node *bt) {
	if (bt){
		printf("%c", bt->data);
		leftPreOrder(bt->lchild);
		leftPreOrder(bt->rchild);
	}
}

void leftInOrder(Node *bt) {
	if (bt){
		leftInOrder(bt->lchild);
		printf("%c", bt->data);
		leftInOrder(bt->rchild);
	}
}

void leftPostOrder(Node *bt) {
	if (bt){
		leftPostOrder(bt->lchild);
		leftPostOrder(bt->rchild);
		printf("%c", bt->data);
	}
}复制代码
能够看到三种遍历方式的实现代码差异很小,其实差异就是V、L、R的顺序不一样,递归方式没啥难点,这里就很少讲了。重点在于下面的非递归实现方式。
递归代码是有缺陷的,毫无疑问会给运行时栈带来沉重的负担,由于每次递归(调用函数)都会在运行时栈上开辟新的函数栈空间,若是二叉树的高度比较大,程序运行可能直接爆掉栈空间,这是不能容忍的。所以,咱们要实现非递归方式的二叉树深度遍历。
寻常的非递归遍历方式实质上就是模拟上面动态图中箭头的转移过程,在三种遍历方式中前序遍历是最简单的,不管是哪种遍历方式,都须要借助栈来实现。由于在深度遍历中会有回溯的过程,这就要求程序能够记住以往遍历过的节点,只有知足这一点才能回溯成功,这就须要借助栈来保存遍历节点的次序。下面是非递归遍历方式的代码:
节点定义:

typedef struct BiNode{
  int data;
  struct BiNode * lchild;
  struct BiNode * rchild;
}BiNode,*BiTree;

前序遍历:

void preOrderBiTree(BiNode * root){
	if(root == NULL)
		return;
	BiNode * node = root;
	stack<BiNode*> nodes;
	while(node || !nodes.empty()){
		while(node != NULL){
			nodes.push(node);
			printf("%d",node->data);
			node = node -> lchild;
		}
		node = nodes.top();//回溯到父节点
		nodes.pop();
		node = node -> rchild;
	}
}复制代码
能够看到,在将节点入栈以前已经访问过节点,栈在这里的做用只有回溯。
中序遍历代码以下:
void inOrderBinaryTree(BiNode * root){
        if(root == NULL)
                return;
        BiNode * node = root;
        stack<BiNode*> nodes;
        while(node || !nodes.empty()){
                while(node != NULL){
                        nodes.push(node);
                        node = node ->lchild;
                }
                node = nodes.top();
                printf("%d ",node ->data);
                nodes.pop();
                node = node ->rchild;
        }
}复制代码
中序遍历中访问节点发生在出栈以前,栈在这里的做用不只仅是回溯,出栈还表明着第二次遍历到该节点。后序遍历更是须要程序实现V以前L和R操做已经完成,这就须要程序具备记忆功能。
后序遍历代码以下:
void PostOrderS(Node *root) {
    Node *p = root, *r = NULL;
    stack<Node*> s;
    while (p!=NULL || !s.empty()) {
        if (p!=NULL) {//走到最左边
            s.push(p);
            p = p->left;
        }
        else {
            p = s.top();
            if (p->right!=NULL && p->right != r)//右子树存在,未被访问
                p = p->right;
            else {
                s.pop();
                visit(p->val);
                r = p;//记录最近访问过的节点
                p = NULL;//节点访问完后,重置p指针
            }
        }//else
    }//while
}复制代码
后序遍历中最重要的逻辑是保证节点的左右子树访问以后再访问该节点,程序中使用变量保存了上次访问的节点。若是节点i存在右子树,而且上个访问的节点是右子树的根节点,才能访问i节点。
毫无疑问,以上经过程序模拟前中后三种遍历过程,就是咱们常说的过程式代码,若是你在面试中遇到这类问题,情急之下不必定能写的出来。究其缘由在于思想不统一,前中后遍历的三种代码实现方式是割裂开的。怎么办?笔者探究了一种实现方式,从思想上统一前中后三种遍历过程。
首先用天然语言描述实现逻辑:
假若有如下二叉树:
咱们来实现后序遍历,后序遍历的逻辑是LRV,也就是说,对于一颗二叉树,咱们但愿首先遍历左子树,而后是右子树,最后是根节点,放到栈里就是这样的:
注意这里,这里看似只是压入了三个节点,实质上已经将整棵树压入到栈里了。由于13在这里表明的不是一个节点,而是整棵左子树,23也是表明着整棵右子树。这里就有疑问了,以这样的方式入栈有个锤子用处,你告诉我怎么访问13这颗左子树?如何解决这个矛盾点才是核心逻辑。咱们能够继续分解左子树,就像这样:
能够看到,13表明的左子树以LRV的方式分解成3部分。这时栈中存在两个节点三颗树,程序的下一步继续弹出栈顶节点,若是是树就继续分解,若是是节点就表明着该节点是第一个能够访问到的节点。
使用这种方式来遍历二叉树,前中后三种遍历的差异只有一点,就是如何分解树。前序是VLR,中序是LVR,后序是LRV,程序的其余部分没有差异。
完整逻辑以下:
首先将根节点压入栈,随后弹出栈顶节点,发现是棵树,分解该树,按照指定的方式将左子树、右子树、根节点压入栈中。随后弹出栈顶节点,若是是棵树就继续分解,若是是节点就访问该节点。程序的结束标志是栈为空,表明整棵树的节点都已经访问完毕。
这种遍历方式笔者称之为分解遍历,由于核心逻辑是如何分解栈中表明树的节点。分解遍历使得前中后非递归遍历以统一的逻辑来处理。
分解遍历要求栈中节点保存额外的属性,即该节点是树仍是结点,就是说咱们须要一种方式知道弹出节点是树仍是结点。方法有不少,能够修改节点定义,添加一个额外的成员变量,或者栈中存储字典,key是节点,value表明该节点是树仍是结点,或者修改栈,为每一个节点保存额外的属性。笔者使用的方法是修改栈,为栈中每一个节点保存额外属性。
参考代码以下:
/*二叉树节点*/
typedef struct node {
	char data;
	struct node *lchild, *rchild;
}Node;

/*二叉树栈*/
typedef struct {
	int top;//指向二叉树节点指针的指针
	struct node* data[MAX];
	bool   isNode[MAX];
}SqStack;

void push(SqStack *&s, Node *bt, bool isNode = true) {
	if (s == NULL)
               return;
	if (s->top == MAX - 1)
	{
		printf("栈满,不能再入栈");
	}
	else
	{
		s->top++;
		s->data[s->top] = bt;
		s->isNode[s->top] = isNode;
	}
}

Node* pop(SqStack *&s) {
	if (s->top == -1)
	{
		printf("栈空,不能出栈");
	}
	else
	{
		Node* temp;
		temp = s->data[s->top];
		s->top--;
		return temp;
	}
}

bool topIsNode(SqStack *&s){
	if (s->top != -1)
		return s->isNode[s->top];
	return false;
}

//从左向右前中后非递归遍历
void leftPreOrder(Node *bt) {
	Node *BT = bt;
	bool isNode = false;
	push(s, BT, isNode);
	while (!EmptyStack(s)){
		isNode = topIsNode(s);
		BT = pop(s);
		if (isNode){
			printf("%c", BT->data);
		}
		else{
			if (BT->rchild != NULL)
				push(s, BT->rchild, false);
			if (BT->lchild != NULL)
				push(s, BT->lchild, false);
			push(s, BT, true);
		}		
	}
}

void leftSimPreOrder(Node *bt) {
	Node *BT = bt;
	push(s, BT);
	while (!EmptyStack(s)){
		BT = pop(s);
		printf("%c", BT->data);
		if (BT->rchild != NULL)
			push(s, BT->rchild);
		if (BT->lchild != NULL)
			push(s, BT->lchild);
	}
}

void leftInOrder(Node *bt) {
	Node *BT = bt;
	bool isNode = false;
	push(s, BT, isNode);
	while (!EmptyStack(s)){
		isNode = topIsNode(s);
		BT = pop(s);
		if (isNode){
			printf("%c", BT->data);
		}
		else{
			if (BT->rchild != NULL)
				push(s, BT->rchild, false);
			push(s, BT, true);
			if (BT->lchild != NULL)
				push(s, BT->lchild, false);
		}
	}
}

void leftPostOrder(Node *bt) {
	Node *BT = bt;
	bool isNode = false;
	push(s, BT, isNode);
	while (!EmptyStack(s)){
		isNode = topIsNode(s);
		BT = pop(s);
		if (isNode){
			printf("%c", BT->data);
		}
		else{
			push(s, BT, true);
			if (BT->rchild != NULL)
				push(s, BT->rchild, false);
			if (BT->lchild != NULL)
				push(s, BT->lchild, false);
		}
	}
}复制代码
参考代码中前中后非递归遍历的逻辑几乎一致,差异在于分解树以后,以怎样的方式入栈。并且对于前序遍历,笔者进行了简化。由于前序遍历中,树以VLR的方式分解,入栈以后,栈顶老是结点,所以能够省去判断栈顶是树仍是结点的逻辑。
使用栈的二叉树遍历到此已经探究完毕,更多内容请看下一章节。

数据结构与算法-二叉查找树
node

相关文章
相关标签/搜索