数据结构和算法二叉树详解,动图+实例

Posted Linux猿

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构和算法二叉树详解,动图+实例相关的知识,希望对你有一定的参考价值。


🎈 作者:Linux猿

🎈 简介:CSDN博客专家🏆,华为云享专家🏆,Linux、C/C++、面试、刷题、算法尽管咨询我,关注我,有问题私聊!

🎈 关注专栏:图解数据结构和算法(优质好文持续更新中……)🚀

🎈 欢迎小伙伴们点赞👍、收藏⭐、留言💬


目录

🍓一、什么是二叉树

🍓二、二叉树特性

✨2.1 概念

✨2.2 定理

🍓三、二叉树存储

✨3.1 链式存储

✨3.2 顺序存储

🍓四、二叉树遍历

✨4.1 前序遍历

🚩4.1.1 动图演示

🚩4.1.2 代码实现

✨4.2 中序遍历

🚩4.2.1 动图演示

🚩4.2.2 代码实现

✨4.3 后续遍历

🚩4.3.1 动图演示

🚩4.3.2 代码实现

✨4.4 层次遍历

🚩4.4.1 非递归版本

🚩4.4.2 动图演示

🍓五、特殊二叉树

✨5.1 满二叉树

✨5.2 完全二叉树

🍓六、实战演练

✨6.1 求二叉树深度

🚩6.1.1 递归版本

🚩6.1.2 非递归版本

✨6.2  翻转二叉树

🍓七、总结


二叉树是数据结构和算法中很重要的一部分,树中最典型的就数二叉树了,下面就来详细讲解下。

🍓一、什么是二叉树

二叉树(Binary tree)是一种每个节点最多有两个孩子节点的树,左右孩子节点不能随意颠倒。如下图所示:

图 1 真实的二叉树

 搞错了,重来,应该是这样的,如下所示:

图2 二叉树示例

 在上图中,二叉树每个节点最多有两个孩子节点,可以为空,可以只有一个左孩子或右孩子。

🔶🔶🔶🔶🔶 我是分割线 🔶🔶🔶🔶🔶

🍓二、二叉树特性

✨2.1 概念

叶子节点:在一棵树中,没有孩子节点(节点度为 0)的节点称为叶子节点;

树的深度:树的深度是从根节点到最下面一层的叶子节点的层数,根节点的深度为 0 (这里不同的教程可能有差异,这里以维基百科为准);

树的高度:树的高度是从最下面一层的叶子节点到根节点的层数为高度,最下面一层的叶子节点高度为 0;

父节点/父亲节点:如果一个节点有子节点,则当前节点为子节点的父节点,根节点没有父节点;

子节点/孩子节点:与父节点相连的下一层节点为该父节点的子节点;

兄弟结点:具有相同父节点的节点,互为兄弟节点;

下面来看一张图来理解下,如下所示:

图3  二叉树特性

✨2.2 定理

(1)二叉树第 i 层上的节点数目最多为 2^(i-1) (i >= 1);

(2)深度为 k 的二叉树至多有 (2^k) -1 个节点 (k ≥ 1);

(3)包含 n 个节点的二叉树的高度至少为 log2(n+1);

(4)在任意一棵二叉树中,若叶子结点的个数为 N0,度为 2 的节点数为 N2,则 N0 = N2+1;

其中,第 4 条经常在考试中考到。

🔶🔶🔶🔶🔶 我是分割线 🔶🔶🔶🔶🔶

🍓三、二叉树存储

✨3.1 链式存储

typedef struct BiTNode
{
    Type data;
    struct BiTNode *left;  // 指向左孩子
    struct BiTNode *right; // 指向右孩子
}BiTNode,*BiTree;

上面的结构体表示的二叉树节点中的任意节点的存储,其中:

(1)data : 表示节点存储的数据;

(2)left :指向当前节点的左孩子,如果没有则为空(NULL/nullptr);

(3)right:指向当前节点的右孩子,如果没有则为空(NULL/nullptr);

