树
树的题目基本都是二叉树,但是面试官还没有说是不是二叉树的时候千万不要先把答案说出来,要是面试官说是多叉树,而你做的是二叉树就直接挂了!
一. 树的三种遍历。前序、中序、后序,如果直接考遍历,就肯定是让你写非递归代码的(递归版太弱智了),具体写法,要不你记下来,要不参考“递归”部分的,怎么递归转非递归,另一个就是给个中序+前序(后序),让你还原二叉树,中序必须给,不然还原不了(解不唯一),一般递归解决;
二. 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 }