原文: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
參見二叉樹基礎。node
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; }
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); }
不考慮數據內容。結構相贊成味着對應的左子樹和對應的右子樹都結構相同。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; }
對於每一個節點,咱們交換它的左右孩子便可。面試
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); }
最低公共祖先,即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; //都在左子樹或右子樹 }
首先找到兩個節點的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; }
若是給定節點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; //若是左右子樹都沒找到 }
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用的很是巧妙,建議讀者配合上面的圖模擬下算法流程。
前序:[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; }
若設二叉樹的深度爲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; }
在後續遍歷獲得的序列中,最後一個元素爲樹的根結點。從頭開始掃描這個序列,比根結點小的元素都應該位於序列的左半部分;從第一個大於跟結點開始到跟結點前面的一個元素爲止,全部元素都應該大於跟結點,由於這部分元素對應的是樹的右子樹。根據這樣的劃分,把序列劃分爲左右兩部分,咱們遞歸地確認序列的左、右兩部分是否是都是二元查找樹。
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); //左右子樹是否都知足 }
一棵二叉查找樹的中序遍歷序列,正好是升序序列。
若是節點中有指向父親節點的指針(假如根節點的父節點爲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互調便可,很簡單。
二分查找樹的中序遍歷即爲升序排列,問題就在於如何在遍歷的時候更改指針的指向。一種簡單的方法時,遍歷二分查找樹,將遍歷的結果放在一個數組中,以後再把該數組轉化爲雙鏈表。若是題目要求只能使用$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; }
咱們能夠採用自頂向下的方法。先找到中間節點做爲根節點,而後遞歸左右兩部分。全部咱們須要先找到中間節點,對於單鏈表來講,必需要遍歷一邊,可使用快慢指針加快查找速度。
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)$。