二叉樹的各種操作(面試必備)

https://www.61mon.com/index.php/archives/191/

本篇針對面試中常見的二叉樹操作作個總結:
(1):前序遍歷,中序遍歷,後序遍歷;
(2):層次遍歷;
(3):求樹的節點數;
(4):求樹的葉子數;
(5):求樹的深度;
(6):求二叉樹第 k 層的節點個數;
(7):判斷兩棵二叉樹是否結構相同;
(8):求二叉樹的鏡像;
(9):求兩個節點的最低公共祖先節點;
(10):求任意兩節點距離;
(11):找出二叉樹中某個節點的所有祖先節點;
(12):不使用遞歸和棧遍歷二叉樹;
(13):二叉樹前序中序推後序;
(14):判斷二叉樹是不是完全二叉樹;
(15):判斷是否是二叉查找樹的後序遍歷結果;
(16):給定一個二叉查找樹中的節點,找出在中序遍歷下它的後繼和前驅;
(17):二分查找樹轉化爲排序的循環雙鏈表;
(18):有序鏈表轉化爲平衡的二分查找樹;
(19):判斷是否是二叉查找樹。

1 前序遍歷,中序遍歷,後序遍歷;展開目錄

1.1 前序遍歷展開目錄

對於當前節點,先輸出該節點,然後輸出它的左孩子,最後輸出它的右孩子。以上圖爲例,遞歸的過程如下:
(1):輸出 1,接着左孩子;
(2):輸出 2,接着左孩子;
(3):輸出 4,左孩子爲空,再接着右孩子;
(4):輸出 6,左孩子爲空,再接着右孩子;
(5):輸出 7,左右孩子都爲空,此時 2 的左子樹全部輸出,2 的右子樹爲空,此時 1 的左子樹全部輸出,接着 1 的右子樹;
(6):輸出 3,接着左孩子;
(7):輸出 5,左右孩子爲空,此時 3 的左子樹全部輸出,3 的右子樹爲空,至此 1 的右子樹全部輸出,結束。

而非遞歸版本只是利用 stack 模擬上述過程而已,遞歸的過程也就是出入棧的過程。注意加粗部分的理解。

/* 前序遍歷遞歸版 */
void PreOrderRec(Node * node)
{
    if (node == nullptr)
        return;
    cout << node->data << " ";   //先輸出當前節點   
    PreOrderRec(node->left);     //然後輸出左孩子
    PreOrderRec(node->right);    //最後輸出右孩子
}

/* 前序遍歷非遞歸版 */
void PreOrderNonRec(Node * node)
{
    stack<Node*> S;
    Node * p = node;
    while (p || !S.empty())
    {
        while (p)
        {                            
            cout << p->data << " ";  //先輸出當前節點  
            S.push(p);
            p = p->left;  //然後輸出左孩子
        }  //while結束意味着左孩子已經全部輸出
        if (!S.empty())
        {
            p = S.top()->right;  //最後輸出右孩子
            S.pop();
        }
    }
}

1.2 中序遍歷展開目錄

對於當前節點,先輸出它的左孩子,然後輸出該節點,最後輸出它的右孩子。以(1.1)圖爲例:
(1):1-->2-->4,4 的左孩子爲空,輸出 4,接着右孩子;
(2):6 的左孩子爲空,輸出 6,接着右孩子;
(3):7 的左孩子爲空,輸出 7,右孩子也爲空,此時 2 的左子樹全部輸出,輸出 2,2 的右孩子爲空,此時 1 的左子樹全部輸出,
     輸出 1,接着 1 的右孩子;
(4):3-->5,5 左孩子爲空,輸出 5,右孩子也爲空,此時 3 的左子樹全部輸出,而 3 的右孩子爲空,至此 1 的右子樹全部輸出,
     結束。

/* 中序遍歷遞歸版 */
void InOrderRec(Node * node)
{
    if (node == nullptr)
        return;
    InOrderRec(node->left);     //先輸出左孩子
    cout << node->data << " ";  //然後輸出當前節點
    InOrderRec(node->right);    //最後輸出右孩子
}

