// #  -*- coding:utf-8 -*-
// #  @Author: Mr.chen(ai-chen2050@qq.com)
// #  @Date: 2018-08-17 16:32:55 // 注:此为第三弹,主要讲解树(但是面试中大多提到的都是二叉树)相关面试笔试题,要求手写。
// 在树的面试题中也会涉及到大量的指针,对于二叉树来说最应该先关注的就是它的前中后序遍历,
// 对于它们来说都有递归和非递归遍历,还有层次遍历,说明如下图所示:


/*
1、重建二叉树,输入某二叉树的前序和中序,请重建该二叉树。假设输入的前序和中序序列都不含重复的数字,例如输入前序的序列为{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6} 思路:首先前序序列中的第一个节点一定是根节点,中序序列中,根节点在中间,其左右子树在其两边,故我们可以根据中序找到根节点,左边的即为左子树,右边的即为右左子树,之后再其左右两边的序列递归去构造生成二叉树。代码如下:
*/#include<iostream>
#include<set>
#include<map>
#include<stack>
#include<deque>
#include<list>
#include<stdexcept>using namespace std;// 二叉树的节点结构体为:如下。
struct BiTreeNode
{int         m_value;BiTreeNode* m_pLeft;BiTreeNode* m_pRight;
};BiTreeNode* Construct(int* preOrder,int* inOrder,int length)
{if(preOrder == NULL || inOrder == NULL || length <= 0)return NULL;return ConstructCore(preOrder,preOrder+length-1,inOrder,inOrder+length-1);
}BiTreeNode* ConstructCore(int* sPreOrder,int* ePreOrder,int* sInOrder,int* eInOrder)
{// 前序遍历的第一个节点是根节点的值int rootValue = sPreOrder[0];BiTreeNode* root = new BiTreeNode();root->m_value = rootValue;root->m_pLeft = root->m_pRight = NULL;// 定义递归出口if(sPreOrder == ePreOrder){if(sInOrder == eInOrder && *sPreOrder == *sInOrder)return root;else throw std::exception("Invalid input.");}// 在中序中找到根节点的值int* rootInOrder = sInOrder;while(rootInOrder < eInOrder && *rootInOrder != rootValue)++ rootInOrder;if(rootInOrder == eInOrder && *rootInOrder != rootValue)throw std::exception("Invalid input...")// 左子树的节点个数int leftLength = rootInOrder - sInOrder;int* leftPreOrderEnd = sPreOrder + leftLength;if(leftLength > 0)                                  //构建左子树   root->m_pLeft = ConstructCore(sPreOrder + 1,leftPreOrderEnd,sInOrder,rootInOrder - 1);if(leftLength < ePreOrder - sPreOrder)               //构建右子树root->m_pRight = ConstructCore(leftPreOrderEnd+1,ePreOrder,rootInOrder+1,eInOrder)return root;
}/*
2、求二叉树的深度:输入一颗二叉树的根节点,求该树的深度。从根节点到页节点依次经过的节点(含根和叶节点)形成树的一颗路径,最长路径的长度即为树的深度。思路:我们可以从另外一个角度来考虑树的深度。如果树只有一个节点,则深度为 1,如果根节点只有左子树而没有右子树,则树的深度应该是左子树加 1,反之,是右子树加 1,故我们很容易想到递归实现。
*/
int treeDepth(BiTreeNode* pRoot)
{if(NULL == pRoot)return 0;// 计算左右子树的深度int nLeft = treeDepth(pRoot->m_pLeft);int nRight = treeDepth(pRoot->m_pRight);return nLeft > nRight? (nLeft + 1):(nRight + 1);
}/*
2.1 面试官加深难度, 追问: 输入一颗二叉树的根节点,判断它是不是平衡二叉树,如果某二叉树中任意节点的左右子树深度相差不超过 1,那么它就是一颗平衡二叉树。法一:需要重复遍历一个节点多次,基于上述代码的改进,法二:采用后序遍历的方法遍历每一个节点,在遍历一个节点之前,我们就已经遍历了该节点的左右子树。只要在遍历一个节点的时候,记录它的深度,就可以一边遍历,一边判断该节点是否平衡的了。
*///法一:需要重复遍历一个节点多次
bool isBalanced(BiTreeNode* pRoot)
{if(NULL == pRoot)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);
}   // 由于重复遍历会影响性能,故我们推荐下面这种基于后序遍历的方法。// 法二:每个节点只遍历一次
bool isBalanced(BiTreeNode* pRoot,int* pDepth)
{if(NULL == pRoot){*pDepth = 0;return true;}int left,right;if(isBalanced(pRoot->m_pLeft, &left) && isBalanced(pRoot->m_pRight, &right)){int diff = left - right;if(diff <= 1 && diff >= -1){*pDepth = 1 + (left > right? left:right );return true;}}return false;
}// 调用时,只需给上面的函数传入一个二叉树的根节点和表示节点深度的整形变量即可。
bool isBalancedTree(BiTreeNode* pRoot)
{int depth = 0;return isBalanced(pRoot, &depth);
}

