二叉樹操做(面試必備)

原文:https://subetter.com/algorith...php

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

1-4

  參見二叉樹基礎node

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 || k < 1)
        return 0;
    if (k == 1)
        return 1;
    return GetKLevel(node->left, k - 1) + GetKLevel(node->right, k - 1);
}

7 判斷兩棵二叉樹是否結構相同

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

bool StructureCmp(Node * node1, Node * node2)
{
    if (node1 == nullptr && node2 == nullptr)
        return true;
    else if (node1 == nullptr || node2 == nullptr)
        return false;
    bool result_left = StructureCmp(node1->left, node2->left);
    bool result_right = StructureCmp(node1->right, node2->right);
    return result_left && result_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份量顯然是不現實的,而線索化正好解決了這個問題,其含義就是利用節點的右孩子空指針,指向該節點在中序序列中的後繼。下面具體來看看如何使用線索化來完成對二叉樹的遍歷。
spa

  先看前序遍歷,步驟以下:
  (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;
  第二步:根據中序可知4 7 2爲根節點1的左子樹和8 5 9 3 6爲根節點1的右子樹;
  第三步:遞歸實現,把4 7 2當作新的一棵樹和8 5 9 3 6也當作新的一棵樹;
  第四步:在遞歸的過程當中輸出後序。

/* 前序遍歷和中序遍歷結果以長度爲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 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(\frac n2)+\frac n2$得,因此上述算法的時間複雜度爲$O(nlogn)$。
  不妨換個思路,採用自底向上的方法:

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

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

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

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

相關文章
相關標籤/搜索