/* 前序遍歷非遞歸版 */
void InOrderNonRec(Node * node)
{
    stack<Node*> S;
    Node * p = node;
    while (p || !S.empty())
    {
        while (p)
        {
            S.push(p);   
            p = p->left;  
        }  //while結束意味着左孩子爲空
        if (!S.empty())
        {
            cout << S.top()->data << " ";  //左孩子已經全部輸出,接着輸出當前節點
            p = S.top()->right;            //左孩子全部輸出,當前節點也輸出後,最後輸出右孩子
            S.pop();
        }
    }
}

1.3 後續遍歷展開目錄

對於當前節點,先輸出它的左孩子,然後輸出它的右孩子,最後輸出該節點。依舊以(1.1)圖爲例:
(1):1->2->4->6->7,7 無左孩子,也無右孩子,輸出 7,此時 6 無左孩子,而 6 的右子樹也全部輸出,輸出 6,此時 4 無左子
     樹,而 4 的右子樹全部輸出,輸出 4,此時 2 的左子樹全部輸出,且 2 無右子樹,輸出 2,此時 1 的左子樹全部輸出,接着轉
     向右子樹;
(2):3->5,5 無左孩子,也無右孩子,輸出 5,此時 3 的左子樹全部輸出,且 3 無右孩子,輸出 3,此時 1 的右子樹全部輸出,輸
     出 1,結束。

非遞歸版本中,對於一個節點,如果我們要輸出它,只有它既沒有左孩子也沒有右孩子或者它有孩子但是它的孩子已經被輸出(由此設置 pre 變量)。若非上述兩種情況,則將該節點的右孩子和左孩子依次入棧,這樣就保證了每次取棧頂元素的時候, 先依次遍歷左子樹和右子樹。

/* 後序遍歷遞歸版 */
void PostOrderRec(Node * node)
{
    if (node == nullptr)
        return;
    PostOrderRec(node->left);   //先輸出左孩子
    PostOrderRec(node->right);  //然後輸出右孩子
    cout << node->data << " ";  //最後輸出當前節點
}

/* 後序遍歷非遞歸版 */
void PostOrderNonRec(Node * node)
{
    Node * pre = nullptr;
    Node * cur = node;
    stack<Node*> S;
    S.push(cur);
    while (!S.empty())
    {
        cur = S.top();
        if ((!cur->left && !cur->right) ||                     //第一個輸出的必是無左右孩子的葉子節點,只要第一個節點輸出,
            (pre && (pre == cur->left || pre == cur->right)))  //以後的pre就不會是空。此處的判斷語句加入一個pre,只是用來
        {                                                      //確保可以正確輸出第一個節點。
            cout << cur->data << " ";  //左右孩子都全部輸出,再輸出當前節點
            pre = cur;
            S.pop();
        }
        else
        {
            if (cur->right)
                S.push(cur->right);  //先進右孩子,再進左孩子,取出來的纔是左孩子
            if (cur->left)
                S.push(cur->left);
        }
    }
}

2 層次遍歷展開目錄

void LevelOrder(Node * node)
{
    Node * p = node;
    queue<Node*> Q;  //隊列
    Q.push(p);
    while (!Q.empty())
    {
        p = Q.front();
        cout << p->data << " ";
        Q.pop();
        if (p->left)
            Q.push(p->left);  //注意順序,先進左孩子
        if (p->right)
            Q.push(p->right);
    }
}

3 求樹的節點數展開目錄

int CountNodes(Node * node)
{
    if (node == nullptr)
        return 0;
    return CountNodes(node->left) + CountNodes(node->right) + 1;
}

4 求樹的葉子數展開目錄

int CountLeaves(Node * node)
{
    if (node == nullptr)
        return 0;
    if (!node->left && !node->right)
        return 1;
    return CountLeaves(node->left) + CountLeaves(node->right);
}

5 求樹的深度展開目錄

int GetDepth(Node * node)
{
    if (node == nullptr)
        return 0;
    int left_depth = GetDepth(node->left) + 1;
    int right_depth = GetDepth(node->right) + 1;
    return left_depth > right_depth ? left_depth : right_depth;
}

6 求二叉樹第 k 層的節點個數展開目錄

int GetKLevel(Node * node, int k)
{
    if (node == nullptr)
        return 0;
    if (k == 1)
        return 1;
    return GetKLevel(node->left, k - 1) + GetKLevel(node->right, k - 1);
}

