遍歷是二叉樹的一類重要操作,也是二叉樹的其它操作和應用的算法基本框架
二叉樹(Binary Tree)
- 定義:含有n(n>=0)個結點的有限集合。當n=0時爲空二叉樹,
在非空二叉樹中:有且僅有一個根結點;其餘節點劃分爲兩互不相交的子集L和R,其中L和R也是一棵二叉樹,分別稱爲左子樹和右子樹 - 術語(部分)
- 層次:根爲第1層,根的孩子爲第2層,依次計數
- 深度(高度):最大層次稱爲高度
- 度:結點的孩子個數
- 內部結點(分支結點):非葉子結點
- 葉子結點:度爲0的結點
- 滿二叉樹(Full Binary Tree):一棵深度爲k且有
2k−1 個結點的二叉樹 - 完全二叉樹(Complete Binary Tree):深度爲k且含有n個結點的二叉樹,其每個結點都與深度爲k的滿二叉樹中編號從1至n的結點一一對應。
- 性質:
- 在非空二叉樹的第i層最多有
2k−1 個結點(i≥1)。 —- 可用數學歸納法證明 - 深度爲k的二叉樹最多有
2k−1 —- 基於上面性質,求等比數列和 - 對於任意的一棵二叉樹,如果度爲0的結點個數爲n0,度爲2的結點個數爲n2,則n0 = n2+1
- 具有n個結點的完全二叉樹的深度爲「log2n」+1
- 對於含n個結點的完全二叉樹中的編號爲i(i≤i≤n)的結點:
- 如果i = 1,則該結點爲數的根,沒有雙親。否則其雙親爲⌊i/2⌋
- 如果2i>n,則i結點沒有左孩子,否則其左孩子編號爲2i
- 如果2i+1>n,則i結點沒有右孩子,否則其右孩子編號爲2i+1
- 在非空二叉樹的第i層最多有
二叉樹的存儲結構
順序存儲結構
- 採用數組形式存儲,根據上面最後一條性質判定父子關係,容易造成空間浪費,例如:
鏈式存儲結構
二叉鏈表
typedef struct BiTNode{
int data; // 數據域
struct BiTNode *lchild, *rchild; // 左右孩子
}BiTNode, BiTree;
三叉鏈表
typedef struct BiTNode{
int data; // 數據域
struct BiTNode *lchild, *rchild, *parent; // 左右孩子 ,及雙親
}BiTNode, BiTree;
二叉樹的遍歷
遍歷方式可分爲:深度優先遍歷,廣度優先遍歷。
深度優先遍歷
而深度優先遍歷又可分爲:先序遍歷、中序遍歷、後序遍歷。其中又可區分遞歸遍歷跟非遞歸遍歷。
遞歸遍歷
如果L、D、R表示左子樹、根、右子樹,那麼3種算法可表示(都是爭對跟結點D而言的):
DLR:先序
LDR:中序
LRD:後序
這裏只展示中序遞歸遍歷:
// 中序遞歸遍歷
void InOrderTraverse(BiTree T,Status (*visit)(int elem)){
if( T == NULL ) return ;
InOrderTraverse(T->lchild,visit);
visit(T->data);
InOrderTraverse(T->rchild,visit);
}
非遞歸遍歷:
這裏又可分爲使用棧和不使用棧的情況
使用棧非遞歸遍歷
- 使用棧的中序非遞歸遍歷
// 從T結點出發,沿左分支不斷深入,直到左分支爲空的結點,沿途結點入棧S
BiTNode *GoFastLeft(BiTree T,LStack &S){
if( NULL == T ) return NULL;
while( T->lchild != NULL ){
Push(S,T);
T = T->lchild;
}
return T;
}
void InOrderTraverse(BiTree T, Status (*visit)(int elel)){
LStack s; InitStack(s);
BiTree p;
p = GoFastLeft(T,s); // 找到最左下的結點
while(p!=NULL){
visit(p->data);
if( p->rchild != NULL ){ // 令p指向其右孩子爲根的子樹的最左下結點
p = GoFastLeft(p->rchild,s);
}else if( !StackEmpty(s) ){
Pop(s,p);
}else{
p = NULL;
}
}
}
示意圖:
- 使用棧的先序非遞歸遍歷
根據示意圖,可以改成先序非遞歸遍歷,只是將訪問語句改變一下位置,代碼爲:
// 從T結點出發,沿左分支不斷深入,直到左分支爲空的結點,沿途結點入棧S
BiTNode *GoFastLeft(BiTree T,LStack &S,Status (*visit)(int elem)){
if( NULL == T ) return NULL;
while( T->lchild != NULL ){
visit(T->data); // ...
Push(S,T);
T = T->lchild;
}
visit(T->data);// ...
return T;
}
void InOrderTraverse(BiTree T, Status (*visit)(int elem)){
LStack s; InitStack(s);
BiTree p;
p = GoFastLeft(T,s); // 找到最左下的結點
while(p!=NULL){
if( p->rchild != NULL ){ // 令p指向其右孩子爲根的子樹的最左下結點
p = GoFastLeft(p->rchild,s,visit);
}else if( !StackEmpty(s) ){
Pop(s,p);
}else{
p = NULL;
}
}
}
- 使用棧的後序非遞歸遍歷
那麼怎麼改成後序呢??
二叉樹的非遞歸後序遍歷算法:
前序、中序、後序的非遞歸遍歷中,要數後序最爲麻煩,如果只在棧中保留指向結點的指針,那是不夠的,必須有一些額外的信息存放在棧中。方法有很多,這裏只舉一種,先定義棧結點的數據結構。
typedef struct{
Node * p;
int rvisited;
}SNode //Node 是二叉樹的結點結構,rvisited==1代表p所指向的結點的右結點已被訪問過。
lastOrderTraverse(BiTree bt){
//首先,從根節點開始,往左下方走,一直走到頭,將路徑上的每一個結點入棧。
p = bt;
while(bt){
push(bt, 0); //push到棧中兩個信息,一是結點指針,一是其右結點是否被訪問過
bt = bt.lchild;
}
//然後進入循環體
while(!Stack.empty()){ //只要棧非空
sn = Stack.getTop(); // sn是棧頂結點
//注意,任意一個結點N,只要他有左孩子,則在N入棧之後,N的左孩子必然也跟着入棧了(這個體現在算法的後半部分),所以當我們拿到棧頂元素的時候,可以確信這個元素要麼沒有左孩子,要麼其左孩子已經被訪問過,所以此時我們就不關心它的左孩子了,我們只關心其右孩子。
//若其右孩子已經被訪問過,或是該元素沒有右孩子,則由後序遍歷的定義,此時可以visit這個結點了。
if(!sn.p.rchild || sn.rvisited){
p = pop();
visit(p);
}
else //若它的右孩子存在且rvisited爲0,說明以前還沒有動過它的右孩子,於是就去處理一下其右孩子。
{
//此時我們要從其右孩子結點開始一直往左下方走,直至走到盡頭,將這條路徑上的所有結點都入棧。
//當然,入棧之前要先將該結點的rvisited設成1,因爲其右孩子的入棧意味着它的右孩子必將先於它被訪問(這很好理解,因爲我們總是從棧頂取出元素來進行visit)。由此可知,下一次該元素再處於棧頂時,其右孩子必然已被visit過了,所以此處可以將rvisited設置爲1。
sn.rvisited = 1;
//往左下方走到盡頭,將路徑上所有元素入棧
p = sn.p.rchild;
while(p != 0){
push(p, 0);
p = p.lchild;
}
}//這一輪循環已結束,剛剛入棧的那些結點我們不必管它了,下一輪循環會將這些結點照顧的很好。
}
}
而我自己寫的算法則是不斷訪問左邊子樹後將其切斷,雖然能實現,但是不是很好,這裏就不展示。
不使用棧
- 不使用棧的先序非遞歸遍歷
void PreOrderTraverse(TriTree T, Status (*visit)(int elem)){
TriTree p, pr;
if( T == NULl ) return ;
p = T;
while(p!=NULL){
visit(p->data);
if(p->lchild != NULL){
p = p->lchild;
}else if( p->rchild != NULL ){
p = p->rchild;
}else{
// ★★往回查找,找到第一個有右孩子的p結點,並且該右子樹沒被訪問過(由pr標記),找不到程序結束
while(p!=NULL && (p->rchild==pr||p->rchild==NULL)){
pr = p;
p = p->parent;
}
if( p!=NULL ) p = p->rchild;
}
}
}
- 不使用棧的中序非遞歸遍歷
TriTree GoFastLeft(TriTree T){
if( T == NULL ) return NULL;
while(T->lchild != NULL ){
T = T->lchild;
}
return T;
}
void InOrder(TriTree PT, void (*visit)(TElemType)){
TriTree t;
if( PT == NULL ) return ;
t = GoFastLeft(PT);
while(t != NULL){
visit(t->data);
if( t->rchild != NULL){
t = GoFastLeft(t->rchild);
}else{
// 右元素爲NULL,返回上一層
if( t->parent != NULL ){
// 如果是其雙親的左孩子,說明雙親還沒visit操作
if( t->parent->lchild == t ){
t = t->parent;
}else{
// 如果是其雙親的右孩子,說明雙親已經被visit操作,繼續向上
while( t->parent != NULL && t->parent->rchild == t ){
t = t->parent;
}
if( t->parent == NULL ){
t = NULL;
}else{
t = t->parent;
}
}
}else{
t = NULL;
}
}
}
}
- 不使用棧的後序非遞歸遍歷
typedef struct TriTNode {
TElemType data;
struct TriTNode *lchild, *rchild, *parent;
int mark; // 標誌域(在三叉鏈表的結點中增設一個標誌域
// (mark取值0,1或2)以區分在遍歷過程中到達該結點
// 時應繼續向左或向右或訪問該結點。)
} TriTNode, *TriTree;
void PostOrder(TriTree T, void (*visit)(TElemType)){
TriTree t;
if( T == NULL ) return ;
t = T;
while( t!= NULL ){
if( t->mark == 0 ){ // 向左(左邊是否爲空)
if( t->lchild == NULL && t->rchild != NULL ){ // 左邊爲空,直接標記爲1,並訪問操作,
t->mark = 1; // 表示待visit操作,並向上
t = t->rchild;
}else if( t->lchild == NULL && t->rchild == NULL ){ // 左右爲空,直接訪問並向上
t->mark = 1;
visit(t->data);
t = t->parent;
}else{
t->mark = 2; // 進入左邊,標記表示待向右
t = t->lchild;
}
} else if( t->mark == 2 ){ // 向右操作 (可能爲空)
if( t->rchild == NULL ){
t->mark = 1; // 表示待visit操作,並向上
visit(t->data);
t = t->parent;
}else{
t->mark = 1;
t = t->rchild;
}
} else if( t->mark == 1 ){ //visit操作,並向上
visit(t->data);
t = t->parent;
}
}
}
廣度優先遍歷
void LevelOrderTraverse(BiTree T, Status (*visit)(int elem)){
if( T == NULL ) return ;
LQueue Q; InitQueue(Q);
BiTree p = T;
visit(T->data);
EnQueue(Q,p);
while( !QueueEmpty(Q) ){
DeQueue(Q,p);
if(p->lchild != NULL){ // 處理左孩子
visit(T->lchild->data);
EnQueue(Q,p->lchild);
}
if(p->rchild != NULL){ // 處理右孩子
visit(T->rchild->data);
EnQueue(Q,p->rchild);
}
}
}
最後,如果有哪裏錯誤或者不足,希望能夠指出,謝謝!