【数据结构与算法】堆 / 堆排序 / TopK问题(Heap)

文章目录

  • 1.堆
  • 2.C语言实现堆
    • [2.1 堆结构与基本操作](#2.1 堆结构与基本操作)
    • [2.2 其它辅助操作](#2.2 其它辅助操作)
    • [2.3 堆的基本操作](#2.3 堆的基本操作)
      • [2.3.1 插入](#2.3.1 插入)
      • [2.3.2 删除](#2.3.2 删除)
  • [3. 堆排序](#3. 堆排序)
  • [4. TopK](#4. TopK)
  • [5. 所有代码](#5. 所有代码)

1.堆

堆总是一棵完全二叉树 ,而完全二叉树更适合使用**顺序结构(数组)**存储,完全二叉树前h-1层是满的,最后一层不一定是满的,但节点一定连续的。需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。

非完全二叉树不适合用数组来存储,因为存在空间浪费。而极端的二叉搜索树则会造成更多浪费,二叉搜索树即右孩子节点比父节点大,左孩子节点比父节点小的树。

使用数组存储二叉树,基于父子节点下标间的关系:leftchild = parent * 2 + 1,rightchild = parent * 2 + 2,parent = (child - 1) / 2,如果打破这种存储关系则数组无法表示二叉树,所以数组存储非完全二叉树注定要浪费空间。

将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。最大堆即任意一个父节点都大于等于孩子节点,最小堆即任意一个父节点都小于等于孩子节点。

满二叉树实际上也是一棵特殊的完全二叉树,树的每一层都是满节点即是满二叉树。满二叉树有这样一个规律,第一层的节点数为2^0^,第二层节点数为2^1^,假设树的高度为h,则第h层的节点数为2^h-1^,不难看出是一个等比数列,所以满二叉树的节点数量为2^h^-1。

基于这样一个规律,则完全二叉树最多有2^h^-1个节点,最少有2^h-1^个节点:[2^h-1^,2^h^-1]。知道了节点数量,也就知道了树的高度h:假设N是节点数量,N = 2^h-1^,则h = log~2~(N) + 1;N = 2^h^-1,则h = log~2~(N+1)。

2.C语言实现堆

2.1 堆结构与基本操作

堆结构看起来与顺序表无异,毕竟都是数组实现。不一样的是逻辑结构,顺序表是线性结构,堆是树形结构 。堆的基本操作只有插入和删除 ,应用场景有堆排序和TopK(前k个最大或最小的数)。

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

// 堆结构
typedef int datatype;
typedef struct Heap {
	datatype* arr;
	int size;
	int capacity;
} Heap;

// 其它辅助操作
void HeapInit(Heap* heap);
void HeapDestroy(Heap* heap);
datatype HeapTop(Heap* heap); // 取堆顶元素
size_t HeapSize(Heap* heap);
bool HeapEmpty(Heap* heap);

// logn
void HeapPush(Heap* heap, datatype val);
void HeapPop(Heap* heap); // 删除堆顶元素
// 插入或删除时,堆向上、向下调整
void Swap(datatype* x, datatype* y);
void AdjustUp(datatype* arr, int child);
void AdjustDown(datatype* arr, int size, int parent);

// 堆排序 nlogn、求TopK nlogk
void HeapSort(datatype* arr, int size);
void PrintTopK(const char* file, int k);

2.2 其它辅助操作

c 复制代码
void HeapInit(Heap* heap) {
	assert(heap);
	heap->arr = NULL;
	heap->size = heap->capacity = 0;
}

void HeapDestroy(Heap* heap) {
	assert(heap);
	free(heap->arr);
	heap->arr = NULL;
	heap->size = heap->capacity = 0;
}

datatype HeapTop(Heap* heap) {
	assert(heap && heap->arr && heap->size > 0);
	return heap->arr[0];
}

size_t HeapSize(Heap* heap) {
	assert(heap);
	return heap->size;
}

bool HeapEmpty(Heap* heap) {
	assert(heap);
	return heap->size == 0;
}

2.3 堆的基本操作

2.3.1 插入

直接往数组插入数据,然后再向上调整即可。以构建最小堆举例,只要插入的数据比父节点小,就与父节点交换,重复这个操作直到不能再做交换。

c 复制代码
void HeapPush(Heap* heap, datatype val) {
	assert(heap);
	if (heap->size == heap->capacity) { // 空间不够时扩容
		heap->capacity = heap->capacity == 0 ? 10 : heap->capacity * 2;
		datatype* tmp = realloc(heap->arr, sizeof(datatype) * heap->capacity);
		if (tmp == NULL) {
			perror("HeapPush malloc failed.");
			exit(-1);
		}
		heap->arr = tmp;
	}
	// 插入
	heap->arr[heap->size++] = val;
	AdjustUp(heap->arr, heap->size - 1);
}
void Swap(datatype* x, datatype* y) {
	int tmp = *x;
	*x = *y;
	*y = tmp;
}
// logn
void AdjustUp(datatype* arr, int child) {
	int parent = (child - 1) / 2;
	while (child > 0 && arr[child] < arr[parent]) {
		Swap(&arr[child], &arr[parent]); // up
		child = parent;
		parent = (child - 1) / 2;
	}
}

如果将while循环的条件arr[child] < arr[parent]改成大于arr[child] > arr[parent],则是调整构建最大堆。

c 复制代码
Heap heap;
HeapInit(&heap);
// 建堆 nlogn
HeapPush(&heap, 67864);
HeapPush(&heap, 7432);
HeapPush(&heap, 854312);
HeapPush(&heap, 909876);
HeapPush(&heap, 8765);
HeapPush(&heap, 2345678);
HeapPush(&heap, 2563);
HeapPush(&heap, 12676);
HeapPush(&heap, 6543);
HeapPush(&heap, 2167);
for (int i = 0; i < heap.size; i++) {
	printf("%d ", heap.arr[i]);
}
HeapDestroy(&heap);

2.3.2 删除

删除堆元素,只能删除堆顶元素,这是合理的规定,其它诸如任意删除、删除最后一个元素的操作对堆而言都是没有意义的。

如果是直接删除堆顶元素,数组剩下的元素不再构成堆,所以不能这么做。还是以最小堆为例,(1)标准的实现是:将堆顶元素与数组最后一个元素进行交换,即最小的数与较大的数交换,接着删除最后一个元素,然后再向下调整,目的是将堆顶元素往下沉,重新构建最小堆;(2)向下调整的思路:从左右孩子节点中选出最小的,这个孩子节点比父节点小就做交换,重复这个操作。

c 复制代码
void HeapPop(Heap* heap) {
	assert(heap && heap->arr && heap->size > 0);
	Swap(&heap->arr[0], &heap->arr[--heap->size]);
	AdjustDown(heap->arr, heap->size, 0);
}
void Swap(datatype* x, datatype* y) {
	int tmp = *x;
	*x = *y;
	*y = tmp;
}
// logn
void AdjustDown(datatype* arr, int size, int parent) {
	int child = parent * 2 + 1; // left or right 
	child = child + 1 < size && arr[child + 1] < arr[child]
										? ++child : chi
	// child<parent调整为小堆,child>parent调整为大堆 
	while (child < size && arr[child] < arr[parent]) {
		Swap(&arr[child], &arr[parent]); // down
		parent = child;
		child = parent * 2 + 1; // left or right
		child = child+1 < size && arr[child+1] < arr[child]
										? ++child : child;
	}
}

如果将arr[child + 1] < arr[child]改成arr[child + 1] > arr[child],以及while循环的条件arr[child] < arr[parent]改成大于arr[child] > arr[parent],则是调整构建最大堆。

c 复制代码
Heap heap;
HeapInit(&heap);
// 建堆 nlogn
HeapPush(&heap, 67864);
HeapPush(&heap, 7432);
HeapPush(&heap, 854312);
HeapPush(&heap, 909876);
HeapPush(&heap, 8765);
HeapPush(&heap, 2345678);
HeapPush(&heap, 2563);
HeapPush(&heap, 12676);
HeapPush(&heap, 6543);
HeapPush(&heap, 2167);
while (!HeapEmpty(&heap)) {
	printf("%d ", HeapTop(&heap));
	HeapPop(&heap);
}
HeapDestroy(&heap);

这其实相当于排了个序,时间复杂度为nlogn。不过由于还有插入操作的时间复杂度nlogn,所以整体时间复杂度为2n*2logn。

另外这也能解决TopK问题:

c 复制代码
int k = 3; 
while (k--) {
	printf("%d ", HeapTop(&heap));
	HeapPop(&heap);
}

3. 堆排序

前面借助堆基本操作Top和Pop也能做到堆排序,不过却比较麻烦,因为需要实现堆的基本操作。这里所指的堆排序是指,直接将数组构建成堆然后排序,不包含建堆的时间则堆排序的时间复杂度为nlogn。

c 复制代码
// 排升序则构建大堆,排降序则构建小堆
void HeapSort(datatype* arr, int size) {
	// 建堆 O(nlogn) 
	//for (int i = 1; i < size; ++i) {
	//	AdjustUp(arr, i);  
	//}

	// 建堆 O(n) 并非是nlogn
	for (int i = (size - 1 - 1) / 2; i >= 0; --i) {
		AdjustDown(arr, size, i);
	}

	// 排序 O(nlogn)
	for (int end = size - 1; end > 0; --end) {
		Swap(&arr[0], &arr[end]);
		AdjustDown(arr, end, 0);
	}
}

利用向下调整建堆的时间复杂度为O(n)的原因是,最后一层不需要向下调整,直接从倒数第二层开始向下调整,这节省了很多时间复杂度,毕竟最后一层的节点占了堆节点总数一半。每一层的节点数量越多,向下调整次数越少;每一层的节点数量越少,向下调整的次数才越多。

而利用向上调整构建堆,从最后一层的节点往上,则会耗费较多时间复杂度,因为最后一层也需要向上调整。

4. TopK

如果数据量太大,比如一千万个数据,以int来算则是四千万字节,差不多相当于40G内存,如果还是按以前那样将所有数据插入堆中求TopK显然是不可能的。

c 复制代码
void PrintTopK(const char* file, int k) {
	// 文件
	FILE* fr = fopen(file, "r");
	if (fr == NULL) {
		perror("PrintTopK fopen failed.");
		exit(-1);
	}

	// 大小为k的堆
	datatype* minheap = (datatype*)malloc(sizeof(datatype) * k);
	if (minheap == NULL) {
		perror("PrintTopK malloc failed.");
		exit(-1);
	}
	// 从文件读取前k个建小堆
	for (int i = 0; i < k; i++) {
		fscanf(fr, "%d", &minheap[i]);
		AdjustUp(minheap, i); // child<parent
	}

	// 从文件挨个读取,寻找TopK
	int val = 0;
	while (fscanf(fr, "%d", &val) != EOF) {
		if (val > *minheap) {
			*minheap = val; 
			// 大的往下沉 child<parent
			AdjustDown(minheap, k, 0); 
		}
	}

	// 打印TopK
	for (int i = 0; i < k; i++) {
		printf("%d ", minheap[i]);
	}
	printf("\n");
	free(minheap);
	minheap = NULL;
	fclose(fr);
}

5. 所有代码

c 复制代码
#pragma once

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>

// 堆结构
typedef int datatype;
typedef struct Heap {
	datatype* arr;
	int size;
	int capacity;
} Heap;

void HeapInit(Heap* heap);
void HeapDestroy(Heap* heap);

void HeapPush(Heap* heap, datatype val);
void HeapPop(Heap* heap);
// 插入或删除时,堆向上、向下调整
void Swap(datatype* x, datatype* y);
void AdjustUp(datatype* arr, int child);
void AdjustDown(datatype* arr, int size, int parent);

datatype HeapTop(Heap* heap);
size_t HeapSize(Heap* heap);
bool HeapEmpty(Heap* heap);

// 堆排序、求TopK
void HeapSort(datatype* arr, int size);
void PrintTopK(const char* file, int k);
c 复制代码
#define _CRT_SECURE_NO_WARNINGS 1

#include "Heap.h"

void HeapInit(Heap* heap) {
	assert(heap);
	heap->arr = NULL;
	heap->size = heap->capacity = 0;
}

void HeapDestroy(Heap* heap) {
	assert(heap);
	free(heap->arr);
	heap->arr = NULL;
	heap->size = heap->capacity = 0;
}

void HeapPush(Heap* heap, datatype val) {
	assert(heap);
	if (heap->size == heap->capacity) {
		heap->capacity = heap->capacity == 0 ? 10 : heap->capacity * 2;
		datatype* tmp = realloc(heap->arr, sizeof(datatype) * heap->capacity);
		if (tmp == NULL) {
			perror("HeapPush malloc failed.");
			exit(-1);
		}
		heap->arr = tmp;
	}
	heap->arr[heap->size++] = val;
	AdjustUp(heap->arr, heap->size - 1);
}

void HeapPop(Heap* heap) {
	assert(heap && heap->arr && heap->size > 0);
	Swap(&heap->arr[0], &heap->arr[--heap->size]);
	AdjustDown(heap->arr, heap->size, 0);
}

datatype HeapTop(Heap* heap) {
	assert(heap && heap->arr && heap->size > 0);
	return heap->arr[0];
}

size_t HeapSize(Heap* heap) {
	assert(heap);
	return heap->size;
}

bool HeapEmpty(Heap* heap) {
	assert(heap);
	return heap->size == 0;
}

void Swap(datatype* x, datatype* y) {
	int tmp = *x;
	*x = *y;
	*y = tmp;
}

void AdjustUp(datatype* arr, int child) {
	int parent = (child - 1) / 2;
	while (child > 0 && arr[child] < arr[parent]) {
		Swap(&arr[child], &arr[parent]); // up
		child = parent;
		parent = (child - 1) / 2;
	}
}

void AdjustDown(datatype* arr, int size, int parent) {
	int child = parent * 2 + 1; //child+1为右孩子节点
	child = child + 1 < size && arr[child + 1] < arr[child]
										? ++child : child;
	// child<parent调整为小堆,child>parent调整为大堆 
	while (child < size && arr[child] < arr[parent]) {
		Swap(&arr[child], &arr[parent]); // down
		parent = child;
		child = parent * 2 + 1; // left or right
		child = child+1 < size && arr[child+1] < arr[child]
										? ++child : child;
	}
}

// 升序构建大堆,降序构建小堆
void HeapSort(datatype* arr, int size) {
	// 建堆 O(nlogn) 
	//for (int i = 1; i < size; ++i) {
	//	AdjustUp(arr, i);  
	//}

	// 建堆 O(n) 
	for (int i = (size - 1 - 1) / 2; i >= 0; --i) {
		AdjustDown(arr, size, i);
	}

	// 排序 O(nlogn)
	for (int end = size - 1; end > 0; --end) {
		Swap(&arr[0], &arr[end]);
		AdjustDown(arr, end, 0);
	}
}

void PrintTopK(const char* file, int k) {
	// 文件
	FILE* fr = fopen(file, "r");
	if (fr == NULL) {
		perror("PrintTopK fopen failed.");
		exit(-1);
	}

	// 大小为k的堆
	datatype* minheap = (datatype*)malloc(sizeof(datatype) * k);
	if (minheap == NULL) {
		perror("PrintTopK malloc failed.");
		exit(-1);
	}
	// 从文件读取前k个建小堆
	for (int i = 0; i < k; i++) {
		fscanf(fr, "%d", &minheap[i]);
		AdjustUp(minheap, i); // child<parent
	}

	// 从文件挨个读取,寻找TopK
	int val = 0;
	while (fscanf(fr, "%d", &val) != EOF) {
		if (val > *minheap) {
			*minheap = val; 
			// 大的往下沉 child<parent
			AdjustDown(minheap, k, 0); 
		}
	}

	// 打印TopK
	for (int i = 0; i < k; i++) {
		printf("%d ", minheap[i]);
	}
	printf("\n");
	free(minheap);
	minheap = NULL;
	fclose(fr);
}
c 复制代码
#define _CRT_SECURE_NO_WARNINGS 

#include "Heap.h"
#include <time.h>

static void CreateNData();

int main() {
	//Heap heap;
	//HeapInit(&heap);
	 建堆 nlogn
	//HeapPush(&heap, 67864);
	//HeapPush(&heap, 7432);
	//HeapPush(&heap, 854312);
	//HeapPush(&heap, 909876);
	//HeapPush(&heap, 8765);
	//HeapPush(&heap, 2345678);
	//HeapPush(&heap, 2563);
	//HeapPush(&heap, 12676);
	//HeapPush(&heap, 6543);
	//HeapPush(&heap, 2167);
	//printf("%zd\n", HeapSize(&heap));
	//for (int i = 0; i < heap.size; i++) {
	//	printf("%d ", heap.arr[i]);
	//}
	//printf("\n");

	// top k
	//int k = 3; 
	//while (k--) {
	//	printf("%d ", HeapTop(&heap));
	//	HeapPop(&heap);
	//}
	//printf("\n");

	// Push nlogn + 排序 nlogn =  O(2nlogn)
	//while (!HeapEmpty(&heap)) {
	//	printf("%d ", HeapTop(&heap));
	//	HeapPop(&heap);
	//}
	//HeapDestroy(&heap);
	//printf("\n");

	// 堆排序
	//int arr[] = { 67864,7432,854312,909876,8765,2345678,2563,12676,6543,2167 };
	//HeapSort(arr, sizeof arr / sizeof arr[0]);
	//for (int i = 0; i < sizeof arr / sizeof arr[0]; i++) {
	//	printf("%d ", arr[i]);
	//}

	// 大量数据下的TopK
	//CreateNData(); 
	PrintTopK("data.txt", 6);
	return 0;
}

static void CreateNData() {
	int n = 100000;
	srand((unsigned int)time(0));
	FILE* fw = fopen("data.txt", "w");
	if (fw == NULL) {
		perror("CreateNData fopen failed.");
		exit(-1);
	}
	for (int i = 0; i < n; i++) {
		fprintf(fw, "%d\n", rand() % n);
	}
	fclose(fw);
}
相关推荐
pianmian12 小时前
python数据结构基础(7)
数据结构·算法
闲晨2 小时前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
好奇龙猫4 小时前
【学习AI-相关路程-mnist手写数字分类-win-硬件:windows-自我学习AI-实验步骤-全连接神经网络(BPnetwork)-操作流程(3) 】
人工智能·算法
sp_fyf_20244 小时前
计算机前沿技术-人工智能算法-大语言模型-最新研究进展-2024-11-01
人工智能·深度学习·神经网络·算法·机器学习·语言模型·数据挖掘
ChoSeitaku5 小时前
链表交集相关算法题|AB链表公共元素生成链表C|AB链表交集存放于A|连续子序列|相交链表求交点位置(C)
数据结构·考研·链表
偷心编程5 小时前
双向链表专题
数据结构
香菜大丸5 小时前
链表的归并排序
数据结构·算法·链表
jrrz08285 小时前
LeetCode 热题100(七)【链表】(1)
数据结构·c++·算法·leetcode·链表
oliveira-time5 小时前
golang学习2
算法
@小博的博客5 小时前
C++初阶学习第十弹——深入讲解vector的迭代器失效
数据结构·c++·学习