【数据结构与算法】堆 / 堆排序 / 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,如果打破这种存储关系则数组无法表示二叉树,所以数组存储非完全二叉树注定要浪费空间。

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

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

基于这样一个规律,则完全二叉树最多有2h-1个节点,最少有2h-1个节点:[2h-1,2h-1]。知道了节点数量,也就知道了树的高度h:假设N是节点数量,N = 2h-1,则h = log2(N) + 1;N = 2h-1,则h = log2(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);
}
相关推荐
Y4090019 分钟前
C语言转Java语言,相同与相异之处
java·c语言·开发语言·笔记
YuTaoShao10 分钟前
【LeetCode 热题 100】994. 腐烂的橘子——BFS
java·linux·算法·leetcode·宽度优先
Wendy14418 小时前
【线性回归(最小二乘法MSE)】——机器学习
算法·机器学习·线性回归
拾光拾趣录8 小时前
括号生成算法
前端·算法
棐木8 小时前
【C语言】动态内存管理
c语言·free·malloc·realloc·calloc·动态内存
渣呵9 小时前
求不重叠区间总和最大值
算法
拾光拾趣录9 小时前
链表合并:双指针与递归
前端·javascript·算法
好易学·数据结构9 小时前
可视化图解算法56:岛屿数量
数据结构·算法·leetcode·力扣·回溯·牛客网
香蕉可乐荷包蛋10 小时前
AI算法之图像识别与分类
人工智能·学习·算法
chuxinweihui10 小时前
stack,queue,priority_queue的模拟实现及常用接口
算法