✨3.2 顺序存储

#define SIZE 1000    // 节点数
typedef Type BiTree[SIZE];

上面表示的是二叉树的顺序存储结构,其中:

(1)SIZE:表示节点总数;

(2)BiTree[SIZE] :表示存储节点的数组,使用下标标识节点,一般根节点存储在下标为 1 的结点(便于计算父节点和子节点),假设父节点的下标为 i,左孩子节点则为 i*2,右孩子节点为 i*2 + 1,如下图所示:

图4 顺序存储

🔶🔶🔶🔶🔶 我是分割线 🔶🔶🔶🔶🔶

🍓四、二叉树遍历

二叉树有四种遍历方法,分别是:前序遍历、中序遍历、后续遍历、层次遍历。遍历是将树的所有节点访问且仅访问一次。其中,前序遍历、中序遍历、后续遍历是按照访问根节点顺序的不同来区分的。

层次遍历是从上到下从左往右进行遍历。

✨4.1 前序遍历

前序遍历先访问根节点,然后,再以同样的方式访问左子树和右子树,直到遍历完所有节点。具体步骤如下所示:

(1)访问根节点;

(2)遍历左子树;

(3)遍历右子树;

🚩4.1.1 动图演示

下面来看一下动图演示,如下所示:

图5 前序遍历

 上图中,序号表示前序遍历访问的节点顺序,颜色也是按照这个顺序依次变化的。一定要记住:根->左->右。

🚩4.1.2 代码实现

这里列出前序遍历的两种实现,分别是递归版本和非递归版本,递归版本代码简洁,更好理解一些,非递归版本需要使用一个栈来模拟递归过程,原理都是一样的。

(1)递归版本

// 存储树的单个节点
struct Node {
    int data; // 存储节点数据
    struct Node *left;  // 左孩子节点
    struct Node *right; // 右孩子节点
};

// 前序遍历
void preOrder(Node *root){
    if(root != NULL){ // 节点非空
        cout<<root->data<<endl; //1.访问根节点
        preOrder(root->left);  //2.遍历左子树
        preOrder(root->right); //3.遍历右子树
    }
}

(2)非递归版本

// 树的单个节点
struct Node {
    int data; // 存储节点数据
    struct Node *left;  // 左孩子节点
    struct Node *right; // 右孩子节点
};

// 前序遍历 非递归版本
void preOrder(Node *root)
{
    stack<Node*> s; // 栈,用来模拟递归
    if(root != NULL) { // 判断根节点是否为空
        s.push(root);
    }
    while (!s.empty()) {
        Node *p = s.top(); // 从栈里取出下一个要访问的节点
        s.pop();           // 从栈中删除
        if (p != NULL) {
            cout << p->data <<endl;
            if(p->right != NULL)
                s.push(p->right);   //先放右子树,因为是栈,先放进去的后访问
            if(p->left != NULL)
                s.push(p->left);
       }
    }
}

如果有不理解栈的同学,可以看这篇讲解栈和队列的文章: 

动图+万字,详解栈和队列(实例讲解)【建议收藏】

✨4.2 中序遍历

中序遍历先访问左子树,然后,访问根节点,最后,遍历右子树,直到遍历完所有节点。具体步骤如下所示:

(1)遍历左子树;

(2)访问根节点;

(3)遍历右子树;

🚩4.2.1 动图演示

下面来看一下动图演示如下所示:

图6 中序遍历

同样,上图中的序号即是中序遍历访问顺序,颜色也会相应变化。一定要记住:左->根->右。 

🚩4.2.2 代码实现

这里同样也列出中序遍历的两种实现,分别是递归版本和非递归版本。

(1)递归版本

// 树的单个节点
struct Node {
    int data; // 存储节点数据
    struct Node *left;  // 左孩子节点
    struct Node *right; // 右孩子节点
};

