完全二叉树与堆底层原理深度剖析 | 手写C++大顶堆实现
完全二叉树与堆底层原理深度剖析 | 手写C++大顶堆实现
- 前言💫
- 🌳 一、前置基石:完全二叉树深度解析
- 1.1 完全二叉树数组存储的底层逻辑
- 1.2 节点编号映射公式📐
- 规则① 编号从 1 开始
- 规则② 编号从 0 开始(编程数组常用)
- 1.3 字符图解:数组与完全二叉树映射
- 树形逻辑结构
- 对应一维数组存储
- ⛰️ 二、堆的定义与核心性质拆解
- 2.1 堆的本质定义
- 2.2 堆中极值分布规律
- 进阶极值位置分析
- 🔧 三、堆元素插入 & 向上(上浮)调整
- 3.1 插入规则
- 3.2 上浮调整原理(大顶堆)
- 3.3 C++ 大顶堆插入 + 上浮核心代码
- 📉 四、堆顶弹出 & 向下(下沉)调整
- 4.1 堆弹出规则
- 4.2 下沉调整原理(大顶堆)
- 4.3 C++ 堆弹出 + 下沉完整代码
- 💡 五、数据结构终极核心心法
- 📝 六、知识点总结
前言💫
在编程算法的江湖里,堆绝对是不可或缺的核心数据结构,无论是堆排序、优先队列、TopK 问题求解,背后都离不开堆的支撑。
而堆并非凭空诞生,它深度依附于完全二叉树,二者有着密不可分的羁绊。很多同学学堆只记操作、不懂底层逻辑,上手写代码一头雾水。
今天带你由浅入深,从完全二叉树底层逻辑出发,层层拆解堆的定义、性质、元素插入、堆顶弹出、上浮 / 下沉调整原理,搭配字符图解 + 可直接运行的 C++ 源码,彻底吃透堆的核心精髓,打通数据结构思维壁垒!
🌳 一、前置基石:完全二叉树深度解析
1.1 完全二叉树数组存储的底层逻辑
完全二叉树最大的特性:可以用一段连续的一维数组完成存储,无需像普通二叉树那样存储指针、浪费空间。
究其根本:完全二叉树的父节点与子节点编号存在固定数学映射关系,节点之间排布紧凑、无空缺空位,天然适配连续数组存储。
我们要建立一种顶级编程思维:
逻辑层面是二维树形结构,存储层面是一维数组结构
高手看数组,眼中自动映射出树形结构;新手看数组,只看到一串数字。
1.2 节点编号映射公式📐
设定父节点编号为i ii,分两种编号规则:
规则① 编号从 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]
树形逻辑结构
5 / 6 3 / / 2 1 9 8 / 12 11对应一维数组存储
下标:0 1 2 3 4 5 6 7 8 数值:5 6 3 2 1 9 8 12 11二者信息完全等价,只是逻辑视图和存储视图的区别,这是学习堆必须建立的直觉思维。
⛰️ 二、堆的定义与核心性质拆解
2.1 堆的本质定义
堆是特殊的完全二叉树,在完全二叉树结构基础上,额外增加了父子节点大小约束规则,分为两大类:
大顶堆(最大堆):树中任意父节点值 > 左右子节点值
小顶堆(最小堆):树中任意父节点值 < 左右子节点值
核心规律:只约束父子节点大小关系,兄弟节点之间无任何大小约束,可随意交换位置且不破坏堆结构。
2.2 堆中极值分布规律
大顶堆:全局最大值一定在堆顶根节点
小顶堆:全局最小值一定在堆顶根节点
进阶极值位置分析
以大顶堆为例:
第二大值:只会出现在根节点的左孩子 或 右孩子
第三大值:可能出现在第二层 或 第三层所有节点
第四大值:可能分布在第二层、第三层、第四层
原理很简单:只有父子有大小层级,横向兄弟无约束,层级越靠下,节点越有可能成为次大值。
字符简易示意:
根(第1层) 最大值 / 左子(2层) 右子(2层) 第二大候选 / / 3层节点 3层节点 第三大候选🔧 三、堆元素插入 & 向上(上浮)调整
3.1 插入规则
堆是完全二叉树,必须遵守完全二叉树的排布规则:
新元素默认添加在完全二叉树最后一层最左侧
映射到数组:直接追加到数组末尾
插入后大概率会破坏大顶堆 / 小顶堆的性质,因此需要向上上浮调整,重新维护堆的规则。
3.2 上浮调整原理(大顶堆)
将新元素放在数组末尾
循环将当前节点与父节点比较
若当前节点 > 父节点,交换二者位置
持续向上比对,直到满足堆性质 或 到达堆顶
3.3 C++ 大顶堆插入 + 上浮核心代码
#include<iostream>#include<vector>usingnamespacestd;classMaxHeap{private:vector<int>heap;// 向上上浮调整voidupAdjust(intidx){// 找到父节点下标while(idx>0){intparent=(idx-1)/2;// 子节点小于等于父节点,无需调整if(heap[idx]<=heap[parent])break;// 交换父子节点swap(heap[idx],heap[parent]);// 向上继续比对idx=parent;}}public:// 堆中插入元素voidpush(intval){heap.push_back(val);// 从最后一个元素开始上浮调整upAdjust(heap.size()-1);}// 获取堆顶元素inttop(){returnheap.empty()?-1:heap[0];}// 判断堆是否为空boolempty(){returnheap.empty();}};intmain(){MaxHeap hp;// 批量插入元素hp.push(5);hp.push(6);hp.push(3);hp.push(12);cout<<"大顶堆堆顶最大值:"<<hp.top()<<endl;return0;}📉 四、堆顶弹出 & 向下(下沉)调整
4.1 堆弹出规则
堆的弹出操作只弹出堆顶元素(大顶堆弹最大值,小顶堆弹最小值)。
弹出后规则:
不能直接删除堆顶,会破坏完全二叉树连续存储特性
用数组最后一个元素覆盖堆顶空位
数组长度减一,舍弃末尾无效元素
从堆顶开始向下下沉调整,重新维护堆性质
4.2 下沉调整原理(大顶堆)
以当前根节点为基准,找到左右子节点中的最大值
若当前节点 < 最大子节点,交换位置
往下一层继续比对,直到满足堆性质 或 到达叶子节点
4.3 C++ 堆弹出 + 下沉完整代码
在上面MaxHeap类中追加以下方法:
// 向下下沉调整voiddownAdjust(intidx){intn=heap.size();while(true){intleft=2*idx+1;// 左孩子intright=2*idx+2;// 右孩子intmaxPos=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;}}// 弹出堆顶元素voidpop(){if(heap.empty())return;// 末尾元素覆盖堆顶heap[0]=heap.back();// 删除末尾元素heap.pop_back();// 从堆顶开始下沉调整downAdjust(0);}💡 五、数据结构终极核心心法
学完堆的所有操作,我们可以领悟所有数据结构的通用本质:
数据结构 = 定义结构性质 + 代码操作维护性质
结构定义:规定数据结构长什么样、具备什么约束规则
堆:是完全二叉树 + 父子节点大小规则
队列:先进先出、队尾入队队头出队
结构操作:插入、删除、查找等操作,最终目的都是维护既定性质
堆插入上浮、弹出下沉,都是为了不破坏大 / 小顶堆规则
队列不允许头部入队,否则就失去队列本身的性质
学习数据结构不要死记代码、死背步骤,先理解性质,再理解如何维护性质,一通百通,所有数据结构都能快速上手。
📝 六、知识点总结
完全二叉树是堆的底层载体,可通过固定公式实现数组与树形映射;
堆分为大顶堆、小顶堆,仅约束父子大小,兄弟无大小关系;
插入走末尾追加 + 向上上浮,弹出走末尾补位 + 向下下沉;
上浮从下往上比对父节点,下沉从上往下比对子节点最大值;
数据结构核心思想:定义规则,用操作维护规则。
