常见技巧
1、与二叉树有关的代码有大量指针操作,每次使用指针的时候,都要问自己有没有可能为nullptr,如果是该怎么处理。
2、如果面试题要求处理一棵树的遍历序列,可先找到二叉树的根节点,再基于根节点将序列拆分为左子树对应的子序列和右子树对应的子序列,接下来再递归的处理两个子序列。
3、几乎全是考察树的遍历
4、大部分题目可用递归解题:递归函数的组织方式一般是,当前节点怎么考虑,左子树递归,右子树递归。
01 面试题7:重建二叉树
题意:输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。
1、根据前序遍历序列的第一个数字创建根节点
2、在中序序列找到根节点的位置,确定左、右子树的数量
3、在前、中序序列划分了左右子树范围后,递归调用函数去分别构建左右子树
TreeNode* reConstructBinaryTree(vector<int> pre,vector<int> vin) {
int length1 = pre.size();
int length2 = vin.size();
if(length1 != length2 || length1 < 1 || length2 < 1)
return nullptr;
int length = length1;
return ConstructCore(pre, vin, 0, length -1, 0, length -1);
}
//要理解递归函数的参数和返回值。该函数输入为(先序序列,中序序列,先序序列首位置,末位置,中序序列首位置,末位置)
//输出为 “该段树” 的根节点的指针
TreeNode* ConstructCore(vector<int> &pre, vector<int> &vin,
int startPre, int endPre, int startIn, int endIn){
if(startPre > endPre || startIn > endIn) //递归终止条件
return nullptr;
TreeNode* root = new TreeNode(pre[startPre]); //新建根节点
for(int i = startIn; i <= endIn; i++){
if(vin[i] == pre[startPre]){ //找到中序遍历序列中的根节点
root->left = ConstructCore(pre, vin, startPre+1, startPre+i-startIn, startIn, i-1);
root->right= ConstructCore(pre, vin, startPre+i-startIn+1, endPre, i+1, endIn);
break;
}
}
return root;
}
02 面试题8:二叉树的下一个节点
题意:给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。
本题需要画图和举例来具体分析规律,中序遍历:左、根、右
分类讨论:
1、该节点有右子树:下一节点是右子树的最左子节点
2、没有右子树:
(1)是父节点的左孩子:下一节点是父节点
(2)是父节点的右孩子:往上父方向遍历,直到找到1个节点,是它父亲的左孩子,它父亲即为所求
TreeLinkNode* GetNext(TreeLinkNode* pNode)
{
if(pNode == nullptr)
return nullptr;
TreeLinkNode* pNext = nullptr;
if(pNode -> right){ //该节点有右子树
TreeLinkNode* pRight = pNode -> right;
while(pRight -> left){
pRight = pRight -> left;
}
pNext = pRight; //返回右子树的最左子节点
}
else if(pNode -> right == nullptr ){ //没有右子树
if(pNode -> next == nullptr) //如果也没有父亲(根节点)
return nullptr;
TreeLinkNode* pParent = pNode -> next;
if(pParent -> left == pNode){ //没有右子树,但是是父节点的左孩子
pNext = pParent; //返回父亲
}
else if(pParent -> right ==pNode){ //没有右子树,是父节点的右孩子
TreeLinkNode* pCurrent = pNode;
while(pParent != nullptr && pCurrent == pParent ->right){
pCurrent = pParent;
pParent = pParent -> next;
}
pNext = pParent;
}
}
return pNext;
}
03 面试题26:树的子结构
题意:输入两棵二叉树A,B,判断B是不是A的子结构.约定空树不是任意一个树的子结构
先判断根是否相等,相等的话再比较左孩子和右孩子,左右的比较肯定是递归。当然不是的话,我们还要把左孩子和root2树做同样的对比。
1、遍历(递归更简洁)二叉树A,发现某点R的值和B的根节点相同,则调用DoesTree1HaveTree2()函数
2、判断A中以R为根的子树是否和B右相同的结构。也可用递归。如果以R和B值相同,则递归判断各自的左右节点的值是否相同。终止条件是到达了A或B的叶子节点。
bool HasSubtree(TreeNode* pRoot1, TreeNode* pRoot2)
{
if(pRoot1 == nullptr || pRoot2 == nullptr)
return false;
bool res = false; //标记
if(pRoot1->val == pRoot2->val) res = DoesTree1HaveTree2(pRoot1, pRoot2);
//如果不等
if(!res) res = HasSubtree(pRoot1 -> left, pRoot2);
//如果还不等
if(!res) res = HasSubtree(pRoot1 -> right, pRoot2);
return res;
}
//判断是否具有相同结构
bool DoesTree1HaveTree2(TreeNode* pRoot1, TreeNode* pRoot2){
//第一次进该函数肯定都不为空,这是递归结束条件
if(pRoot2 == nullptr) //如果Tree2到头了,都对应了,返回true
return true;
if(pRoot1 == nullptr) //如果Tree2没到头,Tree1却到头了,返回false
return false;
//第一次进该函数肯定相等,这也是递归结束条件,之后不相等了返回false
if( !(pRoot1 -> val == pRoot2 -> val) )
return false;
return DoesTree1HaveTree2(pRoot1->left, pRoot2->left) && DoesTree1HaveTree2(pRoot1->right, pRoot2->right);
}
04 面试题27:二叉树的镜像
题意:操作给定的二叉树,将其变换为源二叉树的镜像。
画图,理解过程:先序遍历每个节点,如果节点有子节点,就交换。可用递归和非递归实现
//递归:左右子树都镜像好,然后交换
void Mirror(TreeNode *pRoot) {
//如果pRoot为空,或者根节点没孩子,则返回
if(pRoot == nullptr || (pRoot->left == nullptr && pRoot->right == nullptr))
return;
//有孩子:孩子子树镜像好,再交换位置
Mirror(pRoot->left);
Mirror(pRoot->right);
TreeNode *tmp;
tmp = pRoot->left;
pRoot->left = pRoot->right;
pRoot->right = tmp;
return;
}
//使用队列的非递归,也可以使用栈的非递归。这里出入的顺序无所谓,只有被处理就行
void Mirror(TreeNode *pRoot) {
if(pRoot == nullptr) return;
queue<TreeNode*> qu;
qu.push(pRoot);//根入队列
TreeNode* p = nullptr;
while(qu.size()){
p = qu.front();
qu.pop();
//交换
TreeNode* tmp = p -> left;
p -> left = p -> right;
p -> right = tmp;
if(p -> left) qu.push(p -> left);
if(p -> right) qu.push(p -> right);
}
}
05 面试题28:对称的二叉树
题意:请实现一个函数,用来判断一颗二叉树是不是对称的。注意,如果一个二叉树同此二叉树的镜像是同样的,定义其为对称的。
(1)定义两种先序遍历:根,左,右;根,右,左。两种都走一遍,存到两个容器,再逐个元素比较是否相等,叶子节点的空孩子存为0,以防止结构不相等但是所有值都相等的情况。
bool isSymmetrical(TreeNode* pRoot)
{
if(pRoot == nullptr)
return true;
vector<int> vc1; vector<int> vc2;
//两种方式的前序遍历都走一遍,存到两个容器,再逐个元素比较是否相等
InOrder1(pRoot, vc1); InOrder2(pRoot, vc2);
int len1 = vc1.size(); int len2 = vc2.size();
if(len1 != len2) return false;
for(int i = 0; i < len1; ++i){
if(vc1[i] != vc2[i])
return false;
}
return true;
}
//前序遍历:根,左,右
void InOrder1(TreeNode* pRoot, vector<int>& vc){
//碰到空指针,写0,前序遍历有用,中序没用
if(pRoot == nullptr){
vc.push_back(0);
return;
}
vc.push_back(pRoot -> val);
InOrder1(pRoot -> left, vc);
InOrder1(pRoot -> right, vc);
}
//前序遍历:跟,右,,左
void InOrder2(TreeNode* pRoot, vector<int>& vc){
if(pRoot == nullptr){
vc.push_back(0);
return;
}
vc.push_back(pRoot -> val);
InOrder2(pRoot -> right, vc);
InOrder2(pRoot -> left, vc);
}
(2)左子树的右子树和右子树的左子树应该相等。
bool isSymmetrical(TreeNode* pRoot)
{
return isSymmetrical(pRoot, pRoot);
}
bool isSymmetrical(TreeNode* pRoot1, TreeNode* pRoot2){
if(pRoot1 == nullptr && pRoot2 == nullptr) return true;//俩都为空
if(pRoot1 == nullptr || pRoot2 == nullptr) return false;//一个为空一个不为空
//俩都不为空
if(pRoot1 -> val != pRoot2 -> val) return false;
//如果相等,则判断pRoot1的左子树与pRoot2的右子树、pRoot1的右子树与pRoot2的左子树是否对称
return isSymmetrical(pRoot1 -> left, pRoot2 -> right) && isSymmetrical(pRoot1 -> right, pRoot2 -> left);
}
06 面试题32:从上到下打印二叉树
题意1:从上到下按层打印二叉树,同一层结点从左至右输出。每一层输出一行。
层次遍历,用一个队列。出队的同时它的孩子分别入队。广度优先遍历的退化形式,都要用到队列。
题意2:分行打印。
也是用队列,但是额外需要两个变量:toBePrinted保存当前层没有被打印的点数,nextLevel保存下一层的点数。出栈的时候有孩子nextLevel++,toBePrinted–。toBePrinted==0的时候,toBePrinted = nextLevel,nextLevel = 0;
类似于求二叉树的深度
题意3:之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印
举例找规律
准备两个栈。打印奇数层(奇数层出栈)的时候,其左孩子先于右孩子入另一个栈;打印偶数层时,其右孩子先于左孩子入另一个栈
07 面试题33:二叉搜索树的后序遍历序列
题意:输入一个数组,判断是不是某二叉搜索树的后序遍历的结果。假设输入的数组的任意两个数字都互不相同。
递归思路。首先要想清楚二叉搜索树的性质:左子树 < 根节点 < 右子树。后序遍历的特点是:根节点在最后。所以对于二叉搜索树的后序遍历,根节点在最后,最前面一部分是左子树(小于根),之后一部分是右子树(大于根)。
1、确定root(数组最后一个元素end位置)
2、从头遍历序列,找到第一个 > root的元素,位置为m,0m-1为左子树,mend-1为右子树
3、白遍历右子树,若发现右小于root的,返回false;
4、重复1,2,3 递归判断左、右子树是否认为二叉搜索树。
08 面试题34:二叉树中和为某一值的路径
题意:输入一颗二叉树的根节点和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。
1、访问某一节点时,先把该节点添加到路径,并累加该节点的值。
2、若该节点为叶子,且路径中值等于输入,则保存该路径。
3、若不是叶子,或是叶子但是值不相等,则访问子节点。
vector<vector<int> > FindPath(TreeNode* root,int expectNumber) {
vector<vector<int>> res;
if(root == nullptr)
return res;
int currentSum = 0;
vector<int> path;
FindPath(root, expectNumber, path, currentSum, res);
return res;
}
void FindPath(TreeNode* root, int expectNumber, vector<int>& path, int currentSum, vector<vector<int>>& res){
if (root == nullptr) return;
currentSum += root -> val; //访问到了该根节点
path.push_back(root -> val);
bool isLeaf = (root->left == nullptr) && (root->right == nullptr);
if(isLeaf && (currentSum == expectNumber)) //如果节点是叶子,且路径和等于输入值,则保存该路径
res.push_back(path);
else{ //如果不是叶子节点,则遍历它的子节点
if(root->left != nullptr)
FindPath(root->left, expectNumber, path, currentSum, res); //递归函数内,只考虑一个节点的所有情况。
if(root->right != nullptr)
FindPath(root->right, expectNumber, path, currentSum, res);
}
path.pop_back();//返回父节点之前,在路径上删除该节点
}
};
09 面试题36:二叉搜索树与双向链表
题意:输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。
(1)非递归实现:核心是中序遍历的非递归算法(二叉搜索树的中序刚好是排序的),每遍历一点,修改当前遍历节点与前一遍历节点的指针指向。
TreeNode* Convert(TreeNode* pRootOfTree)
{
//1.核心是中序遍历的非递归算法。2.修改当前遍历节点与前一遍历节点的指针指向。
if(pRootOfTree == nullptr) return nullptr;
stack<TreeNode*> st;
TreeNode* pCur = pRootOfTree; //保存中序遍历序列的当前节点
TreeNode* pPre = nullptr;// 用于保存中序遍历序列的上一节点
bool isFirst = true; //表示首节点
while(pCur != nullptr || !st.empty()){
//找到最左边的节点
while(pCur != nullptr){
st.push(pCur);
pCur = pCur -> left;
}
//最左边节点出栈,访问
pCur = st.top();
st.pop();
if(isFirst){ //找双向链表的第一个节点
pRootOfTree = pCur;// 将中序遍历序列中的第一个节点记为root
pPre = pRootOfTree;
isFirst = false;
}else{
pPre -> right = pCur;
pCur -> left = pPre;
pPre = pCur;
}
pCur = pCur -> right;
}
return pRootOfTree;
}
(2) 递归实现:
1、左子树(如果有)构造成双链表,返回链表头(可以尾)节点
2、定位到左子树尾节点,当前节点和 左子树尾节点连接
3、右子树构造成双链表,返回头
4、右子树头和当前节点连接
5、根据左子树是否为空返回(左子树的头/当前节点)
10 面试题37:序列化二叉树
题意:二叉树的序列化是指:把一棵二叉树按照某种遍历方式的结果以某种格式保存为字符串,序列化时通过 某种符号表示空节点(#),以 ! 表示一个结点值的结束(value!)。二叉树的反序列化是指:根据某种遍历顺序得到的序列化字符串结果str,重构二叉树。
注意用先序遍历。
11 面试题40:最小的k个数
题意:输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4,。
(1)利用 Partition 函数。O(n)
vector<int> GetLeastNumbers_Solution(vector<int> input, int k) {
vector<int> res;
int len = input.size();
if(k > len || len <= 0 || k <= 0) return res;
int start = 0; int end = len - 1;
int index = partition(input, start, end);
while(index != k - 1) //不断循环直到index == k-1跳出循环
{
if(index > k - 1)
{
end = index - 1;
index = partition(input, start, end);
}
else
{
start = index + 1;
index = partition(input,start, end);
}
}
for(int i = 0; i < k; ++i)
res.push_back(input[i]);
return res;
}
(2)创建一个容器来存储最小的k个数字。适合于处理海量数据。O(n*logk)
a) 先把前k个放入容器
b) 容器满了之后,做三件事:
1) 在容器的k个整数中找到最大的数字
2) 比较当前数和最大树
3) 可能要删除容器的最大数 并且 插入新数
该容器可以为树
12 面试题54:二叉搜索树的第k大节点
题意:给定一颗二叉搜索树,请找出其中的第k大的结点。。
中序遍历,第k个。用非递归写法方便
13 面试题55:二叉树的深度
题意1:输入一棵二叉树,求该树的深度。
(1)递归。哪个孩子的深度大,就把这个深度加上自己本身的1,就是实际的深度。
(2)非递归。队列实现层次遍历,多加两个变量count 和nextCount。类似于分行打印二叉树。
int TreeDepth(TreeNode* pRoot)
{
if(pRoot == nullptr)
return 0;
/*** 递归写法
int nleft = TreeDepth(pRoot -> left);
int nright = TreeDepth(pRoot -> right);
return (nleft > nright) ? (nleft + 1) : (nright + 1);
*/
queue<TreeNode*> qu;
qu.push(pRoot);
int count = 0;
int nextCount = 1; //下一层
int depth = 0;
while(qu.size()){
TreeNode* top = qu.front();
qu.pop();
count++;
if(top->left)
qu.push(top->left);
if(top->right)
qu.push(top->right);
if(count == nextCount){ //本层全出栈了,即遍历了
nextCount = qu.size(); //下一层的数目等于尚在队列中的数目
count = 0; //数目置零,等待为下一层遍历个数计数
depth++; //遍历完一层,深度+1
}
}
return depth;
}
14 面试题68:树中两个节点的最低公共祖先
题意:这是一组题目
(1)如果是二叉搜索树:把两个节点的值和根节点比较,如果都小于根节点,就都跟其左孩子比较;如果都大于根,就都跟其右孩子比较;如果一大一小,则根节点就是最低公共祖先了。
(2)如果有指向父节点的指针:可转化为求两个链表的第一个公共节点。
(3)普通树: 遍历两次,用两个链表保存从根到该节点的路径,再求这两个链表的最后一个公共节点