二叉树总结

什么是二叉树?

引用自百度百科
在计算机科学中,二叉树是每个节点最多有两个子树的树结构。通常子树被称作“左子树”(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);
}

参考文章

二叉树-你必须要懂!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • 树的概述 树是一种非常常用的数据结构,树与前面介绍的线性表,栈,队列等线性结构不同,树是一种非线性结构 1.树的定...
    Jack921阅读 4,373评论 1 31
  • 四、树与二叉树 1. 二叉树的顺序存储结构 二叉树的顺序存储就是用数组存储二叉树。二叉树的每个结点在顺序存储中都有...
    MinoyJet阅读 1,452评论 0 7
  • 数据结构和算法--二叉树的实现 几种二叉树 1、二叉树 和普通的树相比,二叉树有如下特点: 每个结点最多只有两棵子...
    sunhaiyu阅读 6,336评论 0 14
  • 一直以来,我都很少使用也避免使用到树和图,总觉得它们神秘而又复杂,但是树在一些运算和查找中也不可避免的要使用到,那...
    24K男阅读 6,660评论 5 14
  • 基于树实现的数据结构,具有两个核心特征: 逻辑结构:数据元素之间具有层次关系; 数据运算:操作方法具有Log级的平...
    yhthu阅读 3,943评论 1 5