二叉树 2 堆

前置理解 帮助学习 学习目的

堆 是一种有 父子间大小 关系要求 的 完全二叉树

所以分大堆 和 小堆

建堆 就是 将一棵完全二叉树 通过调整算法 实现 刚才的关系就好

因此 堆有这样的 特点 堆 顶 必然是 这个数组里的最值

通过 依次拿取堆顶数据 后继续调整 建堆 再 拿去

首先拿到的是 最大值 其次是第二大的 再其次是第三大的

就这样可以实现对该树 (数组)中的数据 进行 有序读取

间接实现了 排序

push 的目的是为了 保持堆的性质

pop 是为了取出堆顶的数据 能得到出最值

或者说 有了 排序的特点 就是 堆排序 算法的前世 前传

1 区分数据结构中的堆栈 和 操作系统里的 堆 栈帧 区分

💡 关键误区**:名字相同,概念完全 无关**

一、左侧:数据结构层面(逻辑结构,和内存无关)

二、右侧:操作系统内存分区(物理内存,程序运行时的存储空间)

2 简单分类 大根堆 和 小根堆

1 都是基于 完全二叉树

2 只要求父子之间的大小关系 对于兄弟节点之间并没有 要求

因此 不会得出 大根堆 一定的降序 小根堆一定是 升序的结论 纯属偶然


堆是完全二叉树,通常用数组顺序存储,分两类:大根堆、小根堆。

1. 大根堆(大顶堆)

父节点 ≥ 左右两个子节点的值 ,堆顶(树根)是整棵堆里最大值。

公式:arr[父] ≥ arr[左孩子] && arr[父] ≥ arr[右孩子]

2. 小根堆(小顶堆)

父节点 ≤ 左右两个子节点的值 ,堆顶(树根)是整棵堆里最小值

公式:arr[父] ≤ arr[左孩子] && arr[父] ≤ arr[右孩子]

简单记忆口诀: x堆 父亲节点是该子树里 的 相对x 值

大堆 该子树里 较大值 小堆 该子树里 较小值

3 堆 的 用途

1. TOP-K 问题(最常用)

① 求前 K 大 → 小根堆(容量 = K)

先把前 K 个数入堆,构建小根堆(堆顶是当前 K 个里最小值)

剩下每个数:比堆顶大就替换堆顶、堆化;否则跳过

最终堆内就是 最大的 K 个数字 比方一亿个数中最小的十个数

2. 堆排序

根堆实现升序排序:

复制代码
 ### 无序数组建成大根堆(堆顶是最大值)
复制代码
 ### 堆顶和末尾元素交换,末尾固定为有序值,剩余数组重新堆化
复制代码
 ### 循环往复,数组从后往前生成有序序列

4 堆的实现

one

two

three

5 **数组(顺序表)**插入数据后 利用向上调整算法

大顶堆上浮(向上调整 ) 完整逻辑梳理

起点下标 = 新插入元素的数组末尾下标(尾插位置)

循环当前节点父节点 对比: 大顶堆规则:父 ≥ 左右孩子,任意子树根是子树最大值

复制代码
 ### 当前节点>父节点:违背大顶堆规则 → **父子交换**,更新当前下标为父下标,继续向上找 新父节点 比对
复制代码
 ### 当前节点 ≤ 父节点:满足堆性质 → 上浮终止

边界:当前下标走到0(根节点),没有父节点,直接结束。

所以最少就是一次都不要调整 插入就满足堆的规则

最坏是 层数-1 ==边数 就是该叶节点的值 比它到根路径上所有的祖先节点的值都小 就是需要交换 边数次 就是 有几个祖先 就是交换几次

中间 的都有可能

需要利用之前的 下标寻找父亲的方法

变相说明 堆 是一棵完全二叉树 但是 有限制 有父子关系规则

实际存储结构 物理结构就是数组 插入就是尾插 + 向上调整 (上浮)

  • 物理结构:数组(连续内存),逻辑结构:完全二叉树
  • 新增元素第一步永远数组尾部插入,堆的特性靠上浮维护
  • 完全二叉树特性决定堆能用数组无空隙存储,没有空洞浪费空间

如果是大堆的话 就是 根节点最大

-- 拓宽就是 每一棵子树的根节点 是最大的

总结 不就是 任意二叉堆 的堆顶 是 最值

利用最开始插入位置的下标 作为起点下标

1 用 该节点的值 与它的父亲节点比较

如果不满足 父子关系 就可以利用向上调整算法 来实现 节点交换

结束的条件是 任意一棵子树 总满足 父亲节点的值 >= 子节点

但是兄弟之间并未要求

最后 尾插定点位,从该下标往上,大顶堆孩子比爹大就互换,直到爹≥孩子或到树根。

6 插入数据到 堆里

前置理解

实际上的 堆 就是 完全 二叉树

我们用 连续存储 (数组)的方式(注 :也可以用链表 ) 来实现 堆

尽管我们控制的是 下标和 数组 我们要想象 控制的是二叉树

但我们这里先用 数组 (顺序表)来实现

第一步 传入孩子的下标

第二步 计算最近的父亲下标

第三步 比较 父亲和孩子节点 满足堆的要求 就break 退出循环

不满足 就去交换父子 关系 倒反天罡一次 {

里面有 1 交换父子值

2 更新下标 原来的父亲下标 更新为新的儿子 下标

向上找新的父亲 继续循环比较

3 利用新的儿子下标 锁定新的 父亲的下标

4 只要 child 的下标 = 0 || break 时 结束循环

}

如 上图中 最开始是判断 parent>0实际上是巧合 刚好 歪打正着