7 判斷兩棵二叉樹是否結構相同展開目錄

不考慮數據內容。結構相同意味着對應的左子樹和對應的右子樹都結構相同。

bool StructureCmp(Node * node1, Node * node2)
{
    if (node1 == nullptr && node2 == nullptr)
        return true;
    else if (node1 == nullptr || node2 == nullptr)
        return false;
    return StructureCmp(node1->left, node2->left) && Str1uctureCmp(node1->right, node2->right);
}

8 求二叉樹的鏡像展開目錄

對於每個節點,我們交換它的左右孩子即可。

void Mirror(Node * node)
{
    if (node == nullptr)
        return;
    Node * temp = node->left;
    node->left = node->right;
    node->right = temp;
    Mirror(node->left);
    Mirror(node->right);
}

9 求兩個節點的最低公共祖先節點展開目錄

最低公共祖先,即 LCA(Lowest Common Ancestor),見下圖:

結點 3 和結點 4 的最近公共祖先是結點 2,即 LCA(3,4)=2。在此,需要注意到當兩個結點在同一棵子樹上的情況,如結點 3 和結點 2 的最近公共祖先爲 2,即 LCA(3,2)=2。同理 LCA(5,6)=4,LCA(6,10)=1。

Node * FindLCA(Node * node, Node * target1, Node * target2)
{
    if (node == nullptr)
        return nullptr;
    if (node == target1 || node == target2)
        return node;
    Node * left = FindLCA(node->left, target1, target2);
    Node * right = FindLCA(node->right, target1, target2);
    if (left && right)  //分別在左右子樹
        return node;
    return left ? left : right;  //都在左子樹或右子樹
}

10 求任意兩節點距離展開目錄

首先找到兩個節點的 LCA,然後分別計算 LCA 與它們的距離,最後相加即可。

int FindLevel(Node * node, Node * target)
{
    if (node == nullptr)
        return -1;
    if (node == target)
        return 0;
    int level = FindLevel(node->left, target);  //先在左子樹找
    if (level == -1)
        level = FindLevel(node->right, target);  //如果左子樹沒找到,在右子樹找
    if (level != -1)  //找到了,回溯
        return level + 1;
    return -1;  //如果左右子樹都沒找到
}

int DistanceNodes(Node * node, Node * target1, Node * target2)
{
    Node * lca = FindLCA(node, target1, target2);  //找到最低公共祖先節點
    int level1 = FindLevel(lca, target1); 
    int level2 = FindLevel(lca, target2);
    return level1 + level2;
}

11 找出二叉樹中某個節點的所有祖先節點展開目錄

如果給定節點 5,則其所有祖先節點爲 4,2,1。

bool FindAllAncestors(Node * node, Node * target)
{
    if (node == nullptr)
        return false;
    if (node == target)
        return true;
    if (FindAllAncestors(node->left, target) || FindAllAncestors(node->right, target))  //找到了
    {
        cout << node->data << " ";
        return true;  //回溯
    }
    return false;  //如果左右子樹都沒找到
}

12 不使用遞歸和棧遍歷二叉樹展開目錄

1968 年,高德納(Donald Knuth)提出一個問題:是否存在一個算法,它不使用棧也不破壞二叉樹結構,但是可以完成對二叉樹的遍歷?隨後 1979 年,James H. Morris 提出了二叉樹線索化,解決了這個問題。(根據這個概念我們又提出了一個新的數據結構,即線索二叉樹,因線索二叉樹不是本文要介紹的內容,所以有興趣的朋友請移步線索二叉樹。)

前序,中序,後序遍歷,不管是遞歸版本還是非遞歸版本,都用到了一個數據結構 -- 棧,爲何要用棧?那是因爲其它的方式沒法記錄當前節點的 parent, 而如果在每個節點的結構裏面加個 parent 分量顯然是不現實的,而線索化正好解決了這個問題,其含義就是利用節點的右孩子空指針,指向該節點在中序序列中的後繼。下面具體來看看如何使用線索化來完成對二叉樹的遍歷。

