二叉树总结

来源:互联网 发布:油画 知乎 编辑:程序博客网 时间:2024/06/05 01:02

什么是二叉树?

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