關於二叉樹
二叉樹作為樹的一種,是一種重要的數據結構,也是面試官經常考的東西。昨天看了一下關於樹中的面試題,發現二叉樹中的面試題比較常見的題型大概有下面幾個:創建一顆二叉樹(先序,中序,后序)、遍歷一顆二叉樹(先序,中序,后序和層次遍歷)、求二叉樹中葉子節點的個數、求二叉樹的高度、求二叉樹中兩個節點的最近公共祖先、打印和為某一值的全部路徑、求某一節點是否在一個樹中等等。
再詳細的說這些面試題之前,不妨先看一下幾種常見的二叉樹:
完全二叉樹:若二叉樹的高度是h,除第h層之外,其他(1~h-1)層的節點數都達到了最大個數,並且第h層的節點都連續的集中在最左邊。想到點什么沒?實際上,完全二叉樹和堆聯系比較緊密哈~~~
滿二叉樹:除最后一層外,每一層上的所有節點都有兩個子節點,最后一層都是葉子節點。
哈夫曼樹:又稱為最有數,這是一種帶權路徑長度最短的樹。哈夫曼編碼就是哈夫曼樹的應用。
平衡二叉樹:所謂平衡二叉樹指的是,左右兩個子樹的高度差的絕對值不超過 1。
紅黑樹:紅黑樹是每個節點都帶顏色的樹,節點顏色或是紅色或是黑色,紅黑樹是一種查找樹。紅黑樹有一個重要的性質,從根節點到葉子節點的最長的路徑不多於最短的路徑的長度的兩倍。對於紅黑樹,插入,刪除,查找的復雜度都是O(log N)。
二叉樹中的那些面試題
再具體說二叉樹中的那些面試題之前,我們先看一下二叉樹中的每個節點是什么樣的,以及為了完成這些面試題,二叉樹中聲明的函數原型是什么樣的:
二叉樹的節點:BinTreeNode

1 //二叉樹的節點類 2 class BinTreeNode 3 { 4 private: 5 int data; 6 BinTreeNode *left,*right; 7 public: 8 //利用初始化列表完成data,left,rightn的初始化 9 BinTreeNode(const int &item,BinTreeNode *lPtr = NULL,BinTreeNode *rPtr = NULL):data(item) ,left(lPtr),right(rPtr){}; 10 void set_data(int item) 11 { 12 data = item; 13 } 14 int get_data()const 15 { 16 return data; 17 } 18 void set_left(BinTreeNode *l) 19 { 20 left = l; 21 } 22 BinTreeNode* get_left()const 23 { 24 return left; 25 } 26 void set_right(BinTreeNode *r) 27 { 28 right = r; 29 } 30 BinTreeNode* get_right()const 31 { 32 return right; 33 } 34 };
二叉樹原型:BinTree,其中包含了這篇文章中要完成的函數原型的完整聲明。

1 //二叉樹 2 class BinTree 3 { 4 private: 5 BinTreeNode *root; 6 public: 7 BinTree(BinTreeNode *t = NULL):root(t){}; 8 ~BinTree(){delete root;}; 9 void set_root(BinTreeNode *t) 10 { 11 root = t; 12 } 13 BinTreeNode* get_root()const 14 { 15 return root; 16 } 17 //1.創建二叉樹 18 BinTreeNode* create_tree(); 19 //2.前序遍歷 20 void pre_order(BinTreeNode *r)const; 21 //3.中序遍歷 22 void in_order(BinTreeNode *r)const; 23 //4.后序遍歷 24 void post_order(BinTreeNode *r)const; 25 //5.層次遍歷 26 void level_order(BinTreeNode *r)const; 27 //6.獲得葉子節點的個數 28 int get_leaf_num(BinTreeNode *r)const; 29 //7.獲得二叉樹的高度 30 int get_tree_height(BinTreeNode *r)const; 31 //8.交換二叉樹的左右兒子 32 void swap_left_right(BinTreeNode *r); 33 //9.求兩個節點pNode1和pNode2在以r為樹根的樹中的最近公共祖先 34 BinTreeNode* get_nearest_common_father(BinTreeNode *r,BinTreeNode *pNode1,BinTreeNode *pNode2)const; 35 //10.打印和為某一值的所有路徑 36 void print_rout(BinTreeNode *r,int sum)const; 37 //11.判斷一個節點t是否在以r為根的子樹中 38 bool is_in_tree(BinTreeNode *r,BinTreeNode *t)const; 39 };
2.1 創建一顆二叉樹
創建一顆二叉樹,可以創建先序二叉樹,中序二叉樹,后序二叉樹。我們在創建的時候為了方便,不妨用‘#’表示空節點,這時如果先序序列是:6 4 2 3 # # # # 5 1 # # 7 # #,那么創建的二叉樹如下:
下面是創建二叉樹的完整代碼:穿件一顆二叉樹,返回二叉樹的根。

