完全二叉树与堆底层原理深度剖析 | 手写C++大顶堆实现

完全二叉树与堆底层原理深度剖析 | 手写C++大顶堆实现

  • 前言💫
  • [🌳 一、前置基石:完全二叉树深度解析](#🌳 一、前置基石:完全二叉树深度解析)
    • [1.1 完全二叉树数组存储的底层逻辑](#1.1 完全二叉树数组存储的底层逻辑)
    • [1.2 节点编号映射公式📐](#1.2 节点编号映射公式📐)
      • [规则① 编号从 1 开始](#规则① 编号从 1 开始)
      • [规则② 编号从 0 开始(编程数组常用)](#规则② 编号从 0 开始(编程数组常用))
    • [1.3 字符图解:数组与完全二叉树映射](#1.3 字符图解:数组与完全二叉树映射)
  • [⛰️ 二、堆的定义与核心性质拆解](#⛰️ 二、堆的定义与核心性质拆解)
    • [2.1 堆的本质定义](#2.1 堆的本质定义)
    • [2.2 堆中极值分布规律](#2.2 堆中极值分布规律)
  • [🔧 三、堆元素插入 & 向上(上浮)调整](#🔧 三、堆元素插入 & 向上(上浮)调整)
    • [3.1 插入规则](#3.1 插入规则)
    • [3.2 上浮调整原理(大顶堆)](#3.2 上浮调整原理(大顶堆))
    • [3.3 C++ 大顶堆插入 + 上浮核心代码](#3.3 C++ 大顶堆插入 + 上浮核心代码)
  • [📉 四、堆顶弹出 & 向下(下沉)调整](#📉 四、堆顶弹出 & 向下(下沉)调整)
    • [4.1 堆弹出规则](#4.1 堆弹出规则)
    • [4.2 下沉调整原理(大顶堆)](#4.2 下沉调整原理(大顶堆))
    • [4.3 C++ 堆弹出 + 下沉完整代码](#4.3 C++ 堆弹出 + 下沉完整代码)
  • [💡 五、数据结构终极核心心法](#💡 五、数据结构终极核心心法)
  • [📝 六、知识点总结](#📝 六、知识点总结)

前言💫

在编程算法的江湖里,绝对是不可或缺的核心数据结构,无论是堆排序、优先队列、TopK 问题求解,背后都离不开堆的支撑。

而堆并非凭空诞生,它深度依附于完全二叉树,二者有着密不可分的羁绊。很多同学学堆只记操作、不懂底层逻辑,上手写代码一头雾水。

今天带你由浅入深,从完全二叉树底层逻辑出发,层层拆解堆的定义、性质、元素插入、堆顶弹出、上浮 / 下沉调整原理,搭配字符图解 + 可直接运行的 C++ 源码,彻底吃透堆的核心精髓,打通数据结构思维壁垒!


🌳 一、前置基石:完全二叉树深度解析

1.1 完全二叉树数组存储的底层逻辑

完全二叉树最大的特性:可以用一段连续的一维数组完成存储,无需像普通二叉树那样存储指针、浪费空间。

究其根本:完全二叉树的父节点与子节点编号存在固定数学映射关系,节点之间排布紧凑、无空缺空位,天然适配连续数组存储。

我们要建立一种顶级编程思维:

逻辑层面是二维树形结构 ,存储层面是一维数组结构

高手看数组,眼中自动映射出树形结构;新手看数组,只看到一串数字。

1.2 节点编号映射公式📐

设定父节点编号为 i i i,分两种编号规则:

规则① 编号从 1 开始

  • 左孩子编号: b o l d s y m b o l 2 i boldsymbol{2i} boldsymbol2i

  • 右孩子编号: b o l d s y m b o l 2 i + 1 boldsymbol{2i+1} boldsymbol2i+1

  • 父节点编号: b o l d s y m b o l i / 2 boldsymbol{i/2} boldsymboli/2(整数除法)

规则② 编号从 0 开始(编程数组常用)

  • 左孩子编号: b o l d s y m b o l 2 i + 1 boldsymbol{2i+1} boldsymbol2i+1

  • 右孩子编号: b o l d s y m b o l 2 i + 2 boldsymbol{2i+2} boldsymbol2i+2

  • 父节点编号: b o l d s y m b o l ( i − 1 ) / 2 boldsymbol{(i-1)/2} boldsymbol(i−1)/2(整数除法)

1.3 字符图解:数组与完全二叉树映射

示例数组:[5,6,3,2,1,9,8,12,11]

树形逻辑结构

Plain 复制代码
5
      /   
     6     3
    /    / 
   2   1 9   8
  / 
12  11

对应一维数组存储

Plain 复制代码
下标:0  1  2  3  4  5  6  7  8
数值:5  6  3  2  1  9  8 12 11

二者信息完全等价 ,只是逻辑视图存储视图的区别,这是学习堆必须建立的直觉思维。


⛰️ 二、堆的定义与核心性质拆解

2.1 堆的本质定义

堆是特殊的完全二叉树 ,在完全二叉树结构基础上,额外增加了父子节点大小约束规则,分为两大类:

  • 大顶堆(最大堆) :树中任意父节点值 > 左右子节点值

  • 小顶堆(最小堆) :树中任意父节点值 < 左右子节点值

核心规律:只约束父子节点 大小关系,兄弟节点之间无任何大小约束,可随意交换位置且不破坏堆结构。

2.2 堆中极值分布规律

  1. 大顶堆 :全局最大值一定在堆顶根节点

  2. 小顶堆 :全局最小值一定在堆顶根节点

进阶极值位置分析

以大顶堆为例:

  • 第二大值:只会出现在根节点的左孩子 或 右孩子

  • 第三大值:可能出现在第二层 或 第三层所有节点

  • 第四大值:可能分布在第二层、第三层、第四层

原理很简单:只有父子有大小层级,横向兄弟无约束,层级越靠下,节点越有可能成为次大值。

字符简易示意:

Plain 复制代码
根(第1层)  最大值
      /       
  左子(2层)  右子(2层)  第二大候选
  /         /   
3层节点  3层节点      第三大候选

🔧 三、堆元素插入 & 向上(上浮)调整

3.1 插入规则

堆是完全二叉树,必须遵守完全二叉树的排布规则:

新元素默认添加在完全二叉树最后一层最左侧

映射到数组:直接追加到数组末尾

插入后大概率会破坏大顶堆 / 小顶堆的性质 ,因此需要向上上浮调整,重新维护堆的规则。

3.2 上浮调整原理(大顶堆)

  1. 将新元素放在数组末尾

  2. 循环将当前节点与父节点比较

  3. 若当前节点 > 父节点,交换二者位置

  4. 持续向上比对,直到满足堆性质 或 到达堆顶

3.3 C++ 大顶堆插入 + 上浮核心代码

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

class MaxHeap {
private:
    vector<int> heap;

    // 向上上浮调整
    void upAdjust(int idx) {
        // 找到父节点下标
        while (idx > 0) {
            int parent = (idx - 1) / 2;
            // 子节点小于等于父节点,无需调整
            if (heap[idx] <= heap[parent]) break;
            // 交换父子节点
            swap(heap[idx], heap[parent]);
            // 向上继续比对
            idx = parent;
        }
    }

public:
    // 堆中插入元素
    void push(int val) {
        heap.push_back(val);
        // 从最后一个元素开始上浮调整
        upAdjust(heap.size() - 1);
    }

    // 获取堆顶元素
    int top() {
        return heap.empty() ? -1 : heap[0];
    }

    // 判断堆是否为空
    bool empty() {
        return heap.empty();
    }
};

int main() {
    MaxHeap hp;
    // 批量插入元素
    hp.push(5);
    hp.push(6);
    hp.push(3);
    hp.push(12);
    
    cout << "大顶堆堆顶最大值:" << hp.top() << endl;
    return 0;
}

📉 四、堆顶弹出 & 向下(下沉)调整

4.1 堆弹出规则

堆的弹出操作只弹出堆顶元素(大顶堆弹最大值,小顶堆弹最小值)。

弹出后规则:

  1. 不能直接删除堆顶,会破坏完全二叉树连续存储特性

  2. 数组最后一个元素覆盖堆顶空位

  3. 数组长度减一,舍弃末尾无效元素

  4. 从堆顶开始向下下沉调整,重新维护堆性质

4.2 下沉调整原理(大顶堆)

  1. 以当前根节点为基准,找到左右子节点中的最大值

  2. 若当前节点 < 最大子节点,交换位置

  3. 往下一层继续比对,直到满足堆性质 或 到达叶子节点

4.3 C++ 堆弹出 + 下沉完整代码

在上面MaxHeap类中追加以下方法:

cpp 复制代码
// 向下下沉调整
void downAdjust(int idx) {
    int n = heap.size();
    while (true) {
        int left = 2 * idx + 1;   // 左孩子
        int right = 2 * idx + 2;  // 右孩子
        int maxPos = idx;

        // 找三者最大值下标
        if (left < n && heap[left] > heap[maxPos]) maxPos = left;
        if (right < n && heap[right] > heap[maxPos]) maxPos = right;

        // 当前节点已是最大,无需调整
        if (maxPos == idx) break;
        swap(heap[idx], heap[maxPos]);
        // 下沉到最大值位置继续调整
        idx = maxPos;
    }
}

// 弹出堆顶元素
void pop() {
    if (heap.empty()) return;
    // 末尾元素覆盖堆顶
    heap[0] = heap.back();
    // 删除末尾元素
    heap.pop_back();
    // 从堆顶开始下沉调整
    downAdjust(0);
}

💡 五、数据结构终极核心心法

学完堆的所有操作,我们可以领悟所有数据结构的通用本质

数据结构 = 定义结构性质 + 代码操作维护性质

  1. 结构定义:规定数据结构长什么样、具备什么约束规则

    • 堆:是完全二叉树 + 父子节点大小规则

    • 队列:先进先出、队尾入队队头出队

  2. 结构操作 :插入、删除、查找等操作,最终目的都是维护既定性质

    • 堆插入上浮、弹出下沉,都是为了不破坏大 / 小顶堆规则

    • 队列不允许头部入队,否则就失去队列本身的性质

学习数据结构不要死记代码、死背步骤,先理解性质,再理解如何维护性质,一通百通,所有数据结构都能快速上手。


📝 六、知识点总结

  1. 完全二叉树是堆的底层载体,可通过固定公式实现数组与树形映射;

  2. 堆分为大顶堆、小顶堆,仅约束父子大小,兄弟无大小关系;

  3. 插入走末尾追加 + 向上上浮 ,弹出走末尾补位 + 向下下沉

  4. 上浮从下往上比对父节点,下沉从上往下比对子节点最大值;

  5. 数据结构核心思想:定义规则,用操作维护规则

相关推荐
WangN21 小时前
【通识】宇树G1_29DOF速度跟踪训练—逐章学习手册
人工智能·python·学习·机器人·具身智能
青山木1 小时前
Hot 100 --- 缺失的第一个正数
算法·leetcode·哈希算法
农民小飞侠1 小时前
[leetcode] 165. Compare Version Numbers
java·算法·leetcode
砍材农夫1 小时前
物联网实战|Spring Boot + Netty 搭建 MQTT 消息路由与流转层
java·spring boot·后端·物联网·spring
装不满的克莱因瓶2 小时前
掌握语义分割经典模型 FCN——从像素分类到端到端分割的奠基之作
人工智能·python·深度学习·算法·机器学习·分类·数据挖掘
黄毛火烧雪下2 小时前
Java 基础笔记:文件、递归与字符编码
java·开发语言·笔记
学计算机的计算基2 小时前
链表算法上篇:LeetCode 206/234/141/142/160/21 题解与易错点
java·笔记·算法·链表
信也科技布道师2 小时前
从Istio 503 NC 错误深入理解 Mesh 路由全链路原理
java·服务器·网络
大白话_NOI2 小时前
【洛谷 P2678】 [NOIP2015 提高组] 跳石头 超详细题解
c++·算法