第37篇 手撕二叉树与堆的底层逻辑

目录

[1. 树 (Tree) 的基本概念](#1. 树 (Tree) 的基本概念)

[1.2 树的相关术语](#1.2 树的相关术语)

[1.3 树的表示:孩子兄弟表示法](#1.3 树的表示:孩子兄弟表示法)

[1.4 树形结构的实际运用场景](#1.4 树形结构的实际运用场景)

[2. 二叉树 (Binary Tree)](#2. 二叉树 (Binary Tree))

[2.2 特殊的二叉树](#2.2 特殊的二叉树)

二叉树的重要性质

[2.3 二叉树的存储结构](#2.3 二叉树的存储结构)

[3. 实现顺序结构二叉树:堆 (Heap)](#3. 实现顺序结构二叉树:堆 (Heap))

[3.2 堆的实现](#3.2 堆的实现)

[3.2.1 向上调整算法 (AdjustUp) 与堆的插入](#3.2.1 向上调整算法 (AdjustUp) 与堆的插入)

[3.2.2 向下调整算法 (AdjustDown) 与堆的删除](#3.2.2 向下调整算法 (AdjustDown) 与堆的删除)

[3.3 堆的应用](#3.3 堆的应用)

[3.3.1 堆排序 (Heap Sort)](#3.3.1 堆排序 (Heap Sort))

[3.3.2 TOP-K 问题](#3.3.2 TOP-K 问题)

[4. 实现链式结构二叉树](#4. 实现链式结构二叉树)

[4.1 前、中、后序遍历](#4.1 前、中、后序遍历)

[4.3 层序遍历](#4.3 层序遍历)

[4.4 判断是否为完全二叉树](#4.4 判断是否为完全二叉树)

[6. 二叉树选择题解析](#6. 二叉树选择题解析)


1. 树 (Tree) 的基本概念

树是一种非线性的数据结构,由 n (n≥0) 个有限结点组成一个具有层次关系的集合。它看起来像一棵倒挂的树,根朝上,叶朝下。

  • 递归定义

    1. 有一个特殊的结点,称为根结点 (Root),它没有前驱结点。
    2. 除根结点外,其余结点被分成 M (M>0) 个互不相交的集合 T1, T2, ..., Tm,其中每一个集合 Ti 又是一棵结构与树类似的子树。
    3. 每棵子树的根结点有且只有一个前驱,可以有0个或多个后继。
  • 核心特征

    • 子树之间不能有交集(否则就是图结构)。
    • 除了根结点,每个结点有且仅有一个父结点。
    • 一棵有 N 个结点的树有 N-1 条边。
1.2 树的相关术语

  • 父结点/双亲结点 (Parent):若一个结点含有子结点,则这个结点称为其子结点的父结点。
  • 子结点/孩子结点 (Child):一个结点含有的子树的根结点称为该结点的子结点。
  • 结点的度 (Degree of Node):一个结点有几个孩子,它的度就是多少。
  • 树的度 (Degree of Tree):一棵树中,所有结点的度的最大值。
  • 叶子结点/终端结点 (Leaf):度为 0 的结点。
  • 分支结点/非终端结点 (Branch):度不为 0 的结点。
  • 兄弟结点 (Sibling):具有相同父结点的结点。
  • 结点的层次 (Level):从根开始定义,根为第 1 层,根的子结点为第 2 层,以此类推。
  • 树的高度或深度 (Height/Depth):树中结点的最大层次。
  • 祖先 (Ancestor):从根到该结点所经分支上的所有结点。
  • 子孙 (Descendant):以某结点为根的子树中任一结点。
  • 路径 (Path):从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。
  • 森林 (Forest):由 m (m>0) 棵互不相交的树的集合。
1.3 树的表示:孩子兄弟表示法

树的存储表示方式有多种,如双亲表示法、孩子表示法等。最常用的是孩子兄弟表示法

cpp 复制代码
struct TreeNode 
{
    struct Node* child;   // 指向左边开始的第一个孩子结点
    struct Node* brother; // 指向其右边的下一个兄弟结点
    int data;             // 结点中的数据域
};

这种方法巧妙地将一棵复杂的多叉树转化为一个二叉链表结构,便于统一处理。

1.4 树形结构的实际运用场景
  • 文件系统:计算机的文件系统(如 Windows 资源管理器)是树形结构的典型应用。设备和驱动器是根,文件夹是分支结点,文件是叶子结点,清晰地表达了层级关系。

2. 二叉树 (Binary Tree)

二叉树是树形结构中最常用的一种。一棵二叉树是结点的一个有限集合,该集合或者为空,或者由一个根结点加上两棵分别称为左子树和右子树的二叉树组成。

  • 特点
    1. 二叉树不存在度大于 2 的结点。
    2. 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树
2.2 特殊的二叉树
  • 满二叉树 (Full Binary Tree):一个二叉树,如果每一层的结点数都达到最大值,则这个二叉树就是满二叉树。如果层数为 K,则结点总数为 2^k - 1。
  • 完全二叉树 (Complete Binary Tree) :对于深度为 K 的,有 n 个结点的二叉树,当且仅当其每一个结点都与深度为 K 的满二叉树中编号从 1 至 n 的结点一一对应时,称之为完全二叉树。满二叉树是一种特殊的完全二叉树
二叉树的重要性质
  1. 若规定根结点的层数为 1,则一棵非空二叉树的第 i 层上最多有 2^(i-1) 个结点。
  2. 若规定根结点的层数为 1,则深度为 h 的二叉树的最大结点数是 2^h - 1
  3. 若规定根结点的层数为 1,具有 n 个结点的满二叉树的深度 h = log₂(n+1)
  4. 对任何一棵二叉树,如果度为 0 的叶结点个数为 n₀,度为 2 的分支结点个数为 n₂,则有 n₀ = n₂ + 1
    • 证明 :设度为1的节点数为 n₁,总节点数 N = n₀ + n₁ + n₂。树的边数为 N-1。从度的角度看,边数也等于 0n₀ + 1n₁ + 2*n₂。联立两式:n₀ + n₁ + n₂ - 1 = n₁ + 2n₂,化简得 n₀ = n₂ + 1。
2.3 二叉树的存储结构
  • 顺序结构 :使用数组存储。一般只适合表示完全二叉树 ,因为非完全二叉树会造成空间浪费。现实中,堆 (Heap) 这种特殊的二叉树通常使用顺序结构的数组来存储。
  • 链式结构 :用链表来表示。每个结点由三个域组成:数据域、左指针域、右指针域。
    • 二叉链leftChild, data, rightChild。这是最常用的结构。
    • 三叉链leftChild, data, rightChild, parent。多了一个指向父节点的指针,便于某些操作。

3. 实现顺序结构二叉树:堆 (Heap)

堆是一种特殊的完全二叉树,使用数组存储。它满足以下性质:

  • 堆中某个结点的值总是不大于(小堆)或不小于(大堆)其父结点的值。
  • 堆总是一棵完全二叉树。

堆的性质(数组下标从0开始)

对于序号为 i 的结点:

  1. 若 i > 0,其双亲序号为 (i-1)/2;若 i=0,则为根结点,无双亲。
  2. 若 2i+1 < n,其左孩子序号为 2i+1;否则无左孩子。
  3. 若 2i+2 < n,其右孩子序号为 2i+2;否则无右孩子。
3.2 堆的实现

堆的底层结构为数组,定义如下:

cpp 复制代码
typedef int HPDataType;
typedef struct Heap 
{
    HPDataType* a;
    int size;
    int capacity;
} HP;
3.2.1 向上调整算法 (AdjustUp) 与堆的插入
  • 插入逻辑:将新数据插入到数组的尾部,然后进行向上调整,直到满足堆的性质。

  • 代码实现

    cpp 复制代码
    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;
            }
        }
    }
    
    void HPPush(HP* php, HPDataType x) 
        {
        // 1. 检查容量,不足则扩容
        if (php->size == php->capacity) 
        {
            // ... realloc 逻辑 ...
        }
        // 2. 插入数据到尾部
        php->a[php->size] = x;
        php->size++;
        // 3. 向上调整
        AdjustUp(php->a, php->size - 1);
    }
  • 向上调整建堆时间复杂度 :O(NlogN)。

    • 推导 :假设是满二叉树,高度为 h。第 i 层有 2^(i-1) 个节点,最多需要向上调整 i-1 次。总移动次数 T(h) = 2¹1 + 2²2 + ... + 2^(h-1)*(h-1)。通过错位相减法,最终推导出 T(n) ≈ nlog₂n。
3.2.2 向下调整算法 (AdjustDown) 与堆的删除
  • 删除逻辑:删除堆顶数据。将堆顶元素与最后一个元素交换,然后删除数组最后一个数据(即原堆顶),再对新的堆顶进行向下调整。

  • 前提:左右子树必须已经是一个堆。

  • 代码实现

    cpp 复制代码
    void AdjustDown(HPDataType* a, int n, int parent) 
    {
        int child = parent * 2 + 1; // 假设左孩子
        while (child < n) 
        {
            // 1. 选出左右孩子中大的那个(大堆)
            if (child + 1 < n && a[child + 1] > a[child]) 
            {
                ++child;
            }
            // 2. 孩子比父亲大,就交换
            if (a[child] > a[parent]) 
            {
                Swap(&a[child], &a[parent]);
                parent = child;
                child = parent * 2 + 1;
            } 
            else 
            {
                break;
            }
        }
    }
    
    void HPPop(HP* php) 
        {
        assert(php->size > 0);
        // 1. 交换堆顶和堆尾
        Swap(&php->a[0], &php->a[php->size - 1]);
        // 2. 删除堆尾
        php->size--;
        // 3. 向下调整
        AdjustDown(php->a, php->size, 0);
    }
  • 向下调整建堆时间复杂度 :O(N)。

    • 推导:从最后一个非叶子节点开始,从右到左、从下到上进行向下调整。假设树高为 h,第 i 层有 2^(i-1) 个节点,最多需要向下调整 h-i 次。总移动次数 T(h) = 2⁰*(h-1) + 2¹*(h-2) + ... + 2^(h-2)*1。通过错位相减法,最终推导出 T(n) ≈ n。
3.3 堆的应用
3.3.1 堆排序 (Heap Sort)
  • 版本二(原地排序,推荐)

    1. 建堆:对数组使用向下调整算法建堆(O(N))。升序建大堆,降序建小堆。
    2. 排序:将堆顶元素与堆尾元素交换,堆的有效大小减1,然后对新的堆顶进行向下调整(O(logN))。重复此过程直到堆中只剩一个元素。
    • 总时间复杂度 :O(N) + O(NlogN) = O(NlogN)
3.3.2 TOP-K 问题
  • 问题:求数据集合中前 K 个最大或最小的元素。
  • 最佳方案 :使用堆。
    1. 用数据集合中前 K 个元素来建堆。
      • 求前 K 个最大 的元素,则建小堆
      • 求前 K 个最小 的元素,则建大堆
    2. 用剩余的 N-K 个元素依次与堆顶元素比较。
      • 如果新元素比堆顶元素更符合条件(例如,找最大时,新元素比小堆的堆顶大),则替换堆顶元素,并进行一次向下调整。
    3. 遍历完成后,堆中剩余的 K 个元素就是所求结果。
  • 时间复杂度:O(K + (N-K)logK)。

4. 实现链式结构二叉树

链式二叉树通过指针连接,其结构定义如下:

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

二叉树的定义是递归式的,因此其操作也基本都是递归实现。

4.1 前、中、后序遍历

遍历规则基于访问根结点的时机:

  • 前序遍历 (Preorder):根结点 -> 左子树 -> 右子树
  • 中序遍历 (Inorder):左子树 -> 根结点 -> 右子树
  • 后序遍历 (Postorder):左子树 -> 右子树 -> 根结点

代码实现

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

void InOrder(BTNode* root) {
    if (root == NULL) { printf("N "); return; }
    InOrder(root->left);
    printf("%d ", root->val);
    InOrder(root->right);
}

void PostOrder(BTNode* root) {
    if (root == NULL) { printf("N "); return; }
    PostOrder(root->left);
    PostOrder(root->right);
    printf("%d ", root->val);
}
4.3 层序遍历
  • 定义:从根结点出发,自上而下,自左至右逐层访问树的结点。
  • 实现 :需要借助队列
    1. 根节点入队。
    2. 当队列不为空时,取出队头节点并访问。
    3. 如果该节点有左孩子,左孩子入队。
    4. 如果该节点有右孩子,右孩子入队。
    5. 重复步骤2-4。
4.4 判断是否为完全二叉树
  • 思路 :基于层序遍历。
    1. 进行层序遍历,但遇到空节点(NULL)时不跳出,而是将其子节点(也是NULL)继续入队。
    2. 当从队列中取出一个 NULL 节点时,停止入队操作。
    3. 检查队列中剩余的所有节点,如果全都是 NULL,则是完全二叉树;如果还有非 NULL 节点,则不是。

6. 二叉树选择题解析

二叉树性质题

  1. 某二叉树共有 399 个结点,其中有 199 个度为 2 的结点,则该二叉树中的叶子结点数为()

    • 解析:根据性质 n₀ = n₂ + 1。已知 n₂ = 199,所以 n₀ = 199 + 1 = 200。
    • 答案:B (200)
  2. 在具有 2n 个结点的完全二叉树中,叶子结点个数为 ()

    • 解析:在完全二叉树中,度为1的节点 n₁ 只能是 0 或 1。总节点数 N = n₀ + n₁ + n₂ = 2n。又因为 n₀ = n₂ + 1,所以 N = 2n₀ + n₁ - 1 = 2n。即 2n₀ = 2n - n₁ + 1。因为 2n₀ 和 2n 都是偶数,所以 n₁ 必须是 1。代入得 2n₀ = 2n - 1 + 1 = 2n,所以 n₀ = n。
    • 答案:A (n)
  3. 一棵完全二叉树的结点数位为531个,那么这棵树的高度为 ()

    • 解析:深度为 h 的满二叉树节点数为 2^h - 1。2⁹ - 1 = 511,2¹⁰ - 1 = 1023。因为 511 < 531 ≤ 1023,所以高度为 10。
    • 答案:B (10)
  4. 一个具有 767 个结点的完全二叉树,其叶子结点个数为 ()

    • 解析:N = 767。根据完全二叉树性质,n₁ 只能是 0 或 1。N = n₀ + n₁ + n₂ = 2n₀ + n₁ - 1 = 767。所以 2n₀ = 768 - n₁。因为 2n₀ 是偶数,768 也是偶数,所以 n₁ 必须是 0。代入得 2n₀ = 768,n₀ = 384。
    • 答案:B (384)

链式二叉树遍历选择题

  1. 某完全二叉树按层次输出(同一层从左到右)的序列为 ABCDEFGH。该完全二叉树的前序序列为()

    • 解析 :根据层序序列 ABCDEFGH 还原完全二叉树结构:A是根,B是A左孩子,C是A右孩子,D是B左孩子,E是B右孩子,F是C左孩子,G是C右孩子,H是D左孩子。前序遍历(根左右):A -> B -> D -> H -> E -> C -> F -> G。
    • 答案:A (ABDHECFG)
  2. 二叉树的先序遍历和中序遍历如下:先序遍历:EFHIGJK; 中序遍历:HFIEJKG. 则二叉树根结点为()

    • 解析:先序遍历的第一个节点就是根节点。
    • 答案:A (E)
  3. 设一课二叉树的中序遍历序列:badce,后序遍历序列:bdeca,则二叉树前序遍历序列为 ____。

    • 解析
      1. 后序 bdeca 的最后一个 a 是根。
      2. 在中序 badce 中找到 aa 左边 b 是左子树,右边 dce 是右子树。
      3. 看右子树:后序 dec 的最后一个 c 是右子树的根。中序 dce 中,c 左边 d 是左孩子,右边 e 是右孩子。
      4. 树结构为:a 是根,左孩子 b,右孩子 c;c 的左孩子 d,右孩子 e。
      5. 前序遍历(根左右):a -> b -> c -> d -> e。
    • 答案:D (abcde)
  4. 某二叉树的后序遍历序列与中序遍历序列相同,均为 ABCDEF,则按层次输出(同一层从左到右)的序列为

    • 解析
      1. 后序 ABCDEF 的最后一个 F 是根。
      2. 中序 ABCDEF 中,F 在最后,说明 F 没有右子树,ABCDE 是其左子树。
      3. 递归分析左子树:后序 ABCDEE 是根,中序 ABCDEE 在最后,说明 E 没有右子树,ABCD 是其左子树。
      4. 以此类推,这棵树是一个所有节点都只有左孩子的单支树:F -> E -> D
相关推荐
知无不研1 小时前
算法:判断一个数是不是2的幂
数据结构·算法
luj_17689 小时前
残熵算法实时化三大瓶颈突破
c语言·开发语言·网络·经验分享·算法
mCell10 小时前
你以为短链接只是 Hash + 301/302?
后端·算法·架构
码少女12 小时前
数据结构——哈希表的基础
数据结构·散列表
KaMeidebaby12 小时前
卡梅德生物技术快报|抗体亲和力成熟工业化调控新机制:差异性浆细胞增殖工艺优化思路
java·开发语言·人工智能·算法·机器学习·架构·spark
luj_176812 小时前
心形曲线轨迹控制三大关键技术
c语言·开发语言·c++·经验分享·算法
tmlx3I08113 小时前
高光谱拼接算法(六)RANSAC 误匹配剔除
人工智能·算法·机器学习
不相心 -w-13 小时前
基础-滑动窗口-(定长 | 不定长)
算法
醉城夜风~13 小时前
Java详解经典算法题:接雨水(三种实现方案+原理剖析)
java·开发语言·算法