1 //創建二叉樹,這里不妨使用前序創建二叉樹,遇到‘#’表示節點為空 2 BinTreeNode* BinTree::create_tree() 3 { 4 char item; 5 BinTreeNode *t,*t_l,*t_r; 6 cin>>item; 7 if(item != '#') 8 { 9 BinTreeNode *pTmpNode = new BinTreeNode(item-48); 10 t = pTmpNode; 11 t_l = create_tree(); 12 t->set_left(t_l); 13 t_r = create_tree(); 14 t->set_right(t_r); 15 return t; 16 } 17 else 18 { 19 t = NULL; 20 return t; 21 } 22 }
2.2 二叉樹的遍歷
二叉樹的遍歷分為:先序遍歷,中序遍歷和后序遍歷,這三種遍歷的寫法是很相似的,利用遞歸程序完成也是灰常簡單的:

1 //前序遍歷 2 void BinTree::pre_order(BinTreeNode *r)const 3 { 4 BinTreeNode *pTmpNode = r; 5 if(pTmpNode != NULL) 6 { 7 cout<<pTmpNode->get_data()<<" "; 8 pre_order(pTmpNode->get_left()); 9 pre_order(pTmpNode->get_right()); 10 } 11 } 12 //中序遍歷 13 void BinTree::in_order(BinTreeNode *r)const 14 { 15 BinTreeNode *pTmpNode = r; 16 if(pTmpNode != NULL) 17 { 18 in_order(pTmpNode->get_left()); 19 cout<<pTmpNode->get_data()<<" "; 20 in_order(pTmpNode->get_right()); 21 } 22 } 23 //后序遍歷 24 void BinTree::post_order(BinTreeNode *r)const 25 { 26 BinTreeNode *pTmpNode = r; 27 if(pTmpNode != NULL) 28 { 29 post_order(pTmpNode->get_left()); 30 post_order(pTmpNode->get_right()); 31 cout<<pTmpNode->get_data()<<" "; 32 } 33 }
2.3 層次遍歷
層次遍歷也是二叉樹遍歷的一種方式,二叉樹的層次遍歷更像是一種廣度優先搜索(BFS)。因此二叉樹的層次遍歷利用隊列來完成是最好不過啦,當然不是說利用別的數據結構不能完成。

1 //層次遍歷 2 void BinTree::level_order(BinTreeNode *r)const 3 { 4 if(r == NULL) 5 return; 6 deque<BinTreeNode*> q; 7 q.push_back(r); 8 while(!q.empty()) 9 { 10 BinTreeNode *pTmpNode = q.front(); 11 cout<<pTmpNode->get_data()<<" "; 12 q.pop_front(); 13 if(pTmpNode->get_left() != NULL) 14 { 15 q.push_back(pTmpNode->get_left()); 16 } 17 if(pTmpNode->get_right() != NULL) 18 { 19 q.push_back(pTmpNode->get_right()); 20 } 21 } 22 }
2.4 求二叉樹中葉子節點的個數
樹中的葉子節點的個數 = 左子樹中葉子節點的個數 + 右子樹中葉子節點的個數。利用遞歸代碼也是相當的簡單,易懂。

1 //獲取葉子節點的個數 2 int BinTree::get_leaf_num(BinTreeNode *r)const 3 { 4 if(r == NULL)//該節點是空節點,比如建樹時候用'#'表示 5 { 6 return 0; 7 } 8 if(r->get_left()==NULL && r->get_right()==NULL)//該節點並不是空的,但是沒有孩子節點 9 { 10 return 1; 11 } 12 //遞歸整個樹的葉子節點個數 = 左子樹葉子節點的個數 + 右子樹葉子節點的個數 13 return get_leaf_num(r->get_left()) + get_leaf_num(r->get_right()); 14 }
2.5 求二叉樹的高度
求二叉樹的高度也是非常簡單,不用多說:樹的高度 = max(左子樹的高度,右子樹的高度) + 1 。

