什么是二叉树?
引用自百度百科:
在计算机科学中,二叉树是每个节点最多有两个子树的树结构。通常子树被称作“左子树”(left subtree)和“右子树”(right subtree),同样的左右子树也都是二叉树.
前言
本文代码实现为 C/C++,因为不是一个完整的讲解文章,只是个人思路,所以说思路讲解可能有不足之处,有错误请指出.
节点定义
使用单向链表的形式,只保存当前节点的子节点和权值,不保存父节点.
typedef struct Node{
int value;//权值,根据实际情况而定,这里用数字
Node *lChild;//左儿子节点
Node *rChild;//右儿子节点
}BNode;
二叉树的遍历
二叉树的遍历分为三种:
- 前序遍历
先遍历当前节点(根节点),然后遍历左儿子节点,再遍历右儿子节点 - 中序遍历
先遍历左儿子节点,然后遍历当前节点(根节点),再遍历右儿子节点 - 后序遍历
先遍历左儿子节点,然后遍历右儿子节点,再遍历当前节点(根节点)
我们创建二叉树的时候一般用的是递归的方法,但是二叉树的遍历可以有递归和非递归两种方式.下面会分别给出二叉树遍历递归和非递归的思路和代码.
PS:建议看懂思路后再看代码实现,然后手动模拟一下,效果更佳.
前序遍历
递归版:
先遍历根节点,然后遍历左子树,再遍历右子树
非常经典的递归思想,理解二叉树的性质和递归思想即可得出.
void preorderTraversal(BNode *rootNode) {
if(rootNode != NULL) {
printf("%d ", rootNode->value);
preorderTraversal(rootNode->lChild);
preorderTraversal(rootNode->rChild);
}
}
非递归版:
使用栈来实现,因为 C++ 中有现成的库,所以说就不手动模拟栈了,当遍历到一个节点的时候,先将此节点输出,然后一直遍历其左儿子,依次循环,直到碰到叶子节点,然后开始弹,也就是递归过程中的回溯。
对于任一结点node:
- 访问结点node,并将结点node入栈;
- 判断结点node的左孩子是否为空
- 若为空,则取栈顶结点并进行出栈操作,并将栈顶结点的右孩子置为当前的结点node,循环至1
- 若不为空,则将node的左孩子置为当前的结点node;
- 直到node为NULL并且栈为空,则遍历结束。
void preorderTraversalNonrecursive(BNode *rootNode) {
stack<BNode *>s;
if (rootNode == NULL ) {
return ;
}
BNode *tempNode = rootNode;
while(!s.empty() || tempNode != NULL) {
while(tempNode != NULL) {
printf("%d ", tempNode->value);
s.push(tempNode);
tempNode = tempNode->lChild;
}
if (!s.empty()) {
tempNode = s.top();
s.pop();
tempNode = tempNode->rChild;
}
}
}
中序遍历
递归版:
先遍历左子树,然后遍历根节点,再遍历右子树
void inorderTraversal(BNode *rootNode) {
if(rootNode != NULL) {
inorderTraversal(rootNode->lChild);
printf("%d ", rootNode->value);
inorderTraversal(rootNode->rChild);
}
}
非递归版:
和先序遍历一样的思想,因为要先访问左子树,然后再访问根节点(当前节点),那么在栈弹出的时候进行输出即为所求.
对于任一结点node:
- 若其左孩子不为空,则将node入栈并将node的左孩子置为当前的node,然后对当前结点P再进行相同的处理;
- 若其左孩子为空,则取栈顶元素并进行出栈操作,访问该栈顶结点,然后将当前的node置为栈顶结点的右孩子;
- 直到node为NULL并且栈为空则遍历结束
void inorderTraversalNonrecursive(BNode *rootNode) {
stack<BNode *>s;
if (rootNode == NULL ) {
return ;
}
BNode *tempNode = rootNode;
while(!s.empty() || tempNode != NULL) {
while(tempNode != NULL) {
s.push(tempNode);
tempNode = tempNode->lChild;
}
if (!s.empty()) {
tempNode = s.top();
s.pop();
printf("%d ", tempNode->value);
tempNode = tempNode->rChild;
}
}
}
后序遍历
递归版:
先遍历左子树,然后遍历右子树,再遍历根节点
void postorderTraversal(BNode *rootNode) {
if(rootNode != NULL) {
postorderTraversal(rootNode->lChild);
postorderTraversal(rootNode->rChild);
printf("%d ", rootNode->value);
}
}
非递归版:
后序遍历的非递归版和先序遍历、中序遍历不一样,这里我用两个栈来实现,一个栈来保存遍历节点并不断的进行弹出,一个栈来保存节点的遍历顺序,最后遍历第二个栈.
void postorderTraversalNonrecursive(BNode *rootNode) {
stack<BNode *>s1;
stack<BNode *>s2;
if (rootNode == NULL ) {
return ;
}
s1.push(rootNode);
while(!s1.empty()) {
BNode *tempNode = s1.top();
s1.pop();
s2.push(tempNode);
if (tempNode->lChild) {
s1.push(tempNode->lChild);
}
if (tempNode->rChild) {
s1.push(tempNode->rChild);
}
}
while(!s2.empty()) {
printf("%d ", s2.top()->value);
s2.pop();
}
}
分层遍历二叉树
即每次输出二叉树的一层,例如:
实现思路:广度优先遍历(BFS),使用队列来实现,每次遍历当前节点的左右儿子节点,并将其加入队列中,然后进行队列的弹出直到队列为空.
void levelTraversal(BNode *rootNode) {
queue<BNode *>q;
q.push(rootNode);
while(!q.empty()) {
BNode *tempNode = q.front();
q.pop();
printf("%d ", tempNode->value);
if (tempNode->lChild) {
q.push(tempNode->lChild);
}
if (tempNode->rChild) {
q.push(tempNode->rChild);
}
}
}
S型打印二叉树
这里我使用的是队列 + 栈来实现的,队列存储当前遍历的节点的序列,栈中存储下一层遍历的节点顺序,使用一个 level 来判断遍历方向
void STraversal(BNode *rootNode) {
queue<BNode *>q;
stack<BNode *>s;
int level = 1;//根节点为第一层
q.push(rootNode);
while(!q.empty()){
BNode *temp;
while(!q.empty()){
temp = q.front();
printf("%d ", temp->value);
if (level % 2) {//下一层要从左到右遍历
if (temp->rChild != NULL) {
s.push(temp->rChild);
}
if (temp->lChild != NULL) {
s.push(temp->lChild);
}
} else {//下一层要从右到左遍历
if (temp->lChild != NULL) {
s.push(temp->lChild);
}
if (temp->rChild != NULL) {
s.push(temp->rChild);
}
}
q.pop();
}
while(!s.empty()){
temp = s.top();
//将下一层节点的按照遍历顺序加入队列中
q.push(temp);
s.pop();
}
level++;
}
}
二叉树深度
一棵空树的深度为0,只有根节点的数的深度为1,所以说一个数的深度 = max(左子树的深度,右子树的深度) + 1(当前节点),使用递归来求.
int findDeepOfTree(BNode *rootNode){
if(rootNode == NULL){
return 0;
}
return max(findDeepOfTree(rootNode->lChild), findDeepOfTree(rootNode->rChild)) + 1;
}
树的宽度
树的宽度即是节点最多的一层的节点数,根据前面分层遍历二叉树的原理,每次从队列中取出一层的节点,将其子节点加入队列,然后查看节点数,即队列的大小.
int findWidthOfTree(BNode *rootNode){
queue<BNode *>q;
q.push(rootNode);
int ansWidth = 1;
while(!q.empty()) {
for(int i = 0; i < q.size(); i++) {
BNode *tempNode = q.front();
q.pop();
if (tempNode->lChild) {
q.push(tempNode->lChild);
}
if (tempNode->rChild) {
q.push(tempNode->rChild);
}
}
ansWidth = max(ansWidth, (int)q.size());
}
return ansWidth;
}
求叶子节点数
叶子节点,即不含子节点的节点,当一个节点的左右儿子皆为空的时候,此节点为叶子节点,所以可以得出:树的叶子节点数 = 左子树的叶子节点数 + 右子树的叶子节点数.
int findNumOfLeafNode(BNode *rootNode){
if(rootNode == NULL) {
return 0;
}
if(rootNode->lChild == NULL && rootNode->rChild == NULL) {
return 1;
}
return findNumOfLeafNode(rootNode->lChild) + findNumOfLeafNode(rootNode->rChild);
}
求树的一层有多少节点
根节点某层的节点数 = 左子树中某层的节点数 + 右子树中某层的节点数
设置一个层数变量 level,在递归过程中,通过 level 的减小模拟下降的过程,当level = 1的时候说明到达了我们要找的那一层,那么返回当前节点数,也就是1.
int findNumOfNodeOnLevel(BNode *rootNode, int level) {
if (level == 0 || rootNode == NULL) {
return 0;
}
if(level == 1){
return 1;
}
return findNumOfNodeOnLevel(rootNode->lChild, level - 1) + findNumOfNodeOnLevel(rootNode->rChild, level - 1);
}
求树的直径(树上两个节点间的最大距离)
这里有两种方案:
方案一:
直接遍历每个点,模拟当前点为两个节点路径的转折点时的最大距离,那么也就是当前节点的左子树深度 + 右子树深度,然后取最大值
时间复杂度: O(n^2)
int findDiameterOfTree1(BNode *rootNode) {
if(rootNode == NULL) {
return 0;
}
return max(max(findDiameterOfTree1(rootNode->lChild), findDiameterOfTree1(rootNode->rChild)), findDeepOfTree(rootNode->lChild) + findDeepOfTree(rootNode->rChild));
}
方案二:
在求子树深度的时候就将最长距离求出
int findMaxDeep(BNode *rootNode, int &ans) {
if (rootNode == NULL) {
return 0;
}
int leftDeep = findMaxDeep(rootNode->lChild, ans);
int rightDeep = findMaxDeep(rootNode->rChild, ans);
ans = max(ans, leftDeep + rightDeep);
return max(leftDeep, rightDeep) + 1;
}
int findDiameterOfTree2(BNode *rootNode) {
int ans = 0;
findMaxDeep(rootNode, ans);
return ans;
}
求一个节点到根节点的路径
定义一个从某节点到根节点的栈,这里使用 vector 来实现,具体步骤:
- 将当前节点压入栈中
- 寻找当前节点左子树是否含有要寻找节点(递归进行)
- 如果有,栈中存放的就是路径
- 如果没有,再寻找右子树是否含有要寻找节点
- 如果有,栈中存放的就是路径
- 如果都没有,弹出当前节点,在遍历栈中的上一个节点
bool findPathFromNodetoRoot(BNode *rootNode, BNode *node, vector<BNode *> &path) {
if (rootNode == NULL || node == NULL) {
return false;
}
path.push_back(rootNode);
if (rootNode == node) {
return true;
}
bool isFind = findPathFromNodetoRoot(rootNode->lChild, node, path);
if (isFind == false) {
isFind = findPathFromNodetoRoot(rootNode->rChild, node, path);
}
if (isFind == false) {
path.pop_back();
}
return isFind;
}
求两个节点的最近公共祖先
如果当前遍历到了一个根节点,那么检查我们要查找的两个节点在当前节点的哪个子树中,会有三种情况:
- 都在左子树
- 都在右子树
- 一个在左子树,一个在右子树
情况一和情况二的时候我们需要返回节点所在子树的遍历结果,情况三的时候说明我们已经找到了最近公共祖先(不明白的最好手动模拟一下)。
BNode * findCommonNodeOfTwoNode(BNode *rootNode, BNode *node1, BNode *node2) {
if (rootNode == NULL || node1 == NULL || node2 == NULL) {
return NULL;
}
if (node1 == rootNode || node2 == rootNode) {
return rootNode;
}
BNode *leftLCANode = findCommonNodeOfTwoNode(rootNode->lChild, node1, node2);//检查左子树
BNode *rightLCANode = findCommonNodeOfTwoNode(rootNode->rChild, node1, node2);//检查右子树
if (leftLCANode != NULL && rightLCANode != NULL) {//一个在左一个在右,找到目标
return rootNode;
}
if (leftLCANode == NULL) {//全在右子树
return rightLCANode;
} else {//全在左子树
return leftLCANode;
}
}
还有一种方案就是将两个节点到根节点的路径都求出,然后开始对比,如果遇到第一个相同的节点即为所求,这个留给读者自己实现吧.
求两个节点间的路径
分别将两个节点到根节点的路径求出,然后对根节点到两个节点的最近公共祖先节点的路径进行去重即可.
void findPathBetweenTwoNode(BNode *rootNode, BNode *node1, BNode *node2, vector<BNode *> &path){
vector<BNode *>path1;
findPathFromNodetoRoot(rootNode, node1, path1);
vector<BNode *>path2;
findPathFromNodetoRoot(rootNode, node2, path2);
vector<BNode *>::iterator it1 = path1.begin();
vector<BNode *>::iterator it2 = path2.begin();
BNode *LCANode;
int cnt = 0;
//去重
while(it1 != path1.end() && it2 != path2.end()) {
if (*it1 == *it2) {
LCANode = *it1;
cnt++;
}
it1++;
it2++;
}
for(int i = cnt - 1; i < path1.size(); i++) {
path.push_back(path1[i]);
}
reverse(path.begin(), path.end());
for(int i = cnt; i < path2.size(); i++) {
path.push_back(path2[i]);
}
}
翻转二叉树
这里穿插一个小故事,2015 年 6 月 10 日,Homebrew 的作者 Max Howell 在 twitter 上发表了如下一内容:
Google: 90% of our engineers use the software you wrote (Homebrew), but you can’t invert a binary tree on a whiteboard so fuck off.
大概意思就是:他在应聘谷歌的时候被面试官说:虽然有90%的人在用你写的软件,但是你不会翻转二叉树,所以说我们不会录用你.
因为 Max Howell 的影响力,所以这件事情一下子广为流传.
回到正题:翻转二叉树,即是二叉树的镜像,也就是将二叉树的左右子树对调,这里有两种方案;
递归版:
void swapTree(BNode *&root){
BNode *tmp = root->lChild;
root->lChild = root->rChild;
root->rChild = tmp;
}
void turnBTree(BNode *rootNode) {
if (rootNode == NULL) {
return ;
}
turnBTree(rootNode->lChild);
turnBTree(rootNode->rChild);
swapTree(rootNode);
}
非递归版:
void invertBinaryTreeNonrecursive2(BNode *root) {
if(root == NULL){
return ;
}
stack<BNode *>s;
s.push(root);
while(!s.empty())
{
BNode *tmp = s.top();
s.pop();
swapTree(tmp);
if(tmp->lChild){
s.push(tmp->lChild);
}
if(tmp->rChild){
s.push(tmp->rChild);
}
}
}
判断完全二叉树
完全二叉树即叶节点只能出现在最下层和次下层,并且最下面一层的结点都集中在该层最左边的若干位置的二叉树
所以可得出判断的两个条件:
- 如果某个节点的右子树不为空,则它的左子树必须不为空
- 如果某个节点的右子树为空,则排在它后面的节点必须没有孩子节点
所以我们可以设置一个标志位flag,当子树满足完全二叉树时,设置flag=YES。当flag=YES而节点又破坏了完全二叉树的条件,那么它就不是完全二叉树。
bool checkIsCompleteBTree(BNode *rootNode) {
if (rootNode == NULL) {
return true;
}
queue<BNode *>q;
q.push(rootNode);
bool flag = false;
while(!q.empty()){
BNode *tempNode = q.front();
q.pop();
if (tempNode->lChild == NULL && tempNode->rChild != NULL) {
return false;
}
if (tempNode->lChild == NULL && tempNode->rChild == NULL) {
flag = true;
}
if (flag == true && tempNode->lChild != NULL && tempNode->rChild != NULL) {
return false;
}
if (tempNode->lChild != NULL) {
q.push(tempNode->lChild);
}
if (tempNode->rChild != NULL) {
q.push(tempNode->rChild);
}
}
return flag;
}
判断平衡二叉树
又被称为AVL树(有别于AVL算法),且具有以下性质:它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树.
递归检查每个节点的左右子树的高度之差是否符合要求,返回即可.
bool checkLR(BNode *rootNode, int &height) {
if (rootNode == NULL) {
return true;
}
if (rootNode->value > rootNode->rChild->value || rootNode->value < rootNode->lChild->value) {
return false;
}
bool lAns = checkLR(rootNode->lChild, height);
int lHeight = height;
bool rAns = checkLR(rootNode->rChild, height);
int rHeight = height;
height = max(lHeight, rHeight) + 1;
if (lAns == true && rAns == true && abs(lHeight - rHeight) <= 1) {
return true;
} else {
return false;
}
}
bool checkBalanceBTree(BNode *rootNode) {
int height = 0;
return checkLR(rootNode, height);
}