二叉树详解

个人主页流年如梦

专栏《零基础轻松入门C语言》 《数据结构:从入门到掌握》

文章目录

一.数的基础概念

1.1什么是树

树是由n(n≥0)个结点组成的层次关系非线性集合

  1. 有且仅有一个根节点,无前驱
  2. 除根外,其他结点分成互不相交的子树
  3. 树是递归定义
  4. N个结点的树有N-1条边

现实中的二叉树

1.2树的常用术语

术语 意思
父结点/双亲结点 若⼀个结点含有子结点,则这个结点称为其子结点的父结点
子结点/孩子结点 ⼀个结点含有的⼦树的根结点称为该结点的⼦结点
结点的度 ⼀个结点有⼏个孩子,他的度就是多少
树的度 ⼀棵树中,最大的结点的度称为树的度
叶子结点/终端结点 度为0的结点称为叶结点
分支结点/非终端结点 度不为0的结点
兄弟结点 具有相同父结点的结点互称为兄弟结点(亲兄弟)
结点的层次 从根开始定义起,根为第1层,根的⼦结点为第2层,以此类推
树的高度或深度 树中结点的最大层次
结点的祖先 从根到该结点所经分⽀上的所有结点
路径 ⼀条从树中任意节点出发,沿⽗节点 --> 子节点连接,达到任意节点的序列
子孙 以某结点为根的子树中任⼀结点都称为该结点的子孙
森林 由m(m>0)棵互不相交的树的集合称为森林

1.3树的表示(孩子兄弟表示法)

孩子兄弟表示法 是树的表示的其中一种,优势在于用二叉树结构表示普通树,统一实现逻辑

c 复制代码
struct Node
{
    int data;
    struct Node* Child;
    struct Node* Brother;
};

二.二叉树

2.1概念

  1. 每个结点最多2个孩子
  2. 子树分左右、有序
  3. 五种基本形态:空树、只有根、只有左、只有右、左右都有

2.2特殊二叉树

2.2.1满二叉树

  1. 每一层结点数都达到最大
  2. 高度 h --> 结点总数 2^h − 1

如下图所示:

2.2.2完全二叉树

  1. 从满二叉树编号 1~n 一一对应,最后一层连续靠左
  2. 满二叉树是特殊的完全二叉树

如下图所示:

再看看这棵二叉树是不是完全二叉树:

显而易见,这棵二叉树不是完全二叉树,即非完全二叉树 ,因为它的最后一层不是从左向右依次排序

2.3性质

  1. i层最多2^(i-1)个结点
  2. 高度h最多2^h − 1个结点
  3. 叶子数 n0 = 度为2结点数 n2 + 1
  4. 完全二叉树高度 h = log₂(n+1) 向上取整
  5. 数组下标 i:
    左孩子:2*i+1
    右孩子:2*i+2
    父节点:(i-1)/2

三.二叉树的存储结构

3.1顺序存储(底层是数组)

  1. 适合完全二叉树
  2. 非完全二叉树会空间浪费
  3. 典型应用:

3.2链式存储(二叉链)

最常用,灵活、无浪费

c 复制代码
typedef int BTDataType;
typedef struct BinaryTreeNode
{
    BTDataType data;
    struct BinaryTreeNode* left;
    struct BinaryTreeNode* right;
} BTNode;

四.顺序结构二叉树 --> 堆

堆是完全二叉树,它满足:

大根堆 :父 孩子
小根堆 :父 孩子

4.1堆的结构定义

c 复制代码
typedef int HPDataType;
typedef struct Heap
{
    HPDataType* a;
    int size;
    int capacity;
} HP;

4.2堆的初始化

c 复制代码
void HPInit(HP* php)
{
    assert(php);
    php->a = NULL;
    php->size = 0;
    php->capacity = 0;
}

4.3堆的销毁

c 复制代码
void HPDestroy(HP* php)
{
    assert(php);
    free(php->a);
    php->a = NULL;
    php->size = php->capacity = 0;
}

4.4向上调整法