先看前序遍歷,步驟如下:
(1):如果當前節點的左孩子爲空,則輸出當前節點並將其右孩子作爲當前節點;
(2):如果當前節點的左孩子不爲空,在當前節點的左子樹中找到當前節點在中序遍歷下的前驅節點;
  (2.1):如果前驅節點的右孩子爲空,將它的右孩子設置爲當前節點,輸出當前節點並把當前節點更新爲當前節點的左孩子;
  (2.2):如果前驅節點的右孩子爲當前節點,將它的右孩子重新設爲空,當前節點更新爲當前節點的右孩子;
(3):重複以上(1)和(2),直到當前節點爲空。

/* 前序遍歷 */
void PreOrderMorris(Node * root)
{
    Node * cur = root;
    Node * pre = nullptr;
    while (cur)
    {
        if (cur->left == nullptr)  //(1)
        {
            cout << cur->data << " ";
            cur = cur->right;
        }
        else
        {
            pre = cur->left;
            while (pre->right && pre->right != cur)  //(2),找到cur的前驅節點
                pre = pre->right;

            if (pre->right == nullptr)  //(2.1),cur未被訪問,將cur節點作爲其前驅節點的右孩子
            {
                cout << cur->data << " ";
                pre->right = cur;
                cur = cur->left;
            }
            else  //(2.2),cur已被訪問,恢復樹的原有結構,更改right指針 
            {
                pre->right = nullptr;
                cur = cur->right;
            }
        }
    }
}

再來看中序遍歷,和前序遍歷相比只改動一句代碼,步驟如下:
(1):如果當前節點的左孩子爲空,則輸出當前節點並將其右孩子作爲當前節點;
(2):如果當前節點的左孩子不爲空,在當前節點的左子樹中找到當前節點在中序遍歷下的前驅節點;
  (2.1):如果前驅節點的右孩子爲空,將它的右孩子設置爲當前節點,當前節點更新爲當前節點的左孩子;
  (2.2):如果前驅節點的右孩子爲當前節點,將它的右孩子重新設爲空,輸出當前節點,當前節點更新爲當前節點的右孩子;
(3):重複以上(1)和(2),直到當前節點爲空。

/* 中序遍歷 */
void InOrderMorris(Node * root)
{
    Node * cur = root;
    Node * pre = nullptr;
    while (cur)
    {
        if (cur->left == nullptr)  //(1)
        {
            cout << cur->data << " ";
            cur = cur->right;
        }
        else
        {
            pre = cur->left;
            while (pre->right && pre->right != cur)  //(2),找到cur的前驅節點
                pre = pre->right;

            if (pre->right == nullptr)  //(2.1),cur未被訪問,將cur節點作爲其前驅節點的右孩子
            {
                pre->right = cur;
                cur = cur->left;
            }
            else  //(2.2),cur已被訪問,恢復樹的原有結構,更改right指針 
            {
                cout << cur->data << " ";
                pre->right = nullptr;
                cur = cur->right;
            }
        }
    }
}

最後看下後序遍歷,後序遍歷有點複雜,需要建立一個虛假根節點 dummy,令其左孩子是 root。並且還需要一個子過程,就是倒序輸出某兩個節點之間路徑上的各個節點。

步驟如下:
(1):如果當前節點的左孩子爲空,則將其右孩子作爲當前節點;
(2):如果當前節點的左孩子不爲空,在當前節點的左子樹中找到當前節點在中序遍歷下的前驅節點;
  (2.1):如果前驅節點的右孩子爲空,將它的右孩子設置爲當前節點,當前節點更新爲當前節點的左孩子;
  (2.2):如果前驅節點的右孩子爲當前節點,將它的右孩子重新設爲空,倒序輸出從當前節點的左孩子到該前驅節點這條路徑上
       的所有節點,當前節點更新爲當前節點的右孩子;
(3):重複以上(1)和(2),直到當前節點爲空。

struct Node
{
    int data;
    Node * left;
    Node * right;
    Node(int  data_, Node * left_, Node * right_)
    {
        data = data_;
        left = left_;
        right = right_;
    }
};

void ReversePrint(Node * from, Node * to)
{
    if (from == to)
    {
        cout << from->data << " ";
        return;
    }
    ReversePrint(from->right, to);
    cout << from->data << " ";
}

