二叉树

之前写了一些链表和排序的blog,其中有说到多链表,堆,其中提到了一种特殊的数据结构:树。
人们发明树结构,用于储存和搜索海量的数据。

树的种类

无序树:树中任意节点的子结点之间没有顺序关系,这种树称为无序树,也称为自由树。
有序树:树中任意节点的子结点之间有顺序关系,这种树称为有序树;
二叉树:每个节点最多含有两个子树的树称为二叉树;
完全二叉树:二叉树的所有子树要么没有孩子,要么一定有左孩子。堆是一种完全二叉树。
捕获
红黑树:红黑树是把树中的结点定义为红、黑两种颜色,并通过规则确保从根结点到叶结点的最长路径的长度不超过最短路径的两倍。在STL中,setmultisetmapmultimap等数据结构都是基于红黑树实现的。
满二叉树:除最后一层无任何子节点外,每一层上的所有结点都有两个子结点(最后一层上的无子结点的结点为叶子结点)。
霍夫曼树:带权路径最短的二叉树称为哈夫曼树或最优二叉树。

如果二叉树不是平衡的,那么它就退化为一个链表,而搜索时间也退化为一个线性函数。

树的深度

定义一棵树的根结点层次为1,其他节点的层次是其父结点层次加1。一棵树中所有结点的层次的最大值称为这棵树的深度。

树的遍历

所谓遍历(Traversal)是指沿着某条搜索路线,依次对树中每个结点均做一次且仅做一次访问。访问结点所做的操作依赖于具体的应用问 题。 遍历是二叉树上最重要的运算之一,是二叉树上进行其它运算之基础。

遍历命名

根据访问结点操作发生位置命名:

① NLR:前序遍历(Preorder Traversal亦称(先序遍历))
——访问根结点的操作发生在遍历其左右子树之前。如10、6、4、8、14、12、16。
② LNR:中序遍历(Inorder Traversal)
——访问根结点的操作发生在遍历其左右子树之中(间)。如4、6、8、10、12、14、16。
③ LRN:后序遍历(Postorder Traversal)
——访问根结点的操作发生在遍历其左右子树之后。如4、8、6、12、16、14、10。

中序遍历的投影法

捕获

一般二叉树的定义

1
2
3
4
struct BiTNode{
TElemType data;
BiTNode *lchild,*rchild;
};

二叉树的遍历操作

先序遍历(DLR)

先序遍历的递归过程为:

(1) 访问根结点
(2) 先序遍历根结点的左子树
(3) 先序遍历根结点的右子树
若二叉树为空,遍历结束。

先序遍历二叉树的递归算法如下:

1
2
3
4
5
6
7
void PreOrder(BiTree bt) /*先序遍历二叉树bt*/
{
if (bt==NULL) return; /*递归调用的结束条件*/
Visite(bt->data); /*访问结点的数据域*/
PreOrder(bt->lchild); /*先序递归遍历bt 的左子树*/
PreOrder(bt->rchild); /*先序递归遍历bt 的右子树*/
}

中序遍历(LDR)

中序遍历的递归过程为:

(1)中序遍历根结点的左子树;
(2)访问根结点;
(3)中序遍历根结点的右子树。
若二叉树为空,遍历结束。

中序遍历二叉树的递归算法如下:

1
2
3
4
5
6
7
void InOrder(BiTree bt) /*中序遍历二叉树bt*/
{
if (bt==NULL) return; /*递归调用的结束条件*/
InOrder(bt->lchild); /*中序递归遍历bt 的左子树*/
Visite(bt->data); /*访问结点的数据域*/
InOrder(bt->rchild); /*中序递归遍历bt 的右子树*/
}

后序遍历(LRD)

后序遍历的递归过程为:

(1)后序遍历根结点的左子树
(2)后序遍历根结点的右子树
(3)访问根结点
若二叉树为空,遍历结束。

后序遍历二叉树的递归算法如下:

1
2
3
4
5
6
7
void PostOrder(BiTree bt) /*后序遍历二叉树bt*/
{
if (bt==NULL) return; /*递归调用的结束条件*/
PostOrder(bt->lchild); /*后序递归遍历bt 的左子树*/
PostOrder(bt->rchild); /*后序递归遍历bt 的右子树*/
Visite(bt->data); /*访问结点的数据域*/
}

从表面上看,从代码中,遍历语句的位置可以看出是什么遍历。

层次遍历(宽度优先遍历)

由层次遍历的定义可以推知,在进行层次遍历时,对一层结点访问完后,再按照它们的访问次序对各个结点的左孩子和右孩子顺序访问,这样一层一层进行,先遇到的结点先访问,这与队列的操作原则比较吻合。因此,在进行层次遍历时,可设置一个队列结构,遍历从二叉树的根结点开始,首先将根结点指针入队列,然后从对头取出一个元素,每取一个元素,执行下面两个操作:

(1) 访问该元素所指结点
(2) 若该元素所指结点的左、右孩子结点非空,则将该元素所指结点的左孩子指针和右孩子指针顺序入队
此过程不断进行,当队列为空时,二叉树的层次遍历结束。

