再写堆(堆的性质,向下调整,建堆,堆的插入删除初始化,堆排序,TopK问题)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/liuyuchen282828/article/details/102503218

堆的概念

如果有一个关键码的集合K={k0,k1,k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储再一个一维数组中,并满足:Ki<=K2i+1且Ki<=K2i+1(Ki >= K2i+1 且 Ki >= K2i+2),i=0,1,2,3…。则称为小堆(或大堆)。将根结点最大的堆叫做最大堆或大根堆,根结点最小的堆叫做最小堆或小根堆

堆的性质

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

堆的向下调整算法

顺序存储的完全二叉树

已知[parent]
[left] = 2*[parent]+1;
[right] = 2*[parent]+2;

已知[child] 无论左右
[parent] = ([child]-1)/2

基本步骤

建立小堆

  1. 要调整root所在结点,前提:root的左右结点子树已经满足堆的性质
  2. 如果root所在结点已经是叶子结点,调整结束
  3. 找到左右孩子中最小的一个min
  4. if (array[root] <= array[min])。调整结束
  5. else: swap(&array[root],&array[min]) ; [min] = [root] ; 再回到1重复这几个步骤

代码实现

void AdjustDown(int array[], int size, int root)
{
	//判断 root 是否是叶子结点
	//因为 堆是完全二叉树,所以没有左孩子一定没有右孩子
	//又因为堆是顺序存储的
	//所以,找到左孩子的下标,如果左孩子的下标越界了,则没有左孩子
	while (1)
	{
		int left = 2 * root + 1;
		if (left >= size){
			//越界了,就是叶子结点
			return;
		}

		//走完上面一定有左孩子,判读是否有右孩子
		//找到左右孩子最小的一个
		int right = 2 * root +2;
		int min = left;//最开始就认为最小的值为左孩子
		if (right < size && array[right] < array[left]){
			//没有越界,就有右孩子,如果右孩子的值小于左孩子的值
			min = right;
		}

		//比较array[min]  array[root]
		if (array[root] <= array[min]){
			return;
		}

		//调整,交换值
		int t = array[root];
		array[root] = array[min];
		array[min] = t;

		//需要继续向下调整,以min作为结点
		root = min;
	}
}

测试

void PrintArray(int array[], int size)
{
	for (int i =0; i < size; ++i){
		printf("%  d", array[i]);
	}
	printf("\n");
}

void TestAdjustDown()
{
	int array[] = { 27, 15, 19, 18, 28, 34, 65, 49, 25, 37 };
	int size = sizeof array / sizeof(int);
	PrintArray(array, size);

	AdjustDown(array, size, 0);
	PrintArray(array, size);
}

时间复杂度(logn)

在这里插入图片描述

建堆

循环变量i从最后一个非叶子节点开始,到0结束,在这之间不断的做向下调整

最后一个非叶子节点 = 最后一个结点的双亲结点( size-1) = ((size-1)-1)/2

在这里插入图片描述

代码实现

//建堆
void CreateHeap(int array[], int size)
{
	for (int i = (size - 2) / 2; i >= 0; i--)
	{
	//不断的向下调整
		AdjustDown(array, size, i);
	}
}

测试

时间复杂度O(n)

void TestCreateHeap(){
	int array[] = { 15, 37, 2, 45, 63, 9, 18, 7, 16, 13 };
	int size = sizeof(array) / sizeof(int);

	CreateHeap(array, size);
	PrintArray(array, size);
}

在这里插入图片描述

堆的初始化

//初始化
void HeapInit(Heap *heap, int array[], int size){
	memcpy(heap->array, array, size*sizeof(int));
	heap->size = size;
	CreateHeap(heap->array, size);
}

堆的删除

  1. 只能删除堆顶元素,删其他位置没有意义
  2. 拿数组中最后一个元素,覆盖掉数组第一个元素,也就是堆顶元素,然后向下调整

代码实现

//删除
void HeapPop(Heap *heap)
{
	heap->array[0] = heap->array[heap->size - 1];
	AdjustDown(heap->array, heap->size - 1,0);
	heap->size--;
}

堆的插入

  1. 往数组中最后一个位置插入,然后向上调整
//向上调整
/*
array 数组
size 数组的长度
child 要向上调整的结点下标
*/

//小堆的情况
void AdjustUp(int array[], int size, int child)
{
	while (1)
	{
		//已经到堆顶位置
		if (child == 0)
		{
			return;
		}

		int parent = (child - 1) / 2;
		//父结点的值比孩子的值小的就不用调整
		if (array[parent] <= array[child]){
			return;
		}

		//交换
		int t = array[parent];
		array[parent] = array[child];
		array[child] = t;

		child = parent;
	}
}
/插入操作
void HeapPush(Heap *heap, int val){
	heap->array[heap->size++] = val;
	AdjustUp(heap->array, heap->size, heap->size - 1);
}

堆排序

堆排序不能找最小的放到最前,要不然堆的结构会被破坏

堆排序的注意事项

  1. 排升序,建大堆
  2. 排降序,建小堆
  3. 原因是,重新调整回堆的成本更小(向下调整(O(logn)) < 建堆(O(n)))
i的意义是被选出的最大的数的个数
for(i < size-1){
	//一次循环,找出一个最大的数放到最后
	swap(&array[0],&array[size-1-i])
	AdjustDown(array,size-1-i);
}

代码实现

//大堆的情况

void AdjustDown222(int array[], int size, int root)
{
	//判断 root 是否是叶子结点
	//因为 堆是完全二叉树,所以没有左孩子一定没有右孩子
	//又因为堆是顺序存储的
	//所以,找到左孩子的下标,如果左孩子的下标越界了,则没有左孩子
	while (1)
	{
		int left = 2 * root + 1;
		if (left >= size){
			//越界了,就是叶子结点
			return;
		}

		//走完上面一定有左孩子,判读是否有右孩子
		//找到左右孩子最小的一个
		int right = 2 * root + 2;
		int min = left;//最开始就认为最小的值为左孩子
		if (right < size && array[right] > array[left]){
			//没有越界,就有右孩子,如果右孩子的值小于左孩子的值
			min = right;
		}

		//比较array[min]  array[root]
		if (array[root] >= array[min]){
			return;
		}

		//调整,交换值
		int t = array[root];
		array[root] = array[min];
		array[min] = t;

		//需要继续向下调整,以min作为结点
		root = min;
	}
}
void AdjustUp222(int array[], int size, int child)
{
	while (1)
	{
		//已经到堆顶位置
		if (child == 0)
		{
			return;
		}

		int parent = (child - 1) / 2;
		//父结点的值比孩子的值小的就不用调整
		if (array[parent] >= array[child]){
			return;
		}

		//交换
		int t = array[parent];
		array[parent] = array[child];
		array[child] = t;

		child = parent;
	}
}

//堆排序
//升序,建大堆

void HeapSort(int array[], int size)
{
	CreateHeap(array, size);
	//i 表示被找出的最大的数的个数
	for (int i = 0; i < size - 1; i++)
	{
		//每次循环,会找出最大的一个数放到最后

		int t = array[0];
		array[0] = array[size - i - 1];
		array[size - i - 1] = t;

		//进行向下调整,数据规模是size-1-i;
		AdjustDown222(array, size - 1 - i, 0);
	}
}

测试

void TestHeapSort()
{
	int array[] = { 39, 129, 12, 38, 27, 9, 33, 2, 14 };
	int size = sizeof(array) / sizeof(int);

	HeapSort(array, size);

	PrintArray(array, size);
}

在这里插入图片描述

测试堆排序与冒泡排序的速度

#define SIZE 50000

void TestSortSpeed(){
	srand(20190104);
	int array[SIZE] ;
	for (int i = 0; i < SIZE; i++){
		array[i] = rand() % 10 * 10000;
	}
	int s = time();
	HeapSort(array, SIZE);
	int e = time();
	printf("%d\n", e - s);
	
	
	
}

堆排序速度:
在这里插入图片描述
冒泡排序速度:
在这里插入图片描述

TopK问题

在海量数据中(n>>100*10000),找最大的k=10个数

/*
类似伪代码,实际中,size是海量的,内存中放不下,需要借助文件操作
*/
void TopK(int array[], int size, int k){
	int *heap = (int *)malloc(sizeof(int)*k);
	for (int i = 0; i < k; i++){
		heap[i] = array[i];
	}

	//针对heap建小堆
	CreateHeap(heap, k);

	for (int j = k; j < size; j++){
		if (array[j]>heap[0]){
			heap[0] = array[j];
			AdjustDown(heap, k, 0);
		}
	}
}

猜你喜欢

转载自blog.csdn.net/liuyuchen282828/article/details/102503218