/*
3、树的子结构,题目:输入两颗二叉树 A 和 B,判断 B 是不是 A 的子结构,子结构定义如下图:一般来说,树的指针的操作比起链表来说更多,如果面试官想加大难度,则会考察树,在涉及二叉树的题目中,由于涉及到很多的指针操作,故一定要注意检查边界条件,即检查空指针,如果没有检查,而导致程序崩溃,则是很尴尬和忌讳的事情。故在操作指针时,一定要注意这个指针是不是为 NULL,如果是,该怎么处理。本题可以分两步来看,第一步是从树 A 中查找与根节点值一样的节点,实际上是对树的遍历,一般遍历树可以有递归和迭代循环,一般递归代码较为简单,一般会优先考虑,第二步是判断 A 树中以 R 为根节点的子树是不是和树 B 具有相同的结构,同样,我们也可以采用递归的方法来做。
*/


// 第一步: 递归遍历,寻找相同节点
bool hasSubTree(BiTreeNode* pRoot1,BiTreeNode* pRoot2)
{bool result = false;// 异常值检测if(NULL != pRoot1 && NULL != pRoot2){if(pRoot1->m_value == pRoot2->m_value)result = doseTree1HasTree2(pRoot1,pRoot2);if(!result)result = hasSubTree(pRoot1->m_pLeft,pRoot2);if(!result)result = hasSubTree(pRoot1->m_pRight,pRoot2);}return result;
}// 第二步:比较子结构,同样可以用递归的方法
bool doseTree1HasTree2(BiTreeNode* pRoot1,BiTreeNode* pRoot2)
{// 定义递归出口if(NULL == pRoot2)return true;if(NULL == pRoot1)return false;if(pRoot1->m_value != pRoot2->m_value)return false;return doseTree1HasTree2(pRoot1->m_pLeft,pRoot2->m_pLeft) &&doseTree1HasTree2(pRoot1->m_pRight,pRoot2->m_pRight);
}
/*
4、二叉树的镜像(画图(几何的视角去看待)让抽象问题形象化),二叉树的镜像对大多数没有接触过得人来说,算是一个新的感念,此时可以通过几何画图的方式来让问题明朗化。如下图:通过观察图,我们来总结解决该问题的方法。首先交换根节点的两个子节点,此时子节点的子节点的左右子树的相对关系保持不变。故还需要交换子节点的子节点的左右子树,知道叶子节点。思路如下图,所示。通过前序遍历这棵树的每个节点,如果该节点有子节点,就交换该节点的两个子节点,当交换完所有的非叶子节点的左右子节点后,就得到了树的镜像。
*/

解题思路图,如下:

