前言
在面试过程中,关于二叉树的面试题绝对不在少数,这一块也是各个公司喜欢出的考题。本文总结了一些经典的关于二叉树的面试题,本文会持续更新。在本文中涉及的二叉树节点没有特别声明的情况则默认为:
typedef struct BTNode
{
int _value;
BTNode* _left;
BTNode* _right;
}BTNode* pBTNode;
正文
题目:输入一个整数和一棵二元树。从树的根结点开始往下访问一直到叶结点所经过的所有结点形成一条路径。打印出和与输入整数相等的所有路径。
分析:这是百度的一道面试题,这题主要考察面试者对递归和二叉树的理解
解法:当访问到某一结点时,把该结点添加到路径上,并累加当前结点的值。如果当前结点为叶结点并且当前路径的和刚好等于输入的整数,则当前的路径符合要求,我们把它打印出来。如果当前结点不是叶结点,则继续访问它的子结点。当前结点访问结束后,递归函数将自动回到父结点。因此我们在函数退出之前要在路径上删除当前结点并减去当前结点的值,以确保返回父结点时路径刚好是根结点到父结点的路径。我们不难看出保存路径的数据结构实际上是一个栈结构,因为路径要与递归调用状态一致,而递归调用本质就是一个压栈和出栈的过程。(来源:何海涛先生的博客)
代码:
void printAllRouteV(pNode root, int sum)
{
void _printAllRouteV(pNode, vector<int>&, int);
if(root == NULL) return ;
vector<int> q;
_printAllRouteV(root, q, sum);
}
void _printAllRouteV(pNode t, vector<int>& q, int sum)
{
if(t == NULL || t->value > sum) return;
if( (t->left == NULL && t->right == NULL) && sum == t->value)
{
//到达叶子节点,并且刚和和为给定的值
vector<int>::iterator iter = q.begin();
while(iter != q.end())
{
cout << *iter << " ";
iter++;
}
cout << t->value << endl;
return;
}
q.push_back(t->value);
_printAllRouteV(t->left, q, sum - t->value);
_printAllRouteV(t->right, q, sum - t->value);
q.pop_back();
}
题目:给定一棵二叉树和其中的两个节点X和Y,求出X和Y的公共最低祖先
解法1:自定向下的方法。该方法从根节点开始,分别判断其X和Y是否包含在其左/右子树中.当X和Y同时在左/右子树中时,则它们的最低公共祖先也必定在左/右子树中,此时递归进入左/右子树中。如果X和Y分别在不同的子树中,则当前节点为它们的共同祖先。
代码:
bool hasNode(pBTNode root, pBTNode x)
{
if(x == NULL || root == x) return true;
if(root == NULL) return false;
return (hasNode(root->_left, x) || hasNode(root->_right,x));
}
pBTNode LCA(pBTNode root, pBTNode X, pBTNode Y)
{
if(root == NULL) return NULL;
bool left_x = hasNode(root->_left, X);
bool left_y = hasNode(root->_left, Y);
if(left_x && left_y) return LCA(root->_left, X, Y); //Lowest common ancestor in left subtree
bool right_x = hasNode(right->_right, X);
bool right_y = hasNode(root->_right, Y);
if(right_x && right_y) return LCA(root->_right, X, Y); //Lowest common ancestor in right subtree
if((left_x && right_y) || (left_y && right_x)) return root; //root is the lowest common ancestor
return NULL; //At least one of node X and Y is not in the tree
}
解法2:
自底向上的方法。在自定向下的方法中,函数HasNode的本质就是遍历一棵树,其时间复杂度是O(n)(n是树中结点的数目)。由于我们根结点开始,要对每个结点调用函数HasNode。因此总的时间复杂度是O(n2)。但实际上我们可以通过自底向上的方法来解决这个问题,自底向上遍历结点,一旦遇到结点等于p或者q,则将其向上传递给它的父结点。父结点会判断它的左右子树是否都包含其中一个结点,如果是,则父结点一定是这两个节点p和q的LCA,传递父结点到root。如果不是,我们向上传递其中的包含结点p或者q的子结点,或者NULL(如果子结点不包含任何一个)。其实这种方法就利用的就是后序遍历,即首先判断左子树中是否包含其中的一个节点,然后判断右子树是否包含其中的而一个节点,最后到根节点来进行判断,这样一来便能够避免自顶向下的重复计算,该方法的复杂度是O(n)。
代码:
/**
*Note: This method require the user to make sure X and Y are in the tree
*/
pBTNode LCA(pBTNode root, pBTNode X, pBTNode Y)
{
if(root == NULL) return NULL;
if(root == X || root == Y) return root;
pBTNode l = LCA(root->_left, X, Y);
pBTNode r = LCA(root->_right, X, Y);
if(l != NULL && r != NULL) return root;
return (l!=NULL)?l:r;
}
扩展:
这个问题又两个扩展形式:
1.二叉树是BST(二叉查找树)
这种情况下问题比较简单,对于当前节点,如果两个节点的值均大于当前节点值,则它们的最低公共祖先在当前节点的右子树中;如果它们的值都小于当前节点的值,则它们的最低公共祖先节点在当前节点的左子树中。否则当前节点为它们的共同祖先节点。(为了确保给定的两个节点在二叉树中,我们可以在找到这个祖先节点之后,从这个节点出发分辨查找它们是否存在)
2.二叉树的节点结构中多了一个指向父节点的指针
这种情况下,从一个节点沿着它的父亲指针能够得到一条从当前节点到根节点的一条单链表。那么这个问题实际上就变成找出两条单链表的第一个交点问题了。这个问题的解法可以参见何海涛先生的博客:http://zhedahht.blog.163.com/blog/static/254111742008053169567/
题目:给定一棵二叉树的前序遍历序列和中序遍历序列,要求构造出这棵二叉树
解法:这个问题的关键在于如何根据前序和中序遍历序列确定出树的根节点以及左子树和右子树分别包含的值。根据前序遍历的特点,我们知道该序列的第一个值即是树的根节点的值,这样我们便可以在中序序列中找到这个值出现的位置,那么根据中序遍历的特性,便可以知道该位置左边的所有值均属于左子树,其右边的值属于右子树。然后我们便可以统计出左子树的节点个数,根据这个个数信息便可以在前序遍历序列中找到左子树和右子树的前序遍历序列。最后递归重构左子树和右子树即可。
代码:
//
//利用前序和中序遍历重建二叉树
#include <iostream>
using namespace std;
struct BTNode{
BTNode *pLeft;
BTNode *pRight;
char chValue;
};
void delete_tree(BTNode *root)
{
if(root == NULL) return;
delete_tree(root->pLeft); //删除左子树
delete_tree(root->pRight); //删除右子树
delete root;
}
BTNode* rebuild_bin_tree_impl(char *pre_order, int pre_start, int pre_end, char *in_order, int in_start, int in_end)
{
if(pre_order == NULL || in_order == NULL) return NULL;
if(pre_start >= pre_end || in_start >= in_end ) return NULL;
if(pre_end - pre_start != in_end - in_start) throw "pre_order and in_order string don't match";
BTNode *root = new BTNode();
root->chValue = pre_order[pre_start];
int i = in_start;
while(i < in_end && in_order[i] != pre_order[pre_start]) i++; //找到左右子树的分界点
if(i == in_end)
{
//输入的字符串有问题
delete root;
throw "pre_order and in_order string don't match";
}
try
{
root->pLeft = rebuild_bin_tree_impl(pre_order, pre_start+1, pre_start + i - in_start + 1, in_order, in_start, i); //构建左子树
root->pRight = rebuild_bin_tree_impl(pre_order, pre_start + i - in_start + 1, pre_end, in_order, i+1, in_end); //构建右子树
}
catch (...)
{
delete root;
throw;
}
return root;
}
BTNode* rebuild_bin_tree(char *pre_order, char *in_order)
{
return rebuild_bin_tree_impl(pre_order, 0, strlen(pre_order), in_order, 0, strlen(in_order));
}
void post_visit(BTNode *root)
{
if(root == NULL) return;
post_visit(root->pLeft);
post_visit(root->pRight);
cout << root->chValue << " ";
}
int main()
{
char *pre_order = "baaaa";
char *in_order = "aabaa";
BTNode *root = NULL;
try
{
root = rebuild_bin_tree(pre_order, in_order);
}
catch(char *e)
{
cout << e << endl;
}
post_visit(root);
delete_tree(root);
root = NULL;
system("pause");
}
题目:给定一个二叉查找树,只允许修改指针的情况下,将二叉树改成已序的双链表
解法:由于二叉查找树的中序遍历便是一个有序的序列,所以我们这里需要做的就是利用中序遍历方式来完成转换过程
代码:
pBTNode toDblLinkLst(pBTNode root)
{
if(root == NULL) return NULL;
stack<pNode> s;
pBTNode header = NULL, t = root, tmp = NULL, p;
while(t != NULL) { s.push(t); t = t->left;}
header = s.top();
p = header;
s.pop();
t = header->right;
while(t != NULL) {s.push(t); t = t->left;}
while(!s.empty())
{
tmp = s.top();
t = tmp->right;
s.pop();
p->right = tmp;
tmp->left = p;
p = p->right;
while(t != NULL) { s.push(t); t = t->left;}
}
return header;
}
题目:判断一棵二叉树是否为平衡二叉树
分析:这个题目的最直观的的方法就是通过求节点的高度来递归的判断每一个棵子树是否平衡,当所有子树均平衡时,则该二叉树为平衡二叉树。这种方法的代码实现如下:
bool IsBalanced(pBTNode root)
{
if(root == NULL)
return true;
int left = TreeDepth(pRoot->m_pLeft);
int right = TreeDepth(pRoot->m_pRight);
int diff = left - right;
if(diff > 1 || diff < -1)
return false;
return IsBalanced(pRoot->m_pLeft) && IsBalanced(pRoot->m_pRight);
}
这中方法虽然实现起来很方便且代码简洁,但是由于这里节点的高度存在重复计算,所以效率不高。为了提高效率我们只需要在遍历的过程中记录下节点的深度信息即可。根据这种思路的实现代码如下:
/**
*当二叉树平衡时,该函数返回该二叉树的高度;否则返回-1
*/
int _isBalance(pBTNode root)
{
if(root == NULL) return 0;
int l = _isBalance(root->_left);
int r = _isBalacne(root->_right);
if(l == -1 || r == -1 || abs(l-r) > 1) return -1; //返回-1表示以root为根节点的子树不平衡
return ((l>r)?l:r)+1; //以root为根节点的子树为平衡子树
}
/**
*判断一棵二叉树是否为平衡二叉树
*/
bool isBalance(pBTNode root)
{
return (_isBalance(root) != -1);
}
注:这个题目大家也可以参见何海涛先生的博客:
http://zhedahht.blog.163.com/blog/static/25411174201142733927831/
题目:判断一个输入序列是否是某棵二叉查找树的后序遍历结果
分析:这题主要考查的是我们对二叉查找树以及二叉树的后续遍历过程的理解
解法:在后续遍历得到的序列中,最后一个元素为树的根结点。从头开始扫描这个序列,比根结点小的元素都应该位于序列的左半部分;从第一个大于跟结点开始到跟结点前面的一个元素为止,所有元素都应该大于跟结点,因为这部分元素对应的是树的右子树。根据这样的划分,把序列划分为左右两部分,我们递归地确认序列的左、右两部分是不是都是二元查找树。
代码:
/**
*判断一个序列是否为二叉查找树的后序遍历序列
*/
bool verifyPostSequece(int *arr, int n)
{
if(arr == NULL || n <= 0) return false;
int root = arr[n-1];
int i = 0;
for(i = 0; i < n-1 && arr[i] < root; i++); //找到左子树和右子树的分界点
int j = i;
for(;j < n-1 && arr[j] > root; j++);
if(i < n-1 && j != n-1) return false; //如果右子树序列中有小于根节点,则该序列不是二叉查找树的后序遍历序列
bool left = true; //左子树序列是否为二叉查找树遍历序列标识
if(i > 0) left = verifyPostSequece(arr, i);
bool right = true; //右子树序列是否为二叉查找树遍历序列标识
if(i < n-1) right = verifyPostSequece(arr+i, n-i-1);
return (left && right);
}
题目:层序遍历二叉树,按一层一行的形式输出
分析:这个问题本质上是在考察我们队二叉树层序遍历的理解。 我们对二叉树的层序遍历都比较熟悉,只需要利用一个队列保存待访问的节点。首先将根节点加入队尾,然后只要队列不空,则去除队头节点,访问该节点并将该节点的左右子节点加入队尾;这样一直到队列为空,此时则已经按层序方式遍历了所有的节点。但是这种方法无法让我们按照一层一行输出,因此我们需要一种方式能够记录每一层的节点信息。
解法1:利用两个队列的方式。设这两个队列分别为q1和q2,首先将根节点插入到队列q1的队尾,然后只要q1或q2不为空,我们则进行按一下步骤进行遍历输出节点
1.如果q1不空,则q1设为当前访问队列,而q2设为存储队列用于存储访问节点的子节点;否则相反,q1为存储队列,q2设为访问队列
2.只要访问队列不为空,则取访问队列队头节点,输出该节点信息并将其左右子节点一次插入存储队列的队尾;
3.输出换行符,并进行下一次循环
代码:
void BTLayerTraverse(pBTNode root)
{
queue<pBTNode> q1, q2;
q1.push(root);
queue<pBTNode> *visitQ = &q1, *storeQ = &q2; //初始时q1作为访问队列,q2作为存储队列
//只要访问队列不为空,则遍历未结束
while(!(visitQ->empty()))
{
pBTNode currNode = visitQ->front();
visitQ->pop();
//输出当前节点信息
cout << currNode->_value << " ";
//将当前节点左右子节点一次插入存储队列
if(currNode->_left != NULL) storeQ->push(currNode->_left);
if(currNode->_right != NULL) storeQ->push(currNode->_right);
//若访问队列为空,则说明一层已经访问完毕
if(visitQ->empty())
{
cout << endl;
//交换访问队列和存储队列
queue<pBTNode> *temp = visitQ;
visitQ = storeQ;
storeQ = temp;
}
}
}
解法2:我们需要里用一个队列和两个int变量a和b,初始化a和b设为0。首先将根节点加入队列队尾,并设b=1。然后只要队列不为空则按以下步骤进行,即可完成:
1.取出队头节点输出该节点信息,并将该节点的左右子节点加入队尾,并且将a+=1;
2.当a==b时,说明一层已经访问完毕,所以输出一个换行符,然后设置a=0,b设为队列的元素个数;
代码:
void BTLayerTraverse(pBTNode x)
{
if(x == NULL) return;
queue<pBTNode> q;
int a = 0, b = 1, i = 0;
q.push(x);
while(!q.empty())
{
pBTNode tmp = q.front();
q.pop();
cout << tmp->_value << " ";
//将当前节点的左右孩子节点依次加入队列
if(tmp->_left != NULL) q.push(tmp->_left);
if(tmp->_right != NULL) q.push(tmp->_right);
a++;
if(a == b)
{
cout << endl;
a = 0;
b = q.size();
}
}
}
注:我们只需要对解法2 进行简单的修改便能够用来计算一棵树的宽度了。
题目:
给定一个已序(升序)数组,要求用这个数组元素创建一棵搜索二叉树,使得二叉树的高度最小
解析:
当涉及到二叉树时,我们最先想到的便是递归。毫不例外,这个问题也可以用递归来解决。算法主要思想就是,对于任何一个节点,我们都尽可能的使其左右子树的节点数相等。算法步骤如下:
1.取数组的中位数作为二叉树的根节点;
2.取数组前半部分的中位数作为根节点的左子树,插入二叉树中;
3.取数组后半部分的中位数作为根节点的有字数,插入二叉树中;
就这样,一直递归的将数组不断的二分,并插入到二叉树的合适位置,最终便能够得到一颗完全二叉树了
BTNode* _createBTree(int *arr, int begin, int end)
{
if(arr == NULL || begin > end || begin < 0 || end < 0) return NULL;
int mid = begin + ((end-begin) >> 1);
BTNode *t = new BTNode;
t->_value = arr[mid];
t->left = _createBTree(arr, begin, mid - 1); //数组前半部分为二叉树的左子树
t->right = _createBTree(arr, mid+1, end); //数组的后半部分为二叉树的右子树
return t;
}//_createBTree
BTNode* createBTree(int *arr, int n)
{
if(arr == NULL || n <= 0) return NULL;
return _createBTree(arr, 0, n-1);
}//createBTree