// 中序遍历 递归版本
void inOrder(Node *root)
{
    if(root != NULL) { // 判断是否为空
        inOrder(root->left); // 递归遍历左子树
        cout << root->data <<endl;   // 访问节点
        inOrder(root->right); // 递归遍历右子树
    }
}

(2)非递归版本

// 中序遍历 非递归版本
void inOrder(Node *root)
{
    stack<Node *> s; // 栈,模拟递归
    while (root != NULL || !s.empty()) {
        if (root != NULL) {   //1.遍历左子树
            s.push(root);
            root = root->left;
        } else {
            root = s.top();
            cout << root->data <<endl; //2.访问节点
            s.pop();            // 将访问过的节点从栈中删除
            root = root->right; //3.遍历右子树
        }
    }
}

✨4.3 后续遍历

前序遍历先访问根节点,然后,再以同样的方式访问左子树和右子树,直到遍历完所有节点。具体步骤如下所示:

(1)访问根节点;

(2)遍历左子树;

(3)遍历右子树;

🚩4.3.1 动图演示

下面来看一下动图演示,如下所示:

图7 后续遍历

 后序遍历相对于前序和中序而言,更难理解一些,一定要记住:左->右->根。

🚩4.3.2 代码实现

(1)递归版本

// 树的单个节点
struct Node {
    int data; // 存储节点数据
    struct Node *left;  // 左孩子节点
    struct Node *right; // 右孩子节点
};

// 后续遍历 递归版本
void postOrder(Node *root)
{
    if(root != NULL) { // 判断是否为空
        postOrder(root->left);  // 遍历左子树
        postOrder(root->right); // 遍历右子树
        cout << root->data <<endl; // 访问节点
    }
}

(2)非递归版本

// 树的单个节点
struct Node {
    int data; // 存储节点数据
    struct Node *left;  // 左孩子节点
    struct Node *right; // 右孩子节点
};

// 后序遍历 非递归版本
void postOrder(Node* root){
    Node* curt = root;
    Node* pos = NULL;
    stack<Node*> s; // 栈,用于模拟递归
    while (curt || !s.empty()) {
        while (curt) {
            s.push(curt);
            curt = curt->left;
        }
        Node* top = s.top();
        if(top->right == NULL || top->right == pos) {
            cout<<top->data<<endl;
            pos = top;
            s.pop();
        } else {
            curt = top->right;
        }
    }
}

✨4.4 层次遍历

二叉树层次遍历的顺序是按照树的深度,从上到下,从左到右进行遍历的。

🚩4.4.1 非递归版本

// 树的单个节点
struct Node {
    int data; // 存储节点数据
    struct Node *left;  // 左孩子节点
    struct Node *right; // 右孩子节点
};

// 层次遍历 非递归版本
void levelTraversal(Node* root)
{
	queue<Node*> q; // 队列
	q.push(root);
	while (!q.empty()) { // 非空才循环
		Node* curt = q.front();  // 取出最前面的元素
		q.pop();                 // 删除访问的元素
		cout<<curt->data<<endl;  // 访问当前元素
		if (curt->left)          // 判断左孩子
			q.push(curt->left);
		if(curt->right)
			q.push(curt->right);  // 判断右孩子
	}
}

🚩4.4.2 动图演示

下面来看一下层次遍历的动图演示,如下所示:

图8 层次遍历

 层次遍历是按照由上到下,由左到右进行遍历,正好符合广度优先搜索的思路。

🍓五、特殊二叉树

在众多的二叉树中,有写二叉树具有一些独特的特性,这样的二叉树有:满二叉树和完全二叉树,下面就来看一下。

✨5.1 满二叉树

满二叉树除了叶子节点外,非叶子节点都有两个孩子节点,如下所示:

图9 满二叉树

 上图是一个深度为 3 的满二叉树,可以看到,除叶子节点外,非叶子节点都有两个孩子。

✨5.2 完全二叉树

完全二叉树是除了最后一层外,其它层的节点数都有两个孩子节点,如下图所示:

图10 完全二叉树

🍓六、实战演练