c 复制代码
void AdjustUp(HPDataType* a, int child)
{
    int parent = (child - 1) / 2;
    while (child > 0)
    {
        // 大根堆
        if (a[child] > a[parent])
        {
            Swap(&a[child], &a[parent]);
            child = parent;
            parent = (child - 1) / 2;
        }
        else
        {
            break;
        }
    }
}

🧐分析:从孩子向上找父,不满足堆则交换,直到满足堆或到根

4.5堆插入

c 复制代码
void HPPush(HP* php, HPDataType x)
{
    assert(php);
    if (php->size == php->capacity)
    {
        int newcap = php->capacity == 0 ? 4 : php->capacity * 2;
        HPDataType* tmp = (HPDataType*)realloc(php->a, newcap * sizeof(HPDataType));
        if (!tmp)
        {
            perror("realloc fail");
            return;
        }
        php->a = tmp;
        php->capacity = newcap;
    }
    php->a[php->size] = x;
    php->size++;
    AdjustUp(php->a, php->size - 1);
}

🧐分析:扩容 --> 尾插 --> 向上调整

4.6向下调整法(用于删除堆顶)

👉前提是左右子树都是堆

c 复制代码
void AdjustDown(HPDataType* a, int n, int parent)
{
    int child = parent * 2 + 1;
    while (child < n)
    {
        // 选更大的孩子
        if (child + 1 < n && a[child + 1] > a[child])
        {
            child++;
        }
        if (a[child] > a[parent])
        {
            Swap(&a[child], &a[parent]);
            parent = child;
            child = parent * 2 + 1;
        }
        else
        {
            break;
        }
    }
}

🧐分析:选出较大孩子,交换下沉,直到满足堆

4.7删除堆顶

c 复制代码
void HPPop(HP* php)
{
    assert(php);
    assert(php->size > 0);
    Swap(&php->a[0], &php->a[php->size - 1]);
    php->size--;
    AdjustDown(php->a, php->size, 0);
}

🧐分析:堆顶与最后交换 --> 删除最后 --> 向下调整,以保证堆结构不被破坏

4.8取堆顶、判空以及大小

c 复制代码
HPDataType HPTop(HP* php)
{
    assert(php);
    assert(php->size > 0);
    return php->a[0];
}

bool HPEmpty(HP* php)
{
    assert(php);
    return php->size == 0;
}

int HPSize(HP* php)
{
    assert(php);
    return php->size;
}

五.堆的应用

5.1堆排序

  1. 升序(从小到大)--> 建大堆

    每次把最大值放到末尾,最终从低到高有序

  2. 降序(从大到小)--> 建小堆

    每次把最小值放到末尾,最终从高到低有序

  3. 时间复杂度:O(NlogN)

  4. 建堆复杂度:O(N)

如下所示(以建大堆例):

c 复制代码
void HeapSort(int* a, int n)
{
    for (int i = (n - 1 - 1) / 2; i >= 0; --i)
    {
        AdjustDown(a, n, i);
    }
    int end = n - 1;
    while (end > 0)
    {
        Swap(&a[0], &a[end]);
        AdjustDown(a, end, 0);
        --end;
    }
}

5.2Top-K问题

5.2.1了解Top-K问题

海量数据或大量数据 中,找出最大的K个或最小的K个

例如:

  1. 从1亿个数里找出最大的100个
  2. 从1000万个商品里找出销量前10名
  3. 从日志里找出访问量最高的5个IP

5.2.2最优解法 --> 堆

核心思想:
找前K大 --> 建小堆
找前K小 --> 建大堆

  1. 小根堆堆顶是堆里最小的
  2. 遍历所有数据,比堆顶大就替换它,再调整
  3. 最后堆里剩下的就是 最大的K个数

其中时间复杂度为O(NlogK)

5.2.3代码实现

(1)交换函数