1 //獲得二叉樹的高度 2 int BinTree::get_tree_height(BinTreeNode *r)const 3 { 4 if(r == NULL)//節點本身為空 5 { 6 return 0; 7 } 8 if(r->get_left()==NULL && r->get_right()==NULL)//葉子節點 9 { 10 return 1; 11 } 12 int l_height = get_tree_height(r->get_left()); 13 int r_height = get_tree_height(r->get_right()); 14 return l_height >= r_height ? l_height + 1 : r_height + 1; 15 }
2.6 交換二叉樹的左右兒子
交換二叉樹的左右兒子,可以先交換根節點的左右兒子節點,然后遞歸以左右兒子節點為根節點繼續進行交換。樹中的操作有先天的遞歸性。。

1 //交換二叉樹的左右兒子 2 void BinTree::swap_left_right(BinTreeNode *r) 3 { 4 if(r == NULL) 5 { 6 return; 7 } 8 BinTreeNode *pTmpNode = r->get_left(); 9 r->set_left(r->get_right()); 10 r->set_right(pTmpNode); 11 swap_left_right(r->get_left()); 12 swap_left_right(r->get_right()); 13 }
2.7 判斷一個節點是否在一顆子樹中
可以和當前根節點相等,也可以在左子樹或者右子樹中。

1 //判斷一個節點t是否在以r為根的子樹中 2 bool BinTree::is_in_tree(BinTreeNode *r,BinTreeNode *t)const 3 { 4 if(r == NULL) 5 { 6 return false; 7 } 8 else if(r == t) 9 { 10 return true; 11 } 12 else 13 { 14 bool has = false; 15 if(r->get_left() != NULL) 16 { 17 has = is_in_tree(r->get_left(),t); 18 } 19 if(!has && r->get_right()!= NULL) 20 { 21 has = is_in_tree(r->get_right(),t); 22 } 23 return has; 24 } 25 }
2.8 求兩個節點的最近公共祖先
求兩個節點的公共祖先可以用到上面的:判斷一個節點是否在一顆子樹中。(1)如果兩個節點同時在根節點的右子樹中,則最近公共祖先一定在根節點的右子樹中。(2)如果兩個節點同時在根節點的左子樹中,則最近公共祖先一定在根節點的左子樹中。(3)如果兩個節點一個在根節點的右子樹中,一個在根節點的左子樹中,則最近公共祖先一定是根節點。當然,要注意的是:可能一個節點pNode1在以另一個節點pNode2為根的子樹中,這時pNode2就是這兩個節點的最近公共祖先了。顯然這也是一個遞歸的過程啦:

1 //求兩個節點的最近公共祖先 2 BinTreeNode* BinTree::get_nearest_common_father(BinTreeNode *r,BinTreeNode *pNode1,BinTreeNode *pNode2)const 3 { 4 //pNode2在以pNode1為根的子樹中(每次遞歸都要判斷,放在這里不是很好。) 5 if(is_in_tree(pNode1,pNode2)) 6 { 7 return pNode1; 8 } 9 //pNode1在以pNode2為根的子樹中 10 if(is_in_tree(pNode2,pNode1)) 11 { 12 return pNode2; 13 } 14 bool one_in_left,one_in_right,another_in_left,another_in_right; 15 one_in_left = is_in_tree(r->get_left(),pNode1); 16 another_in_right = is_in_tree(r->get_right(),pNode2); 17 another_in_left = is_in_tree(r->get_left(),pNode2); 18 one_in_right = is_in_tree(r->get_right(),pNode1); 19 if((one_in_left && another_in_right) || (one_in_right && another_in_left)) 20 { 21 return r; 22 } 23 else if(one_in_left && another_in_left) 24 { 25 return get_nearest_common_father(r->get_left(),pNode1,pNode2); 26 } 27 else if(one_in_right && another_in_right) 28 { 29 return get_nearest_common_father(r->get_right(),pNode1,pNode2); 30 } 31 else 32 { 33 return NULL; 34 } 35 }
可以看到這種做法,進行了大量的重復搜素,其實有另外一種做法,那就是存儲找到這兩個節點的過程中經過的所有節點到兩個容器中,然后遍歷這兩個容器,第一個不同的節點的父節點就是我們要找的節點啦。 實際上這還是采用了空間換時間的方法。
2.9 從根節點開始找到所有路徑,使得路徑上的節點值和為某一數值(路徑不一定以葉子節點結束)
這道題要找到所有的路徑,顯然是用深度優先搜索(DFS)啦。但是我們發現DFS所用的棧和輸出路徑所用的棧應該不是一個棧,棧中的數據是相反的。看看代碼:注意使用的兩個棧。