✨6.1 求二叉树深度

假设有一二叉树,如下所示:

图11 二叉树求深度

🚩6.1.1 递归版本

代码实现(递归版本)

#include <iostream>
using namespace std;

// 二叉树节点存储
typedef struct BiTNode
{
    int data;
    struct BiTNode *left;  // 指向左孩子
    struct BiTNode *right; // 指向右孩子
}BiTNode,*BiTree;

// 求二叉树深度
int binaryTreeDepth(BiTree root)
{
    if(root == nullptr) // 空的话返回 0
        return 0;
    else {
        int leftDepth = binaryTreeDepth(root->left); // 求左子树的深度
        int rightDepth = binaryTreeDepth(root->right); // 求右子树的深度
        return max(leftDepth, rightDepth) + 1;
    }  
}


int main() {

    // 创建一个二叉树
    BiTNode * root = new(BiTNode);
    BiTNode * node1 = new(BiTNode);
    BiTNode * node2 = new(BiTNode);
    BiTNode * node3 = new(BiTNode);
    BiTNode * node4 = new(BiTNode);
    BiTNode * node5 = new(BiTNode);

    root->left = node1;
    root->right = node2;

    node1->left = node3;
    node1->right = node4;
    node4->left = nullptr;
    node4->right = node5;

    node2->left = nullptr;
    node2->right = nullptr;

    node3->left = nullptr;
    node3->right = nullptr;

    node5->left = nullptr;
    node5->right = nullptr;

    // 调用求二叉树深度
    cout<<"Binary Tree Depth = "<<binaryTreeDepth(root)-1<<endl; // 减 1 是因为从 0 开始计算的深度

}

🚩6.1.2 非递归版本

代码实现(非递归)

#include <iostream>
#include <queue>
using namespace std;

// 二叉树节点存储
typedef struct BiTNode
{
    int data;
    struct BiTNode *left;  // 指向左孩子
    struct BiTNode *right; // 指向右孩子
}BiTNode,*BiTree;

// 求二叉树深度 递归版本
int binaryTreeDepth(BiTree root)
{
    if(root == nullptr) // 空的话返回 0
        return 0;
    else {
        int leftDepth = binaryTreeDepth(root->left); // 求左子树的深度
        int rightDepth = binaryTreeDepth(root->right); // 求右子树的深度
        return max(leftDepth, rightDepth) + 1;
    }  
}

//求二叉树深度 非递归版本
int binaryTreeDepth(BiTree root)
{
    queue<BiTree> q;
    if(!root) return 0;
    q.push(root);
    int depth = 0;
    while(!q.empty()) {
        int num = q.size();
        depth++;
        while(num--) {
            BiTree curt = q.front();
            q.pop();
            if(curt->left) q.push(curt->left);
            if(curt->right) q.push(curt->right);
        }
    }
    return depth;
} 


int main() {

    // 创建一个二叉树
    BiTNode * root = new(BiTNode);
    BiTNode * node1 = new(BiTNode);
    BiTNode * node2 = new(BiTNode);
    BiTNode * node3 = new(BiTNode);
    BiTNode * node4 = new(BiTNode);
    BiTNode * node5 = new(BiTNode);

    root->left = node1;
    root->right = node2;

    node1->left = node3;
    node1->right = node4;
    node4->left = nullptr;
    node4->right = node5;

    node2->left = nullptr;
    node2->right = nullptr;

    node3->left = nullptr;
    node3->right = nullptr;

    node5->left = nullptr;
    node5->right = nullptr;

    // 调用求二叉树深度
    cout<<"Binary Tree Depth = "<<binaryTreeDepth(root)-1<<endl; // 减 1 是因为从 0 开始计算的深度,如果从1开始计算,则不用减 1 操作

}

✨6.2  翻转二叉树

假设有一棵二叉树,如下所示:

图12 二叉树翻转前

 上图中,使用大写字母来标识每一个节点,翻转上图的二叉树后,如下所示:

