遞歸一題三解-將二分查找樹(BST)轉化成循環雙鏈表(DLL)

題目來自leetcode: 已知一個BST(binary search tree), 將其原地轉化成一個循環的排序過的雙鏈表(circular sorted double linked list)。

說明:BST的節點有兩個指針left, right, 分別指向比它小,和比它大的節點。變成DLL之後,由於DLL節點原本有prev 和 next 指針分別和之前和之後的節點,這裏假定原left指針指向之前,原right 指向之後節點。關於題意可以參考下圖:

                                               圖一. BST to DLL 示例()

如圖一所示,黑線是原本的BST中left和right指針,紅色箭頭表示轉化成DLL後的next指針(借用原right指針)。

可以感覺到,大體是個遍歷BST的過程,天生適合遞歸。本文會介紹三種解法,雖然都使用遞歸,但思路各有不同。

方法一:這個方法是我原創的,借鑑了中序遍歷(in-order)遍歷的思想。

附中序遍歷:

void InOrder(node* root){  //LRV
    InOrder(root->left);
    display(root->val);
    InOrder(root->right);
}
中序遍歷中,遞歸的順序依次是left, curr, right。考慮到當前節點(curr)對於後續節點(nextt)即意味着前向節點(prev),爲了保證在每個遞歸函數中均勻處理,首先處理curr節點與prev節點間的關聯,然後將curr作爲前向節點傳給右子樹節點,最後返回curr所在子樹的尾節點給後續

沿用圖一中的BST作爲已知。圖二展示了當前處理節點2的情況,在函數結束時,將自身作爲前向節點去處理節點3。


                                                                     圖二. getTail()

對於DLL的頭節點(head), 在遞歸函數的調用棧中,最底層調用(即最左邊葉子節點)時,它的prev爲空,因此它就是整個DLL的head。 對於DLL的尾節點(tail),這裏最頂層遞歸函數返回的就是tail,所以這裏等頂層遞歸函數返回後,再將頭節點和尾節點鏈接起來。實現代碼如下:

node* getTail(node* curr, node*& pPrev){
    if(curr==0) return 0;
    node* tmp = getTail(curr->left, pPrev);
    if(tmp==0){
        if(pPrev==0){
            pPrev = curr; //head of sorted DLL
        }else{
            pPrev->right = curr;
            curr->left = pPrev;
        }
    }else{
        tmp->right = curr;
        curr->left = tmp;
    }

    tmp = getTail(curr->right, curr);
    return tmp==0 ? curr : tmp;
}
node* BST2SortedDLL_01(node* root){
    node *head = 0, *tail = 0;
    tail = getTail(root, head);
    if(head==0 || tail==0){
        return 0;
    }
    tail->right = head;
    head->left = tail;
    return head;
}

方法二:來自leetcode網站。依然借鑑了中序遍歷的思維,不過遞歸函數不再返回節點給後續,而是在函數體內部就將自身鏈接成一個閉環的DLL。這樣每次都在尾部新插入一個節點,並將頭節點跟它鏈接起來。

                                                                           圖三. bstToDLL(), 插入節點3,和插入節點4

void bstToDLL(node *p, node*& prev, node*& head){
    if(!p) return;
    bstToDLL(p->left, prev, head);
    p->left = prev; //link p and its predecessor(prev)
    if(prev)
      prev->right = p;
    else
      head = p;

    node *right = p->right; //head stays as the real "head" of DLL, it linked to p in every statement call. as a result, it is linked to
    head->left = p; //real "tail" in final function call
    p->right = head;

    prev = p; //p as the prev of next function call
    bstToDLL(right, prev, head);
}
node* BST2SortedDLL_02(node* root){
    node *prev = 0;
    node *head = 0;
    bstToDLL(root, prev, head);
    return head;
}
bstToDLL()的函數實現中,head作爲整個雙向鏈表的頭節點,在第一次被賦值之後,作爲引用永遠不變的傳遞下去。每次將新插入的節點(即目前的尾節點)作爲下一個新節點的前向傳遞下去。由於沒有返回值,所以記得每次都要將頭節點跟當前新插入的節點鏈接,以形成閉環。


方法三:來自leetcode轉載,出處在此。這個方法的特點在於利用了分治(divide-and-conquer)的思維,而沒有考慮中序遍歷。每次把一個節點的左子樹,自身節點,右子樹都變成一個閉環的雙向鏈表,然後一個一個再鏈接起來,最後形成一個全樹的閉環雙向鏈表。當然,遞歸是必不可少的。

                                                                          圖四. append() 和 join()

下面是完整代碼實現。圖四是我根據代碼畫的示意圖,可以幫助理解有關函數。

void join(node* a, node* b){ //link a to b as predecessor of b
    a->right = b;
    b->left = a;
}
node* append(node* a, node* b){//convert alast->a,blast->b to alast->b, blast->a
    if(a==0) return b;
    if(b==0) return a;
    node *aLast = a->left;
    node *bLast = b->left;
    join(aLast, b);
    join(bLast, a);
    return a;
}
node* BST2SortedDLL_03(node* root){
    if(root==0) return 0;
    node *aList = BST2SortedDLL_03(root->left);
    node *bList = BST2SortedDLL_03(root->right);
    root->left = root; //unlink root to append to left half, and append right half to left half seperately
    root->right = root;
    aList = append(aList, root);
    aList = append(aList, bList);
    return aList;
}

不斷的合併兩個已有的閉環雙向鏈表,需要更多的對於整個問題的大局觀,這個解法的確很酷。


小結

1. 二叉樹相關問題,天生適用遞歸。事實上,樹這個概念,就是用遞歸來定義的。

2. 遞歸方法,實質是將一個許多步的處理問題,按照某種方式分配成很多份,每一份由一次函數調用來實現。那麼我們在設計遞歸函數中,首先需要考慮如何分配這些處理。比如方法一和方法二,每一次遞歸函數,僅僅處理(插入)一個新節點進雙向鏈表;方法三中,將當前的左子樹,右子樹分別放進遞歸函數中處理。

3. 遞歸函數是否需要返回值因題而異。很多時候,返回值有助於簡化遞歸函數內部的處理,如方法一。如果有返回值,記得在最頂層的遞歸函數返回後進行必要處理。如果沒有返回值,記得在遞歸函數內部加以處理,如方法二。

4. 遞歸函數要特別注意邊界情況。最可怕的就是缺乏退出條件從而造成無限循環,那簡直是噩夢。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章