一、相關概念
樹是n( n>=0)個有限個數據的元素集合,它的數據的存儲結構形狀像一顆倒過來的樹。根在上,葉在下:如圖所示
1.一個獨立的節點也可看作一棵樹,它既為根節點,又為葉子節點;
2.一個節點也沒有稱作空樹;
3.這是一顆典型的樹,根節點為A;
4.一個節點只有唯一父節點。
節點: 結點包含數據和指向其它節點的指針。
根節點: 樹第一個結點稱為根節點。
結點的度: 結點擁有的子節點個數。
葉節點: 沒有子節點的節點(度為0)。
父子節點: 一個節點father指向另一個節點child, 則child為孩子節點, father為父親節點 。
兄弟節點: 具有相同父節點的節點互為兄弟節點。
節點的祖先: 從根節點開始到該節點所經的所有節點都可以稱為該節點的祖先。
子孫: 以某節點為根的子樹中任一節點都稱為該節點的子孫。
樹的高度: 樹中距離根節點最遠節點的路徑長度。
如圖示:
5.樹的存儲結構
1 struct TreeNode 2 { 3 DataType data; //節點值 4 TreeNode* _firistchild; //第一個孩子 5 TreeMode* _nextchild; //第二個孩子 6 ... 7 };
有時候根據需要還會加入父節點,結構如下:
1 struct TreeNode 2 { 3 DataType data; //節點值 4 TreeNode* _parent; 5 TreeNode* _firistchild; //第一個孩子 6 TreeMode* _nextchild; //第二個孩子 7 ... 8 };
二、二叉樹
1.二叉樹:二叉樹是一棵特殊的樹, 二叉樹每個節點最多有兩個孩子結點, 分別稱為左孩子和右孩子。如圖:
2.存儲結構
1 template <class T> 2 struct TreeNode //定義二叉樹結點 3 { 5 TreeNode<T>* _left; //指向左子樹的指針 6 TreeNode<T>* _right; //指向右子樹的指針 7 T _data; //節點數據 8 TreeNode(const T& n) 9 :_left(NULL) 10 ,_right(NULL) 11 ,_data(n) 12 {} 13 };
有時候根據需要還會加入父節點,結構如下:
1 template <class T> 2 struct TreeNode //定義二叉樹結點 3 { 4 TreeNode<T>* _parent; //指向父節點的指針 5 TreeNode<T>* _left; //指向左子樹的指針 6 TreeNode<T>* _right; //指向右子樹的指針 7 T _data; //節點數據 8 TreeNode(const T& n) 9 :_left(NULL) 10 ,_right(NULL) 11 ,_data(n) 12 {} 13 };
3.特殊的二叉樹
滿二叉樹:高度為N的滿二叉樹有2^N - 1個節點的二叉樹。
完全二叉樹: 若設二叉樹的深度為h, 除第 h 層外, 其它各層 (1~ h-1) 的結點數都達到最大個數, 第 h 層所有的結點都連續集中在最左
邊, 這就是完全二叉樹
對於上面這顆完全二叉樹:
前序遍歷(先根遍歷):( 1) : 先訪問根節點; ( 2) : 前序訪問左子樹; ( 3) : 前序訪問右子樹; 【1 2 3 4 5 6】
中序遍歷: ( 1) : 中序訪問左子樹; ( 2) : 訪問根節點; ( 3) : 中序訪問右子樹; 【3 2 4 1 6 5】
后序遍歷(后根遍歷):( 1) : 后序訪問左子樹; ( 2) : 后序訪問右子樹; ( 3) : 訪問根節點; 【3 4 2 6 5 1】
層序遍歷: ( 1) : 一層層節點依次遍歷。 【1 2 5 3 4 6】
三、對二叉樹的相關操作
1 #pragma once 2 #include <iostream> 3 #include <queue> 4 using namespace std; 5 6 template <class T> 7 struct BinaryTreeNode //定義二叉樹結點 8 { 9 BinaryTreeNode<T>* _left; //指向左子樹的指針 10 BinaryTreeNode<T>* _right; //指向右子樹的指針 11 T _data; //節點數據 12 BinaryTreeNode(const T& n) 13 :_left(NULL) 14 ,_right(NULL) 15 ,_data(n) 16 {} 17 }; 18 19 template <class T> 20 class BinaryTree 21 { 22 typedef BinaryTreeNode<T> Node; 23 public: 24 BinaryTree() //空樹 25 :_root(NULL) 26 {} 27 BinaryTree(const BinaryTree<T>& tree) //拷貝構造 28 { 29 _root = _Copy(tree._root); 30 } 31 ~BinaryTree() //析構二叉樹 32 { 33 _Destroy(_root); 34 _root = NULL; 35 } 36 BinaryTree<T>& operator=(const BinaryTree<T>& tree) //賦值運算符重載 37 { 38 if (this != &tree) { 39 _Destroy(_root); 40 _root = _Copy(tree._root); 41 } 42 return *this 43 } 44 45 /*BinaryTree<T>& operator=(const BinaryTree<T>& tree) 46 { 47 swap(_root, tree._root); 48 return *this; 49 }*/ 50 BinaryTree(T* a, size_t n, const T& invalid) { //invalid無效節點(#)//創建節點個數為N的二叉樹 51 size_t index = 0; 52 _root = _CreatTree(a, n, invalid, index); 53 } 54 void PrevOrder(){ //前序遍歷二叉樹遞歸法 55 _PrevOrder(_root); 56 cout << endl; 57 } 58 void PrevOrderNonR() //前序遍歷二叉樹非遞歸法 59 { 60 _PrevOrderNonR(_root); 61 cout << endl; 62 } 63 void InOrder() //中序遍歷二叉樹遞歸法 64 { 65 _InOrder(_root); 66 cout << endl; 67 } 68 void InOrderNonR() //中序遍歷二叉樹非遞歸法 69 { 70 _InOrderNonR(_root); 71 cout << endl; 72 } 73 void PostOrder() //后序遍歷二叉樹遞歸法 74 { 75 _PostOrder(_root); 76 cout << endl; 77 } 78 void PostOrderNonR() //后序遍歷二叉樹非遞歸法 79 { 80 _PostOrderNonR(); 81 cout << endl; 82 } 83 void LevelOrder() //層序遍歷二叉樹遞歸法 84 { 85 _LevelOrder(_root); 86 cout << endl; 87 } 88 void LevelOrderNonR()//.queue//層序遍歷二叉樹非遞歸法 89 { 90 _LevelOrderNonR(_root); 91 cout << endl; 92 } 93 size_t Size(){ //二叉樹總節點個數 94 return _Size(_root); 95 } 96 size_t CountLeafNode() //葉子節點 97 { 98 return _CountLeafNode(_root); 99 } 100 size_t Depth() //高度 101 { 102 return _Depth(_root); 103 } 104 size_t GetKLevelSize(size_t k) //第K層節點個數 105 { 106 return _GetKLevelSize(_root, k); 107 } 108 Node* Find(const T& x) //查找節點 109 { 110 return _Find(_root, x); 111 } 112 private: 113 Node * _root; //根節點 114 Node* _CreatTree(T* a, size_t n, const T& invalid, size_t &index) { //創建二叉樹樹 115 Node* root = NULL; 116 if (index < n && a[index] != invalid) { 117 root = new Node(a[index]); 118 root->_left = _CreatTree(a, n, invalid, ++index);//遞歸創建左子樹 119 root->_right= _CreatTree(a, n, invalid, ++index);//遞歸創建右子樹 120 } 121 return root; 122 } 123 Node* _Copy(Node* root) //copy二叉樹 124 { 125 if (root == NULL) 126 return NULL; 127 Node* newroot = new Node(root->_data); 128 newroot->_left = _Copy(root->_left); 129 newroot->_right = _Copy(root->_right); 130 } 131 void _Destory(Node* root) //銷毀二叉樹樹 132 { 133 if (root == NULL) 134 return; 135 _Destory(root->_left); 136 _Destory(root->_right); 137 delete root; 138 } 139 };
這里封裝了一些常見操作的接口,而沒有給出具體的實現函數,這些操作也是面試中常見的題型,下一篇將詳細解析這些對二叉樹的基本操作。
1 #include "BinaryTree.h" 2 3 void Test() 4 { 5 int array[] = { 1, 2, 3, '#', '#', 4, '#' , '#', 5, 6, '#', '#', '#' }; 6 //int array[15] = { 1, 2, '#' , 3, '#' , '#' , 4, 5, '#' , 6, '#' , 7, '#' , '#' , 8 }; 7 BinaryTree<int> tree1(array, sizeof(array) / sizeof(array[0]), '#'); 8 //tree1.LevelOrder(); 9 tree1.PrevOrder(); 10 } 11 int main() 12 { 13 Test(); 14 getchar(); 15 return 0; 16 }
另一個問題:為什么要將這么多的接口封裝起來吶?這里是使用C++來實現的,C++是面向對象的語言,在C++中封裝是面向對象編程中的把數據和操作數據的函數綁定在一起的一個概念,這樣能避免受到外界的干擾和誤用,從而確保了安全。
四、上述接口的具體實現函數及一些常見面試題型
1. 二叉樹的前序/中序/后序遍歷(遞歸&非遞歸) ;
2. 二叉樹的層序遍歷 ;
3. 求二叉樹的高度 ;
4. 求二叉樹的葉子節點的個數 ;
5. 求二叉樹第k層的節點個數 ;
6. 判斷一個節點是否在一棵二叉樹中 ;
7.求二叉樹的鏡像;
8.判斷兩顆二叉樹是否相等;
9.從二叉樹中查找結點
1.第一題求解思路:
遞歸法求解,主要思想是轉化為子問題,函數不斷地調用自身來解決子問題,另外一個要點是確定遞歸的終止條件。對於本題,遍歷一棵樹,先遍歷它的左子樹和右子樹,循環采用這種思想,當遇到空節點時停止遍歷。不難寫出如下代碼:
1 void _PrevOrder(Node* root) 2 { //前序遍歷遞歸法 3 if (root == NULL) 4 return; 5 cout << root->_data << " "; 6 _PrevOrder(root->_left); 7 _PrevOrder(root->_right); 8 } 9 void _InOrder(Node* root)//中續遍歷遞歸法 10 { 11 if (root == NULL) 12 { 13 return; 14 } 15 _InOrder(root->_left); 16 cout << root->_data << " "; 17 _InOrder(root->_right); 18 } 19 void _PostOrder(Node* root) //后序遍歷遞歸法 20 { 21 if (root == NULL) 22 { 23 return; 24 } 25 _PostOrder(root->_left); 26 _PostOrder(root->_right); 27 cout << root->_data << " "; 28 }
前序/中序/后序遍歷也叫先根/中根/后根遍歷,主要區別就是遍歷根的先后順序,所以代碼差別不大,主要區別也在訪問根節點的先后順序上。
非遞歸法遍歷二叉樹要借助棧來實現,對於前序遍歷,先訪問根節點,再將節點入棧,再訪問左子樹,再將左節點入棧,再訪問此節點的右子樹,再將左節點入棧,以此方法一直訪問下去直到遇到空節點跳出循環,然后再將節點依次從棧中取出,以同樣方法訪問其右子樹。按此方法實現代碼如下:
1 void _PrevOrderNonR(Node* root) //前序遍歷非遞歸法 2 { 3 Node* cur = root; 4 stack<Node*> s1; 5 // 訪問一顆子樹的開始 6 while (cur || !s1.empty()) 7 { 8 while (cur) 9 { 10 cout << cur->_data << " "; //訪問當前樹的根節點 11 s1.push(cur); 12 cur = cur->_left; //訪問當前樹的左子樹 13 } 14 // 從棧里面取出,以為top節點的右樹還沒有訪問 15 Node* stop = s1.top(); 16 s1.pop(); 17 //子問題,訪問右右子樹 18 cur = stop->_right; 19 } 20 cout << endl; 21 }
中序遍歷,遇到一個節點不訪問,先訪問它的左節點,與前序遍歷相似,先將節點入棧,訪問它的左子樹,先將左子樹的根節點入棧,訪問左子樹的左子樹,以此方法循環下去,直到遇到空節點跳出循環,這時取出棧頂節點先訪問,這個節點要么只有左節點為空,要么左右節點都為空,所以訪問之后將其出棧,再訪問其右節點,方法與前面相同。代碼如下:
1 void _InOrderNonR(Node* root)//中續遍歷非遞歸法 2 { 3 Node* cur = root; 4 stack<Node*> s1; 5 while (cur || !empty()) 6 { 7 while (cur) 8 { 9 //訪問一顆子樹的開始 10 s1.push(cur); 11 cur = cur->_left; //訪問右子樹 12 } 13 Node* stop = s1.top(); 14 cout << cur->_data << " "; 15 s1.pop(); 16 //子問題方法訪問右子樹 17 cur = stop->_right; 18 } 19 cout << endl; 20 }
后序遍歷非遞歸法,思想與前兩種遍歷相同,但是也有需要注意的地方,從根節點到左子樹依次壓棧,遇到空時跳出循環,然后取棧頂節點,若它的右子樹為空則可直接訪問,若不為空則訪問右子樹。因為后序遍歷最后還要要訪問根結點一次,所以要訪問根結點兩次。采取夾標志位的方法解決這個問題。邏輯圖如下:
1 void _PostOrderNonR(Node* root) //后續遍歷非遞歸法 2 { 3 Node* cur = root; 4 Node* prev = NULL; 5 stack<Node*> s1; 6 while (cur || !s1.empty() 7 { 8 while (cur) 9 { 10 s1.push(cur); 11 cur = cur->_left; //先將節點依次入棧 12 } 13 Node* stop = s1.top(); //取棧頂節點先訪問它的右樹 14 if (stop->_right == NULL || stop->_right == prev) 15 { 16 cout << stop->_data << " "; 17 prev = stop; //保存上次訪問的節點 18 s1.pop(); 19 } 20 else { 21 cur = cur->_right; 22 } 23 } 24 cout << endl; 25 }
2.層序遍歷也有兩種思路
非遞歸法:與上述三種遍歷的非遞歸法類似。依然需要借助其他容器來完成,不過這里用的是隊列。這里利用隊列尾進頭出的性質依次將節點保存在隊列中,同時依次取出頭結點訪問,然后保存它的左右節點。
1 void _LevelOrderNonR(Node* root) //層序遍歷非遞歸法 2 { 3 queue<Node*> myq; 4 if (root) 5 myq.push(root); 6 while (!myq.empty()) 7 { 8 Node* front = myq.front(); 9 cout << front->_data <<" "; 10 myq.pop(); 11 if (front->_left) 12 myq.push(front->_left); 13 if (front->_right) 14 myq.push(front->_right); 15 } 16 }
借助高度的遞歸法:
1 void PrintNLevel(Node* root, size_t level) 2 { 3 if (root == NULL || level < 1) //空樹或層數不合理 4 { 5 return; 6 } 7 if (level == 1) 8 { 9 cout << root->data << " "; 10 } 11 PrintNLevel(root->_left, level - 1); 12 PrintNLevel(root->_right, level - 1); 13 } 14 void _LevelOrder(Node* root) 15 { 16 if (root == NULL) 17 { 18 return; 19 } 20 int depth = Depth(root); 21 for (size_t i = 1; i < depth; i++) 22 { 23 PrintNLevel(root, i); 24 cout << endl; 25 } 26 }
這個算法先求出根結點的高度,depth=高度+1。函數PrintNLevel中,當level==1時才進行打印。樹的根的高度是2,depth=3。然后,在_LevelOrder函數中,level從1開始傳參,然后PrintNLevel會打印出1;之后level=2,進入PrintNLevel(root-_left, level - 1)函數和PrintNLevel(root->_right, level - 1),level又等於1,就打印出2,3。以此類推,整棵樹就按層打印出來了。
3.二叉樹的高度
用遞歸法可以很方便的計算出來,子問題是子樹中高度較高的高度再加一,依次遞歸下去,退出條件依然為 root==NULL。
1 size_t _Depth(Node* root) 2 { 3 if (root == NULL) 4 return 0; 5 //子問題 6 _Depth(root->_left) = left; //左子樹的高度 7 _Depth((root->_right) = right; //右子樹的高度 8 return left > right ? left + 1 : right + 1; 9 }
4. 葉子節點的個數
1 size_t _Size(Node* root) { //二叉樹總節點個數 2 if (root = NULL) 3 return; 4 return _Size(root->_left) + _Size(root->_right) + 1; 5 } 6 //求二叉樹葉子節點的個數 7 size_t _CountLeafNode(Node* root) 8 { 9 if (root == NULL) 10 return 0; 11 if (root->_left == NULL && root->_right == NULL)//當左右子樹都為空時即為葉子節點 12 return 1; 13 return _CountLeafNode(root->_left) + _CountLeafNode(root->_right); 14 }
5.第k層的節點個數
1 size_t _CountKLevel(Node* root, size_t k)// 求二叉樹第k層的節點個數。 2 { 3 if (root == NULL) 4 { 5 return 0; 6 } 7 if (k == 1) 8 { 9 return 1; 10 } 11 _CountKLevel(root->_left, k - 1); 12 _CountKLevel(root->_right, k - 1); 13 }
6. 節點是否在樹中
1 bool JudgeNode(Node* root, T x) //判斷一個節點是否在一棵二叉樹中 ; 2 { 3 if (root==NULL) 4 { 5 return false; 6 } 7 if (root->_data==x) 8 { 9 return true; 10 } 11 return JudgeNode(root->_left, x) || JudgeNode(root->_right, x); //找到之后即返回,就不到另一個子樹中查找了 12 }
7.求二叉樹的鏡像
二叉樹的鏡像如圖:
思路是將子節點依次互換即可。
1 //二叉樹的鏡像 2 void MirroRecursively(Node *root) 3 { 4 if (root==NULL) 5 { 6 return ; 7 } 8 if (root->_left==NULL&&root->_right==NULL) 9 { 10 return ; 11 } 12 Node* tmp = root->_left; 13 root->_left = root->_right; 14 root->_right = tmp; 15 if (root->_left) 16 { 17 MirroRecursively(root->_left); 18 } 19 if (root->_right) 20 { 21 MirroRecursively(root->_right); 22 } 23 }
8.二叉樹是否相等
1 //8、比較兩個樹是否相同 2 int is_equal(Node* t1, Node* t2) { 3 if (!t1 && !t2) { //都為空就相等 4 return 1; 5 } 6 if (t1 && t2 && t1->data == t2->data) { //有一個為空或數據不同就不判斷了 7 if (is_equal(t1->_left, t2->_left)) 8 if (is_equal(t1->_right, t2->_right)) { 9 return 1; 10 } 11 } 12 return 0; 13 }
9.從二叉樹中查找結點
1 //二叉樹中查找節點 2 Node* _Find(Node* root, const T& x) 3 { 4 if (root == NULL) 5 { 6 return NULL; 7 } 8 if (root->_data == x) 9 { 10 return root; 11 } 12 Node* ret = _Find(root->_left, x); 13 if (ret) 14 { 15 return ret; 16 } 17 return _Find(root->_right, x); 18 }