图13 二叉树翻转后

 在图中,实现了二叉树的翻转,所谓 “翻转”,实质上是将每个节点的左右孩子节点交换,下面就来看下代码实现,如下所示:

#include <iostream>

using namespace std;

// 二叉树节点存储
typedef struct BiTNode
{
    char data;
    struct BiTNode *left;  // 指向左孩子
    struct BiTNode *right; // 指向右孩子
}BiTNode,*BiTree;

class Solution {
public:
    BiTNode* invertTree(BiTNode* root) {
        if (root == nullptr) {
            return nullptr;
        }
        BiTNode* left = invertTree(root->left); // 递归处理左子树
        BiTNode* right = invertTree(root->right); // 递归处理右子树
        root->left = right; // 交换当前节点的左右子树
        root->right = left;
        return root;
    }
};

// 前序遍历输出二叉树
void printTree(BiTNode* root)
{
    if(root == nullptr)
        return;
    cout<<root->data<<" ";
    if(root->left)
        printTree(root->left);
    if(root->right)
        printTree(root->right);
}

int main()
{
    // 创建二叉树
    BiTNode* rootA = new(BiTNode);
    BiTNode* nodeB = new(BiTNode);
    BiTNode* nodeC = new(BiTNode);
    BiTNode* nodeD = new(BiTNode);
    BiTNode* nodeE = new(BiTNode);
    BiTNode* nodeF = new(BiTNode);
    BiTNode* nodeG = new(BiTNode);

    rootA->left = nodeB;
    rootA->right = nodeC;
    rootA->data = 'A';

    nodeB->left = nodeD;
    nodeB->right = nodeE;
    nodeB->data = 'B';

    nodeC->left = nodeF;
    nodeC->right = nodeG;
    nodeC->data = 'C';

    nodeD->left = nullptr;
    nodeD->right = nullptr;
    nodeD->data = 'D';

    nodeE->left = nullptr;
    nodeE->right = nullptr;
    nodeE->data = 'E';

    nodeF->left = nullptr;
    nodeF->right = nullptr;
    nodeF->data = 'F';

    nodeG->left = nullptr;
    nodeG->right = nullptr;
    nodeG->data = 'G';
    // 翻转前输出
    cout<<"翻转前 ";
    printTree(rootA);
    cout<<endl;
    Solution obj;
    obj.invertTree(rootA);

    // 翻转后输出
    cout<<"翻转后 ";
    printTree(rootA);
    cout<<endl;
    return 0;
}

输出结果为:

linuxy@linuxy:~$ ./inverBinary 
翻转前 A B D E C F G 
翻转后 A C G F B E D 
linuxy@linuxy:~$

 输出的遍历结果是前序遍历的结果,可以看到,将二叉树每个节点的左右子树都翻转了。

🔶🔶🔶🔶🔶 我是分割线 🔶🔶🔶🔶🔶

🍓七、总结

好了,上面就是所有二叉树讲解的所有内容了,在掌握基本的二叉树概念后,还需要多实战,多练习。

关注👇👇👇👇👇👇,获取更多优质内容🤞(比心)!

以上是关于数据结构和算法二叉树详解,动图+实例的主要内容,如果未能解决你的问题,请参考以下文章

❤️《算法和数据结构》小白零基础教学,三十张彩图,C语言配套代码,之 二叉树详解❤️(建议收藏)

❤️《算法和数据结构》小白零基础教学,三十张彩图,C语言配套代码,之 二叉树详解❤️(建议收藏)

八大排序算法——堆排序(动图演示 思路分析 实例代码java 复杂度分析)

二万字《算法和数据结构》三张动图,三十张彩图,C语言基础教学,之 二叉搜索树详解 (建议收藏)

二万字《算法和数据结构》三张动图,三十张彩图,C语言基础教学,之 二叉搜索树详解 (建议收藏)

详解二叉树的遍历问题(前序后序中序层序遍历的递归算法及非递归算法及其详细图示)