void  PostOrderMorris(Node * root)
{
    Node * dummy = new Node(-1, root, nullptr);  //一個虛假根節點
    Node * cur = dummy;
    Node * pre = nullptr;
    while (cur)
    {
        if (cur->left == nullptr)  //(1)
            cur = cur->right;
        else
        {
            pre = cur->left;
            while (pre->right && pre->right != cur)  //(2),找到cur的前驅節點
                pre = pre->right;

            if (pre->right == nullptr)  //(2.1),cur未被訪問,將cur節點作爲其前驅節點的右孩子
            {
                pre->right = cur;
                cur = cur->left;
            }
            else  //(2.2),cur已被訪問,恢復樹的原有結構,更改right指針
            {
                pre->right = nullptr;
                ReversePrint(cur->left, pre);
                cur = cur->right;
            }
        }
    }
}

dummy 用的非常巧妙,建議讀者配合上面的圖模擬下算法流程。

13 二叉樹前序中序推後序展開目錄

方式 序列
前序 [1 2 4 7 3 5 8 9 6]
中序 [4 7 2 1 8 5 9 3 6]
後序 [7 4 2 8 9 5 6 3 1]

以上面圖表爲例,步驟如下:
(1):根據前序可知根節點爲 1;
(2):根據中序可知 4 7 2 爲根節點 1 的左子樹和 8 5 9 3 6 爲根節點 1 的右子樹;
(3):遞歸實現,把 4 7 2 當做新的一棵樹和 8 5 9 3 6 也當做新的一棵樹;
(4):在遞歸的過程中輸出後序。

/* 前序遍歷和中序遍歷結果以長度爲n的數組存儲,pos1爲前序數組下標,pos2爲後序下標 */
int pre_order_arry[n];
int in_order_arry[n];
void PrintPostOrder(int pos1, int pos2, int n)
{
    if (n == 1)
    {
        cout << pre_order_arry[pos1];
        return;
    }
    if (n == 0)
        return;
    int i = 0;
    for (; pre_order_arry[pos1] != in_order_arry[pos2 + i]; i++)
        ;
    PrintPostOrder(pos1 + 1, pos2, i);
    PrintPostOrder(pos1 + i + 1, pos2 + i + 1, n - i - 1);
    cout << pre_order_arry[pos1];
}

當然我們也可以根據前序和中序構造出二叉樹,進而求出後序。

/* 該函數返回二叉樹的根節點 */
Node * Create(int pos1, int pos2, int n)
{
    Node * p = nullptr;
    for (int i = 0; i < n; i++)
    {
        if (pre_order_arry[pos1] == in_order_arry[pos2])
        {
            p = new Node(pre_order_arry[pos1]);
            p->left = Create(pos1 + 1, pos2, i);
            p->right = Create(pos1 + i + 1, pos2 + i + 1, n - i - 1);
            return p;
        }
    }
    return p;
}

14 判斷二叉樹是不是完全二叉樹展開目錄

若設二叉樹的深度爲 h,除第 h 層外,其它各層 (1~h-1) 的結點數都達到最大個數,第 h 層所有的結點都連續集中在最左邊,這就是完全二叉樹(Complete Binary Tree)。如下圖:

首先若一個節點只有右孩子,肯定不是完全二叉樹;其次若只有左孩子或沒有孩子,那麼對於一個高度爲 h 的完全二叉樹,當前節點的高度肯定是 h-1,也就是高度 h 的所有節點都沒有孩子,否則不是完全二叉樹,因此設置 flag 標記當前節點是不是到了 h-1 高度。

bool IsCBT(Node * node)
{
    bool flag = false;
    queue<Node*> Q;
    Q.push(node);
    while (!Q.empty())
    {
        Node * p = Q.front();
        Q.pop();
        if (flag)
        {
            if (p->left || p->right)
                return false;
        }
        else
        {
            if (p->left && p->right)
            {
                Q.push(p->left);
                Q.push(p->right);
            }
            else if (p->right)  //只有右節點
                return false;
            else if (p->left)  //只有左節點
            {
                Q.push(p->left);
                flag = true;
            }
            else  //沒有節點
                flag = true;
        }
    }
    return true;
}

15 判斷是否是二叉查找樹的後序遍歷結果展開目錄

