文章目录
题目一 实现二叉树先序中序后序遍历,包括递归和非递归实现
关于三种遍历方式的递归在数据结构的课本上应该写的很清楚,理解起来很简单,没有多做总结。
通过看左神视频多了一点理解,在二叉树的递归遍历中,因为递归实现本质上是个系统栈,所以每个节点都会访问到三次,包括刚刚访问到当前节点,递归当前节点左子树,递归当前节点右子树。
举例说明:如图1,如果采用先根遍历,访问节点顺序为1,2,4,null,4,null,4,2,5,null,5,null,5,2,1,3,6,null,6,null,6,3,null,3,1,去掉null之后整个访问顺序为:1,2,4,4,4,2,5,5,5,2,1,3,6,6,6,3,1,每个数字都会被访问3次,每个节点被访问第一次的时候输出就是先根遍历1,2,4,5,3,6,被访问第二次的时候输出就是中根遍历4,2,5,1,6,3,被访问第三次的时候输出就是后根遍历4,5,2,6,3,1。
关于非递归实现的思路顺便提几句
- 先根遍历【根左右】—借助一个栈,首先将根节点入栈,循环中,在栈非空的情况下,首先打印栈顶元素,然后对于栈顶元素,有右孩子压入右孩子,有左孩子压入左孩子。【栈先进后出,所以出栈的时候就会变成根左右】
- 中根遍历【左根右】— 同样借助一个栈,当前节点不为空,将其左节点压到栈中,节点向左。当前节点为空,从栈中拿出节点,打印,当前节点往右走。【代码参照左老师代码,循环逻辑很有意思】
- 后根遍历【左右根】— 后根遍历的实现,如果直接非递归实现,当从栈中弹出节点时,且该节点又有右子节点时,需要再次尝试先遍历该右子节点的所有左子节点时,会将该节点丢失。那么,这时需要重新将该节点压栈,但是当再次弹出时,需要区分其右子节点是否已经遍历过了。有关于此实现的一段伪代码,引入了一个成员变量判断是否被访问过。
如果按照根右左的顺序遍历二叉树,最后反过来其实是左右根,因此按照根左右的顺序遍历二叉树,在该打印节点的时候,将节点的值存入栈中,最后弹出,就可以实现了。如第三个图所示。 - 最后层序遍历,使用队列来实现,第四个图简单分析了一下为什么用队列可以实现。就是对于当前节点有左孩子左孩子入队,有右孩子右孩子入队。
图1 图2
图3
图4
#include <iostream>
#include <stack>
#include <queue>
using namespace std;
struct Node{
int value;
Node *leftChild;
Node *rightChild;
};
class Traversal{
public:
/**先根遍历,递归实现**/
void preOrderTraversal(Node *root)
{
if(root == nullptr)
return ;
cout<<root->value<<" ";
if(root->leftChild)
preOrderTraversal(root->leftChild);
if(root->rightChild)
preOrderTraversal(root->rightChild);
}
/**先根遍历,非递归实现,有右先压右,有左后压左**/
void preOrderTraversal_Non(Node *root)
{
if(root == nullptr)
return;
stack<Node*> st;
st.push(root);
while(!st.empty())
{
root = st.top();
st.pop();
cout<<root->value<<" ";
if(root->rightChild)
st.push(root->rightChild);
if(root->leftChild)
st.push(root->leftChild);
}
}
/**中根遍历,递归实现**/
void midOrderTraversal(Node *root)
{
if(root == nullptr)
return ;
if(root->leftChild)
midOrderTraversal(root->leftChild);
cout<<root->value<<" ";
if(root->rightChild)
midOrderTraversal(root->rightChild);
}
/**中根遍历,非递归实现**/
void midOrderTraversal_Non(Node *root)
{
if(root == nullptr)
return;
stack<Node*> st;
while(!st.empty() || root != nullptr)
{
if(root != nullptr)
{
st.push(root);
root = root->leftChild;
}else
{
root = st.top();
st.pop();
cout<<root->value<<" ";
//root的右孩子如果是空,不要重新压入新的值,从栈中取值即可
root = root->rightChild;
}
}
}
/**后根遍历,递归实现**/
void posOrderTraversal(Node *root)
{
if(root == nullptr)
return ;
if(root->leftChild)
posOrderTraversal(root->leftChild);
if(root->rightChild)
posOrderTraversal(root->rightChild);
cout<<root->value<<" ";
}
/**后根遍历,非递归实现**/
void posOrderTraversal_Non(Node *root)
{
if(root == nullptr)
return;
if(root == nullptr)
return;
stack<Node*> st;
st.push(root);
stack<int> tmp;
while(!st.empty())
{
root = st.top();
st.pop();
tmp.push(root->value);
if(root->leftChild)
st.push(root->leftChild);
if(root->rightChild)
st.push(root->rightChild);
}
while(!tmp.empty())
{
cout<< tmp.top() <<" ";
tmp.pop();
}
cout<<endl;
}
/**层序遍历**/
void levelTraversal(Node *root)
{
if(root == nullptr)
return ;
queue<Node *> qu;
qu.push(root);
while(!qu.empty())
{
root = qu.front();
qu.pop();
cout<<root->value<<" ";
if(root->leftChild)
qu.push(root->leftChild);
if(root->rightChild)
qu.push(root->rightChild);
}
}
};
int main()
{
cout << "Hello world!" << endl;
Node *root = new Node;
root->value = 1;
root->leftChild = new Node;
root->leftChild->value = 2;
root->rightChild = new Node;
root->rightChild->value =3;
root->leftChild->leftChild = new Node;
root->leftChild->leftChild->value = 4;
root->leftChild->rightChild = new Node;
root->leftChild->rightChild->value = 5;
root->rightChild->leftChild = new Node;
root->rightChild->leftChild->value = 6;
root->leftChild->leftChild->leftChild = nullptr;
root->leftChild->leftChild->rightChild = nullptr;
root->leftChild->rightChild->leftChild = nullptr;
root->leftChild->rightChild->rightChild = nullptr;
root->rightChild->leftChild->leftChild = nullptr;
root->rightChild->leftChild->rightChild = nullptr;
root->rightChild->rightChild = nullptr;
Traversal tr;
cout<<"先根遍历(递归)"<<endl;
tr.preOrderTraversal(root);
cout<<"\n"<<"先根遍历"<<endl;
tr.preOrderTraversal_Non(root);
cout<<"\n---------------------------------------"<<endl;
cout<<"\n"<<"中根遍历(递归)"<<endl;
tr.midOrderTraversal(root);
cout<<"\n"<<"中根遍历"<<endl;
tr.midOrderTraversal_Non(root);
cout<<"\n---------------------------------------"<<endl;
cout<<"\n"<<"后根遍历(递归)"<<endl;
tr.posOrderTraversal(root);
cout<<"\n"<<"后根遍历"<<endl;
tr.posOrderTraversal_Non(root);
cout<<"\n---------------------------------------"<<endl;
cout<<"\n"<<"层序遍历"<<endl;
tr.levelTraversal(root);
return 0;
}
题目二:如何直观地打印一颗二叉树
class printBinaryTree{
public:
void printTree(Node *root)
{
cout<<"Print Binary Tree:"<<endl;
printInOrder(root,0,"H",17);
cout<<endl;
}
void printInOrder(Node *root,int height,string to,int len)
{
if(root == nullptr)
return;
printInOrder(root->rightChild,height+1,"v",len);
string val = to + to_string(root->value) + to;
int lenM = val.size();
int lenL = (len - lenM) / 2;
int lenR = len - lenM - lenL;
val = getSpace(lenL) + val + getSpace(lenR);
cout<<getSpace(height * len) + val<<endl;
printInOrder(root->leftChild, height + 1, "^", len);
}
string getSpace(int num)
{
string space = " ";
string buf;
for (int i = 0; i < num; i++) {
buf.append(space);
}
return buf;
}
};
打印题目1中树的情况,逆时针90度观察可以获得空间概念的二叉树,^ ^表示其父节点为左上,v v表示父节点为左下,这样如果某一棵二叉树的节点都是一个值,也可以直观地观察到二叉树的形状。
题目三:在二叉树中找到某一个节点的后继节点
【后继节点:在二叉树的中序遍历的序列中, node的下一个节点叫作node的后继节点。】
【题目】 现在有一种新的二叉树节点类型如下:
public class Node {
public int value;
public Node left;
public Node right;
public Node parent;
public Node(int data)
{
this.value = data;
}
}
该结构比普通二叉树节点结构多了一个指向父节点的parent指针。假设有一 棵Node类型的节点组成的二叉树,树中每个节点的parent指针都正确地指向自己的父节点,头节点的parent指向null。只给一个在二叉树中的某个节点node,请实现返回node的后继节点的函数。
时间复杂度:时间复杂度为节点个数【直接中序遍历】也可以实现该功能,即O(N),但是最优的时间复杂度应该为一个节点到其后继节点的距离。
如图一所示,该结构的二叉树,中序遍历结果为:4,2,5,1,6,3。要找这里面任何一个节点的后继节点,首先分析会遇到哪些情况。
- 如果一个节点有右节点,根据左根右,一定会进入到该节点的右子树找它的后继节点,如节点1,2,1的后继节点为6,2的后继节点为5,可以分析出如果一个节点有右节点,那么他的后继节点一定是右子树的最左节点。
- 如果该节点没有右节点,因为没有右节点,所以该节点的左根部分已经遍历完成,如4,5,3,6,这时需要分析这个节点是不是其父节点的左孩子,如果是,说明父节点的左树遍历完成,根据左根右需要访问父节点了,此时返回父节点;如果这个节点不是其父节点的左孩子,说明以父节点为根的子树已经遍历完成,需要分析父节点是不是其父节点的左孩子,循环向上。
根据此思路同时写了找到一个节点前驱节点的代码,实质是一样的。
#include <iostream>
#include <queue>
using namespace std;
struct ParentNode{
int value;
ParentNode *leftChild;
ParentNode *rightChild;
ParentNode *parent;
};
class getSuccessorNode{
public:
/**层序遍历,打印节点**/
void levelTraversal(ParentNode *root)
{
if(root == nullptr)
return ;
queue<ParentNode *> qu;
qu.push(root);
while(!qu.empty())
{
root = qu.front();
qu.pop();
cout<<"value is: "<<root->value<<" ";
if(root->parent != nullptr)
cout<<"parent node is: "<<root->parent->value<<". "<<endl;
else
cout<<"Root!"<<endl;
if(root->leftChild)
qu.push(root->leftChild);
if(root->rightChild)
qu.push(root->rightChild);
}
}
ParentNode* getNextNode(ParentNode *node)
{
if(node == nullptr)
return nullptr;
ParentNode *tmp = node;
if(tmp->rightChild != nullptr)
{
return getLeftest(tmp->rightChild);
}else
{
while(tmp->parent != nullptr)
{
if(tmp == tmp->parent->leftChild)
return tmp->parent;
else
tmp = tmp->parent;
}
return nullptr;
}
}
ParentNode* getLeftest(ParentNode *node)
{
if(node == nullptr)
return nullptr;
while(node->leftChild != nullptr)
{
node = node->leftChild;
}
return node;
}
/**找某个节点的前驱节点**/
ParentNode* getPreviousNode(ParentNode *node)
{
if(node == nullptr)
return nullptr;
ParentNode *tmp = node;
if(tmp->leftChild)//找到左孩子的最右节点
{
return getRightest(tmp->leftChild);
}else
{
while(tmp->parent != nullptr)
{
if(tmp == tmp->parent->rightChild)
return tmp->parent;
else
tmp = tmp->parent;
}
return nullptr;
}
}
ParentNode* getRightest(ParentNode *node)
{
if(node == nullptr)
return nullptr;
while(node->rightChild != nullptr)
{
node = node->rightChild;
}
return node;
}
};
题目四 二叉树的序列化和反序列化
二叉树是存在内存中的,如果下一次需要拿出二叉树复用,能不能直接从字符串中读取恢复二叉树呢?这就是这个题目的意义,将二叉树以序列的方式存储,然后用的时候以反序列化复用。跟编解码类似,规定编解码规则即可。
例如,对于一个二叉树,我们希望读取数据流的过程中就在从头开始构建,因为先序遍历是根左右遍历,所以以先序遍历的结果存储,如果遇到nullptr,就以特殊字符代替,例如‘$’,同时二叉树中每个节点之间也需要以其他特殊字符分割开来。这样反序列化的时候根据特殊字符就可以重构了。
【实现包括先序和层序序列化和反序列化,层序遍历也是从根开始的。】
class SerializeTree{
public:
/**先序序列化**/
string serializeTree(Node *root)
{
if(root == nullptr)
return "$_";
string res = to_string(root->value)+"_";
res = res + serializeTree(root->leftChild);
res = res + serializeTree(root->rightChild);
return res;
}
Node* reserializeTree(string serial)
{
vector<string> res ;
SplitString(serial,res,"_");
queue<string> que;
for(int i=0;i<res.size();i++)
{
que.push(res[i]);
}
return reconTree(que);
}
/**又是因为引用&符号出错,如果没有引用,每一次函数修改que,栈中会保存每个递归的
参数,这样递归回去的时候还是没有pop的的queue,所以会出错
为什么Java没事呢**/
Node* reconTree(queue<string> &que)
{
string val = que.front();
que.pop();
queue<string> ss = que;
if(val == "$")
{
return nullptr;
}
Node *root = new Node;
root->value = atoi(val.c_str());
root->leftChild = reconTree(que);
root->rightChild = reconTree(que);
return root;
}
/**层序序列化**/
string levelSerial(Node *root)
{
if(root == nullptr)
return "$_";
queue<Node*> que;
Node *tmp;
que.push(root);
string res = "";
while(!que.empty())
{
tmp = que.front();
que.pop();
if(tmp != nullptr)
{
res = res + to_string(tmp->value)+"_";
que.push(tmp->leftChild);
que.push(tmp->rightChild);
}
else
{
res = res + "$_";
}
}
return res;
}
Node* levelReconTree(string serial)
{
vector<string> res ;
SplitString(serial,res,"_");
queue<Node*> que;
Node *root = generateNode(res[0]);
if(root != nullptr)
que.push(root);
Node *node = nullptr;
int i = 1;
while(!que.empty())
{
node = que.front();
que.pop();
node->leftChild = generateNode(res[i++]);
node->rightChild = generateNode(res[i++]);
if(node->leftChild != nullptr)
que.push(node->leftChild);
if(node->rightChild != nullptr)
que.push(node->rightChild);
}
return root;
}
Node* generateNode(string s)
{
if(s == "$")
return nullptr;
Node *root= new Node;
root->value = atoi(s.c_str());
root->leftChild = nullptr;
root->rightChild = nullptr;
return root;
}
void SplitString(const string& s, vector<string>& v, const string& c)
{
string::size_type pos1, pos2;
pos2 = s.find(c);
pos1 = 0;
while(string::npos != pos2)
{
v.push_back(s.substr(pos1, pos2-pos1));
pos1 = pos2 + c.size();
pos2 = s.find(c, pos1);
}
if(pos1 != s.length())
v.push_back(s.substr(pos1));
}
};
题目五:折纸问题
【题目】 请把一段纸条竖着放在桌子上,然后从纸条的下边向上方对折1次,压出折痕后展开。此时折痕是凹下去的,即折痕突起的方向指向纸条的背面。如果从纸条的下边向上方连续对折 2 次,压出折痕后展开,此时有三条折痕,从上到下依次是下折痕、下折痕和上折痕。
给定一个输入参数N,代表纸条都从下边向上方连续对折N次, 请从上到下打印所有折痕的方向。 例如:N=1时,打印: down N=2时,打印: down down up
注意折纸的方向都是从下向上折的,每次对折之后,关于之前的折痕,都会是折痕以上为down,折痕以上为up。以下图为例说明,图片来自王同学。
第一次对折,为黑线1.折痕为down,相对于黑线来说,下一次对折,黑线上面部分折痕为down,也就是菊线down2,黑线下面部分折痕为up,也就是菊线2up。下一次对折相对于菊线,上面部分为down,下面部分为up,展开之后会发现相对于两条菊线,上下新增的两条就是down和up。
也就是如下图所示,每一个新的折痕它的新增折痕都是相对于它先下后上,整个的遍历过程为将所有节点压缩到一条线上从左到右遍历,即二叉树的中序遍历,关于遍历第一题总结了,不再写了。
题目六:判断一棵二叉树是否是平衡二叉树
平衡二叉树:任何一棵树的左子树与右子树的高度差不超过1.
这里用递归函数很好用,递归会到达一个节点三次,先访问节点,然后左子树一次,右子树一次
如果以每个节点为头的树都是平衡的,这棵树是平衡的,每次递归需要判断以下几个问题,最后返回左右子树的高度差,如果不超过1就是平衡二叉树。
- 左树是否平衡
- 右树是否平衡
- 左树平衡,左树的高度
- 右树平衡,右树的高度
这里有三种实现方法,三种方法的实质都是一样的。第一种最麻烦,是根据视频学习总结的关于递归函数的涉及通用思路来写的,第2 3种实现更简单一些。
第一种,通用设计思路
- 列出所有可能性【上面列出的四种】
- 整理返回值类型【需要返回子树是否平衡,以及平衡情况下子树的高度】
- 整个递归过程按照同样的结构返回子树的信息
- 整合子树的信息,得到自己需要的信息
- 向上返回
/**根据需要返回子树是否平衡,以及平衡情况下子树的高度,设计的返回结构**/
class isBan{
public:
bool isBanlance;
int depth;
isBan()
{
isBanlance = false;
depth = 0;
}
isBan(bool b,int d)
{
isBanlance = b;
depth = d;
}
};
class BanlancedTree {
public:
bool isBalanced(Node *root) {
if(root == nullptr)
return true;
return isBalancedHelper(root)->isBanlance;
}
isBan* isBalancedHelper(Node *root)
{
if(!root)
{
return new isBan(true,0);
}
isBan* left = isBalancedHelper(root->leftChild);
if(left->isBanlance == false)
return new isBan(false,0);
isBan* right = isBalancedHelper(root->rightChild);
if(right->isBanlance == false)
return new isBan(false,0);
if(abs(left->depth - right->depth)>1)
{
return new isBan(false,0);
}
return new isBan(true,max(left->depth,right->depth)+1);
}
};
以下两种实现方法,第一种在函数中只返回树是否为平衡二叉树,然后输入depth二叉树深度变量,并在递归的过程中改变,得到每次递归左右子树的差值,从而判断左右是否平衡。第2种方法更简单一点,只利用一个int返回值,如果不平衡返回负值,平衡返回子树高度,代码更简洁,复用性不高。
/**
* Definition for binary tree
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
bool isBalanced(TreeNode *root) {
int depth = 0;
return isBalancedHelper(root,depth);
}
bool isBalancedHelper(TreeNode *root,int &depth)
{
if(!root)
{
depth = 0;
return true;
}
int left,right;
if(isBalancedHelper(root->left,left) && isBalancedHelper(root->right,right))
{
if(abs(left-right) <=1)
{
depth = 1 + max(left,right);
return true;
}
}
return false;
}
};
/**
* Definition for binary tree
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
bool isBalanced(TreeNode *root) {
if(!root) return true;
return (helper(root) != -1);
}
int helper(TreeNode *root)
{
if(!root) return 0;
int left,right;
left = helper(root->left);
if(left == -1)
return -1;
right = helper(root->right);
if(right == -1)
return -1;
if(abs(left-right)>1)
return -1;
return 1+max(left,right);
}
};
题目七:判断一棵树是否是搜索二叉树、判断一棵树是否是完全二叉树
搜索二叉树:任何一个节点,左子树上的值都比他的值小,右子树上所有的值都比它的值大
由定义可以想到,如果二叉树中序遍历是依次升序的 ,就是搜索二叉树【一般不出现重复节点,重复节点可以压缩到一个小的结构上】
在中序遍历的过程中保证升序,在非递归版本的中序遍历中进行修改
完全二叉树:
堆的结构,堆是完全二叉树,将二叉树按层遍历,
- 如果一个节点有右孩子没有左孩子,直接返回false
- 如果一个节点不是左右两个孩子都全,或者有左没右,或者左右都没有
后面遇到的所有节点都必须是叶结点,否则不是完全二叉树
class judgeTree{
public:
bool isSearchBiTree(Node *root)
{
if(root == nullptr)
return true;
stack<Node*> s;
int tmp = INT_MIN;
while(!s.empty() || root != nullptr)
{
if(root != nullptr)
{
s.push(root);
root = root->leftChild;
}else
{
root = s.top();
s.pop();
if(root->value > tmp)
tmp = root->value;
else
return false;
root = root->rightChild;
}
}
return true;
}
bool isCompleteBiTree(Node *root)
{
if(root == nullptr)
return true;
queue<Node*> que;
que.push(root);
bool leaf = false;
Node *left;
Node *right;
while(!que.empty())
{
root = que.front();
que.pop();
left = root->leftChild;
right = root->rightChild;
if((leaf && (left != nullptr || right != nullptr)) || (left == nullptr && right != nullptr))
{
return false;
}
if(left)
{
que.push(left);
}
if(right)
{
que.push(right);
}else
{
leaf = true;
}
}
return true;
}
};
题目八:已知一棵完全二叉树,求其节点的个数
要求:时间复杂度低于O(N),N为这棵树的节点个数
如果遍历整棵二叉树,时间复杂度为严格O(N),因为该树是完全二叉树,也就是叶子节点只在最后一层,所以可以利用满二叉树的性质进行加速。
思路:首先遍历左边界,一路走到最左边节点,可以得到该二叉树的层数depth,然后遍历右子树的左边界,如果层数也是depth,说明左子树的满二叉树,下一步递归求右子树节点个数即可。如果右子树的左边界没有到达最后一层,说明右子树是depth-1层的满二叉树,下一步递归求左子树节点个数即可。
int nodeNums(Node *root)
{
if(root == nullptr)
return 0;
int depth = Depth(root);
int rightD = Depth(root->rightChild)+1;//右子树的左边界
if(depth == rightD)
{
//左子树是满的,递归右子树
return nodeNums(root->rightChild)+1+pow(2,depth-1)-1;
}else
{
//右子树是满的递归左子树
return nodeNums(root->leftChild)+1+pow(2,depth-2)-1;
}
}
int Depth(Node *root)
{
if(root == nullptr)
return 0;
int depth = 0;
while(root != nullptr)
{
depth++;
root = root->leftChild;
}
return depth;
}