就最坏情况 当最后一次父子交换时 parent = 0 child = 1

此时 parent = =child = =0; 依旧会进入循环的 parent>=0 就进的

继续画图了解

这样结束了 纯属狗运 就是 石山代码 哈哈哈

和上面这个网图 一样 尽管都可以实现 但是更推荐 使用child >0 的

最坏情况下会少进入一次循环 的 下面这次就不会进去

7 建 堆 过 程 将下面的数 建堆 就可以看作 将下列数据依次 Push 插入之后

利用向上调整算法 就自然 形成 一个堆了 微分思想

拆解问题 将大的问题拆解为可执行的小步骤 循环执行

最后就是一个小堆 上图就是这样子 的

8 删除 pop 堆顶的数据 就是 删除 根节点

主要是覆盖的方法 就会使得 关系乱了 这是表象

不能使用挪动覆盖 删除

本质是 兄弟之间的 大小 是没有限制和要求的

使得 新形成的父子之间 可能是原来的兄弟关系 也是没有大小关系的 就不是堆

解决之道 在于 狸猫换太子 假戏真做 在替身 消失

1 就是将根节点 和 最下层最 右边的节点 交换位置

2 再将最后那个在数组里尾删 就好了 size--

3 利用 向 下调整算法 进行调整 前提是根出发的左右子树都是 成堆的 都是堆

如果是 小堆 向下调整时 大的 向下 这是相对的 小的向上

区别 在于 出发点 向下调整就是 从根出发的

而 向上调整 是从叶节点开始的

间接实现 (伪)堆排序打印

9 无论是向下调整建堆 和 向上调整建堆 单次调整 时间复杂度 都是 log N 的


头文件

#define _CRT_SECURE_NO_WARNINGS 1

#pragma once

#include<stdio.h>

#include<assert.h>

#include<stdlib.h>

#include<stdbool.h>

typedef int HPDataType;

typedef struct Heap

{

HPDataType* a;

int size;

int capacity;

}HP;

void Swap(HPDataType* p1, HPDataType* p2);

void AdjustUp(HPDataType* a, int child);

void AdjustDown(HPDataType* a, int n, int parent);

void HPInit(HP* php);

void HPDestroy(HP* php);

void HPPush(HP* php, HPDataType x);

void HPPop(HP* php);

HPDataType HPTop(HP* php);

bool HPEmpty(HP* php);


源码 源文件

#include"Heap.h"

void HPInit(HP* php)

{

assert(php);

php->a = NULL;

php->size = php->capacity = 0;

}

void HPDestroy(HP* php)

{

assert(php);

free(php->a);

php->a = NULL;

php->size = php->capacity = 0;

}

void Swap(HPDataType* p1, HPDataType* p2)

{

HPDataType tmp = *p1;

*p1 = *p2;

*p2 = tmp;

}

void AdjustUp(HPDataType* a, int child)

{

// 初始条件

// 中间过程

// 结束条件

int parent = (child - 1) / 2;

//while (parent >= 0)

while (child > 0)

{

if (achild < aparent)

{

Swap(&achild, &aparent);

child = parent;

parent = (child - 1) / 2;

}

else

{

break;

}

}

}

void HPPush(HP* php, HPDataType x)

{

assert(php);

if (php->size == php->capacity)

{

int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;

HPDataType* tmp = (HPDataType*)realloc(php->a, newcapacity * sizeof(HPDataType));

if (tmp == NULL)

{

perror("realloc fail");

return;

}

php->a = tmp;

php->capacity = newcapacity;

}

php->aphp-\>size = x;

php->size++;

AdjustUp(php->a, php->size - 1);

}

void AdjustDown(HPDataType* a, int n, int parent)

{

// 先假设左孩子小

int child = parent * 2 + 1;

while (child < n) // child >= n说明孩子不存在,调整到叶子了

{

// 找出小的那个孩子

if (child + 1 < n && achild + 1 < achild)

{

++child;

}

if (achild < aparent)

{

Swap(&achild, &aparent);

parent = child;

child = parent * 2 + 1;

}

else

{

break;

}

}

}

// logN

void HPPop(HP* php)

{

assert(php);

assert(php->size > 0);

Swap(&php->a0, &php->aphp-\>size-1);

php->size--;

AdjustDown(php->a, php->size, 0);

}

HPDataType HPTop(HP* php)

{

assert(php);

assert(php->size > 0);

return php->a0;

}

bool HPEmpty(HP* php)

{

assert(php);

return php->size == 0;

}

相关推荐
wabs6668 小时前
关于贪心算法的思考
算法·贪心算法
社交怪人8 小时前
【判断大小】信息学奥赛一本通C语言解法(题号1043)
算法
Snasph9 小时前
GNU Make 用户手册(中文版)
服务器·算法·gnu
江澎涌9 小时前
拆解与 AI 的一次对话
人工智能·算法·程序员
sheeta19989 小时前
LeetCode 每日一题笔记 日期:2026.06.02 题目:3635. 最早完成陆地和水上游乐设施的时间 II
笔记·算法·leetcode
Lsk_Smion10 小时前
力扣实训 _ [102].层序遍历--前序--后续_递归与非递归的实现
数据结构·算法·leetcode
小欣加油11 小时前
leetcode3751 范围内总波动值I
java·数据结构·c++·算法·leetcode
Halo_tjn12 小时前
反射与设计模式1
java·开发语言·算法
V搜xhliang024613 小时前
临床科研新范式:从选题到投稿,AI智能体如何接管全流程?
运维·数据结构·人工智能·算法·microsoft·数据挖掘·自动化