目录
-
平衡二叉树(剑指欧肥儿)
题目描述:输入一棵二叉树,判断该二叉树是否是平衡二叉树。
解题代码一:
class Solution {
public:
bool IsBalanced_Solution(TreeNode* pRoot) {
//排除特殊情况
if(pRoot == NULL) return true;
int leftTreeDep = getDepth(pRoot -> left);
int rightTreeDep = getDepth(pRoot -> right);
if(leftTreeDep - rightTreeDep > 1 || rightTreeDep - leftTreeDep > 1){
return false;
}
return IsBalanced_Solution(pRoot -> left) && IsBalanced_Solution(pRoot -> right);
}
int getDepth(TreeNode* pRoot){
//排除特殊情况
if(pRoot == NULL) return 0;
//递归算高度
int leftDepth = getDepth(pRoot -> left);
int rightDepth = getDepth(pRoot -> right);
return leftDepth > rightDepth ? leftDepth + 1 : rightDepth + 1;
//return max(leftDepth + 1,rightDepth + 1); 这句和上面一句的return完全等效
}
};
解题关键:
- 平衡二叉树的特点,它可以是一颗空树或它的左右两个子树的高度差的绝对值不超过1,并且它的左右两个子树都是一颗平衡二叉树。
- 单独写出一个计算树的高度的函数getDepth()
- 运用了两个递归,一个计算树的高度,一个判断每个子树是否满足平衡二叉树的特征。
解题代码二:
class Solution {
public:
bool IsBalanced_Solution(TreeNode* pRoot) {
//排除特殊情况
return getDepth(pRoot) != -1;
}
int getDepth(TreeNode* pRoot){
//排除特殊情况
if(pRoot == NULL) return 0;
//递归算高度
int leftDepth = getDepth(pRoot -> left);
if(leftDepth == -1) return -1;
int rightDepth = getDepth(pRoot -> right);
if(rightDepth == -1) return -1;
return abs(leftDepth - rightDepth) > 1 ? -1 : 1 + max(leftDepth, rightDepth);
}
};
解题关键:
- 代码一是对树从上到下,对每层的左右节点进行计算树高度,每一次计算高度,都会重复计算下面的节点的高度,其实是没必要的。
- 代码二是对树从下到上(递归是从下开始递归的),计算每一个节点的高度,但没有重复计算任何一个节点的高度。
- 代码二是对代码一的改进,避免的不必要的计算,但思考量和难度更大。
题外话:
- C++中取绝对值的函数,int abs(int x),在这里不用引入库,可能牛客这个剑指的编译器引入了cmath库。
- C++取两个数的最大值的函数是 int max(int x,int y)
- 上面两个函数都要在,include<cmath> 的前提下
-
二叉树的深度(剑指欧肥儿)
题目描述:输入一棵二叉树,求该树的深度。从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。
解题代码:
class Solution {
public:
int TreeDepth(TreeNode* pRoot)
{
if(pRoot == NULL) return 0;
int left = TreeDepth(pRoot -> left);
int right = TreeDepth(pRoot -> right);
return max(left + 1,right + 1);
//return left > right ? left + 1 : right + 1;
}
};
解题思路:
- 这个代码其实是上一道题的代码一的第二个函数getDepth,而return max(left + 1, right + 1)和上一道题的代码一getDepth的return leftDepth > rightDepth ? leftDepth + 1 : rightDepth + 1;是一个道理,两者完全可以相互替换。
- 这里要说一次,树的深度,结点的层数是从根开始定义起,根为第一层,根的孩子是第二层,以此类推。树中结点的最大层次称为树的深度(Depth)。
题外话:
要被if的判断条件判断为false的语句有,if(NULL) if(0) if(false)
-
二叉树的下一个结点(剑指欧肥儿)
题目描述:给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。
解题代码:
/*
struct TreeLinkNode {
int val;
struct TreeLinkNode *left;
struct TreeLinkNode *right;
struct TreeLinkNode *next;
TreeLinkNode(int x) :val(x), left(NULL), right(NULL), next(NULL) {
}
};
*/
class Solution {
public:
TreeLinkNode* GetNext(TreeLinkNode* pNode)
{
//排除不可能
if(pNode == NULL) return NULL;
//如果该节点有右子节点
TreeLinkNode* p = NULL;
if(pNode -> right != NULL){
p = pNode -> right;
while(p -> left != NULL){
p = p -> left;
}
}
//如果该节点没有右节点
else if(pNode -> next != NULL){
//该节点还是父亲节点的右节点
TreeLinkNode* pp;
p = pNode;
pp = p -> next;
while(p == pp -> right && pp != NULL){
p = pp;
pp = pp -> next;
}
p = pp;
}
return p;
}
};
解题思路:
- 这道题思路比较难,看了 答案也会想很久。
- 对于一个节点,对它进行分了三个类。
a:该节点有右节点,则它的下一个节点是它的右子树的最左节点。
b:该节点没有右节点,该节点还是它父节点的右节点,则我们沿着该节点的父亲节点一路往上遍历,直到找到一个节点x,x是它父亲节点的左节点。如果这样的节点存在,则这个父亲节点就是我们要找的下一个节点。如果这样的节点不存在,则我们要找的下一个节点为空。
c:该节点没有右节点,且是它父节点的左节点,则它的父亲节点就是下一个节点。
-
二叉树的镜像(剑指欧肥儿)
题目描述:操作给定的二叉树,将其变换为源二叉树的镜像。
输入描述:
二叉树的镜像定义:源二叉树 8 / \ 6 10 / \ / \ 5 7 9 11 镜像二叉树 8 / \ 10 6 / \ / \ 11 9 7 5
解题代码一:
/*
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
TreeNode(int x) :
val(x), left(NULL), right(NULL) {
}
};*/
class Solution {
public:
void Mirror(TreeNode *pRoot) {
TreeNode* temp = NULL;
if(pRoot != NULL){
temp = pRoot -> left;
pRoot -> left = pRoot -> right;
pRoot -> right = temp;
if(pRoot -> left != NULL){
Mirror(pRoot -> left);
}
if(pRoot -> right != NULL){
Mirror(pRoot -> right);
}
}
}
};
解题思路:
- 首先根据输入描述,我们得知是将二叉树的每个棵子树的左右值交换。
- 于是从上到下,依次遍历每一层的左右节点并交换。
- 用到了递归思想,每一个非NULL节点都要递归这个函数Mirror。
解题代码二:
/*
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
TreeNode(int x) :
val(x), left(NULL), right(NULL) {
}
};*/
class Solution {
public:
void Mirror(TreeNode *pRoot) {
if(pRoot == NULL) return;
TreeNode* temp = NULL;
temp = pRoot -> left;
pRoot -> left = pRoot -> right;
pRoot -> right = temp;
Mirror(pRoot -> left);
Mirror(pRoot -> right);
}
};
解题思路:
- 思路和代码1一模一样
- 但是这个代码写法更简洁,更一目了然
-
对称的二叉树(剑指欧肥儿)
题目描述:请实现一个函数,用来判断一颗二叉树是不是对称的。注意,如果一个二叉树同此二叉树的镜像是同样的,定义其为对称的。
解题代码一:
/*
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
TreeNode(int x) :
val(x), left(NULL), right(NULL) {
}
};
*/
class Solution {
public:
bool isSymmetrical(TreeNode* pRoot)
{
if(pRoot == NULL) return true;
//首先生成左子树的镜像二叉树
TreeNode* leftRoot = pRoot -> left;
TreeNode* rightRoot = pRoot -> right;
Mirror(leftRoot);
//用镜像二叉树和右子树对比
return IsEqual(leftRoot, rightRoot);
}
void Mirror(TreeNode* pRoot){
if(pRoot == NULL) return;
TreeNode* temp;
temp = pRoot -> left;
pRoot -> left = pRoot -> right;
pRoot -> right = temp;
Mirror(pRoot -> left);
Mirror(pRoot -> right);
}
bool IsEqual(TreeNode* p1, TreeNode* p2){
if(p1 == NULL && p2 == NULL) return true;
if(p1 == NULL && p2 != NULL) return false;
if(p2 == NULL && p1 != NULL) return false;
if(p1 -> val == p2 -> val){
return IsEqual(p1 -> left, p2 ->left) && IsEqual(p1 -> right, p2 -> right);
}else{
return false;
}
}
};
解题思路:
- 首先排除特殊情况,二叉树为空。
- 利用二叉树根节点的左子树生成镜像树,然后与二叉树的根节点的右子树作对比。
- 两个递归代码,一个生成镜像树,一个判断两个二叉树是否相等。
解题代码二:
/*
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
TreeNode(int x) :
val(x), left(NULL), right(NULL) {
}
};
*/
class Solution {
public:
bool isSymmetrical(TreeNode* pRoot)
{
return isSymmetrical(pRoot, pRoot);
}
bool isSymmetrical(TreeNode* p1, TreeNode* p2){
if(p1 == NULL && p1 == NULL) return true;
if(!p1 || !p2) return false;
if(p1 -> val == p2 -> val){
return isSymmetrical(p1 -> left,p2 -> right) && isSymmetrical(p1 -> right,p2 -> left);
}else{
return false;
}
}
};
解题思路:
- 和代码一的思路完全不一样,这个代码是一步到位的。
- 运用了重载的思想(相同函数名和返回值,不同参数个数)。
- 直接把给定的树,当做两棵相同的树,然后从上到下,从左往右得遍历比较两个数组。这个算法可以记下来,比较经典。
-
把二叉树打印成多行(剑指欧肥儿)
题目描述:从上到下按层打印二叉树,同一层结点从左至右输出。每一层输出一行。
解题代码:
/*
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
TreeNode(int x) :
val(x), left(NULL), right(NULL) {
}
};
*/
class Solution {
public:
vector<vector<int> > Print(TreeNode* pRoot) {
vector<vector<int>> res;
if(pRoot == NULL) return res;
queue<TreeNode*> q;
q.push(pRoot);
while(!q.empty()){
int size = q.size();
vector<int> temp;
while(size--){
TreeNode* t;
t = q.front();
q.pop();
temp.push_back(t -> val);
if(t -> left) q.push(t -> left);
if(t -> right) q.push(t -> right);
}
res.push_back(temp);
}
return res;
}
};
解题思路:
- 循环里套循环,将树的每层节点值从左到右依次压入队列.
- 这个方法要记下来了,构思相当巧妙了。
队列:
- 第一次使用队列,记住几个方法,和stack很像。
- 得出队列最前面的值(先进先出)q.front();
- 弹出队列最前面的值 q.pop();
- 得出队列的长度 q.size();
- 把元素压入队列 q.push(t);
-
按之字形顺序打印二叉树(剑指欧肥儿)
题目描述:请实现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,第三行按照从左到右的顺序打印,其他行以此类推。
解题代码:
/*
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
TreeNode(int x) :
val(x), left(NULL), right(NULL) {
}
};
*/
class Solution {
public:
vector<vector<int> > Print(TreeNode* pRoot) {
vector<vector<int>> res;
if(!pRoot) return res;
stack<TreeNode*> s1;
stack<TreeNode*> s2;
int flag = 0;
s1.push(pRoot);
while(!s1.empty() || !s2.empty()){
vector<int> temp;
TreeNode* t;
if(flag == 0){
int size = s1.size();
while(size--){
t = s1.top();
s1.pop();
temp.push_back(t->val);
if(t -> left) s2.push(t -> left);
if(t -> right) s2.push(t -> right);
}
res.push_back(temp);
flag = 1;
}else{
int size = s2.size();
while(size--){
t = s2.top();
s2.pop();
temp.push_back(t -> val);
if(t -> right) s1.push(t -> right);
if(t -> left) s1.push(t -> left);
}
res.push_back(temp);
flag = 0;
}
}
return res;
}
};
解题思路:
- 这道题和上一道题“把二叉树打印成多行(剑指欧肥儿)”的思路类似,不同的是这道题是树的奇偶层按相反顺序打印。
- 经过我自己画二叉树分析,这道题必须要用两个栈,奇行用栈s1装下一行的数字,欧行用栈s2装下一行的数字。
- 对于栈s1是先装节点的左子节点再装右子节点,对于栈2则是先装节点的右子节点再装左子节点。(这个规律由画二叉树分析而来的)。
-
序列化二叉树(剑指欧肥儿)
题目描述:请实现两个函数,分别用来序列化和反序列化二叉树。
解题代码:
/*
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
TreeNode(int x) :
val(x), left(NULL), right(NULL) {
}
};
*/
class Solution {
public:
char* Serialize(TreeNode *root) {
if(!root) return "#";
string r = to_string(root->val);
r.push_back(',');
char *left = Serialize(root->left);
char *right = Serialize(root->right);
char *ret = new char[strlen(left) + strlen(right) + r.size()];
strcpy(ret, r.c_str());
strcat(ret, left);
strcat(ret, right);
return ret;
}
TreeNode* decode(char *&str) {
if(*str=='#'){
str++;
return NULL;
}
int num = 0;
while(*str != ',')
num = num*10 + (*(str++)-'0');
str++;
TreeNode *root = new TreeNode(num);
root->left = decode(str);
root->right = decode(str);
return root;
}
TreeNode* Deserialize(char *str) {
return decode(str);
}
};
解题思路:
- 序列化,就是前序遍历二叉树将每个节点的值输出,举例,一颗二叉树以7为根结点,8和2分别为它的左右两个子节点,生成的序列为"7,8,##2,##",返回指向序列第一个字符的指针
- 反序列化,就是把序列化得到的字符串,改为二叉树,返回二叉树的根节点。
- 这道题涉及到指针的指针,以及指针的指针,难度极高了。
新知识点:
- string 是一个类型,有函数 to_string(int x),这个函数可以把x转为string类型
- string.push_back在string后添加字符。
- strlen(string)和string.size()都可以得出string的长度,不包括‘\0’的长度。
- 将字符串string转为char*类型,用 string.c_str(), string.c_str()一般用在strcpy函数中。
- strcpy(char* a, char* b),将字符串b复制到a,a上相同位上的字符被b的替换,b中最后一个字符'\0'也会放到a里去。
- strcat(char* a, char*b)将字符串b加在字符串a后。而对于string类型的a和b,要把b加在a的后面,用a.push_back(b)。
- 函数返回值为 char* ,不是返回string,string是需要转为char*
- 函数形参为**p,说明指针是需要改变的,假如要取p的值需要用**p,取p的地址用*p。同理函数形参用*&p,也是说明指针是要改变的,假如要取p的值,则用*p,取p的地址用p。这一项正确性存疑。
- 对于char*型的字符串x取长度,用strlen(x),而对于string型的字符串x取长度,用x.size()或x.length();
-
二叉搜索树的第k个结点
题目描述:给定一棵二叉搜索树,请找出其中的第k小的结点。例如, (5,3,7,2,4,6,8) 中,按结点数值大小顺序第三小结点的值为4。
解题代码:
/*
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
TreeNode(int x) :
val(x), left(NULL), right(NULL) {
}
};
*/
class Solution {
public:
TreeNode* KthNode(TreeNode* pRoot, int k)
{
//排除不可能
if (k < 1 || pRoot == NULL) return NULL;
count = 0;
return KthNodeCore(pRoot, k);
}
private:
int count;
TreeNode* KthNodeCore(TreeNode* pRoot, int k){
if(pRoot == NULL) return NULL;
TreeNode* leftnode = KthNodeCore(pRoot -> left, k);
if(leftnode != NULL){
return leftnode; //得到值后一层一层地向上返回
}
if(++count == k){
return pRoot; //返回当前的pRoot值,也就是这颗子树的根
}
TreeNode* rightnode = KthNodeCore(pRoot -> right, k);
if(rightnode != NULL){
return rightnode;
}
return NULL;
}
};
解题思路:
- 二叉搜索树,就是按照中序遍历即可得到排好序的树节点的值,这道题也就是是用中序遍历得到第k大的值。
- 这道题最关键的一点,就是中序遍历的递归,难啃难懂。
- 记住这个代码,应为又tm难又很经典。
-
数据流中的中位数(剑指欧肥儿)
题目描述:如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。我们使用Insert()方法读取数据流,使用GetMedian()方法获取当前读取数据的中位数。
解题代码:
class Solution {
public:
void Insert(int num)
{
//max和min里的数的总个数,为偶则新数存min,为奇则新数存max
if( ( (max.size()+min.size()) & 1 ) == 0){ //已存在的数的个数是偶数
if((max.size() > 0) && num < max[0]){ //如果num比max堆里最大的值小,则把num压入max,max的最大值压入min
max.push_back(num);
push_heap(max.begin(), max.end(), less<int>());
num = max[0];
pop_heap(max.begin(), max.end(), less<int>());
max.pop_back();
}
min.push_back(num);
push_heap(min.begin(), min.end(), greater<int>());
}else{//已存在的数的个数是奇数
if(( min.size() > 0) && (num > min[0])){
min.push_back(num);
push_heap(min.begin(), min.end(), greater<int>());
num = min[0];
pop_heap(min.begin(), min.end(), greater<int>());
min.pop_back();
}
max.push_back(num);
push_heap(max.begin(), max.end(), less<int>());
}
}
double GetMedian()
{
if((min.size() + max.size())%2){//大堆小堆总个数为奇数
return min[0];
}else{//大堆小堆总个数为偶数
return double((min[0] + max[0]))/double(2);
}
}
private:
vector<int> max;
vector<int> min;
};
解题思路:
- 用到大小堆并且保持两堆的节点个数平衡。
- 首先要保证数据平均分配到两个堆里,因此两个堆中数据的数目之差不能超过1。为了实现平均分配,可以在数据的总数目是偶数时把新数据插入最小堆,否则插入最大堆。(用到奇偶数来判断)
- 其次要保证最大堆的所有数据都要小于最小堆中的数据。数据的总数是偶数时,按照前面的分配规则会把新数据插入最小堆,如果此时这个新数据比最大堆的最大值还要小,则把这个数字插入最大堆,然后把最大堆的最大数插入最小堆。由于最终插入最小堆的数字是原最大堆的最大数字,这样就保证了最小堆中所有数字都大于最大堆中的数字。
- 同理,当数据总数是奇数时,新数据也要和min的最小值比较,然后进行相同操作。
新知识点:
- 堆,是一种完全二叉树。一般默认为大堆。
- 大堆是每个父节点必然大于它的子节点的树,根节点是整棵树最大的值。小堆是每个父节点必然小于它的子节点的树,根节点是整棵树最小的值。
- 将vector类型的v数组整理为一个大堆树的序列:push_heap(v.begin(), v.end(). less<int>());整理成小堆的序列:push_heap(v.begin(), v.end(). greater<int>())。
- 整理为大堆的vector类型的v数组最大节点的值为v[0];整理为小堆的vector类型的v数组最小节点的值为v[0]
- 将堆的最大值(根节点)放数组的最后:pop_heap(v.begin(), v.end(). less<int>());同理,将堆的最小值(根节点)放数组的最后pop_heap(v.begin(), v.end(). greater<int>());
- 将vector数组v的最后一个元素弹出:pop_back();
-
重建二叉树(剑指欧肥儿)
题目描述:输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。
解题代码:
/**
* Definition for binary tree
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
TreeNode* reConstructBinaryTree(vector<int> pre,vector<int> vin) {
//递归首先排除NULL
if(pre.empty()) return NULL;
int size = pre.size();
int gen = 0;
vector<int> left_pre, left_in, right_pre, right_in;
//根结点
TreeNode* head = new TreeNode(pre[0]);
//取到中序遍历的根结点
for(int i = 0; i < size; i++){
if(vin[i] == pre[0]){
gen = i;
break;
}
}
//取gen左边的树
for(int i = 0; i < gen; i++){
left_in.push_back(vin[i]);
left_pre.push_back(pre[i+1]);
}
//取gen右边的树
for(int i = gen + 1; i < size; i++){
right_in.push_back(vin[i]);
right_pre.push_back(pre[i]);
}
head -> left = reConstructBinaryTree(left_pre, left_in);
head -> right = reConstructBinaryTree(right_pre, right_in);
return head;
}
};
解题思路:
- 首先要明白如何根据前序遍历和中序遍历得到二叉树。
- 然后递归每一个节点。递归要注意一定不能漏掉递归的结束条件。
- 就算知道怎么重构二叉树的思路,也不定写得出来,所以要把这个代码背下来,如果考到直接拿来用。
由前序和中序得到二叉树的思路:
- 根据前序序列的第一个元素建立根结点;
- 在中序序列中找到该元素,确定根结点的左右子树的中序序列;
- 在前序序列中确定左右子树的前序序列;
- 由左子树的前序序列和中序序列建立左子树;
- 由右子树的前序序列和中序序列建立右子树。
如:已知一棵二叉树的先序遍历序列和中序遍历序列分别是abdgcefh、dgbaechf,求二叉树及后序遍历序列。
先序:abdgcefh—>a bdg cefh
中序:dgbaechf—->dgb a echf得出结论:a是树根,a有左子树和右子树,左子树有bdg结点,右子树有cefh结点。
先序:bdg—>b dg中序:dgb —>dg b
得出结论:b是左子树的根结点,b无右子树,有左子树。
先序:dg—->d g中序:dg—–>dg
得出结论:d是b左子树的根节点,d无左子树,g是d的右子树
然后对于a 的右子树类似可以推出
最后还原: a