在後續遍歷得到的序列中,最後一個元素爲樹的根結點。從頭開始掃描這個序列,比根結點小的元素都應該位於序列的左半部分;從第一個大於跟結點開始到跟結點前面的一個元素爲止,所有元素都應該大於跟結點,因爲這部分元素對應的是樹的右子樹。根據這樣的劃分,把序列劃分爲左右兩部分,我們遞歸地確認序列的左、右兩部分是不是都是二元查找樹。

int array[n];  //長度爲n的序列,注意begin和end遵循的是左閉右閉原則
bool IsSequenceOfBST(int begin, int end)
{
    if (end - begin <= 0)
        return true;
    int root_data = array[end];  //數組尾元素爲根節點
    int i = begin;
    for (; array[i] < root_data; i++);  //取得左子樹
    int j = i;
    for (; j < end; j++)
        if (array[j] < root_data)  //此時右子樹應該都大於根節點;若存在小於,直接return false
            return false;
    return IsSequenceOfBST(begin, i - 1) && IsSequenceOfBST(i, end - 1);  //左右子樹是否都滿足
}

16 給定一個二叉查找樹中的節點,找出在中序遍歷下它的後繼和前驅展開目錄

一棵二叉查找樹的中序遍歷序列,正好是升序序列。
如果節點中有指向父親節點的指針(假如根節點的父節點爲 nullptr),則:
(1):如果當前節點有右孩子,則後繼節點爲這個右孩子的最左孩子;
(2):如果當前節點沒有右孩子;
  (2.1):當前節點爲根節點,返回 nullptr;
  (2.2):當前節點只是個普通節點,也就是存在父節點;
    (2.2.1):當前節點是父親節點的左孩子,則父親節點就是後繼節點;
    (2.2.2):當前節點是父親節點的右孩子,沿着父親節點往上走,直到 n-1 代祖先是 n 代祖先的左孩子,則後繼爲 n 代祖
          先或遍歷到根節點也沒找到符合的,則當前節點就是中序遍歷的最後一個節點,返回 nullptr。

/* 求後繼節點 */
Node * Increment(Node * node)
{
    if (node->right)  //(1)
    {
        node = node->right;
        while (node->left)
            node = node->left;
        return node;
    }
    else  //(2)
    {
        if (node->parent == nullptr)  //(2.1)
            return nullptr;
        Node * p = node->parent;  //(2.2)
        if (p->left == node)  //(2.2.1)
            return p;
        else  //(2.2.2)
        {
            while (p->right == node)
            {
                node = p;
                p = p->parent;
                if (p == nullptr)
                    return nullptr;
            }
            return p;
        }
    }
}

仔細觀察上述代碼,總覺得有點囉嗦。比如,過多的 return,(2)的層次太多。綜合考慮所有情況,改進代碼如下:

Node * Increment(Node * node)
{
    if (node->right)
    {
        node = node->right;
        while (node->left)
            node = node->left;
    }
    else
    {
        Node * p = node->parent;
        while (p && p->right == node)
        {
            node = p;
            p = p->parent;
        }
        node = p;
    }
    return node;
}

上述的代碼是基於節點有 parent 指針的,若題意要求沒有 parent 呢?網上也有人給出了答案,個人覺得沒有什麼價值,有興趣的朋友可以到這裏查看。

而求前驅節點的話,只需把上述代碼的 left 與 right 互調即可,很簡單。

17 二分查找樹轉化爲排序的循環雙鏈表展開目錄

二分查找樹的中序遍歷即爲升序排列,問題就在於如何在遍歷的時候更改指針的指向。一種簡單的方法時,遍歷二分查找樹,將遍歷的結果放在一個數組中,之後再把該數組轉化爲雙鏈表。如果題目要求只能使用 O(1) 內存,則只能在遍歷的同時構建雙鏈表,即進行指針的替換。

我們需要用遞歸的方法來解決,假定每個遞歸調用都會返回構建好的雙鏈表,可把問題分解爲左右兩個子樹。由於左右子樹都已經是有序的,當前節點作爲中間的一個節點,把左右子樹得到的鏈表連接起來即可。

