目录
二叉树
1、二叉树的遍历
二叉树的遍历有先序遍历、中序遍历和后序遍历和宽度优秀遍历。
a、二叉树的递归遍历
使用递归遍历二叉树的时候,根据递归函数调用的特点,可以知道二叉树的每个节点会被访问三次,因此无论是先序遍历、中序遍历还是后序遍历,取决于我们在什么时候对节点进行处理。
递归实现遍历的代码:
struct node
{
int val;
node *left;
node *right;
node(int x) :val(x), left(NULL), right(NULL) {}
};
//二叉树的前序遍历
//先遍历根结点,然后是左结点,之后是右结点
//递归函数,两点
//一曰递归调用终止条件,二曰函数如何递归
void preorder(node * root)
{
if (root == NULL)
return;
cout << root->val << ' ';
preorder(root->left);
preorder(root->right);
}
//二叉树的中序遍历
void midorder(node * root)
{
if (root == NULL)
return;
midorder(root->left);
cout << root->val << ' ';
midorder(root->right);
}
//二叉树的后序遍历
void postorder(node * root)
{
if (root == NULL)
return;
postorder(root->left);
postorder(root->right);
cout << root->val << ' ';
}
b、二叉树的遍历的非递归实现
其实对于二叉树的便利,无论是什么序遍历,重要的是在什么时候处理结点,结点的处理顺序是关键。
(1)前序遍历:使用栈,首先将根结点压栈,之后弹出栈顶的元素进行处理(这里为打印),然后将其右孩子和左孩子一次压栈,然后再弹栈知道栈中没有元素。
代码:
//二叉树的前序遍历
void preorder(node * root)
{
stack<node *>s;
if (root == NULL)
{
cout << "the tree is null!" << endl;
return;
}
s.push(root);
node* cur = NULL;
while (!s.empty())
{
cur = s.top();
s.pop();
cout << cur->val << ' ';
s.push(cur->right);
s.push(cur->left);
}
}
(2)后序遍历:在前序遍历中,我们在将打印结点的时候改成将结点压到另一个栈中,并且将对左右孩子的处理改成先压左节点再压右结点,那么在新的辅助的栈中结点的顺序就是从栈底向上依次为中右左,因此我们将该栈中的节点弹出就可以得到后序遍历的结果。
代码:
//二叉树的后序遍历
void postorder(node *root)
{
stack<node *>s1;
stack<node *>s2;
if (root == NULL)
{
cout << "The Tree is NULL!" << endl;
return;
}
s1.push(root);
while (!s1.empty())
{
s2.push(s1.top());
s1.pop();
s1.push(root->left);
s1.push(root->right);
}
while (!s2.empty())
{
cout << s2.top() << ' ';
s2.pop();
}
}
(3)中序遍历:对于任意一棵二叉树,都可以按照左树将其划分,如下面的图(同一种颜色表示一次划分)。那么我们在遍历二叉树的时候就向左遍历到底,然后对右子树做同样的操作。那么每棵子树的压栈的顺序都是中左,之后处理右子树,那么最后得到的顺序就是中序遍历。
二叉树的非递归的中序遍历
//二叉树的中序遍历
void midorder(node* root)
{
stack<node*> s;
if (root == NULL)
{
cout << "The Tree is NULL!" << endl;
return;
}
while (!s.empty()||root!=NULL)
{
if (root != NULL)
{
s.push(root);
root = root->left;
}
else
{
root = s.top();
cout << root->val << ' ';
s.pop();
root = root->right;//转向另一个方向继续遍历
}
}
}
c、二叉树的宽度优先遍历
二叉树的宽度遍历,使用队列。
代码:
struct node
{
int val;
node *left;
node *right;
node(int x) :val(x), left(NULL), right(NULL) {}
};
//求二叉树宽度
//对二叉树进行宽度遍历
int lenthOfTree(node* root)
{
node* curstart = NULL;//当前层的起始节点
node* curend = NULL;//当前层的终止节点
node* nextstart = NULL;//下一层的开始节点
node* nextend = NULL;//下一层的终止节点
queue<node *> q;
if (root == NULL)
{
cout << "The Tree Is NULL!" << endl;
return 0;
}
curstart = root;
curend = root;
q.push(root);
int len = 0;
int maxlen = 0;
node*cur = NULL;
while (!q.empty())
{
cur = q.front();
q.pop();
//只有当非空的时候才将孩子压入队列中
if(cur->left!=NULL)
q.push(cur->left);
if(cur->right!=NULL)
q.push(cur->right);
len++;
//cout << len << endl;
if (nextstart == NULL)
{
nextstart = cur->left;
}
nextend = cur->right;
//改层结束,要到下一层
if (cur == curend)
{
curstart = nextstart;
curend = nextend;
maxlen = max(maxlen, len);
len = 0;
nextstart = NULL;
nextend = NULL;
}
}
return maxlen;
}
2、二叉树的相关题目
(1)怎样判断一棵二叉树是平衡二叉树
思路:对二叉树进行中序遍历,如果中序遍历的结果是递增的,那么最后这棵二叉树就是平衡二叉树。
启示:对二叉树的问题的求解遍历是一种很重要的方法。
(2)怎样判断一棵完全二叉树
思路:完全二叉树就要满足以下两个条件:
- 在宽度遍历的过程中,如果遇到有结点有右孩子但是没有左孩子那么就不是完全二叉树
- 在宽度遍历的过程中,如果遇到一个结点只有左孩子或者没有还在,那么之后的节点都是叶结点,否则就不是完全二叉树
代码:
//判断二叉树是不是满二叉树
//对二叉树进行宽度遍历
bool isCBT(node * root)
{
queue<node*> q;
if (root == NULL)
return true;
//用来标记是否遇到了第一个不全的节点,如果遇到那么之后都是叶结点
bool leaf = false;
node* l = NULL;
node* r = NULL;
node* cur = NULL;
q.push(root);
while (!q.empty())
{
cur = q.front();
q.pop();
l = cur->left;
r = cur->right;
if ((cur->left == NULL&&cur->right == NULL) || (cur->left != NULL&&cur->right == NULL))
leaf = true;
if (leaf && (cur->left != NULL || cur->right != NULL)||(cur->left==NULL&&cur->right!=NULL))
return false;
if (l)
q.push(l);
if (r)
q.push(r);
}
return true;
}
(3)判断二叉树是否为满二叉树
思路:判断二叉树的节点数目与层数之间的关系。
借鉴:这里涉及关于二叉树题目的套路,二叉树题目的解决一般都是遍历,而遍历使用到递归。递归首先决定返回的是什么,然后确定递归结束的条件,之后是使用黑盒(递归函数),然后再拆黑盒。
代码:
//确定想返回的信息是什么
//确定递归结束的条件
//使用黑盒
//拆黑盒
struct node
{
int val;
node *left;
node *right;
node(int x) :val(x), left(NULL), right(NULL) {}
};
//判断是不是满二叉树
struct getdata {
int level;
int num;
getdata(int x, int y) :level(x), num(y) {}
};
getdata process(node * root,int level)
{
int deep = 0;
int sum = 0;
if (root == NULL)
return getdata(level - 1, 0);
getdata leftinfo = process(root->left,level + 1);
getdata rightinfo = process(root->right, level + 1);
deep = max(leftinfo.level, rightinfo.level);
sum = leftinfo.num + rightinfo.num + 1;//注意还要加上自己,所有的节点为自己加上左右孩子的节点数目
return getdata(deep, sum);
}
bool isFull(node * root)
{
getdata result= process(root, 1);
return ((1 << result.level) - 1) == result.num;//判断节点的数目和高度是否符合
}
(4)判断二叉树为平衡二叉树
思路:平衡二叉树,即左右子树的高度相差不超过一。
仍然使用上述的套路,返回的结果就是数的高度和是否平衡。
代码:
struct node
{
int val;
node *left;
node *right;
node(int x) :val(x), left(NULL), right(NULL) {}
};
//判断是不是满二叉树
struct getdata {
int height;
bool balance;
getdata(int x,bool y) :height(x),balance(y) {}
};
getdata process(node * root)
{
if (root == NULL)
return getdata(0, true);
getdata leftinfo = process(root->left);
getdata rightinfo = process(root->right);
int height = max(leftinfo.height, rightinfo.height) + 1;
//要判断出树是否是平衡的,那么要求子树都是平衡的,然后再看自己是不是平衡的
bool balance = leftinfo.balance&&rightinfo.balance && (abs(leftinfo.height - rightinfo.height) < 2);
return getdata(height, balance);
}
bool isBalance(node * root)
{
getdata result= process(root);
return result.balance;//判断节点的数目和高度是否符合
}
(5)给定一棵二叉树和两个结点,求两个结点的最低公共祖先
思路:该题对应了很多种情况,但是下面的代码考虑了所有了情况。
- 当两个结点中的一个不在树中或者两个结点都不在数中或者数为空,那么返回NULL
- 两节点中的一个结点直接为另一个结点的父节点,那么返回该结点
- 两个结点不是祖先关系,他们有公共祖先,那么返回最低的公共祖先
//大致的思路是从某节点出发寻找要找的两个结点
//如果在该结点是两个结点的共同最低祖先,那么就返回
//最重要的是抵用递归函数,先向下找,再将情况向上报告,汇集得到最后的结果
node * lowestAncestor(node* root, node* n1, node* n2)
{
if (root == NULL || root == n1 || root == n2)
return root;
//从左边的孩子得到信息
node* leftinfo = lowestAncestor(root->left, n1, n2);
node* rightinfo = lowestAncestor(root->right, n1, n2);
//汇集信息返回
if (leftinfo != NULL&&rightinfo != NULL)
return root;
//将左右孩子中得到的消息向上传递
return leftinfo == NULL ? rightinfo : leftinfo;
}
(6)求一个结点的后继结点
后继:后继指的是在二叉树的中序遍历中紧接着自己的节点。
要求:不遍历,找到任意结点的后继结点。
思路: 分情况进行考察:
- 结点有右孩子,那么其后继结点就是右孩子中最左的节点
- 如果结点没有有孩子,那么其后继结点就是其付下结点,并且该祖先结点满足的条件是:该结点是祖先结点的左孩子中最右边的一个。
代码:
struct node
{
int val;
node *left;
node *right;
node *pa;
node(int x) :val(x), left(NULL), right(NULL),pa(NULL) {}
};
//获得某结点的后继结点
node* getSuccessionNode(node* target)
{
//如果是null,那么返回null
if (target == NULL)
return target;
//否则一定有后继结点
//有右子树,返回右子树中最左的一个
if (target->right == NULL)
{
node* r = target->right;
while (r->left != NULL)
{
r = r->left;
}
return r;
}
//没有右孩子,那么在其祖先结点中寻找其后继结点
else
{
node* parent = target->pa;
while (parent != NULL&&parent->left != target)
{
target = parent;
parent = target->pa;
}
return parent;
}
}
(7)二叉树的序列化和反序列化
二叉树的序列化就是将二叉树的结构通过字符串的形式进行保存,而反序列化就是通过这个字符串将二叉树的原来的结果还原。
思路:序列化有前序、中序和后序的方式,关键是判断什么是一个结点的结束,而反序列化就是序列化的逆过程,也就是说序列化的时候是前序,那么反序列化的时候也是前序。
代码1:以前序形式的序列化和反序列化为例,在一个结点之后加上下划线表示一个结点的结束,如果某位置的节点为NULL,那么以#代替。
struct node
{
int val;
node *left;
node *right;
node(int x) :val(x), left(NULL), right(NULL) {}
};
//将二叉树序列化,返回一个二叉树的字符串的形式
string searilizeTree(node* root)
{
if (root == NULL)
return "#_";
string result = to_string(root->val);//将值转化为string
result += searilizeTree(root->left);
result += searilizeTree(root->right);
return result;
}
//根据序列化得到的字符串还原二叉树
node* build(queue<string> q)
{
string s = q.front();
q.pop();
//这里的递归结束的条件与队列中是否还有元素无关
if (s == "#")
return NULL;
//这里递归的过程中会自动消耗字符,等字符消耗结束,那么就完成了建立
int val = atoi(s.c_str());
node * root = new node(val);
root->left = build(q);
root->right = build(q);
return root;
}
//将字符串分割出结点并还原出二叉树
node* rebuildTree(string s)
{
//首先根据字符串将结点分割出来
//因为一个结点使用了一次之后就不再使用,所以使用deque进行储存
queue<string> q;
int len = s.length();
int i = 0;
string val = "";
bool flag = false;
while(i<len)
{
while (s[i] != '_'&&s[i]!='#')
{
flag = true;
val += s[i];
i++;
}
if (flag)
{
q.push(val);
val = "";
flag = false;
}
if (s[i] == '#')
{
q.push("#");
}
i++;
}
//q中存放的是分割之后的字符
//然后根据字符逐个进行建立
node* root =build(q);
return root;
}
代码2:以宽度优先遍历二叉树对二叉树进行序列化和反序列化。
struct node
{
int val;
node *left;
node *right;
node(int x) :val(x), left(NULL), right(NULL) {}
};
//序列化的核心还是进行遍历,进行宽度优先遍历
//因为我们需要知道哪些位置是空的
//所以序列化的时候我们需要在结点进队列之前就判断
//如果进了队列之后我们就不能确定一个结点的左右孩子的具体情况
string serilizeTree(node* root)
{
queue < node*> q;
string res = "";
if (root == NULL)
{
cout << "the tree is empty!";
return res;
}
q.push(root);
res += to_string(root->val);
node* temp = NULL;
while (!q.empty())
{
temp = q.front();
q.pop();
if (temp->left != NULL)
{
//序列化并且入栈
res += to_string(root->left->val);
res += "_";
q.push(root->left);
root = root->left;
}
else
{
res += "#_";
}
if (temp->right != NULL)
{
res += to_string(root->right->val);
res += "_";
q.push(root->right);
root = root->right;
}
else
{
res += "#_";
}
}
return res;
}
//反序列化的过程和序列化的过程是一致的
node* build(queue<string> q)
{
node* root = NULL;
string s = q.front();
if (s == "#")
root = NULL;
else
root = new node(atoi(s.c_str()));
queue<node*> tmp;
node* cur = NULL;
if (root != NULL)
tmp.push(root);
while (!tmp.empty())
{
cur = tmp.front();
tmp.pop();
s = q.front();
q.pop();
if (s == "#")
root->left = NULL;
else
root->right = new node(atoi(s.c_str()));
s = q.front();
q.pop();
if (s == "#")
root->right = NULL;
else
root->right = new node(atoi(s.c_str()));
if (root->left != NULL)
tmp.push(root->left);
if (root->right != NULL)
tmp.push(root->right);
}
return root;
}
//对序列进行分割
node* rebuildTree(string s)
{
//首先根据字符串将结点分割出来
//因为一个结点使用了一次之后就不再使用,所以使用deque进行储存
queue<string> q;
int len = s.length();
int i = 0;
string val = "";
bool flag = false;
while (i<len)
{
while (s[i] != '_'&&s[i] != '#')
{
flag = true;
val += s[i];
i++;
}
if (flag)
{
q.push(val);
val = "";
flag = false;
}
if (s[i] == '#')
{
q.push("#");
}
i++;
}
//q中存放的是分割之后的字符
//然后根据字符逐个进行建立
node* root = build(q);
return root;
}
应用:二叉树的序列化使得将二叉树的这样一个有结构的数据结构字符串化,相当于将其一维化,这样我们可以处理很多的问题,例如:
二叉树序列化的应用:
(1)、判断二叉树的子树值结构。
题目:给定两棵二叉树A和B,判断A中是否有子树的结构与B的结构是一样的。
思路:如果直接进行比较,也就是从A的每个节点出发,与B的结构进行对比,看是否两者的结构是一致的,这样做法的时间复杂度是O(N*M)(N、M分别是A、B的节点数目)。
优化:我们可以将A树和B树进行序列化,两者都变为字符串,然后判断B是否为A的子串,判断是否为子串的问题,我们可以使用KMP算法。
(2)、折纸问题。
题目:吧一段纸条竖着放在桌子上,然后从纸条的下边向上方对折1次,压出折痕后展开。
此时折痕是凹下去的,即折痕凸起的方向指向纸条的背面。如果从纸条的下边向上方连续对折2次,压出折痕后展开,此时有三条折痕,从上到下依次是下折痕、下折痕和上折痕。
给定一个输入参数N,代表纸条都从下边向上方连续对折N次。请从上到下打印有折痕的方向。
例如N=1,打印:down;N=2时,打印dow down up。
思路:我们对一张这条对折三次展开后(如下图所示),观察:
可以看出,这是一颗二叉树的中序遍历:
这颗二叉树所具有的特点是:
- 以第一次对折的凹折痕为根
- 每个结点的子树左孩子为凹折痕,右孩子是凸折痕
代码:代码中并没有出现或者使用到二叉树的节点,代码的过程实际上就是在遍历我们脑子中与折痕对应的那颗二叉树。
已经知道二叉树结构并且明确知道下一步应该怎么做,所以不用生成二叉树的实际节点。
//打印第i层的节点
void process(int i, int n, bool down)
{
if (i > n)
return;
//进行中序遍历
//某节点的左孩子是凹,右孩子是凸
process(i + 1, n, true);
if (down)
cout << "凹" << ' ';
else
cout << "凸" << ' ';
process(i + 1, n, false);
}
void printAllFolds(int n)
{
process(1, n, true);
}
今天也是努力学习的一天,sigh!!!