【数据结构与算法】-二叉树(2):实现顺序结构二叉树(堆的实现),向上调整算法,向下调整算法,堆排序,TOP-K问题



🦆 个人主页:深邃-

❄️专栏传送门:《C语言》《数据结构与算法》《Web安全》

🌟Gitee仓库:《C语言》《数据结构与算法》


目录


实现顺序结构二叉树

一般堆使用顺序结构的数组来存储数据,堆是一种特殊的二叉树,具有二叉树的特性的同时,还具备其他的特性

堆的概念与结构

如果有一个关键码的集合 K = { k 0 , k 1 , k 2 , ... , k n − 1 } K=\{k_0, k_1, k_2, \dots, k_{n-1}\} K={k0,k1,k2,...,kn−1},把它的所有元素按完全二叉树的顺序存储方式存储 ,在一个一维数组中,并满足: K i ≤ K 2 i + 1 K_i\leq K_{2i+1} Ki≤K2i+1且 K i ≤ K 2 i + 2 K_i\leq K_{2i+2} Ki≤K2i+2 ( K i ≥ K 2 i + 1 (K_i\geq K_{2i+1} (Ki≥K2i+1 且 K i ≥ K 2 i + 2 ) K_i\geq K_{2i+2}) Ki≥K2i+2)则称为小堆(或大堆)将根结点最大的堆叫做最大堆或大根堆,根结点最小的堆叫做最小堆或小根堆。

堆与二叉树的性质

堆具有以下性质:

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

二叉树性质
对于具有 n n n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有结点从 0 0 0 开始编号,则对于序号为 i i i 的结点有:

  • 若 i > 0 i>0 i>0, i i i 位置结点的双亲序号: i − 1 2 \dfrac{i-1}{2} 2i−1(向下取整); i = 0 i=0 i=0, i i i 为根结点编号,无双亲结点。
  • 若 2 i + 1 < n 2i+1<n 2i+1<n,左孩子序号: 2 i + 1 2i+1 2i+1; 2 i + 1 ≥ n 2i+1\geq n 2i+1≥n 则无左孩子。
  • 若 2 i + 2 < n 2i+2<n 2i+2<n,右孩子序号: 2 i + 2 2i+2 2i+2; 2 i + 2 ≥ n 2i+2\geq n 2i+2≥n 则无右孩子。

堆的实现

堆底层结构为数组,因此定义堆的结构为:

  • 头文件
c 复制代码
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>

//定义堆结构
typedef int HPDataType;
typedef struct Heap {
	HPDataType* arr;
	int size;    //有效数据个数
	int capaicty;//空间大小
}HP;

void HPInit(HP* php);

void HPDesTroy(HP* php);

void HPPrint(HP* php);

void Swap(int* x, int* y);
//向下调整算法
void AdjustDown(HPDataType* arr, int parent, int n);
//向上调整算法
void AdjustUp(HPDataType* arr, int child);

void HPPush(HP* php, HPDataType x);
void HPPop(HP* php);
HPDataType HPTop(HP* php);

bool HPEmpty(HP* php);

定义堆结构

因为底层选型是数组,跟之前的顺序表一样,因此不再赘述

c 复制代码
//定义堆结构
typedef int HPDataType;
typedef struct Heap {
	HPDataType* arr;
	int size;    //有效数据个数
	int capaicty;//空间大小
}HP;

堆的初始化,销毁,打印

不再赘述

c 复制代码
#include"Heap.h"

void HPInit(HP* php)
{
	assert(php);

	php->arr = NULL;
	php->size = php->capaicty = 0;
}
void HPDesTroy(HP* php)
{
	assert(php);
	if (php->arr)
		free(php->arr);
	php->arr = NULL;
	php->size = php->capaicty = 0;
}
void HPPrint(HP* php)
{
	for (int i = 0; i < php->size; i++)
	{
		printf("%d ", php->arr[i]);
	}
	printf("\n");
}
void Swap(int* x, int* y)
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}