1 //注意這兩個棧的使用 2 stack<BinTreeNode *>dfs_s; 3 stack<BinTreeNode *>print_s; 4 //打印出從r開始的和為sum的所有路徑 5 void BinTree::print_rout(BinTreeNode *r,int sum)const 6 { 7 if(r == NULL) 8 { 9 return; 10 } 11 //入棧 12 sum -= r->get_data(); 13 dfs_s.push(r); 14 if(sum <= 0) 15 { 16 if(sum == 0) 17 { 18 while(!dfs_s.empty()) 19 { 20 print_s.push(dfs_s.top()); 21 dfs_s.pop(); 22 } 23 while(!print_s.empty()) 24 { 25 cout<<print_s.top()->get_data()<<" "; 26 dfs_s.push(print_s.top()); 27 print_s.pop(); 28 } 29 cout<<endl; 30 } 31 sum += r->get_data(); 32 dfs_s.pop(); 33 return; 34 } 35 //遞歸進入左子樹 36 print_rout(r->get_left(),sum); 37 //遞歸進入右子樹 38 print_rout(r->get_right(),sum); 39 //出棧 40 sum += r->get_data(); 41 dfs_s.pop(); 42 }
最后,給出一點測試代碼:

1 int main() 2 { 3 BinTree tree; 4 /*--------------------------------------------------------------------------*/ 5 cout<<"請輸入二叉樹前序序列進行建樹,'#'代表空節點:"<<endl; 6 tree.set_root(tree.create_tree()); 7 cout<<endl; 8 /*--------------------------------------------------------------------------*/ 9 cout<<"前序遍歷的結果:"; 10 tree.pre_order(tree.get_root()); 11 cout<<endl<<endl; 12 /*--------------------------------------------------------------------------*/ 13 cout<<"中序遍歷的結果:"; 14 tree.in_order(tree.get_root()); 15 cout<<endl<<endl; 16 /*--------------------------------------------------------------------------*/ 17 cout<<"后序遍歷的結果:"; 18 tree.post_order(tree.get_root()); 19 cout<<endl<<endl; 20 /*--------------------------------------------------------------------------*/ 21 cout<<"層次遍歷的結果:"; 22 tree.level_order(tree.get_root()); 23 cout<<endl<<endl; 24 /*--------------------------------------------------------------------------*/ 25 cout<<"該二叉樹葉子節點的個數:"; 26 cout<<tree.get_leaf_num(tree.get_root())<<endl<<endl; 27 /*--------------------------------------------------------------------------*/ 28 cout<<"該二叉樹的高度是:"; 29 cout<<tree.get_tree_height(tree.get_root())<<endl<<endl; 30 /*--------------------------------------------------------------------------*/ 31 tree.swap_left_right(tree.get_root()); 32 cout<<"交換左右子樹之后的先序遍歷結果為:"; 33 tree.pre_order(tree.get_root()); 34 cout<<endl<<endl; 35 /*--------------------------------------------------------------------------*/ 36 BinTreeNode *p1 = tree.get_root()->get_left()->get_right(); 37 BinTreeNode *p2 = tree.get_root()->get_left()->get_left(); 38 BinTreeNode *p3 = tree.get_root()->get_right()->get_right()->get_right(); 39 cout<<p1->get_data()<<" 和 "<<p2->get_data()<<"的最近公共祖先是:"; 40 BinTreeNode *p = tree.get_nearest_common_father(tree.get_root(),p1,p2); 41 cout<<p->get_data()<<endl; 42 cout<<p1->get_data()<<" 和 "<<p3->get_data()<<"的最近公共祖先是:"; 43 p = tree.get_nearest_common_father(tree.get_root(),p1,p3); 44 cout<<p->get_data()<<endl<<endl; 45 /*--------------------------------------------------------------------------*/ 46 cout<<"路徑如下:"<<endl; 47 tree.print_rout(tree.get_root(),12); 48 return 0; 49 }
運行結果:
以上內容,只是本人看過的面試題目的一點總結,可能不夠全,可能其中的代碼也有很多的錯誤,可能其中的內容有點水,但是我想對那部分即將參加面試的應屆畢業生還是有幫助的。。大神們勿噴~~~