c 复制代码
void Swap(int* a, int* b) {
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

(2)小堆向下调整(找前K大)

c 复制代码
void AdjustDown(int* a, int n, int parent) {
    int child = parent * 2 + 1;
    while (child < n) {
        //小堆
        if (child + 1 < n && a[child + 1] < a[child]) {
            child++;
        }
        if (a[child] < a[parent]) {
            Swap(&a[child], &a[parent]);
            parent = child;
            child = parent * 2 + 1;
        } else {
            break;
        }
    }
}

(3)主函数

c 复制代码
//找出N个数中最大的K个
void PrintTopK(int* a, int n, int k)
{
    assert(a);
    assert(k > 0);

    //开一个K大小的数组(开辟空间)
    int* minHeap = (int*)malloc(sizeof(int) * k);
    if (minHeap == NULL)
    {
        perror("malloc fail");
        return;
    }
    //用前K个数据初始化数组
    for (int i = 0; i < k; i++)
    {
        minHeap[i] = a[i];
    }
    //建小堆
    for (int i = (k - 1 - 1) / 2; i >= 0; --i)
    {
        AdjustDown(minHeap, k, i);
    }

    //剩下的 N-K 个数据依次比较
    for (int i = k; i < n; ++i)
    {
        //比堆顶大,替换堆顶,再向下调整
        if (a[i] > minHeap[0])
        {
            minHeap[0] = a[i];
            AdjustDown(minHeap, k, 0);
        }
    }
    printf("最大的 %d 个元素:", k);
    for (int i = 0; i < k; ++i)
    {
        printf("%d ", minHeap[i]);
    }
    printf("\n");

    free(minHeap);
}

int main() {
    int arr[] = {3, 5, 9, 1, 7, 2, 8, 10, 4, 6};
    int n = sizeof(arr) / sizeof(arr[0]);
    int k = 3;
    TopK(arr, n, k);
    return 0;
}

运行结果:

六.链式二叉树(递归是其核心)

6.1创建节点

c 复制代码
BTNode* BuyNode(int x)
{
    BTNode* node = (BTNode*)malloc(sizeof(BTNode));
    node->data = x;
    node->left = NULL;
    node->right = NULL;
    return node;
}

6.2建一棵二叉树(手动创建)

c 复制代码
BTNode* CreateTree()
{
    BTNode* n1 = BuyNode(1);
    BTNode* n2 = BuyNode(2);
    BTNode* n3 = BuyNode(3);
    BTNode* n4 = BuyNode(4);
    BTNode* n5 = BuyNode(5);
    BTNode* n6 = BuyNode(6);

    n1->left = n2;
    n1->right = n4;
    n2->left = n3;
    n4->left = n5;
    n4->right = n6;

    return n1;
}

七.二叉树的遍历

7.1前序遍历(根 --> 左 --> 右)

c 复制代码
void PreOrder(BTNode* root)
{
    if (root == NULL)
    {
        printf("N ");
        return;
    }
    printf("%d ", root->data);
    PreOrder(root->left);
    PreOrder(root->right);
}

🧐分析:先访问根,再递归左,左递归结束后,再递归右

7.2中序遍历(左 --> 根 --> 右)

c 复制代码
void InOrder(BTNode* root)
{
    if (root == NULL)
    {
        printf("N ");
        return;
    }
    InOrder(root->left);
    printf("%d ", root->data);
    InOrder(root->right);
}

7.3后序遍历(左 --> 右 --> 根)

c 复制代码
void PostOrder(BTNode* root)
{
    if (root == NULL)
    {
        printf("N ");
        return;
    }
    PostOrder(root->left);
    PostOrder(root->right);
    printf("%d ", root->data);
}

7.4层序遍历(借助队列)

c 复制代码
void LevelOrder(BTNode* root)
{
    if (!root) return;
    Queue q;
    QueueInit(&q);
    QueuePush(&q, root);

    while (!QueueEmpty(&q))
    {
        BTNode* front = QueueFront(&q);
        QueuePop(&q);
        printf("%d ", front->data);

        if (front->left)
            QueuePush(&q, front->left);
        if (front->right)
            QueuePush(&q, front->right);
    }
    QueueDestroy(&q);
}

🧐分析 :从上到下、从左到右,必须用队列实现

八.二叉树接口(递归是其核心)

8.1求结点总数

c 复制代码
int BinaryTreeSize(BTNode* root)
{
    return root == NULL ? 0 :
        BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1;
}

8.2 求叶子数

c 复制代码
int BinaryTreeLeafSize(BTNode* root)
{
    if (root == NULL) return 0;
    if (root->left == NULL && root->right == NULL) return 1;
    return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}

8.3 求树高度

c 复制代码
int BinaryTreeDepth(BTNode* root)
{
    if (root == NULL) return 0;
    int left = BinaryTreeDepth(root->left);
    int right = BinaryTreeDepth(root->right);
    return left > right ? left + 1 : right + 1;
}

8.4 第k层结点数

c 复制代码
int BinaryTreeLevelKSize(BTNode* root, int k)
{
    if (!root) return 0;
    if (k == 1) return 1;
    return BinaryTreeLevelKSize(root->left, k - 1)
         + BinaryTreeLevelKSize(root->right, k - 1);
}

8.5 查找值为 x 的结点

c 复制代码
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
    if (!root) return NULL;
    if (root->data == x) return root;

    BTNode* left = BinaryTreeFind(root->left, x);
    if (left) return left;

    BTNode* right = BinaryTreeFind(root->right, x);
    if (right) return right;

    return NULL;
}