在下面的层次遍历算法中,二叉树以二叉链表存放,一维数组Queue[MAXNODE]用以实现队列,变量frontrear分别表示当前对首元素和队尾元素在数组中的位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
void LevelOrder(BiTree bt) /*层次遍历二叉树bt*/
{
BiTree Queue[MAXNODE];
int front,rear;


if (bt==NULL)
return;


front=-1;
rear=0;
queue[rear]=bt;


while(front!=rear)
{
front++;
Visite(queue[front]->data); /*访问队首结点的数据域*/


if (queue[front]->lchild!=NULL) /*将队首结点的左孩子结点入队列*/
{
rear++;
queue[rear]=queue[front]->lchild;
}


if (queue[front]->rchild!=NULL) /*将队首结点的右孩子结点入队列*/
{
rear++;
queue[rear]=queue[front]->rchild;
}
}
}

二叉树遍历的非递归实现

从二叉树各种遍历来说,各种遍历都是从根结点开始的,且在遍历过程中经过结点的路线是一样的,只是访问的时机不同而已。这一路线都是从根结点开始沿左子树深入下去,当深入到最左端,无法再深入下去时,则返回,再逐一进入刚才深入时遇到结点的右子树,再进行如此的深入和返回,直到最后从根结点的右子树返回到根结点为止。先序遍历是在深入时遇到结点就访问,中序遍历是在从左子树返回时遇到结点访问,后序遍历是在从右子树返回时遇到结点访问。

这种路线可以用栈来实现。其实递归在本质上就是一个栈结构。

在这一过程中,返回结点的顺序与深入结点的顺序相反,即后深入先返回,正好符合栈结构后进先出的点。因此,可以用栈来帮助实现这一遍历路线。其过程如下。在沿左子树深入时,深入一个结点入栈一个结点,若为先序遍历,则在入栈之前访问之;当沿左分支深入不下去时,则返回,即从堆栈中弹出前面压入的结点,若为中序遍历,则此时访问该结点,然后从该结点的右子树继续深入;若为后序遍历,则将此结点再次入栈,然后从该结点的右子树继续深入,与前面类同,仍为深入一个结点入栈一个结点,深入不下去再返回,直到第二次从栈里弹出该结点,才访问之。

先序遍历的非递归实现

在下面算法中,二叉树以二叉链表存放,一维数组stack[MAXNODE]用以实现栈,变量top用来表示当前栈顶的位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
void NRPreOrder(BiTree bt) /*非递归先序遍历二叉树*/
{
BiTree stack[MAXNODE],p;
int top;


if (bt==NULL)
return;


top=0;
p=bt;


while(!(p==NULL&&top==0))
{
while(p!=NULL)
{
Visite(p->data); /*访问结点的数据域*/


if (top<MAXNODE-1) /*将当前指针p 压栈*/
{
stack[top]=p;
top++;
}
else
{
printf("栈溢出");
return;
}


p=p->lchild; /*指针指向p 的左孩子*/


}


if (top<=0)
return; /*栈空时结束*/
else
{
top--;
p=stack[top]; /*从栈中弹出栈顶元素*/
p=p->rchild; /*指针指向p 的右孩子结点*/
}
}
}

中序遍历的非递归实现

中序遍历的非递归算法的实现,只需将先序遍历的非递归算法中的Visite(p->data)移到p=stack[top]p=p->rchild之间即可。

后序遍历的非递归实现

由前面的讨论可知,后序遍历与先序遍历和中序遍历不同,在后序遍历过程中,结点在第一次出栈后,还需再次入栈,也就是说,结点要入两次栈,出两次栈,而访问结点是在第二次出栈时访问。因此,为了区别同一个结点指针的两次出栈,设置一标志flag,令:

flag = 1 -> 第一次出栈,结点不能访问
flag = 2 -> 第二次出栈,结点可以访问
当结点指针进、出栈时,其标志flag也同时进、出栈。因此,可将栈中元素的数据类型定义为指针和标志flag合并的结构体类型。定义如下:

1
2
3
4
5
typedef struct 
{
BiTree link;
int flag;
} stacktype;

后序遍历二叉树的非递归算法如下。在算法中,一维数组stack[MAXNODE]用于实现栈的结构,指针变量p指向当前要处理的结点,整型变量top用来表示当前栈顶的位置,整型变量sign为结点p的标志量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
void NRPostOrder(BiTree bt) /*非递归后序遍历二叉树bt*/
{
stacktype stack[MAXNODE];
BiTree p;
int top,sign;


if (bt==NULL)
return;


top=-1 /*栈顶位置初始化*/
p=bt;


while (!(p==NULL && top==-1))
{
if (p!=NULL) /*结点第一次进栈*/
{
top++;
stack[top].link=p;
stack[top].flag=1;
p=p->lchild; /*找该结点的左孩子*/
}
else
{
p=stack[top].link;
sign=stack[top].flag;
top--;


if (sign==1) /*结点第二次进栈*/
{
top++;
stack[top].link=p;
stack[top].flag=2; /*标记第二次出栈*/
p=p->rchild;
}
else
{
Visite(p->data); /*访问该结点数据域值*/
p=NULL;
}
}
}
}

A lo hecho, pecho.