雖說對於二叉樹的遍歷操作來說非遞歸法使用用戶自定義的棧來代替遞歸使用時的系統棧,可以得到不小的效率提升,但將二叉樹線索化時能將用戶棧也省略掉進一步提高了效率。
對於二叉樹的鏈表結構,n個結點的二叉樹有n+1個空鏈域(每個葉節點都有兩個空鏈域),而線索二叉樹就把這些空鏈域有效的利用了起來,在一般的二叉樹中,我們只知道每個結點的左右孩子,並不知道某個結點在某種遍歷方式下的直接前驅和直接后繼,如果能夠知道前驅和后繼信息,就可以把二叉樹看作一個鏈表結構,從而可以像遍歷鏈表那樣來遍歷二叉樹,進而提高效率。這對經常需要進行遍歷操作的二叉樹而言,無疑是很有用的。
(1)中序線索二叉樹的構造
線索二叉樹的結點結構如下:
lchild | ltag | data | rtag | rchild |
在二叉樹線索化的過程中會把原有的空指針利用起來作為尋找當前結點的前驅或者后繼的線索,但是這樣的話,線索和樹中原有指向孩子結點的指針分不清,於是給左右孩子結點添加標記(itag、rtag),即:
如果ltag=0,表示lchild為指向孩子結點的指針,如果ltag=1,則表示lchild為線索,指向結點的前驅;
如果rtag=0,表示rchild為指向孩子結點的指針,如果rtag=1,則表示rchild為線索,指向結點的后繼;
結點定義如下:
typedef struct TBTNode{ char data; int ltag,rtag; struct TBTNode *lchild; struct TBTNode *rchild; }TBTNode;
線索化的規則是:左線索指針指向當前結點在中序遍歷序列中的前驅結點,右線索指針指向后繼結點。因此我們需要一個指針p指向當前正在訪問的結點,pre指向p的前驅結點,p的左線索指針如果存在的話直接指向pre(也就是前驅結點),pre的右線索如果存在則指向p(也就是直接后繼)。
如圖,中序遍歷序列為DBHEIAFCG,所以從序列中我們可以直接得出H的前驅為B后繼為E,I的前驅為E后繼為A等等。
void InThread(TBTNode *p, TBTNode *&pre){ if (p != null) { InThread(p->lchild, pre); //對左孩子遞歸,自然第一個操作的結點為最左下結點(不一定為葉結點),如上圖為D if (p->lchild == NULL) { p->lchild = pre; p->ltag = 1; } if (pre != NULL && pre->rchild == NULL) { pre->rchild = p; pre->rtag = 1; } pre = p; //當p將要離開一個訪問過的結點時,pre指向p,所以p指向新結點時,pre顯然是此時p的直接前驅 p = p->rchild; //p指向右孩子,准備將右子樹線索化 InThread(p, pre); } }
(2)遍歷中序線索二叉樹
TBTNode *First(TBTNode *p){//尋找中序序列下第一個結點 while(p->ltag == 0) p=p->lchild; return p; //尋找樹的最左下角結點(不一定是葉節點,如上圖樹中是D } TBTNode *Next(TBTNode *p){ if (p->tag == 0) return First(p->rchild); //中序遍歷下,p結點的下一個結點自然是p的右子樹中First()求出來的結點 else return p->rchild; //rtag==1,此時rchild為線索,指向結點的直接后繼 } void InOrder(TBTNode *root){//遍歷 for (TBTNode *p = First(root) ; p !=NULL ; p = Next(p)) { //對結點操作 } }