/* 合併兩個a,b兩個循環雙向鏈表 */
Node * Append(Node * a, Node * b)
{
    if (a == nullptr) return b;
    if (b == nullptr) return a;

    //分別得到兩個鏈表的最後一個元素
    Node * a_last = a->left;
    Node * b_last = b->left;

    //將兩個鏈表頭尾相連  
    a_last->right = b;
    b->left = a_last;

    a->left = b_last;
    b_last->right = a;

    return a;
}

/* 遞歸的解決二叉樹轉換爲雙鏈表 */
Node * TreeToList(Node * node)
{
    if (node == nullptr) return nullptr;

    //遞歸解決子樹
    Node * left_list = TreeToList(node->left);
    Node * right_list = TreeToList(node->right);

    //把根節點轉換爲一個節點的雙鏈表。方便後面的鏈表合併  
    node->left = node;
    node->right = node;

    //合併之後即爲升序排列
    left_list = Append(left_list, node);
    left_list = Append(left_list, right_list);

    return left_list;
}

18 有序鏈表轉化爲平衡的二分查找樹(Binary Search Tree)展開目錄

我們可以採用自頂向下的方法。先找到中間節點作爲根節點,然後遞歸左右兩部分。所以我們需要先找到中間節點,對於單鏈表來說,必須要遍歷一邊,可以使用快慢指針加快查找速度。

struct TreeNode
{
    int data;
    TreeNode * left;
    TreeNode * right;
    TreeNode(int data_) { data = data_; left = right = nullptr; }
};

struct ListNode
{
    int data;
    ListNode * next;
    ListNode(int data_) { data = data_; next = nullptr; }
};

TreeNode * SortedListToBST(ListNode *  list_node)
{
    if (!list_node) return nullptr;
    if (!list_node->next) return (new TreeNode(list_node->data));

    //用快慢指針找到中間節點  
    ListNode * pre_slow = nullptr;  //記錄慢指針的前一個節點
    ListNode * slow = list_node;    //慢指針
    ListNode * fast = list_node;    //快指針
    while (fast && fast->next)
    {
        pre_slow = slow;
        slow = slow->next;
        fast = fast->next->next;
    }
    TreeNode * mid = new TreeNode(slow->data);

    //分別遞歸左右兩部分  
    if (pre_slow)
    {
        pre_slow->next = nullptr;
        mid->left = SortedListToBST(list_node);
    }
    mid->right = SortedListToBST(slow->next);

    return mid;
}

由 f(n)=2f(n2)+n2 得,所以上述算法的時間複雜度爲 O(nlogn)
不妨換個思路,採用自底向上的方法:

TreeNode * SortedListToBSTRec(ListNode *& list, int start, int end)
{
    if (start > end) return nullptr;

    int mid = start + (end - start) / 2;
    TreeNode * left_child = SortedListToBSTRec(list, start, mid - 1);  //注意此處傳入的是引用
    TreeNode * parent = new TreeNode(list->data);
    parent->left = left_child;
    list = list->next;
    parent->right = SortedListToBSTRec(list, mid + 1, end);
    return parent;
}

TreeNode * SortedListToBST(ListNode * node)
{
    int n = 0;
    ListNode * p = node;
    while (p)
    {
        n++;
        p = p->next;
    }
    return SortedListToBSTRec(node, 0, n - 1);
}

如此,時間複雜度降爲 O(n)

19 判斷是否是二叉查找樹展開目錄

我們假定二叉樹沒有重複元素,即對於每個節點,其左右孩子都是嚴格的小於和大於。

下面給出兩個方法:

方法 1:

bool IsBST(Node * node, int min, int max)
{
    if (node == nullptr)
        return true;
    if (node->data <= min || node->data >= max)
        return false;
    return IsBST(node->left, min, node->data) && IsBST(node->right, node->data, max);
}

IsBST(node, INT_MIN, INT_MAX);

方法 2:

利用二叉查找樹中序遍歷時元素遞增來判斷。

bool IsBST(Node * node)
{
    static int pre = INT_MIN;

    if (node == nullptr)
        return true;
    if (!IsBST(node->left))
        return false;
    if (node->data < pre)
        return false;
    pre = node->data;
    return IsBST(node->right);
}
發佈了84 篇原創文章 · 獲贊 145 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章