二叉树面试题
二叉树作为树的一种,是一种重要的数据结构,也是面试官经常考的东西。昨天看了一下关于树中的面试题,发现二叉树中的面试题比较常见的题型大概有下面几个:创建一颗二叉树(先序,中序,后序)、遍历一颗二叉树(先序,中序,后序和层次遍历)、求二叉树中叶子节点的个数、求二叉树的高度、求二叉树中两个节点的最近公共祖先、打印和为某一值的全部路径、求某一节点是否在一个树中等等。
再详细的说这些面试题之前,不妨先看一下几种常见的二叉树:
完全二叉树:若二叉树的高度是h,除第h层之外,其他(1~h-1)层的节点数都达到了最大个数,并且第h层的节点都连续的集中在最左边。想到点什么没?实际上,完全二叉树和堆联系比较紧密哈~~~
满二叉树:除最后一层外,每一层上的所有节点都有两个子节点,最后一层都是叶子节点。
哈夫曼树:又称为最有数,这是一种带权路径长度最短的树。哈夫曼编码就是哈夫曼树的应用。
平衡二叉树:所谓平衡二叉树指的是,左右两个子树的高度差的绝对值不超过 1。
红黑树:红黑树是每个节点都带颜色的树,节点颜色或是红色或是黑色,红黑树是一种查找树。红黑树有一个重要的性质,从根节点到叶子节点的最长的路径不多于最短的路径的长度的两倍。对于红黑树,插入,删除,查找的复杂度都是O(log N)。
1. 二叉树最近公共父节点
原文地址:
http://blog.csdn.net/wcyoot/article/details/6427179
找寻二叉树中两个节点的公共父节点中最近的那个节点
情况1. 节点只有left/right,没有parent指针,root已知
解析:对于此种情形,只需找到两个节点到根节点的路径,然后就相当于两个链表中找公共节点。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 template<typename T> 2 struct TreeNode1 3 { 4 T data; 5 TreeNode1* pLChild; 6 TreeNode1* pRChild; 7 }; 8 9 #include <vector> 10 11 // 找寻节点路径,倒序,根节点在最后 12 template<typename T> 13 bool FindNodePath(TreeNode1<T>* pRoot, TreeNode1<T>* p, std::vector<TreeNode1<T>*>& path) 14 { 15 if(pRoot == NULL) 16 return false; 17 if(p == pRoot) 18 { 19 path.push_back(pRoot); 20 return true 21 } 22 else if(FindNodePath(pRoot->pLChild, p, path)) 23 { 24 path.push_back(pRoot->pLChild); 25 return true; 26 } 27 else if(FindNodePath(pRoot->pRChild, p, path)) 28 { 29 path.push_back(pRoot->pRChild); 30 return true; 31 } 32 return false; 33 } 34 template<typename T> 35 TreeNode1<T>* FindNearestParentNode(TreeNode1<T>* pRoot, TreeNode1<T>* p1, TreeNode1<T>* p2) 36 { 37 std::vector<TreeNode1<T>*> path1, path2; 38 bool bFind = FindNodePath(pRoot, p1, path1); 39 bFind &= FindNodePath(pRoot, p2, path2); 40 if(!bFind) 41 return NULL; 42 43 TreeNode1<T>* pReturn = NULL; 44 size_t minSize = path1.size() > path2.size() ? path2.size() : path1.size(); 45 46 // 起始点设在可能出现共同节点的部分的起点 47 for(size_t i = path1.size() - minSize, j = path2.size()-minSize; i < path1.size() && j < path2.size(); ++i, ++j) 48 { 49 if(path1[i] == path2[j]) 50 { 51 pReturn = path1[i]; 52 } 53 } 54 55 return pReturn; 56 }
情况2. root未知,但是每个节点都有parent指针
解析:比情况1要简单一些,只需找到两个节点的深度,即可当作两个链表求公共节点。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 template<typename T> 2 struct TreeNode2 3 { 4 T data; 5 TreeNode2* pLChild; 6 TreeNode2* pRChild; 7 TreeNode2* pParent; 8 }; 9 template<typename T> 10 int GetNodeDeepth(TreeNode2<T>* pNode) 11 { 12 int nDeepth = 0; 13 // 此处已判断节点为NULL的情况 14 while(pNode) 15 { 16 ++nDeepth; 17 pNode = pNode->pParent; 18 } 19 return nDeepth; 20 } 21 template<typename T> 22 TreeNode2<T>* FindNearestParentNode(TreeNode2<T>* p1, TreeNode2<T>* p2) 23 { 24 int nDeepth1 = GetNodeDeepth(p1); 25 int nDeepth2 = GetNodeDeepth(p2); 26 27 TreeNode2* pReturn = NULL; 28 // 把深度大的放在1里 29 if(nDeepth1 < nDeepth2) 30 { 31 pReturn = p1; 32 p1 = p2; 33 p2 = pReturn; 34 pReturn = NULL; 35 } 36 for(int i = 0; i < nDeepth1-nDeepth2; ++i) 37 p1 = p1->pParent; 38 while(p1) 39 { 40 if(p1 == p2) 41 { 42 pReturn = p1; 43 break; 44 } 45 p1 = p1->pParent; 46 p2 = p2->pParent; 47 } 48 49 return pReturn; 50 }
解析:如果node节点的值在a,b之间,即 a=<node<= b (假设a < b),那么node就是所求节点。否则根据值的大小取其左孩子或右孩子继续判断。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 template<typename T> 2 TreeNode1<T>* FindNearestParentNode(TreeNode1<T>* pRoot, T a, T b) 3 { 4 if(a > b) 5 { 6 T temp = a; 7 a = b; 8 b = a; 9 } 10 while(pRoot) 11 { 12 if(pRoot->data >= a && pRoot->data <= b) 13 return pRoot; 14 else if(pRoot->data < a && pRoot->data < b) 15 pRoot = pRoot->pLChild; 16 else 17 pRoot = pRoot->pRChild; 18 } 19 return NULL; 20 }
2. 求二叉树中相距最远的两个节点之间的距离
两个节点的距离为两个节点间最短路径的长度。
求两节点的最远距离,实际就是求二叉树的直径。假设相距最远的两个节点分别为A、B,它们的最近共同父节点(允许一个节点是其自身的父节点)为C,则A到B的距离 = A到C的距离 + B到C的距离。
节点A、B分别在C的左右子树下(假设节点C的左右两子树均包括节点C),不妨假设A在C的左子树上,由假设“A到B的距离最大”,先固定B点不动(即B到C的距离不变),根据上面的公式,可得A到C的距离最大,即点A是C左子树下距离C最远的点,即:
A到C的距离 = C的左子树的高度。
同理, B到C的距离 = C的右子树的高度。
因此,本问题可以转化为:“二叉树每个节点的左右子树高度和的最大值”。
static int tree_height(const Node* root, int& max_distance)
{
const int left_height = root->left ? tree_height(root->left, max_distance) + 1 : 0;
const int right_height = root->right ? tree_height(root->right, max_distance) + 1 : 0;
const int distance = left_height + right_height;
if (max_distance < distance) max_distance = distance;
return (left_height > right_height ? left_height : right_height);
}
int tree_diameter(const Node* root)
{
int max_distance = 0;
if (root) tree_height(root, max_distance);
return max_distance;
}
1. 求二叉树中的节点个数
递归解法:
(1)如果二叉树为空,节点个数为0
(2)如果二叉树不为空,二叉树节点个数 = 左子树节点个数 + 右子树节点个数 + 1
参考代码如下:
2. 求二叉树的深度
递归解法:
(1)如果二叉树为空,二叉树的深度为0
(2)如果二叉树不为空,二叉树的深度 = max(左子树深度, 右子树深度) + 1
参考代码如下:
相当于广度优先搜索,使用队列实现。队列初始化,将根节点压入队列。当队列不为空,进行如下操作:弹出一个节点,访问,若左子节点或右子节点不为空,将其压入队列。
要求不能创建新节点,只调整指针。
递归解法:
(1)如果二叉树查找树为空,不需要转换,对应双向链表的第一个节点是NULL,最后一个节点是NULL
(2)如果二叉查找树不为空:
如果左子树为空,对应双向有序链表的第一个节点是根节点,左边不需要其他操作;
如果左子树不为空,转换左子树,二叉查找树对应双向有序链表的第一个节点就是左子树转换后双向有序链表的第一个节点,同时将根节点和左子树转换后的双向有序链 表的最后一个节点连接;
如果右子树为空,对应双向有序链表的最后一个节点是根节点,右边不需要其他操作;
如果右子树不为空,对应双向有序链表的最后一个节点就是右子树转换后双向有序链表的最后一个节点,同时将根节点和右子树转换后的双向有序链表的第一个节点连 接。
参考代码如下:
6. 求二叉树第K层的节点个数
递归解法:
(1)如果二叉树为空或者k<1返回0
(2)如果二叉树不为空并且k==1,返回1
(3)如果二叉树不为空且k>1,返回左子树中k-1层的节点个数与右子树k-1层节点个数之和
参考代码如下:
7. 求二叉树中叶子节点的个数
递归解法:
(1)如果二叉树为空,返回0
(2)如果二叉树不为空且左右子树为空,返回1
(3)如果二叉树不为空,且左右子树不同时为空,返回左子树中叶子节点个数加上右子树中叶子节点个数
参考代码如下:
- int GetLeafNodeNum(BinaryTreeNode * pRoot)
- {
- if(pRoot == NULL)
- return 0;
- if(pRoot->m_pLeft == NULL && pRoot->m_pRight == NULL)
- return 1;
- int numLeft = GetLeafNodeNum(pRoot->m_pLeft); // 左子树中叶节点的个数
- int numRight = GetLeafNodeNum(pRoot->m_pRight); // 右子树中叶节点的个数
- return (numLeft + numRight);
- }
8. 判断两棵二叉树是否结构相同
不考虑数据内容。结构相同意味着对应的左子树和对应的右子树都结构相同。
递归解法:
(1)如果两棵二叉树都为空,返回真
(2)如果两棵二叉树一棵为空,另一棵不为空,返回假
(3)如果两棵二叉树都不为空,如果对应的左子树和右子树都同构返回真,其他返回假
参考代码如下:
9. 判断二叉树是不是平衡二叉树
递归解法:
(1)如果二叉树为空,返回真
(2)如果二叉树不为空,如果左子树和右子树都是AVL树并且左子树和右子树高度相差不大于1,返回真,其他返回假
参考代码:
10. 求二叉树的镜像
递归解法:
(1)如果二叉树为空,返回空
(2)如果二叉树不为空,求左子树和右子树的镜像,然后交换左子树和右子树
参考代码如下:
11. 求二叉树中两个节点的最低公共祖先节点
递归解法:
(1)如果两个节点分别在根节点的左子树和右子树,则返回根节点
(2)如果两个节点都在左子树,则递归处理左子树;如果两个节点都在右子树,则递归处理右子树
参考代码如下:
递归解法效率很低,有很多重复的遍历,下面看一下非递归解法。
非递归解法:
先求从根节点到两个节点的路径,然后再比较对应路径的节点就行,最后一个相同的节点也就是他们在二叉树中的最低公共祖先节点
参考代码如下:
在上述算法的基础上稍加变化即可求二叉树中任意两个节点的距离了。
12. 求二叉树中节点的最大距离
即二叉树中相距最远的两个节点之间的距离。
递归解法:
(1)如果二叉树为空,返回0,同时记录左子树和右子树的深度,都为0
(2)如果二叉树不为空,最大距离要么是左子树中的最大距离,要么是右子树中的最大距离,要么是左子树节点中到根节点的最大距离+右子树节点中到根节点的最大距离,同时记录左子树和右子树节点中到根节点的最大距离。
参考代码如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
int GetMaxDistance(BinaryTreeNode * pRoot, int & maxLeft, int & maxRight) { // maxLeft, 左子树中的节点距离根节点的最远距离 // maxRight, 右子树中的节点距离根节点的最远距离 if(pRoot == NULL) { maxLeft = 0; maxRight = 0; return 0; } int maxLL, maxLR, maxRL, maxRR; int maxDistLeft, maxDistRight; if(pRoot->m_pLeft != NULL) { maxDistLeft = GetMaxDistance(pRoot->m_pLeft, maxLL, maxLR); maxLeft = max(maxLL, maxLR) + 1; } else { maxDistLeft = 0; maxLeft = 0; } if(pRoot->m_pRight != NULL) { maxDistRight = GetMaxDistance(pRoot->m_pRight, maxRL, maxRR); maxRight = max(maxRL, maxRR) + 1; } else { maxDistRight = 0; maxRight = 0; } return max(max(maxDistLeft, maxDistRight), maxLeft+maxRight); }
13. 由前序遍历序列和中序遍历序列重建二叉树
二叉树前序遍历序列中,第一个元素总是树的根节点的值。中序遍历序列中,左子树的节点的值位于根节点的值的左边,右子树的节点的值位
于根节点的值的右边。
递归解法:
(1)如果前序遍历为空或中序遍历为空或节点个数小于等于0,返回NULL。
(2)创建根节点。前序遍历的第一个数据就是根节点的数据,在中序遍历中找到根节点的位置,可分别得知左子树和右子树的前序和中序遍
历序列,重建左右子树。
同样,有中序遍历序列和后序遍历序列,类似的方法可重建二叉树,但前序遍历序列和后序遍历序列不同恢复一棵二叉树,证明略。
14.判断二叉树是不是完全二叉树
若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全
二叉树。
有如下算法,按层次(从上到下,从左到右)遍历二叉树,当遇到一个节点的左子树为空时,则该节点右子树必须为空,且后面遍历的节点左
右子树都必须为空,否则不是完全二叉树。
2.1 创建一颗二叉树
创建一颗二叉树,可以创建先序二叉树,中序二叉树,后序二叉树。我们在创建的时候为了方便,不妨用‘#’表示空节点,这时如果先序序列是:6 4 2 3 # # # # 5 1 # # 7 # #,那么创建的二叉树如下:
下面是创建二叉树的完整代码:穿件一颗二叉树,返回二叉树的根
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
//创建二叉树,这里不妨使用前序创建二叉树,遇到‘#’表示节点为空 BinTreeNode* BinTree::create_tree() { char item; BinTreeNode *t,*t_l,*t_r; cin>>item; if(item != '#') { BinTreeNode *pTmpNode = new BinTreeNode(item-48); t = pTmpNode; t_l = create_tree(); t->set_left(t_l); t_r = create_tree(); t->set_right(t_r); return t; } else { t = NULL; return t; } }
2.2 二叉树的遍历
二叉树的遍历分为:先序遍历,中序遍历和后序遍历,这三种遍历的写法是很相似的,利用递归程序完成也是灰常简单的、
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
//前序遍历 void BinTree::pre_order(BinTreeNode *r)const { BinTreeNode *pTmpNode = r; if(pTmpNode != NULL) { cout<<pTmpNode->get_data()<<" "; pre_order(pTmpNode->get_left()); pre_order(pTmpNode->get_right()); } } //中序遍历 void BinTree::in_order(BinTreeNode *r)const { BinTreeNode *pTmpNode = r; if(pTmpNode != NULL) { in_order(pTmpNode->get_left()); cout<<pTmpNode->get_data()<<" "; in_order(pTmpNode->get_right()); } } //后序遍历 void BinTree::post_order(BinTreeNode *r)const { BinTreeNode *pTmpNode = r; if(pTmpNode != NULL) { post_order(pTmpNode->get_left()); post_order(pTmpNode->get_right()); cout<<pTmpNode->get_data()<<" "; } }
2.3 层次遍历
层次遍历也是二叉树遍历的一种方式,二叉树的层次遍历更像是一种广度优先搜索(BFS)。因此二叉树的层次遍历利用队列来完成是最好不过啦,当然不是说利用别的数据结构不能完成。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
//层次遍历 void BinTree::level_order(BinTreeNode *r)const { if(r == NULL) return; deque<BinTreeNode*> q; q.push_back(r); while(!q.empty()) { BinTreeNode *pTmpNode = q.front(); cout<<pTmpNode->get_data()<<" "; q.pop_front(); if(pTmpNode->get_left() != NULL) { q.push_back(pTmpNode->get_left()); } if(pTmpNode->get_right() != NULL) { q.push_back(pTmpNode->get_right()); } } }
2.4 求二叉树中叶子节点的个数
树中的叶子节点的个数 = 左子树中叶子节点的个数 + 右子树中叶子节点的个数。利用递归代码也是相当的简单,易懂。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
//获取叶子节点的个数 int BinTree::get_leaf_num(BinTreeNode *r)const { if(r == NULL)//该节点是空节点,比如建树时候用'#'表示 { return 0; } if(r->get_left()==NULL && r->get_right()==NULL)//该节点并不是空的,但是没有孩子节点 { return 1; } //递归整个树的叶子节点个数 = 左子树叶子节点的个数 + 右子树叶子节点的个数 return get_leaf_num(r->get_left()) + get_leaf_num(r->get_right()); }
2.5 求二叉树的高度
求二叉树的高度也是非常简单,不用多说:树的高度 = max(左子树的高度,右子树的高度) + 1 。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
//获得二叉树的高度 int BinTree::get_tree_height(BinTreeNode *r)const { if(r == NULL)//节点本身为空 { return 0; } if(r->get_left()==NULL && r->get_right()==NULL)//叶子节点 { return 1; } int l_height = get_tree_height(r->get_left()); int r_height = get_tree_height(r->get_right()); return l_height >= r_height ? l_height + 1 : r_height + 1; }
2.6 交换二叉树的左右儿子
交换二叉树的左右儿子,可以先交换根节点的左右儿子节点,然后递归以左右儿子节点为根节点继续进行交换。树中的操作有先天的递归性。。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
//交换二叉树的左右儿子 void BinTree::swap_left_right(BinTreeNode *r) { if(r == NULL) { return; } BinTreeNode *pTmpNode = r->get_left(); r->set_left(r->get_right()); r->set_right(pTmpNode); swap_left_right(r->get_left()); swap_left_right(r->get_right()); }
2.7 判断一个节点是否在一颗子树中
可以和当前根节点相等,也可以在左子树或者右子树中。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
//判断一个节点t是否在以r为根的子树中 bool BinTree::is_in_tree(BinTreeNode *r,BinTreeNode *t)const { if(r == NULL) { return false; } else if(r == t) { return true; } else { bool has = false; if(r->get_left() != NULL) { has = is_in_tree(r->get_left(),t); } if(!has && r->get_right()!= NULL) { has = is_in_tree(r->get_right(),t); } return has; } }
2.8 求两个节点的最近公共祖先
求两个节点的公共祖先可以用到上面的:判断一个节点是否在一颗子树中。(1)如果两个节点同时在根节点的右子树中,则最近公共祖先一定在根节点的右子树中。(2)如果两个节点同时在根节点的左子树中,则最近公共祖先一定在根节点的左子树中。(3)如果两个节点一个在根节点的右子树中,一个在根节点的左子树中,则最近公共祖先一定是根节点。当然,要注意的是:可能一个节点pNode1在以另一个节点pNode2为根的子树中,这时pNode2就是这两个节点的最近公共祖先了。显然这也是一个递归的过程啦:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
//求两个节点的最近公共祖先 BinTreeNode* BinTree::get_nearest_common_father(BinTreeNode *r,BinTreeNode *pNode1,BinTreeNode *pNode2)const { //pNode2在以pNode1为根的子树中(每次递归都要判断,放在这里不是很好。) if(is_in_tree(pNode1,pNode2)) { return pNode1; } //pNode1在以pNode2为根的子树中 if(is_in_tree(pNode2,pNode1)) { return pNode2; } bool one_in_left,one_in_right,another_in_left,another_in_right; one_in_left = is_in_tree(r->get_left(),pNode1); another_in_right = is_in_tree(r->get_right(),pNode2); another_in_left = is_in_tree(r->get_left(),pNode2); one_in_right = is_in_tree(r->get_right(),pNode1); if((one_in_left && another_in_right) || (one_in_right && another_in_left)) { return r; } else if(one_in_left && another_in_left) { return get_nearest_common_father(r->get_left(),pNode1,pNode2); } else if(one_in_right && another_in_right) { return get_nearest_common_father(r->get_right(),pNode1,pNode2); } else { return NULL; } }
可以看到这种做法,进行了大量的重复搜素,其实有另外一种做法,那就是存储找到这两个节点的过程中经过的所有节点到两个容器中,然后遍历这两个容器,第一个不同的节点的父节点就是我们要找的节点啦。 实际上这还是采用了空间换时间的方法。
2.9 从根节点开始找到所有路径,使得路径上的节点值和为某一数值(路径不一定以叶子节点结束)
这道题要找到所有的路径,显然是用深度优先搜索(DFS)啦。但是我们发现DFS所用的栈和输出路径所用的栈应该不是一个栈,栈中的数据是相反的。看看代码:注意使用的两个栈。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
//注意这两个栈的使用 stack<BinTreeNode *>dfs_s; stack<BinTreeNode *>print_s; //打印出从r开始的和为sum的所有路径 void BinTree::print_rout(BinTreeNode *r,int sum)const { if(r == NULL) { return; } //入栈 sum -= r->get_data(); dfs_s.push(r); if(sum <= 0) { if(sum == 0) { while(!dfs_s.empty()) { print_s.push(dfs_s.top()); dfs_s.pop(); } while(!print_s.empty()) { cout<<print_s.top()->get_data()<<" "; dfs_s.push(print_s.top()); print_s.pop(); } cout<<endl; } sum += r->get_data(); dfs_s.pop(); return; } //递归进入左子树 print_rout(r->get_left(),sum); //递归进入右子树 print_rout(r->get_right(),sum); //出栈 sum += r->get_data(); dfs_s.pop(); }