向上调整算法

堆的插入
将新数据插入到数组的尾上,再进行向上调整算法,直到满足堆。

向上调整算法有一个前提:在新数据之上的左右子树必须是一个堆,才能调整。

向上调整算法

  • 先将元素插入到堆的末尾,即最后一个孩子之后
  • 插入之后如果堆的性质遭到破坏,将新插入结点顺着其双双亲往上调整到合适位置即可
c 复制代码
//向上调整算法logn
void AdjustUp(HPDataType* arr, int child)
{
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		//建大堆:>
		//建小堆: <
		if (arr[child] < arr[parent])
		{
			Swap(&arr[child], &arr[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else {
			break;
		}
	}
}

时间复杂的:logn

堆的插入

堆的插入

c 复制代码
void HPPush(HP* php, HPDataType x)
{
	assert(php);
	//空间不够要增容
	if (php->size == php->capaicty)
	{
		//增容
		int newCapacity = php->capaicty == 0 ? 4 : 2 * php->capaicty;
		HPDataType* tmp = (HPDataType*)realloc(php->arr, newCapacity * sizeof(HPDataType));
		if (tmp == NULL)
		{
			perror("realloc fail!");
			exit(1);
		}
		php->arr = tmp;
		php->capaicty = newCapacity;
	}
	//空间足够
	php->arr[php->size] = x;
	//向上调整
	AdjustUp(php->arr, php->size);
	++php->size;
}

向下调整算法

堆的删除(出堆)

删除堆是删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调整算法。

向下调整算法有一个前提:要调整的堆顶之下的左右子树必须是一个堆,才能调整。

向下调整算法

  • 将堆顶元素与堆中最后一个元素进行交换
  • 删除堆中最后一个元素
  • 将堆顶元素向下调整到满足堆特性为止
c 复制代码
//向下调整算法 logn
void AdjustDown(HPDataType* arr, int parent, int n)
{
	int child = parent * 2 + 1;
	while (child < n)
	{
		//建大堆:<
		//建小堆: >
		if (child + 1 < n && arr[child] > arr[child + 1])
		{
			child++;
		}
		//孩子和父亲比较
		//建大堆:>
		//建小堆:<
		if (arr[child] < arr[parent])
		{
			Swap(&arr[child], &arr[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else {
			break;
		}
	}
}

时间复杂的:logn
parent:父结点,从哪里开始向下调整
n:调整范围的结点个数

堆的删除

c 复制代码
void HPPop(HP* php)
{
	assert(!HPEmpty(php));
	Swap(&php->arr[0], &php->arr[php->size - 1]);
	--php->size;
	//堆顶数据需要向下调整
	AdjustDown(php->arr, 0, php->size);
}

堆的判空

c 复制代码
bool HPEmpty(HP* php)
{
	assert(php);
	return php->size == 0;
}

取堆顶

c 复制代码
HPDataType HPTop(HP* php)
{
	assert(!HPEmpty(php));
	return php->arr[0];
}

堆排序

随便给定一个乱序数组,用堆排序的思想解决

向上调整算法建堆

向上调整算法思想

c 复制代码
//向上调整算法建堆O(n*logn)
for (int i = 0; i < n; i++)
{
	AdjustUp(arr, i);
}

计算向上调整算法建堆时间复杂度
向上调整建堆 错位相减完整推导

原式①:
T ( h ) = 2 1 ⋅ 1 + 2 2 ⋅ 2 + 2 3 ⋅ 3 + ⋯ + 2 h − 2 ⋅ ( h − 2 ) + 2 h − 1 ⋅ ( h − 1 ) (①) T(h)=2^1 \cdot 1+2^2 \cdot 2+2^3 \cdot 3+\dots+2^{h-2} \cdot (h-2)+2^{h-1} \cdot (h-1) \tag{①} T(h)=21⋅1+22⋅2+23⋅3+⋯+2h−2⋅(h−2)+2h−1⋅(h−1)(①)

同乘 2 得式②:
2 T ( h ) = 2 2 ⋅ 1 + 2 3 ⋅ 2 + 2 4 ⋅ 3 + ⋯ + 2 h − 1 ⋅ ( h − 2 ) + 2 h ⋅ ( h − 1 ) (②) 2T(h)=2^2 \cdot 1+2^3 \cdot 2+2^4 \cdot 3+\dots+2^{h-1} \cdot (h-2)+2^h \cdot (h-1) \tag{②} 2T(h)=22⋅1+23⋅2+24⋅3+⋯+2h−1⋅(h−2)+2h⋅(h−1)(②)

② − - − ① 错位相减:
2 T ( h ) − T ( h ) = − 2 1 ⋅ 1 − 2 2 − 2 3 − ⋯ − 2 h − 1 + 2 h ( h − 1 ) T ( h ) = 2 h ( h − 1 ) − ( 2 1 + 2 2 + 2 3 + ⋯ + 2 h − 1 ) \begin{aligned} 2T(h)-T(h) &= -2^1\cdot1 - 2^2 - 2^3 - \dots - 2^{h-1} + 2^h(h-1) \\ T(h) &= 2^h(h-1) - \big(2^1+2^2+2^3+\dots+2^{h-1}\big) \end{aligned} 2T(h)−T(h)T(h)=−21⋅1−22−23−⋯−2h−1+2h(h−1)=2h(h−1)−(21+22+23+⋯+2h−1)

等比数列求和:
2 1 + 2 2 + ⋯ + 2 h − 1 = 2 h − 2 2^1+2^2+\dots+2^{h-1} = 2^h - 2 21+22+⋯+2h−1=2h−2

代入化简:
T ( h ) = 2 h ( h − 1 ) − ( 2 h − 2 ) = h ⋅ 2 h − 2 h − 2 h + 2 = h ⋅ 2 h − 2 h + 1 + 2 \begin{aligned} T(h) &= 2^h(h-1) - (2^h - 2) \\ &= h\cdot 2^h - 2^h - 2^h + 2 \\ &= h\cdot 2^h - 2^{h+1} + 2 \end{aligned} T(h)=2h(h−1)−(2h−2)=h⋅2h−2h−2h+2=h⋅2h−2h+1+2
T ( h ) = 2 h ( h − 2 ) + 2 T(h) = 2^h(h-2) + 2 T(h)=2h(h−2)+2

根据二叉树的性质:
n = 2 h − 1 , h = log ⁡ 2 ( n + 1 ) n = 2^h - 1,\quad h = \log_2(n+1) n=2h−1,h=log2(n+1)

把h换形成n,注意这里变换对应法则,不换自变量(T(h)不将h换成n)
F ( n ) = ( n + 1 ) ( log ⁡ 2 ( n + 1 ) − 2 ) + 2 F(n) = (n+1)\big(\log_2(n+1) - 2\big) + 2 F(n)=(n+1)(log2(n+1)−2)+2
F ( n ) = O ( n log ⁡ 2 n ) F(n) = O(n\log_2 n) F(n)=O(nlog2n)

向下调整算法建堆

向下调整算法思想

c 复制代码
//向下调整算法------建堆O(n)
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
	AdjustDown(arr, i, n);
}

计算向下调整算法建堆时间复杂度

第1层, 2 0 2^0 20 个结点,需要向下移动 h − 1 h-1 h−1 层

第2层, 2 1 2^1 21 个结点,需要向下移动 h − 2 h-2 h−2 层

第3层, 2 2 2^2 22 个结点,需要向下移动 h − 3 h-3 h−3 层

第4层, 2 3 2^3 23 个结点,需要向下移动 h − 4 h-4 h−4 层
... \dots ...

第 h − 1 h-1 h−1层, 2 h − 2 2^{h-2} 2h−2 个结点,需要向下移动 1 1 1 层

需要移动结点总的移动步数为:每层结点个数 × \times × 向下调整次数

T ( h ) = 2 0 ( h − 1 ) + 2 1 ( h − 2 ) + 2 2 ( h − 3 ) + 2 3 ( h − 4 ) + ⋯ + 2 h − 3 ⋅ 2 + 2 h − 2 ⋅ 1 T(h)=2^0 (h-1)+2^1 (h-2)+2^2 (h-3)+2^3 (h-4)+\dots+2^{h-3} \cdot 2+2^{h-2} \cdot 1 T(h)=20(h−1)+21(h−2)+22(h−3)+23(h−4)+⋯+2h−3⋅2+2h−2⋅1

同乘 2:
2 T ( h ) = 2 1 ( h − 1 ) + 2 2 ( h − 2 ) + 2 3 ( h − 3 ) + 2 4 ( h − 4 ) + ⋯ + 2 h − 2 ⋅ 2 + 2 h − 1 ⋅ 1 2T(h)=2^1(h-1)+2^2(h-2)+2^3(h-3)+2^4(h-4)+\dots+2^{h-2}\cdot 2+2^{h-1}\cdot 1 2T(h)=21(h−1)+22(h−2)+23(h−3)+24(h−4)+⋯+2h−2⋅2+2h−1⋅1

错位相减:
T ( h ) = 1 − h + 2 1 + 2 2 + 2 3 + 2 4 + ⋯ + 2 h − 2 + 2 h − 1 T(h)=1-h+2^1+2^2+2^3+2^4+\dots+2^{h-2}+2^{h-1} T(h)=1−h+21+22+23+24+⋯+2h−2+2h−1

整理:
T ( h ) = 2 0 + 2 1 + 2 2 + 2 3 + 2 4 + ⋯ + 2 h − 2 + 2 h − 1 − h T(h)=2^0+2^1+2^2+2^3+2^4+\dots+2^{h-2}+2^{h-1}-h T(h)=20+21+22+23+24+⋯+2h−2+2h−1−h

等比数列求和化简:
T ( h ) = 2 h − 1 − h T(h)=2^h-1-h T(h)=2h−1−h

根据二叉树的性质:
n = 2 h − 1 , h = log ⁡ 2 ( n + 1 ) n=2^h-1,\quad h=\log_2(n+1) n=2h−1,h=log2(n+1)

代入得:
F ( n ) = n − log ⁡ 2 ( n + 1 ) ≈ n F(n)=n-\log_2(n+1) \approx n F(n)=n−log2(n+1)≈n

向下调整算法建堆时间复杂度为:
O ( n ) O(n) O(n)

堆排序的实现

数组建堆,首尾交换,交换后的堆尾数据从堆中删掉,将堆顶数据向下调整选出次大的数据

c 复制代码
//堆排序------------使用的是堆结构的思想 n * logn
void HeapSort(int* arr, int n)
{
	//向下调整算法------建堆O(n)
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(arr, i, n);
	}

	////向上调整算法建堆O(n*logn)
	//for (int i = 0; i < n; i++)
	//{
	//	AdjustUp(arr, i);
	//}

	//n*logn
	int end = n - 1;
	while (end > 0)
	{
		Swap(&arr[0], &arr[end]);
		AdjustDown(arr, 0, end);//logn
		end--;
	}
}

堆排序时间复杂度计算

通过分析发现,堆排序第二个循环中的向下调整,与建堆中的向上调整算法时间复杂度计算一致(也是求和) ,此处不再赘述。(建堆默认向下调整思想

因此,堆排序总的时间复杂度为:
O ( n + n log ⁡ n ) O(n + n\log n) O(n+nlogn)

化简可得堆排序最终时间复杂度:
O ( n log ⁡ n ) O(n\log n) O(nlogn)

TOP-K问题

TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。

比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。

对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。

最佳的方式就是用堆来解决,基本思路如下:

1)用数据集合中前K个元素来建堆

  • 前k个最大的元素,则建小堆 (因为堆顶永远是最小的,通过排出堆顶小元素,存住堆顶下面的大元素,即可用有限的空间找出海量大数据的小元素)
  • 前k个最小的元素,则建大堆

2)用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素

造十万个数据

c 复制代码
void CreateNDate()
{
	// 造数据
	int n = 100000;
	srand(time(0));
	const char* file = "data.txt";
	FILE* fin = fopen(file, "w");
	if (fin == NULL)
	{
		perror("fopen error");
		return;
	}
	for (int i = 0; i < n; ++i)
	{
		int x = (rand() + i) % 1000000;
		fprintf(fin, "%d\n", x);
	}
	fclose(fin);
}




TOP-K代码

c 复制代码
void TopK()
{
	int k = 0;
	printf("请输入K:");
	scanf("%d", &k);

	const char* file = "data.txt";
	FILE* fout = fopen(file, "r");
	if (fout == NULL)
	{
		perror("fopen fail!");
		exit(1);
	}
	//申请空间大小为k的整型数组
	int* minHeap = (int*)malloc(sizeof(int) * k);
	if (minHeap == NULL)
	{
		perror("malloc fail!");
		exit(2);
	}
	//读取文件中k个数据放到数组中
	for (int i = 0; i < k; i++)
	{
		fscanf(fout, "%d", &minHeap[i]);
	}
	//数组调整建堆--向下调整建堆
	//找最大的前K个数,建小堆
	for (int i = (k - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(minHeap, i, k);
	}
	//遍历剩下的n-k个数,跟堆顶比较,谁大谁入堆
	int data = 0;
	while (fscanf(fout, "%d", &data) != EOF)
	{
		if (data > minHeap[0])
		{
			minHeap[0] = data;
			AdjustDown(minHeap, 0, k);
		}
	}

	//打印堆里的数据
	for (int i = 0; i < k; i++)
	{
		printf("%d ", minHeap[i]);
	}
	printf("\n");

	fclose(fout);
}

时间复杂度:(默认向下调整)
O ( n ) = k + ( n − k ) log ⁡ 2 k O(n) = k + (n-k)\log_2 k O(n)=k+(n−k)log2k

空间足够的话

O ( n log ⁡ k ) O(n\log k) O(nlogk) 看似永远优于全部建堆排序: O ( n log ⁡ n ) O(n\log n) O(nlogn)
其实不然,仔细学习我前面的思路你会发现,堆排序浪费时间的地方其实是出堆,而全部建堆排序只需要出堆 K K K次,所以
n + k log ⁡ n    ≪    n log ⁡ k \boldsymbol{n + k\log n \;\ll\; n\log k} n+klogn≪nlogk
时间 :全部建堆排序 ≪    \ll\; ≪ TOP-K
所以在空间无限,全局建堆未尝不可!!!

相关推荐
We་ct4 小时前
LeetCode 5. 最长回文子串:DP + 中心扩展
前端·javascript·算法·leetcode·typescript
王老师青少年编程8 小时前
csp信奥赛C++高频考点专项训练之贪心算法 --【哈夫曼贪心】:合并果子
c++·算法·贪心·csp·信奥赛·哈夫曼贪心·合并果子
叼烟扛炮9 小时前
C++第二讲:类和对象(上)
数据结构·c++·算法·类和对象·struct·实例化
天疆说9 小时前
【哈密顿力学】深入解读航天器交会最优控制中的Hamilton函数
人工智能·算法·机器学习
wuweijianlove10 小时前
关于算法设计中的代价函数优化与约束求解的技术7
算法
leoufung10 小时前
LeetCode 149: Max Points on a Line - 解题思路详解
算法·leetcode·职场和发展
样例过了就是过了10 小时前
LeetCode热题100 最长公共子序列
c++·算法·leetcode·动态规划
HXDGCL10 小时前
矩形环形导轨:自动化循环线的核心运动单元解析
运维·算法·自动化
谭欣辰10 小时前
C++ 排列组合完整指南
开发语言·c++·算法