樹
樹的題目基本都是二叉樹,但是面試官還沒有說是不是二叉樹的時候千萬不要先把答案說出來,要是面試官說是多叉樹,而你做的是二叉樹就直接掛了!
一. 樹的三種遍歷。前序、中序、后序,如果直接考遍歷,就肯定是讓你寫非遞歸代碼的(遞歸版太弱智了),具體寫法,要不你記下來,要不參考“遞歸”部分的,怎么遞歸轉非遞歸,另一個就是給個中序+前序(后序),讓你還原二叉樹,中序必須給,不然還原不了(解不唯一),一般遞歸解決;
二. BST(Binary Search Tree)。這個考法多一點,怎么判斷是不是BST(或者平衡樹,或者完全樹),有序數組(有序鏈表)轉換成BST,在BST中找某個upper_bound的最大值(這個可以給root讓你找符合要求的節點,可以給一個等於upper_bound的節點,有parent指針,讓你找),然后還 有其他其他
三. LCA(Least Common Ancestor,最近公共祖先)。超高頻題,主要考法是給兩個指針和樹的root,找LCA,如果節點有parent節點(這時候就不給root了),就相當於鏈表找第一個交點了,如果沒有parent就要麻煩一點;
四. 序列化與發序列化。這個考法比較簡單,就是寫一個序列化和發序列化的方法,有思考過的話直接就可以秒了,一樣的問題還有字符串數組的序列化。一般思路是加一個記錄分段信息的head或者加一個不會出現的字符作為一種分割。有時候會說任何字符都可能出現,這時候可以用轉義字符(想想C的字符串怎么記錄\的吧)。
樹的遍歷:
遍歷分為三種:前序中序后序。在三種遍歷中,前序和中序遍歷的非遞歸算法都很容易實現,非遞歸后序遍歷實現起來相對來說要難一點。
前序遍歷:按照“根結點-左孩子-右孩子”的順序進行訪問。
1 #include<iostream> 2 using namespace std; 3 4 void presearch1(BinTree *root){//遞歸前序 5 if(root!=NULL){ 6 cout<<root->data<<" "<<endl; 7 presearch1(root->leftchild); 8 presearch1(root->rightchild); 9 } 10 } 11 void presearch2(BinTree *root){//非遞歸前序:相當於直接到最左邊最底部的地方,先是遍歷根節點,然后最左邊然后右邊,然后在向上遍歷。 12 /*根據前序遍歷訪問的順序,優先訪問根結點,然后再分別訪問左孩子和右孩子。即對於任一結點,其可看做是根結點,因此可以直接訪問,訪問完之后,若其左孩子不為空,按相同規則訪問它的左子樹;當訪問其左子樹時,再訪問它的右子樹。因此其處理過程如下: 13 對於任一結點P: 14 1)訪問結點P,並將結點P入棧; 15 2)判斷結點P的左孩子是否為空,若為空,則取棧頂結點並進行出棧操作,並將棧頂結點的右孩子置為當前的結點P,循環至1);若不為空,則將P的左孩子置為當前的結點P; 16 3)直到P為NULL並且棧為空,則遍歷結束。 17 */ 18 stack<BinTree *>s;//創建一個棧用來存儲樹的節點 19 BinTree *p=root; 20 while(p!=NULL || !s.empty()){//樹不為空或者棧不為空 21 while(p!=NULL){//當根節點不為空,將更節點入棧 22 cout<<p->data<<" ,"; 23 s.push(root); 24 p=p->leftchild;//一直到最左邊的孩子,跳出循環 25 } 26 if(!s.empty()){//看棧是否為空,不為空將當前的節點P出棧 27 p=s.top();//將P置為更節點 28 s.pop(); 29 p=p->rightchild;//右移 30 } 31 } 32 }
中序遍歷:按照“左孩子-根結點-右孩子”的順序進行訪問。
1 #include<iostream> 2 using namespace std; 3 4 void insearch1(BinTree *root){//非遞歸中序遍歷 5 /*2.非遞歸實現 6 根據中序遍歷的順序,對於任一結點,優先訪問其左孩子,而左孩子結點又可以看做一根結點,然后繼續訪問其左孩子結點,直到遇到左孩子結點為空的結點才進行訪問,然后按相同的規則訪問其右子樹。因此其處理過程如下: 7 對於任一結點P, 8 1)若其左孩子不為空,則將P入棧並將P的左孩子置為當前的P,然后對當前結點P再進行相同的處理; 9 2)若其左孩子為空,則取棧頂元素並進行出棧操作,訪問該棧頂結點,然后將當前的P置為棧頂結點的右孩子; 10 3)直到P為NULL並且棧為空則遍歷結束 11 */ 12 stack <BinTree *>s; 13 BinTree *p=root; 14 while(p!=NULL || !s.empty()){ 15 while(p!=NULL){ 16 s.push(p); 17 p=p->leftchild; 18 } 19 if(!s.empty()){ 20 p=s.top(); 21 cout<<p->data<<" ,"<<endl; 22 s.pop(); 23 p=p->rightchild; 24 } 25 } 26 }
后序遍歷:按照“左孩子-右孩子-根結點”的順序進行訪問。
1 #include<iostream> 2 using namespace std; 3 4 void postsearch1(BinTree *root){//非遞歸后序遍歷 5 /*要保證根結點在左孩子和右孩子訪問之后才能訪問,因此對於任一結點P,先將其入棧。如果P不存在左孩子和右孩子,則可以直接訪問它; 6 或者P存在左孩子或者右孩子,但是其左孩子和右孩子都已被訪問過了,則同樣可以直接訪問該結點。 7 若非上述兩種情況,則將P的右孩子和左孩子依次入棧,這樣就保證了每次取棧頂元素的時候,左孩子在右孩子前面被訪問,左孩子和右孩子都在根結點前面被訪問。 8 */ 9 stack <BinTree *>s; 10 BinTree *cur;//當前節點 11 BinTree *pre=NULL;//前一次訪問的節點 12 s.push(root); 13 while(!s.empty()){ 14 cur=s.top(); 15 if((cur->lchild==NULL &&cur->rchild==NULL)||(pre!=NULL&& 16 (pre==cur->lchild||pre==cur->rchild))){ 17 //P不存在左孩子右孩子,或者左孩子或者右孩子已經被訪問了 18 cout<<cur->data<<" ,"<<endl; 19 s.pop(); 20 pre=cur; 21 } 22 else{//否則的話就講右孩子左孩子依次入站,這樣就可以先訪問左孩子在訪問右孩子了 23 if(cur->rchild!=NULL){ 24 s.push(cur->rchild); 25 } 26 if(cur->lchild!=NULL){ 27 s.push(cur->lchild); 28 } 29 } 30 } 31 }
二. BST(Binary Search Tree)。
前面提到的BST題目感覺寫起來都不算特別麻煩,大概說說其中一個高頻題:有序鏈表轉BST。一種做法是,遍歷鏈表,找到中點,中點作為root,再遞歸處理左右兩邊,這樣的話時空復雜度是O(nlogn)+O(1),另一種方法是把鏈表所有指針存到vector中,這就轉化成了有序數組轉BST的問題,有了隨機下標訪問,就可以O(1)時間找到中點了,然后還是遞歸處理左右部分,時空復雜度O(n)+O(n)。
題目一:二叉搜索樹轉化為有序雙鏈表
實際上就是對二叉查找樹進行中序遍歷。可以用兩個變量分別保存剛訪問的結點、新鏈表的頭結點,訪問某一個結點A時,設置該節點時left成員指向剛訪問過的結點B,再設置結點B的right成員指向結點A。經過這樣處理,得到的新雙鏈表,除了頭結點的left成員、尾結點的right成員沒有設置外,其它的結點成員都被正確設置。而中序遍歷的特點決定了第一個訪問的數據節點的left成員一定為空指針,最后一個訪問的數據節點的right成員也一定為空指針。因而不需要再對這兩個成員進行額外的設置操作。
時間和空間復雜度都是:O(N)
1 static void tree2list_inorder(Node* root, Node*& prev, Node*& list_head) 2 { 3 /*①:當找到最左節點時,root設為mostleft,prev為NULL,所以當前節點的left指針不用設置,prev的右指針指向當前節點,表示下一個訪問的節點是當前節點 4 ②:依次遞歸,訪問prev右指針指向的那個節點,和上述步驟一樣:當前節點的left指向上一個訪問的節點,上一個訪問的節點的右指針指向當前節點, 5 當前節點被訪問,如果當前節點存在右子樹,再依次進行右子樹的遞歸操作。 6 ③:這里面已經將樹轉換為鏈表,鏈表的頭結點就是樹的leftmost節點,指針也是全部轉換完成,left和right指針都全部指向正確的值。 7 ④:這個算法其實就是把頭結點設置好,然后修改leftright指針的問題 */ 8 if (root->left) //當左子樹不為空,一直遞歸,直到找到最左節點 9 tree2list_inorder(root->left, prev, list_head); 10 root->left = prev;//當前節點的左指針指向上一個節點 11 if (prev)//當上一個訪問節點不為空,上一個節點的右指針指向當前節點,這里就形成了一個鏈表結構 12 prev->right = root; 13 prev = root;//訪問完當前節點,遞歸下一個節點 14 if (list_head == NULL) 15 list_head = root; 16 if (root->right) 17 tree2list_inorder(root->right, prev, list_head); 18 } 19 Node* tree2list(Node* root) 20 { 21 Node* list_head = NULL; 22 Node* prev = NULL; 23 if (root) 24 tree2list_inorder(root, prev, list_head); 25 return list_head; 26 }
題目二:判斷二叉樹是否平衡二叉樹
根據平衡二叉樹的定義:每個結點的左右子樹的高度差小等於1,只須在計算二叉樹高度時,同時判斷左右子樹的高度差即可。
先定義函數計算每個節點的深度,再調用函數去判斷該樹的每個左右節點的差去判斷該樹是否是平衡二叉樹。
遞歸遍歷一個節點多次的算法
1 #include<iostream> 2 using namespace std; 3 4 template 5 static int Depth(BSTreeNode* root){//這個函數是計算樹的深度 6 //遞歸的計算每個節點的深度,方便判斷 7 if(root==NULL) 8 return 0; 9 else{ 10 int leftdepth=Depth(root->left); 11 int rightdepth=Depth(root->right); 12 return 1+(leftdepth>rightdepth?leftdepth:rightdepth); 13 } 14 } 15 //下面是利用遞歸判斷左右子樹的深度是否相差1來判斷是否是平衡二叉樹的函數: 16 template 17 static bool isBalance(BSTreeNode* root){ 18 if(root==NULL) 19 return true; 20 int dis=Depth(root->left)-Depth(root->right); 21 //當該節點的左右子樹深度等於1或者0時繼續遞歸的計算該節點的左右子樹的平衡性,知道遍歷完該樹的每個節點為止 22 23 if(dis>1 ||<-1) 24 return false; 25 else 26 //同時遞歸的遍歷左右子樹 27 return isBalance(root->left)&&isBalance(root->right); 28 }
遍歷一個節點一次的算法
1 #include<iostream> 2 using namespace std; 3 /* 下面是每個節點只遍歷一次的解法: 4 如果我們使用后序遍歷的方法,這樣每當遍歷一個節點的時候,我們就已經 5 遍歷了它的左右子樹,只要在遍歷每個節點的時候記錄它的深度,我們就可以一邊 6 遍歷一邊判斷這個幾點是不是平衡的了。 7 */ 8 bool isBalance(BinaryTreeNode *root,int *depeth){ 9 if(root==NULL){ 10 *depeth=0; 11 return true; 12 } 13 int left,right;//左右深度 14 if(isBalance(root->left,&left) &&isBalance(root->right,&right)){ 15 int diff=left-right; 16 if(diff<=1||diff>=-1){ 17 *depeth=1+(left>right?left:right); 18 return true; 19 } 20 } 21 return false; 22 }
題目三:二叉樹的一系列操作(創建,刪除,插入,排序等)
1 #include<iostream> 2 #include<stack> 3 #include<stdlib.h> 4 5 using namespace std; 6 7 template <class T> 8 class BinarySearchTreeNode{ 9 public : 10 T element; 11 struct BinarySearchTreeNode<T>* left; 12 struct BinarySearchTreeNode<T>* right; 13 struct BinarySearchTreeNode<T>* parent; 14 }; 15 template <class T> 16 class BinarySearchTree{ 17 private: 18 BinarySearchTreeNode<T> * root; 19 public: 20 BinarySearchTree(); 21 // ~BinarySearchTree(); 22 void insert(const T &ele); 23 int remove(const T & ele);//返回移除的節點的值 24 BinarySearchTreeNode<T>* search(const T &ele)const;//查找節點,返回的是一個節點指針 25 T tree_minmum(BinarySearchTreeNode<T>* root)const;//找出最小的節點 26 T tree_maxmum(BinarySearchTreeNode<T>* root)const;//找出最大的節點 27 T tree_successor(const T& elem) const;//前驅 28 T tree_predecessor(const T& elem)const;//后繼 29 int empty()const; 30 void inorder_tree_walk()const;//二叉樹排序 31 BinarySearchTreeNode<T>* get_root()const {return root;}//得到根節點 32 }; 33 template<class T> 34 BinarySearchTree<T> ::BinarySearchTree(){//構造函數 35 root=NULL; 36 } 37 template<class T> 38 void BinarySearchTree<T>::insert(const T&ele){//插入節點 39 /**1. 建立兩個臨時的節點指針,還有一個新節點的指針 40 2. 首先判斷是不是空樹,空樹就先創建一個根節點,然后賦值就行了 41 3. 若果不是空樹,一個指針先指向根節點,插入值小於節點值就接着進入左子樹,大於就進入右子樹,循環 42 直到指針下一個左右子樹為空,此時用另一個指針Y指向當前的這個指針X 43 4. 從Y指針開始,比較當前節點值與插入值大小,如果大於就插入當前節點的左子樹,小於就插入右子樹, 44 Y為插入節點的雙親節點 45 */ 46 if(!empty()){ 47 BinarySearchTreeNode <T>* x; 48 BinarySearchTreeNode <T>* y; 49 BinarySearchTreeNode <T>* newnode=new BinarySearchTreeNode<T>;//創建一個新插入的空的節點 50 x=root; 51 y=NULL; 52 newnode->element=ele;//初始化新插入的節點 53 newnode->left=NULL; 54 newnode->right=NULL; 55 newnode->parent=NULL; 56 while(x){//開始查找遍歷 57 y=x;//用y記錄x指針的位置,找到合適的位置就已經記錄下來 58 if(ele<x->element) 59 x=x->left; 60 else 61 x=x->right; 62 } 63 64 if(y->element > ele)//開始進行插入,最后一步的比較,確定是在當前節點的左子樹還是右子樹 65 y->left = newnode; 66 else 67 y->right = newnode; 68 newnode->parent = y;//設置新節點的雙親為Y 69 } 70 else{ 71 root = new BinarySearchTreeNode<T>; 72 root->element = ele; 73 root->parent =NULL; 74 root->left = NULL; 75 root->right = NULL; 76 } 77 } 78 79 80 template <class T> 81 int BinarySearchTree<T>::remove(const T& ele){ 82 /** 1. 刪除節點有三種情況 83 1)刪除的節點沒有左右子樹:直接把當前節點的父節點的一個左或者右指針置為NULL 84 2)刪除的節點只有一個子樹(左或者右),則通過讓其父節點與其子樹建立一條鏈進行連接 85 3)刪除的節點既有左又有右,刪除當前節點的后繼,再用后繼的值來代替當前節點的值 86 2. 遍歷查找要刪除的節點 87 */ 88 BinarySearchTreeNode<T>* node=search(ele);//找到我們要查找的元素的節點 89 BinarySearchTreeNode<T>* parent_1; 90 if(node!=NULL){ 91 parent_1=node->parent; 92 if(node->left==NULL ||node->right==NULL){//第一、二種情況 93 if(node->left!=NULL){//當前節點的左子樹不為空 94 if(parent_1->left==node) 95 parent_1->left=node->left; 96 if(parent_1->right==node) 97 parent_1->right=node->left; 98 } 99 else if(node->right!=NULL){//當前節點的額右子樹不為空 100 if(parent_1->left==node) 101 parent_1->left=node->right; 102 if(parent_1->right==node) 103 parent_1->right=node->right; 104 } 105 else{//當前節點左右子樹都為空 106 if(parent_1->left==node) 107 parent_1->left=NULL; 108 if(parent_1->right==node) 109 parent_1->right=NULL; 110 } 111 delete node; 112 } 113 else { 114 BinarySearchTreeNode<T>*temp;//臨時節點 115 temp=search(tree_successor(node->element));//找到當前節點的后繼,temp 116 node->element=temp->element; 117 //根據后繼分情況判斷 118 if(temp->parent->left == temp)//一種情況 119 { 120 temp->parent->left = temp->right; 121 temp->right->parent = temp->parent->left; 122 } 123 if(temp->parent->right == temp)//另一種情況 124 { 125 temp->parent->right = temp->right; 126 temp->right->parent = temp->parent->right; 127 } 128 delete temp; 129 } 130 131 return 0; 132 } 133 return -1; 134 } 135 template <class T > 136 BinarySearchTreeNode<T>* BinarySearchTree<T>::search(const T &ele)const{ 137 //遞歸實現查找的策略 138 BinarySearchTreeNode <T>* node=root; 139 while(node){ 140 if(node->element==ele) 141 break; 142 else if(node->element>ele) 143 node=node->left; 144 else 145 node=node->right; 146 } 147 return node; 148 } 149 template <class T> 150 T BinarySearchTree<T>:: tree_minmum(BinarySearchTreeNode<T>* root)const{ 151 BinarySearchTreeNode <T>*node=root; 152 if(node->left){ 153 while(node->left) 154 node=node->left; 155 } 156 return node->element; 157 } 158 template <class T> 159 T BinarySearchTree<T>:: tree_maxmum(BinarySearchTreeNode<T>* root)const{ 160 BinarySearchTreeNode <T>*node=root; 161 if(node->right){ 162 while(node->right) 163 node=node->right; 164 } 165 return node->element; 166 } 167 template <class T> 168 int BinarySearchTree<T>::empty()const{ 169 return {NULL==root}; 170 } 171 template <class T> 172 T BinarySearchTree<T>::tree_successor(const T& elem) const{//求前驅的 173 /** 1. 前驅即是當前節點的左子樹中最大的那個關鍵字 174 2. 沒有左子樹:則返回上一層,查找 175 */ 176 BinarySearchTreeNode<T>* pnode = search(elem); 177 BinarySearchTreeNotemplate <class T> 178 int RedBlackTree<T>:: insert_key(const T& k){//插入函數 179 180 }de<T>* parentnode; 181 if(pnode != NULL) 182 { 183 if(pnode->right) 184 return tree_minmum(pnode->right); 185 parentnode = pnode->parent; 186 while(parentnode && pnode == parentnode->right) 187 { 188 pnode = parentnode; 189 parentnode = parentnode->parent; 190 } 191 if(parentnode) 192 return parentnode->element; 193 else 194 return T(); 195 } 196 return T(); 197 198 } 199 template <class T> 200 T BinarySearchTree<T>::tree_predecessor(const T& elem) const{//求后繼的 201 BinarySearchTreeNode<T>* pnode = search(elem); 202 BinarySearchTreeNode<T>* parentnode; 203 if(pnode != NULL) 204 { 205 if(pnode->right) 206 return tree_maxmum(pnode->right); 207 parentnode = pnode->parent; 208 while(parentnode && pnode == parentnode->left) 209 { 210 pnode = parentnode; 211 parentnode = pnode->parent; 212 } 213 if(parentnode) 214 return parentnode->element; 215 else 216 return T(); 217 } 218 return T(); 219 220 } 221 template <class T> 222 void BinarySearchTree<T>::inorder_tree_walk()const{ 223 if(NULL !=root){ 224 stack <BinarySearchTreeNode<T>*>s;//構建一個存儲節點的棧 225 BinarySearchTreeNode<T>* temp;//創建一個臨時節點 226 temp=root; 227 while(temp!=NULL || !s.empty()){ 228 if(temp!=NULL){ 229 s.push(temp); 230 temp=temp->left; 231 } 232 else{ 233 temp=s.top(); 234 s.pop(); 235 cout<<temp->element<<" "; 236 temp=temp->right; 237 } 238 } 239 240 } 241 }
題目四:求一個二叉樹的深度(從根節點到葉節點依次經過的節點形成的一條路徑,最長路徑的長度為樹的深度)
1 #include<iostream> 2 using namespace std; 3 int treeDepth(BSTree *root){ 4 if(root ==NULL) 5 return 0; 6 int leftdepth=treeDepth(root->left); 7 int rightdepth=treeDepth(root->right); 8 9 return (leftdepth>rightdepth)?(leftdepth+1):(rightdepth+1); 10 } 11
題目五:二叉樹中兩個節點的最近公共祖先節點
這個問題可以分為三種情況來考慮:
情況一:root未知,但是每個節點都有parent指針
此時可以分別從兩個節點開始,沿着parent指針走向根節點,得到兩個鏈表,然后求兩個鏈表的第一個公共節點,這個方法很簡單,不需要詳細解釋的。
情況二:節點只有左、右指針,沒有parent指針,root已知
思路:有兩種情況,一是要找的這兩個節點(a, b),在要遍歷的節點(root)的兩側,那么這個節點就是這兩個節點的最近公共父節點;
二是兩個節點在同一側,則 root->left 或者 root->right 為 NULL,另一邊返回a或者b。那么另一邊返回的就是他們的最小公共父節點。
遞歸有兩個出口,一是沒有找到a或者b,則返回NULL;二是只要碰到a或者b,就立刻返回。
1 #include<iostream> 2 using namespace std; 3 // 二叉樹結點的描述 4 typedef struct BiTNode 5 { 6 char data; 7 struct BiTNode *lchild, *rchild; // 左右孩子 8 }BinaryTreeNode; 9 10 // 節點只有左指針、右指針,沒有parent指針,root已知 11 BinaryTreeNode* findLowestCommonAncestor(BinaryTreeNode* root , BinaryTreeNode* a , BinaryTreeNode* b) 12 { 13 if(root == NULL) 14 return NULL; 15 if(root == a || root == b) 16 return root; 17 //這里往左邊和右邊找a節點,直到找到a節點或者b節點 18 BinaryTreeNode* left = findLowestCommonAncestor(root->lchild , a , b); 19 BinaryTreeNode* right = findLowestCommonAncestor(root->rchild , a , b); 20 if(left && right)//如果a在當前根節點左邊,b在當前根節點右邊,那么返回當前根節點 21 return root; 22 //否則就是a,b在最近公共祖先的同一側,返回其中的一個就行了。 23 return left ? left : right; 24 }
情況三:二叉樹是個二叉查找樹,且root和兩個節點的值(a, b)已知
1 #include<iostream> 2 using namespace std; 3 // 二叉樹是個二叉查找樹,且root和兩個節點的值(a, b)已知 4 BinaryTreeNode* findLowestCommonAncestor(BinaryTreeNode* root , BinaryTreeNode* a , BinaryTreeNode* b) 5 { 6 char min , max; 7 if(a->data < b->data) 8 min = a->data , max = b->data; 9 else 10 min = b->data , max = a->data; 11 while(root) 12 { 13 if(root->data >= min && root->data <= max) 14 return root; 15 else if(root->data < min && root->data < max) 16 root = root->rchild; 17 else 18 root = root->lchild; 19 } 20 return NULL; 21 }