void mirrorRecursively(BiTreeNode* pRoot)
{// 定义递归出口if(pRoot == NULL || (pRoot->m_pLeft == NULL && pRoot->m_pRight == NULL))return;// 首先交换根节点BiTreeNode* pTemp = pRoot->m_pLeft;pRoot->m_pLeft = pRoot->m_pRight;pRoot->m_pRight = pTemp;// recursivelyif(pRoot->m_pLeft)mirrorRecursively(pRoot->m_pLeft);if(pRoot->m_pRight)mirrorRecursively(pRoot->m_pRight)
}// 当使用循环时,需要一个栈来保存遍历到的当前的根结点
void mirrorCircularly(BiTreeNode* pRoot)
{// 用栈保存根结点std::stack<BiTreeNode* > stackRoot;if(pRoot == NULL)return;stackRoot.push(pRoot);// 开始循环while(!stackRoot.empty()){//从栈中获取根结点BiTreeNode* pNode = stackRoot.top();stackRoot.pop();// 首先交换根节点BiTreeNode* pTemp = pNode->m_pLeft;pNode->m_pLeft = pNode->m_pRight;pNode->m_pRight = pTemp;if(pNode->m_pLeft)stackRoot.push(pNode->m_pLeft);if(pNode->m_pRight)stackRoot.push(pNode->m_pRight);}
}/*
5、从上到下打印二叉树,从上到下打印二叉树的每一个节点,同一层的节点按照从左到右的顺序打印。即层次遍历。其实,树是一种简化的图,该题也对应图的广度优先遍历。我们来尝试分析一下,由于是层次遍历,故最先打印头结点,然后我们需要一个容器来保存头结点的两个子节点,并且是先左后右,且容器应该保证先进先出(队列),之后只要队列里面的节点有孩子节点,我们就应该将孩子节点加载到队列末尾,以便后续遍历。直到叶子节点。
*/// 由于 STL 已经帮我们实现了一个很好的双端队列 deque,故我们这里直接采用 deque 作为数据结构
void printFromTopToButtom(BiTreeNode* pRoot)
{if(NULL === pRoot)return;std::deque<BiTreeNode* > treeDeque;// 压入根节点treeDepth.push_back(pRoot);// 循环迭代while(treeDeque.size()){BiTreeNode* pNode = treeDeque.front();treeDeque.pop_front();std::cout<< pNode->m_value <<std::endl;// 将 pNode 的子节点加入队列if(pNode->m_pLeft)treeDeque.push_back(pNode->m_pLeft);if(pNode->m_pRight)treeDeque.push_back(pNode->m_pRight);}
}       // 本题的扩展图的广度优先遍历/*
6、二叉搜索树的后序遍历序列,题目:输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历结果。如果是返回True, 不是返回False,假设输入的两个数组的任意两个数字都不相同。分析:特殊的地方是二叉搜索树是具有排序属性的(左子树小于根节点,根节点小于右子树),加上后序遍历,故我们需要分析出它的背后的规律。在后序遍历中,最后一个数字是树的根节点的值,数组中前面的数字可以分为两部分,第一部分是左子树节点的值,他们都比根节点小;第二部分是右子节点的值,它们都比根节点大。接下来用同样的方法,确定于数组每一部分对应子树的结构。这其实是一个递归的过程。
*/// 找到规律后再写代码,就容易多了。
bool verifySquenceOfBST(int sequence[],int length)
{if(sequence == NULL || length <= 0)return false;// 后序遍历最后一个节点就是根节点int root = sequence[length-1];//找到左子树的序列,在二叉搜索树中,左子树的节点小于根节点int i = 0;for(; i< length-1; ++i){if(sequence[i] > root)break;}//在二叉搜索树中,右子树的节点大于根节点int j = i;for(; j<length-1; ++j){if(sequence[j] < root)return false;}// 判断左子树是不是二叉搜索树bool left = true;if(i>0)left = verifySquenceOfBST(sequence,i);// 判断右子树是不是二叉搜索树bool right = true;if(i < length-1)right = verifySquenceOfBST(sequence+i, length - i +1);return left && right;
}/*
7、二叉树中和为某一个值的路径,题目:输入一个二叉树和一个整数,打印出二叉树中节点值的和为输入整数的所有路径。从树的根节点开始往下一直到叶节点所经过的节点形成一条路径。事例,如下图,对于树的路径和这样一个新概念,很难一下想到完整的思路,故可以先从一两个具体的例子入手,寻找规律。首先加和是从根到叶子,故即遍历中,只有前序遍历是先根节点,故应该是前序遍历,其次具体思路可也参见下图说明。这个题明显是所谓的回溯搜索的过程,参见 labuladong 算法小抄。此外应为要输出整条路径,所以需要将之前遍历过得节点保存下来,这里采用 vector. 在遍历完,不满足条件后需要回溯,即进行逆操作。
*/

