树 与 堆:从 数 据 结 构 基 础 到 算 法 实 践 的 全 面 解 析
- 树
- 定 义
- 基 本 概 念
- 基 本 性 质
- 定 义 结 构
- 二 叉 树
- 定 义
- 特 点
- 满 二 叉 树
- 定 义
- 结 点 数
- 完 全 二 叉 树
- 定 义
- 高 度 为 h 的 完 全 二 叉 树 的 节 点 范 围
- 结 点 数 为 N 的 完 全 二 叉 树 和 满 二 叉 树 的 高 度
- 性 质
- 二 叉 树 的 存 储 结 构
- 顺 序 存 储
- 链 式 结 构
- 堆
- 定 义
- 性 质
- 小 根 堆
- 大 根 堆
- 堆 的 操 作 实 现
- 代 码 全 貌 与 功 能 介 绍
- 堆 排 序
- 建 堆
- 方 法 1 - - - 向 上 调 整 建 堆
- 方 法 2 - - - 向 下 调 整 建 堆
- 方 法 3 - - - 使 用 数 组 建 堆
- 排 序
- 完 整 排 序 过 程
- 建 堆 时 间 复 杂 度
- TOP-k 问 题
- 总 结
💻作 者 简 介:曾 与 你 一 样 迷 茫,现 以 经 验 助 你 入 门 数据 结 构。
💡个 人 主 页:@笑口常开xpr 的 个 人 主 页
📚系 列 专 栏:硬 核 数 据 结 构 与 算 法
✨代 码 趣 语:树 的 直 观 概 念 意 味 着 我 们 对 数 据 进 行 组 织。
💪代 码 千 行,始 于 坚 持,每 日 敲 码,进 阶 编 程 之 路。
📦gitee 链 接:gitee
堆 作 为 计 算 机 科 学 中 一 种 重 要 的 数 据 结 构,在 排 序 算 法、优 先 队 列 和 内 存 管 理 等 领 域 有 着 广 泛 应 用。本 文 将 从 C 语 言 的 角 度 出 发,详 细 解 析 堆 的 实 现 原 理、核 心 操 作 及 其 应 用 场 景。
树
定 义
树 是 一 种 非 线 性 的 数 据 结 构,它 是 由 n(n>=0)个 有 限 结 点 组 成 一 个 具 有 层 次 关 系 的 集 合。
基 本 概 念
结 点 的 度:一 个 结 点 含 有 的 子 树 的 个 数 称 为 该 结 点 的 度; 如 上 图:A 的 度 为 6。
叶 结 点 或 终 端 结 点:度 为 0 的 结 点 称 为 叶 结 点; 如 上 图:B、C、H、I…等 结 点 为 叶 结 点。
非 终 端 结 点 或 分 支 结 点:度 不 为 0 的 结 点;如 上 图:D、E、F、G…等 结 点 为 分 支 结 点。
双 亲 结 点 或 父 结 点:若 一 个 结 点 含 有 子 结 点,则 这 个 结 点 称 为 其 子 结 点 的 父 结 点; 如 上 图:A 是 B 的 父 结 点。
孩 子 结 点 或 子 结 点:一 个 结 点 含 有 的子 树 的 根 结 点 称 为 该 结 点 的 子 结 点;如 上 图:B 是 A 的 孩 子 结 点。
兄 弟 结 点:具 有 相 同 父 结 点 的 结 点 互 称 为 兄 弟 结 点,这 里 指 的 是 亲 兄 弟。如 上 图:B、C 是 兄 弟 结 点。
树 的 度:一 棵 树 中,最 大 的 结 点 的 度 称 为 树 的 度; 如 上 图:树 的 度 为 6。
结 点 的 层 次:从 根 开 始 定 义 起,根 为 第 1 层,根 的 子 结 点 为 第 2 层,以 此 类 推。
树 的 高 度 或 深 度:树 中 结 点 的 最 大 层 次; 如 上 图:树 的 高 度 为 4。
堂 兄 弟 结 点:双 亲 在 同 一 层 的 结 点 互 为 堂 兄 弟;如 上 图:H、I 互 为 兄 弟 结 点。
结 点 的 祖 先:从 根 到 该 结 点 所 经 分 支 上 的 所 有 结 点;如 上 图:A 是 所 有 结 点 的 祖 先。
子 孙:以 某 结 点 为 根 的 子 树 中 任 一 结 点 都 称 为 该 结 点 的 子 孙。如 上 图:所 有 结 点 都 是 A 的 子 孙。
森 林:由 m(m>0)棵 互 不 相 交 的 树 的 集 合 称 为 森 林。
注 意
1、树 只 有 度 的 概 念,有 向 图 有 入 度 和 出 度 的 概 念,这 一 点 应 严 格 区 分。
2、树 的 高 度 或 深 度 是 从 1 开 始 的,如 上 图 树 的 高 度 是 4,如 果 是 从 0 开 始 的,无 法 计 算 空 树 的 高 度。
3、数 的 结 构 是 根 朝 上,叶 朝 下 的。
基 本 性 质
1、任 何 一 棵 树 都 是 有 根 和 子 树 构 成 的。
2、子 树 是 不 相 交 的。如 果 相 交,会 导 致 递 归 的 死 循 环。
3、除 了 根 结 点 外,每 个 结 点 有 且 仅 有 一 个 父 结 点。
4、一 棵 N 个 结 点 的 树 有 N - 1 条 边。
定 义 结 构
方 法 1 - - - 已 知 树 的 度
#define N 5 //假设已知树的度为5
struct TreeNode
{struct TreeNode* a[N]; //指针数组int data;
};
方 法 2 - - - 顺 序 表 或 链 表
//链表节点结构体
typedef struct ListNode
{TreeNode* data; //指向树节点struct ListNode* next; //指向下一个链表节点
}ListNode;//树节点结构体
typedef struct TreeNode
{int data; //树节点数据ListNode* children; //指向子节点链表的头节点
}TreeNode;//树的结构体
typedef struct Tree
{TreeNode* root; // 树的根节点
}Tree;
方 法 3 - - - 左 孩 子 右 兄 弟
typedef int DataType;
struct Node
{struct Node* firstChild1; //第一个孩子结点struct Node* pNextBrother; //指向其下一个兄弟结点DataType data; //结点中的数据域
};
二 叉 树
定 义
一 棵 二 叉 树 是 结 点 的 一 个 有 限 集 合,该 集 合:
- 或 者 为 空
- 由 一 个 根 结 点 加 上 两 棵 别 称 为 左 子 树 和 右 子 树 的 二 叉 树 组 成。
特 点
1、二 叉 树 不 存 在 度 大 于 2 的 结 点。
2、二 叉 树 的 子 树 有 左 右 之 分,次 序 不 能 颠 倒。
二 叉 树 只 能 有 以 下 几 种 情 况 构 成:
满 二 叉 树
定 义
一 个 二 叉 树,如 果 每 一 个 层 的 结 点 数 都 达 到 最 大 值,则 这 个 二 叉 树 就 是 满 二 叉 树。
结 点 数
如 果 一 个 二 叉 树 的 层 数 为 K,第 1 层 的 结 点 数 为 2 ^ 0,第 2 层 的 结 点 数 为 2 ^ 1,第 3 层 的 结 点 数 为 2 ^ 2,以 此 类 推,最 后 一 层 的 结 点 数 为 2 ^ (k-1),结 点 总 数 是 (2 ^ k) - 1,则 它 就 是 满 二 叉 树。
前 K - 1 层 都 是 满 的,最 后 一 层 要 求 是 从 左 到 右 是 连 续 的。
完 全 二 叉 树
定 义
对 于 深 度 为 K 的,有 n 个 结 点 的 二 叉 树,当 且 仅 当 其 每 一 个 结 点 都 与 深 度 为 K 的 满 二 叉 树 中 编 号 从 1 至 n 的 结 点 一 一 对 应 时 称 之 为 完 全 二 叉 树。 满 二 叉 树 是 一 种 特 殊 的 完 全 二 叉 树。
高 度 为 h 的 完 全 二 叉 树 的 节 点 范 围
最 多 时 为 满 二 叉 树,即 第 1 层 为 2 ^ 0 个,第 2 层 为 2 ^ 1 个,依 次 类 推,最 后 一 层 为 2 ^ (h - 1) 个,使 用 等 比 数 列 公 式 可 得:1*(1 - 2 ^ h)/1-2=2 ^ h - 1。
最 少 时 最 后 一 层 只 有 1 个 节 点,即 第 1 层 为 2 ^ 0 个,第 2 层 为 2 ^ 1 个,依 次 类 推,倒 数 第 2 层 有 2 ^ (h - 2) 个,对 前 面 h - 1 层 使 用 等 比 数 列 公 式 可 得:1*(1 - 2 ^ (h - 1))/ 1 - 2 = 2 ^ (h - 1) - 1。总 共 有 2 ^ (h - 1) - 1 + 1 = 2 ^ (h - 1) 个 节 点。
最 多(最 后 一 层 最 多 2 ^ (h - 1) 个,即 满 二 叉 树):2 ^ h - 1
最 少(最 后 一 层 最 少 1 个):2 ^ (h - 1)
结 点 数 为 N 的 完 全 二 叉 树 和 满 二 叉 树 的 高 度
满 二 叉 树
高 度 为 h 的 完 全 二 叉 树 有 2 ^ h - 1 个 节 点,即 2 ^ h - 1 = N,h = log2(N+1),约 等 于 log2(N)。
完 全 二 叉 树
高 度 为 h 的 完 全 二 叉 树 的 节 点 范 围 :
最 多:2 ^ h - 1
最 少:2 ^ (h - 1)
最 多:2 ^ h - 1 = N,h = log2(N+1),约 等 于 log2(N)。
最 少:2 ^ (h - 1),h = log2N+1,约 等 于 log2(N)。
性 质
1、若 规 定 根 结 点 的 层 数 为 1,则 一 棵 非 空 二 叉 树 的 第 i 层 上 最 多 有 2^(i-1) 个 结 点。
2、若 规 定 根 结 点 的 层 数 为 1,则 深 度 为 h 的 二 叉 树 的 最 大 结 点 数 是 2 ^ h - 1。
3、对 任 何 一 棵 二 叉 树,如 果 度 为 0 其 叶 结 点 个 数 为 n0, 度 为 2 的 分 支 结 点 个 数 为 n2,则 有 n0=n2+1。完 全 二 叉 树 的 度 为 1 的 节 点 的 个 数 要 么 是 1 要 么 是 0。
4、若 规 定 根 结 点 的 层 数 为 1,具 有 n 个 结 点 的 满 二 叉 树 的 深 度,h=log2(n + 1)。
(ps:log2(n+1) 是 log 以 2 为 底,n + 1 为 对 数)
5、对 于 具 有 n 个 结 点 的 完 全 二 叉 树,如 果 按 照 从 上 至 下 从 左 至 右 的 数 组 顺 序 对 所 有 结 点 从 0 开 始 编 号,则 对 于 序 号 为 i 的 结 点 有:
(1)若 i > 0,i 位 置 结 点 的 双 亲 序 号 :( i - 1 ) / 2;i = 0,i 为 根 结 点 编 号,无 双 亲 结 点。
(2)若 2 i + 1 = n ,否 则 无 左 孩 子
(3)若 2 i + 2 = n ,否 则 无 右 孩 子
二 叉 树 的 存 储 结 构
二 叉 树 一 般 可 以 使 用 两 种 结 构 存 储,一 种 顺 序 结 构,一 种 链 式 结 构。
顺 序 存 储
顺 序 结 构 存 储 就 是 使 用 数 组 来 存 储,一 般 使 用 数 组 表 示 完 全 二 叉 树,因 为 不 是 完 全 二 叉 树 会 有 空 间 的 浪 费。只 有 堆 才 会 使 用 数 组 来 存 储。二 叉 树 顺 序 存 储 在 物 理 结 构 上 是 一 个 数 组,在 逻 辑 结 构 上 是 一 颗 二 叉 树。
表 示 二 叉 树 的 值 在 数 组 位 置 中 父 子 下 标 关 系
父 母 和 孩 子 下 标 的 关 系:parent = (child - 1) / 2
父 母 和 左 孩 子 下 标 的 关 系:leftchild = parent * 2 + 1
父 母 和 右 孩 子 下 标 的 关 系:rightchild = parent * 2 + 2
链 式 结 构
二 叉 树 的 链 式 存 储 结 构 是 指 用 链 表 来 表 示 一 棵 二 叉 树 ,即 用 链 来 指 示 元 素 的 逻 辑 关 系。 通 常 的 方 法 是 链 表 中 每 个 结 点 由 三 个 域 组 成,数 据 域 和 左 右 指 针 域,左 右 指 针 分 别 用 来 给 出 该 结 点 左 孩 子 和 右 孩 子 所 在 的 链 结 点 的 存 储 地 址。链 式 结 构 又 分 为 二 叉 链 和 三 叉 链。
堆
定 义
堆 是 一 种 特 殊 的 完 全 二 叉 树 数 据 结 构。堆 使 用 数 组 来 存 储,即 顺 序 存 储 方 式。
性 质
结 构 性
堆 必 须 是 完 全 二 叉 树,即 除 了 最 后 一 层 外,其 他 层 的 节 点 都 是 满 的,且 最 后 一 层 的 节 点 靠 左 排 列。
有 序 性
1、堆 中 某 个 结 点 的 值 总 是 不 大 于 或 不 小 于 其 父 结 点 的 值。
2、堆 总 是 一 棵 完 全 二 叉 树。
注 意:堆 不 一 定 有 序,没 有 限 制 左 右 孩 子 之 间 的 大 小 关 系。
小 根 堆
树 中 所 有 的 父 亲 都 小 于 或 等 于 孩 子。堆 顶 的 数 据 最小。
大 根 堆
树 中 所 有 的 父 亲 都 大 于 或 等 于 孩 子。堆 顶 的 数 据 最大。
堆 的 操 作 实 现
代 码 全 貌 与 功 能 介 绍
整 个 堆 由 三 个 主 要 文 件 构 成:Heap.h、Heap.c 和 test.c。这 种 多 文 件 的 架 构 设 计,有 助 于 将 不 同 功 能 模 块 分 离,提 高 代 码 的 可 读 性、可 维 护 性 与 可 扩 展 性。
Heap.h
Heap.h 包 含 了 堆 所 需 的 头 文 件 引 用、常 量 定 义 以 及 函 数 声 明。
test.c
test.c 是 堆 的 主 逻 辑 文 件,负 责 处 理 用 户 输 入 和 代 码 流 程 的 控 制。
Heap.c
Heap.c 则 实 现 了 堆 的 具 体 功 能 函 数。
下 面 展 示
完 整 代 码
。
读 者 可 以 将 这 段 代 码 复 制 到 自 己 的 编 译 器 中 运 行。
Heap.h
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>typedef int HPDataType;
typedef struct Heap
{HPDataType* a;int size;int capacity;
}HP;//堆的初始化
void HeapInit(HP* php);//堆的销毁
void HeapDestory(HP* php);//堆的插入
void HeapPush(HP* php, HPDataType x);//交换数据
void Swap(HPDataType* p1, HPDataType* p2);//向上调整
void AdjustUP(HPDataType* a, int child);//删除堆顶的数据
//挪动删除:
//效率低下
//父子兄弟关系全乱了
//间接删除
void HeapPop(HP* php);//输出数据
void HeapPrint(HP* php);//向下调整
void AdjustDown(HPDataType* a, int n, int parent);//输出堆顶元素
HPDataType HeapTop(HP* php);//堆的元素个数
int HeapSize(HP* php);//堆是否为空
bool HeapEmpty(HP* php);//将数据保存到文件中
void HeapSave(HP* php);//从文件中读取数据
void HeapLoad(HP* php);//函数指针
void HeapPushMiddle(void(*pf)(HP* php, HPDataType x), HP* php);void HeapMiddle(HPDataType(*pf)(HP* php), HP* php, int num);
test.c
#include "Heap.h"
void menu()
{printf("*********************************************\n");printf("***** 0. HeapExit 1. HeapPush ******\n");printf("***** 2. HeapPop 3. HeapPrint ******\n");printf("***** 4. HeapTop 5. HeapSize ******\n");printf("***** 6. HeapEmpty ******\n");printf("*********************************************\n");
}
void test()
{int input = 0;HP hp;HeapInit(&hp);do{menu();printf("请输入你想要进行的操作:>");scanf("%d", &input);switch (input){case 0:HeapSave(&hp);HeapDestory(&hp);break;case 1:HeapPushMiddle(HeapPush, &hp);break;case 2:HeapPop(&hp);HeapPrint(&hp);break;case 3:HeapPrint(&hp);break;case 4:HeapMiddle(HeapTop, &hp, 4);break;case 5:HeapMiddle(HeapSize, &hp, 5);break;case 6:HeapMiddle(HeapEmpty, &hp, 6);break;default:printf("输入无效,请重新输入\n");break;}} while (input);
}
int main()
{test();return 0;
}
Heap.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "Heap.h"void HeapInit(HP* php)
{assert(php);php->a = (HPDataType*)malloc(sizeof(HPDataType) * 4);if (php->a == NULL){perror("malloc fail");return;}php->size = 0;php->capacity = 4;HeapLoad(php);
}void HeapSave(HP* php)
{assert(php);FILE* pf = fopen("Heap.txt", "w");if (pf == NULL){perror("fopen fail");return;}int i = 0;for (i = 0; i < php->size; i++){fprintf(pf, "%d ", php->a[i]);}fclose(pf);pf = NULL;printf("保存文件成功\n");
}
void HeapLoad(HP* php)
{assert(php);FILE* pf = fopen("Heap.txt", "r");if (pf == NULL){perror("Load fail");return;}int num = 0;int index = 0;while (fscanf(pf, "%d", &num) == 1){HeapPush(php, num);}fclose(pf);pf = NULL;
}void Swap(HPDataType* p1, HPDataType* p2)
{HPDataType tmp = *p1;*p1 = *p2;*p2 = tmp;
}void HeapPush(HP* php, HPDataType x)
{assert(php);if (php->capacity == php->size){HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * php->capacity * 2);if (tmp == NULL){perror("realloc fail");return;}php->a = tmp;php->capacity *= 2;}php->a[php->size] = x;php->size++;AdjustUP(php->a, php->size - 1);
}
//大根堆
//除了child这个位置,前面的数据构成了堆
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 AdjustDown(HPDataType* a, int n, int parent)
{//最坏log2N(堆的高度)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;}}
}//小根堆
//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 AdjustDown(HPDataType* a, int n, int parent)
//{
// //最坏log2N(堆的高度)
// 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;
// }
// }
//}
void HeapPop(HP* php)
{assert(!HeapEmpty(php));assert(php);//删除数据Swap(&php->a[0], &php->a[php->size - 1]);php->size--;AdjustDown(php->a, php->size, 0);
}HPDataType HeapTop(HP* php)
{assert(php);return php->a[0];
}int HeapSize(HP* php)
{assert(php);return php->size;
}bool HeapEmpty(HP* php)
{assert(php);return php->size == 0;
}
void HeapMiddle(HPDataType(*pf)(HP* php), HP* php, int num)
{assert(php);if (num == 4){int ret = pf(php);printf("堆顶元素是:%d\n", ret);}else if (num == 5){int ret = pf(php);printf("堆的元素个数是:%d\n", ret);}else{bool ret = pf(php);if (ret == true){printf("堆是空\n");}else{printf("堆不是空\n");}}
}void HeapDestory(HP* php)
{assert(php);free(php->a);php->a = NULL;php->capacity = 0;php->size = 0;
}void HeapPushMiddle(void(*pf)(HP* php, HPDataType x), HP* php)
{assert(php);int x = 0;printf("请输入你想要插入的数字:>");scanf("%d", &x);pf(php, x);HeapPrint(php);
}void HeapPrint(HP* php)
{assert(php);int i = 0;for (i = 0; i < php->size; i++){printf("%d ", php->a[i]);}printf("\n");
}
注意
上 面 是 大 根 堆 的 代 码,如 果 想 要 改 成 小 根 堆 的 代 码 只 需 将 Heap.c 中 的 大 根 堆 的 两 个 函 数 代 码 注 释,再 将 小 根 堆 的 代 码 取 消 注 释 即 可。如 下 两 段 代 码 所 示:
大 根 堆
//大根堆
//除了child这个位置,前面的数据构成了堆
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 AdjustDown(HPDataType* a, int n, int parent)
{//最坏log2N(堆的高度)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;}}
}
小 根 堆
//小根堆
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 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;}}
}
堆 排 序
在 使 用 堆 排 序 之 前 应 先 将 数 组 中 的 元 素 先 建 成 堆,然 后 进 行 排 序。
建 堆
这 里 如 果 要 排 升 序,推 荐 建 大 堆,假 如 建 小 堆,第 一 个 元 素 最 小,但 如 果 再 进 行 排 序,后 面 的 每 一 个 元 素 都 需 要 重 新 建 堆,时 间 复 杂 度 太 高,所 以 排 升 序 时,应 该 建 大 堆。下 面 以 建 大 堆 为 例。
方 法 1 - - - 向 上 调 整 建 堆
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 HeapSort(int* a, int n)
{int i = 0;for (i = 1; i < n; i++){AdjustUP(a, i);}
}
方 法 2 - - - 向 下 调 整 建 堆
void AdjustDown(HPDataType* a, int n, int parent)
{//最坏log2N(堆的高度)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;}}
}
void HeapSort(int* a, int n)
{int i = 0;for (i = (n - 1 - 1) / 2; i >= 0; i--)//n-1是最后数字的下标,然后是parent = (child-1)/2的公式{AdjustDown(a, n, i);}
}
方 法 3 - - - 使 用 数 组 建 堆
typedef struct Heap
{HPDataType* a;int size;int capacity;
}HP;
//使用数组建堆
void HeapInitArray(HP* php, int* a, int n)
{assert(php);php->a = (HPDataType*)malloc(sizeof(HPDataType) * 4);if (php->a == NULL){perror("malloc fail");return;}php->size = n;php->capacity = n;//建堆int i = 0;for (i = (n - 2) / 2; i >= 0; i--){AdjustDown(php->a, php->size, i);}
}
排 序
void AdjustDown(HPDataType* a, int n, int parent)
{//最坏log2N(堆的高度)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;}}
}
void HeapSort(int* a, int n)
{int end = n - 1;while (end > 0){Swap(&a[end], &a[0]);AdjustDown(a, end, 0);end--;}
}
完 整 排 序 过 程
#include<stdio.h>
typedef int HPDataType;
void Swap(HPDataType* p1, HPDataType* p2)
{HPDataType tmp = *p1;*p1 = *p2;*p2 = tmp;
}
void AdjustDown(HPDataType* a, int n, int parent)
{//最坏log2N(堆的高度)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;}}
}
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 HeapSort(int* a, int n)
{//向上调整建堆int i = 0;for (i = 1; i < n; i++){AdjustUP(a, i);}//堆排序int end = n - 1;while (end > 0){Swap(&a[end], &a[0]);AdjustDown(a, end, 0);end--;}//建堆 --- 向下调整建堆//时间复杂度:O(N-log2(N)) = O(N)//int i = 0;//for (i = (n - 1 - 1) / 2; i >= 0; i--)//n-1是最后数字的下标//{// AdjustDown(a, n, i);//}//int end = n - 1;//while (end > 0)//{// Swap(&a[end], &a[0]);// AdjustDown(a, end, 0);// end--;//}
}
void test1()
{int a[] = { 1,3,5,7,6,4,9,2,8,0 };//对数组排序int sz = sizeof(a) / sizeof(a[0]);HeapSort(a, sz);int i = 0;//for (i = sz - 1; i >= 0; i--)//{// printf("%d ", a[i]);//}for (i = 0; i < sz; i++){printf("%d ", a[i]);}printf("\n");
}
int main()
{test1();return 0;
}
时 间 复 杂 度:O(n*log2(n))
建 堆 时 间 复 杂 度
因 为 堆 是 完 全 二 叉 树,而 满 二 叉 树 也 是 完 全 二 叉 树,这 里 使 用 满 二 叉 树 来 证 明 (时 间 复 杂 度 本 来 看 的 就 是 近 似 值,多 几 个 结 点 不 影 响 最 终 结 果)。
第 1 层 有 2 ^ 0 个 节 点,需 要 向 下 移 动 h - 1 层。
第 2 层 有 2 ^ 1 个 节 点,需 要 向 下 移 动 h - 2 层。
第 3 层 有 2 ^ 2 个 节 点,需 要 向 下 移 动 h - 3 层。
…
第 h - 2 层 有 2 ^ ( h - 3 ) 个 节 点,需 要 向 下 移 动 2 层。
第 h - 1 层 有 2 ^ ( h - 2 ) 个 节 点,需 要 向 下 移 动 1 层。
则 需 要 移 动 结 点 总 的 移 动 步 数 为:
T(n) = 2 ^ 0 * (h-1) + 2 ^ 1 * (h-2)+2 ^ 2 * (h-3) +…+ 2 ^ (h-3) * 2+2 ^ (h-2)*1 (1)
2 * T(n) = 2 ^ 1 * (h-1) + 2 ^ 2 * (h-2)+2 ^ 3 * (h-3) +…+ 2 ^ (h-2) * 2+2 ^ (h-1)*1 (2)
(2) - (1)得:
T(n) = 2 ^ h - 1 - h
n = 2 ^ h - 1
h = log2(n+1)
T(n)=n- log2(n+1)
所 以 T(n) 约 等 于 n。建 堆 的 时 间 复 杂 度 约 为 O(N)。
TOP-k 问 题
求 数 据 结 合 中 前 K 个 最 大 的 元 素 或 者 最 小 的 元 素,一 般 情 况 下 数 据 量 都 比 较 大。最 佳 的 方 式 就 是 用 堆 排 序 来 解 决,基 本 思 路 如 下:
1、用 数 据 集 合 中 前 K 个 元 素 来 建 堆
(1)前 k 个 最 大 的 元 素,则 建 小 堆
(2)前 k 个 最 小 的 元 素,则 建 大 堆
2、用 剩 余 的 N - K 个 元 素 依 次 与 堆 顶 元 素 来 比 较,不 满 足 则 替 换 堆 顶 元 素。
3、将 剩 余 N - K 个 元 素 依 次 与 堆 顶 元 素 比 完 之 后,堆 中 剩 余 的 K 个 元 素 就 是 所 求 的 前 K 个 最 小 或 者 最 大 的 元 素。
下 面 以 随 机 生 成 10000 个 数 据 为 例 使 用 堆 排 序 找 出 最 大 的 前 K 个 数。
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
#include <time.h>
void Swap(int* p1, int* p2)
{int tmp = *p1;*p1 = *p2;*p2 = tmp;
}
void AdjustDown(int* a, int n, int parent)
{//最坏log2N(堆的高度)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;}}
}
void PrintTopK(const char* file, int k)
{//1. 建堆 --- 用a中前k个元素建堆int* topk = (int*)malloc(sizeof(int) * k);if (topk == NULL){perror("malloc fail");return;}FILE* fout = fopen(file, "r");if (fout == NULL){perror("fopen fail");return;}//读取文件中的前k个数据建小堆int i = 0;for (i = 0; i < k; i++){fscanf(fout, "%d", &topk[i]);}//建堆for (i = (k - 2) / 2; i >= 0; i--){AdjustDown(topk, k, i);}//2.将剩余n-k个元素依次与堆顶元素交换,不满则则替换int val = 0;int ret = fscanf(fout, "%d", &val);while (ret != EOF){if (val > topk[0]){topk[0] = val;AdjustDown(topk, k, 0);}ret = fscanf(fout, "%d", &val);}for (i = 0; i < k; i++){printf("%d ", topk[i]);}printf("\n");free(topk);fclose(fout);fout = NULL;
}
void CreateNData()
{int n = 10000;srand(time(0));const char* file = "num.txt";FILE* fin = fopen(file, "w");if (fin == NULL){perror("file fail");return;}for (size_t i = 0; i < n; ++i){int x = rand() % 10000;fprintf(fin, "%d\n", x);}PrintTopK(file, 10);fclose(fin);fin = NULL;
}
int main()
{//CreateNData();PrintTopK("num.txt", 10);return 0;
}
总 结
树 与 堆 的 本 质 是 对 数 据 的 层 次 化 与 有 序 化 组 织。树 提 供 了 非 线 性 数 据 的 基 础 建 模 方 式,而 堆 通 过 完 全 二 叉 树 与 有 序 性 的 结 合,实 现 了 极 值 操 作 的 高 效 性。从 理 论 概 念 到 代 码 实 现,二 者 均 体 现 了 数 据 结 构 “逻 辑 结 构” 与 “存 储 结 构 ” 的 紧 密 关 联,是 算 法 设 计 中 不 可 或 缺 的 基 础 模 块。