8.6 销毁二叉树

c 复制代码
void BinaryTreeDestroy(BTNode** root)
{
    if (*root == NULL) return;
    BinaryTreeDestroy(&(*root)->left);
    BinaryTreeDestroy(&(*root)->right);
    free(*root);
    *root = NULL;
}

🎯总结

  1. 二叉树是递归定义的非线性结构,最多 2 个孩子,有序
  2. 顺序存储适合完全二叉树,典型实现
  3. 链式存储最常用,二叉链结构简单高效
  4. 堆的核心是向上或向下调整 ,插入删除O(logN)
  5. 堆可用于堆排序、Top-K、优先级队列
  6. 链式二叉树依靠递归实现绝大多数接口
  7. 四种遍历:前 / 中 / 后 / 层序
  8. 时间复杂度:遍历类O(N),堆操作O(logN)
  9. 堆排序关键:升序建大堆,降序建小堆

⚠️易错点

  1. 归没有终止条件导致栈溢出
  2. 堆插入或删除忘记调整,破坏堆结构
  3. 求树高误用减法,应该取大+1
  4. 层序遍历忘记判空
  5. 销毁 二叉树不置空,形成野指针
  6. 完全二叉树下标计算错误
  7. 遍历顺序混淆:前序根最先,后序根最后
  8. 堆排序升序或降序搞反:升序大堆,降序小堆

👀 关注 我们一路同行,从入门到大师,慢慢沉淀、稳步成长
❤️ 点赞 鼓励原创,让优质内容被更多人看见
⭐ 收藏 收好核心知识点与实战技巧,需要时随时查阅
💬 评论 分享你的疑问或踩坑经历,一起交流避坑、共同进步

相关推荐
xiaoxiaoxiaolll1 小时前
Nature Communications:三维超原子库+原子层保护,突破全彩VR超透镜量产瓶颈
人工智能·算法
仍然.1 小时前
算法题目---栈
算法
博界IT精灵1 小时前
二叉排序树和平衡二叉树(哈喜老师)
数据结构·考研
feifeigo1231 小时前
基于布谷鸟算法的配电网分布式电源选址定容 MATLAB 实现
开发语言·算法·matlab
qq3862461962 小时前
推荐几本C语言书籍
c语言·指针·函数·学习资料·编程书籍
MicroTech20252 小时前
微算法科技(NASDAQ: MLGO)噪声图像的量子图像边缘提取算法:技术革新与产业赋能
科技·算法·量子计算
大模型最新论文速读2 小时前
EvoLM:8B 模型自写评分标准,RL 后超越 GPT-4
人工智能·深度学习·算法·机器学习·自然语言处理
木子墨5162 小时前
工程算法实战 | 从LRU到手写本地缓存:LinkedHashMap → 双向链表+哈希表 → Caffeine 原理
java·数据结构·算法·链表·缓存
数智工坊2 小时前
【Offline RL1】离线强化学习全景:从基础理论到前沿算法与工业落地
算法