void findPath(BiTreeNode* pRoot,int expectedSum)
{if(pRoot == NULL)return;// 用 vector 模拟 stackstd::vector<int> path;int currentSum = 0;FindPath(pRoot,expectedSum,path,currentSum);
}void FindPath(BiTreeNode* pRoot,int expectedSum,std::vector<int>& path,int &currentSum)
{// 加入根节点的值,压入栈currentSum += pRoot->m_value;path.push_back(pRoot->m_value);// 如果是叶节点,并且路径上的叶节点上值之和等于输入的值,则打印出这条路径bool isLeaf = pRoot->m_pLeft == NULL && pRoot->m_pRight == NULL;if(currentSum == expectedSum && isLeaf){printf("The path is Find: ");std::vector<int>::iterator iter = path.begin();for(; iter < path.end(); ++iter){print("%d\t", *iter);}printf("\n");}// 如果不是叶节点就遍历它的子节点if(pRoot->m_pLeft != NULL)FindPath(pRoot->m_pLeft,expectedSum,path,currentSum);if(pRoot->m_pRight != NULL)FindPath(pRoot->m_pRight,expectedSum,path,currentSum);// 在返回父节点的时候,在路径上删除当前节点,并在 currentSum 中减去当前的值currentSum -= pRoot->m_value;path.pop_back();
}
/*
8、求树中两个结点的最低公共祖先。注:由于这里涉及到树的类型,不同的类型有不同的解法,这里就默认是最为普通的树。如果是二叉搜索树,则是比较方便的。因为二叉搜索树是排过序的,如果当前节点比输入的两个节点的值都大,说明两个节点都在左子树,反之,在右子树。若在左边,则下一步遍历左子节点,若在右边,则遍历右子节点。依次递归遍历直到找到两个第一个在两个节点值之间的节点,即为最低公共祖先。如果不是二叉搜索树,是一般的树,且二叉树都不是,但是有指向父节点的指针。则可以将其中一条路径转换为从输入节点到根节点的链表,此时问题就可以转化为两个链表求取第一个公共子节点的问题。如果也没有指向父节点的指针,就是最为普通的树。则可以采用辅助空间存储从根节点到输入节点的链表,然后在求取最后公共子节点的问题。算法如下:
*/
bool GetNodePath(TreeNode* pRoot,TreeNode* pNode,std::list<TreeNode* > & path)
{if(pRoot == pNode)return true;path.push_back(pRoot);bool found = false;std::vector<TreeNode* >::iterator i = pRoot->m_vChildrne.begin();while(!found && i<pRoot->m_vChildrne.end()){found = GetNodePath(*i,pNode,path);++i;}if(!found)path.pop_back();return found;
}TreeNode* getLastCommonNode(const std::list<TreeNode* > &path1,const std::list<TreeNode* > & path2)
{std::list<TreeNode* >::const_iterator iter1 = path1.begin();std::list<TreeNode* >::const_iterator iter2 = path2.begin();TreeNode* pLastNode = NULL;while(iter1 != path1.end() && iter2 != path2.end()){if(*iter1 == *iter2)pLastNode = *iter1;++ iter1;++ iter2;}return pLastNode;
}TreeNode* getLastCommonParent(TreeNode* pRoot,TreeNode* pNode1,TreeNode* pNode2)
{if(pRoot == NULL || pNode1 == NULL || pNode2 == NULL)return NULL;std::list<TreeNode* > path1;GetNodePath(pRoot,pNode1,path1);std::list<TreeNode* > path2;GetNodePath(pRoot,pNode2,path2);return GetLastCommonNode(path1,path2);
}/*
9、求最小的 K 个数,若果不能改变数组顺序,建议直接使用堆结构(适合处理海量数据)。如 stl 里面的优先级队列 priority_queue,或者使用红黑树结构的 set、map 等。这里使用 set
*/
typedef std::multiset<int, std::greater<int> >  intSet;
typedef std::multiset<int, std::greater<int> >::iterator  setIterator;void GetLeastNumber(const std::vector<int> & data,intSet & leastNumbers,int k)
{leastNumbers.clear();if(k<1 || k > data.size())return;vector<int>::const_iterator iter = data.begin();for(; iter != data.end(); ++iter){// 先插入 k 个值if((leastNumbers.size()) < k)leastNumbers.intsert(*iter);else {   // 然后在调整setIterator iterGreastest = leastNumbers.begin();if(*iter < *(leastNumbers.begin())){leastNumbers.erase(iterGreastest);leastNumbers.insert(*iter);}}}
}

面试基础算法及编程 第三弹(树(二叉树)相关:主要考察指针相关的操作)相关推荐

  1. 面试基础算法及编程 第二弹(链表相关:主要考察指针的应用)

    // # -*- coding:utf-8 -*- // # @Author: Mr.chen(ai-chen2050@qq.com) // # @Date: 2018-08-16 16:35:13 ...

  2. WORKNC2018-2017基础到实战编程视频教程 三四五轴 大模编程

    WORKNC2018-2017基础到实战编程视频教程 三四五轴 大模编程 链接:https://pan.baidu.com/s/1fMCumv4y7wD0Jtzzr64vBA 提取码:vn76

  3. 经典c语言基础代码大全,10个经典的C语言面试基础算法及代码

    <10个经典的C语言面试基础算法及代码>由会员分享,可在线阅读,更多相关<10个经典的C语言面试基础算法及代码(24页珍藏版)>请在人人文库网上搜索. 1.10个经典的C语言面 ...

  4. 零基础入门STM32编程(三)

    前情回顾 通过前面两篇文章的学习,我们已经对STM32有了一定的了解,知道了STM32单片机的基本分类和不同产品间的特点等知识,今天起围绕STM32F103xx继续深入浅出地学习单片机编程. 一.总线 ...

  5. 面试基础算法、及编程 第一弹

    // # -*- coding:utf-8 -*- // # @Author: Mr.chen(ai-chen2050@qq.com) // # @Date: 2018-07-31 17:54:26 ...

  6. [Python面试][基础算法题]Python基础面试(基础算法题目较多)

    Python基础 文件操作 1.有一个jsonline格式的文件file.txt大小约为10K 2.补充缺失的代码 模块与包 3.输入日期, 判断这一天是这一年的第几天? 4.打乱一个排好序的list ...

  7. 编程的50种基础算法代码,编程常用算法有哪些

    1.哈希表(散列表) hash_map基于hash table(哈希表).哈希表最大的优点,就是把数据的存储和查找消耗的时间大大降低,几乎可以看成是常数时间:而代价仅仅是消耗比较多的内存.然而在当前可 ...

  8. 10个经典的C语言面试基础算法及代码

    算法是一个程序和软件的灵魂,作为一名优秀的程序员,只有对一些基础的算法有着全面的掌握,才会在设计程序和编写代码的过程中显得得心应手.本文是近百个C语言算法系列的第二篇,包括了经典的Fibonacci数 ...

  9. 【基础算法】编程初学者入门必须掌握的算法——两值交换

    (注:本文一部分图片来源于网络,如有侵权请联系我,我将在第一时间移除相关插图) 本文目录 1.两值交换是要实现什么? 2.为什么说每一个编程人都必须掌握两值交换? 3.两值交换:初学者常走的误区 4. ...

最新文章

  1. cocos2dx 3.3 异步加载纹理
  2. mybatis实现自定义SQL并且请求参数是集合
  3. libcurl linux 静态链接库_Linux学习:Makefile 模板(动态库、静态库、可执行程序)...
  4. 记一次企业级爬虫系统升级改造(一)
  5. 第一章:线性空间和线性变换
  6. 在webLogic 10.3中部署Hibernate 3.5出现 ClassNotFoundException解决办法
  7. 启程 一些高考后的想法
  8. 使用vSAN RVC进一步了解vSAN环境
  9. DevOps: 一例高负载多并发服务器连接池满的异常排解过程
  10. 计算机中振荡器作用,振荡器工作原理介绍
  11. golang命令行贪吃蛇
  12. 计算机usb接口无法读取,USB设备识别不了,USB接口无法识别设备怎么办
  13. 计算机最小系统法的安装流程,最简单的系统安装步骤
  14. Web测试中定位bug方法
  15. 如何利用电脑榨干闲置的带宽资源?
  16. Bakkt能否开启下一轮牛市?这篇「Bakkt专访」一窥究竟
  17. Markdown 图片左右对齐、居中、大小设置
  18. 求平方根问题 (C++ 实现)
  19. Mysql搜寻姓_mysql – 用于搜索名称和姓氏的最有效的sql架构
  20. 2023年大学英语B统考题库网考大学英语B试题(完整版)

热门文章

  1. zendstudio快捷键收录
  2. VI操作--跳到最后一行和跳到最后一行的最后一个字符
  3. PHP基于phpqrcode类生成二维码
  4. windows-server-2012R2离线中文语言包安装
  5. android 键盘 自动消失,android 软键盘 回到键 消失事件 监听
  6. python能和c语音交互吗_Python和C语言交互--ctypes,struct
  7. 服务器训练数据 关闭终端,一文明白使用nohup将服务器训练程序后台运行不关闭+随时通过查看训练情况输出与visdom可视化...
  8. linux上clisp运行lisp,apache - 重新访问linux + apache2 + mod_lisp + CLISP - 堆栈内存溢出
  9. mybatis 映射成多个list_SSM:Mybatis架构与原理
  10. SpringCloud Ribbon实战以及Ribbon随机策略RandomRule的源码浅析(六)