題目描述
給定一棵二叉排序樹(BST),將該樹轉換成一棵雙向迴圈鏈表。請看下面的圖示說明,你可以更清楚的瞭解題意。
BST的結構定義如下:
struct node { int data; struct node* left; struct node* right;};typedef struct node Node;
圖1)一棵儲存1-5的二叉搜尋樹(BST)
圖2)根據上面的BST轉換得到的有序迴圈鏈表。其中,樹的左右孩子指標替換成了pre和next指標,分別指向鏈表的前一個和後一個結點。
分析看到這個題目,相信大多數人第一反應就是中序遍曆這棵二叉樹,同時改變樹中結點的left和right指標。這裡需要額外考慮的是如何將最後一個結點的right指標指向第一個結點,如所展示的那樣。 圖3)一個雙向迴圈鏈表以中序遍曆遍曆一棵二叉樹的時候,每遍曆到一個結點,我們就可以修改該結點的left指標指向前一個遍曆到的結點,因為在後續操作中我們不會再用到left指標;與此同時,我們還需要修改前一個遍曆結點的right指標,讓前一個遍曆結點的right指標指向當前結點。比如我們遍曆到結點2,則我們修改結點2的left指標指向結點1,同時需要修改結點1的right指標指向結點2。
需要注意一點,這裡的前一個遍曆結點不是當前結點的父結點,而是當前結點的前一個比它小的結點。
看似問題已經解決,慢著,我們其實落下了重要的兩步。1)我們沒有對頭結點head賦值。 2)最後一個結點的right指標沒有指向第一個結點。
解決這兩個問題的方案非常簡單:在每次遞迴調用的時候,更新
當前遍曆結點的right指標讓其指向頭結點head,同時更新
頭結點head的left指標讓其指向當前遍曆結點。當遞迴調用結束的時候,鏈表的頭尾結點會指向正確的位置。不要忘記只有一個結點的特殊情況,它的left和right指標都是指向自己。 圖4)只有一個結點的雙向迴圈鏈表這個解法非常的精巧,因為該演算法是對中序遍曆的一個改進,因此它的時間複雜度為O(N),N為結點數目。當然,相比中序遍曆,我們在每次遞迴調用過程中增加了額外的賦值操作。
// 這是一個改進的中序遍曆// prev (初始化為NULL) 用於跟蹤前一個遍曆結點// 當遞迴結束時,head指向鏈表的頭部void treeToDoublyList(Node *p, Node *& prev, Node *& head) { if (!p) return; treeToDoublyList(p->left, prev, head); // 當前結點p的left指向前一個結點prev p->left = prev; if (prev) prev->right = p; //前一個結點的right指向當前結點 else head = p; //如果前面沒有結點,則設定head為當前結點(當前結點為最小的結點)。 //遞迴結束後,head的left指標指向最後一個結點,最後一個結點的右指標指向head結點。注意儲存p的right指標,因為在後面代碼中會修改該指標。 Node *right = p->right; head->left = p; p->right = head; prev = p;//更新前一個結點 treeToDoublyList(right, prev, head); } //主函數,初始化prev和head為NULLNode* treeToDoublyList(Node *root) { Node *prev = NULL; Node *head = NULL; treeToDoublyList(root, prev, head); return head;}
代碼解析這個遞迴演算法挺複雜的,還是從函數功能上來理解會容易懂一點。函數treeToDoublyList(Node *p, Node *&prev, Node *&head)的功能是將以p為根結點的樹轉換成有序的雙向迴圈鏈表。函數執行前,prev總是指向遍曆到p之前的前一個結點。函數執行完成後,prev指向p,head指向鏈表頭結點。以上面圖中的二叉樹為例,treeToDoublyList(Node *p, Node *&prev, Node *&head)過程如下:根結點為4,調用treeToDoublyList(p->left, prev, head)遍曆完左子樹後,左子樹構成有序雙向迴圈鏈表1=2=3,prev指向的是結點3,head指向當前雙向迴圈鏈表的頭結點1。更新p->left=prev,即4的left指標指向3,設定prev->right=p,即3的right指標指向4。同時更新head結點1的left為節點4,4的right指標更新為1,則此時雙向迴圈鏈表變成1=2=3=4,接下來遞迴遍曆右子樹結點,保持雙向迴圈鏈表的性質。