[学懂数据结构]堆的简单实现及应用(篇二)

125 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第22天,点击查看活动详情

前言

        在讲完线性表后,接下来要介绍树结构中的二叉树,本文是基于C语言实现的。

        本文就来分享一波作者对数据结构二叉树的学习心得与见解。本篇属于第九篇,继续介绍二叉树的顺序结构——堆相关内容,建议阅读本文之前先把前面的文章看看。

        笔者水平有限,难免存在纰漏,欢迎指正交流。

原地建堆

        下面我们给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在我们通过算法,把它构建成一个堆。根节点左右子树不是堆,我们怎么调整呢?

向下调整算法建堆

        向下调整算法有一个前提:左右子树必须是一个堆,才能调整。 如果是从上向下进行的话,不能保证每一个结点的左右子树都是堆,不过最下面一层不需要调整,因为它们是都是叶结点,本身就是堆,那不妨就从下向上进行调整

        这里我们从倒数的第一个非叶结点的子树开始调整,使用向下调整算法,每次调整完后目标下标减1,然后再调整......一直循环过程到根节点的树,就可以调整成堆。

        比如下图建大根堆:

image-20220808163432524

代码实现

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

void AdjustDown(HPDataType* arr, int sz, int parent)
{
	int minChild = parent * 2 + 1;

	while (minChild < sz)
	{
		if (minChild + 1 < sz && arr[minChild + 1] < arr[minChild])
			++minChild;

		if (arr[minChild] < arr[parent])
		{
			Swap(&arr[minChild], &arr[parent]);
			parent = minChild;
			minChild = parent * 2 + 1;
		}
		else
			break;
	}

}

 void HeapCreateDown(HPDataType* arr, size_t n)
 {
     for(int i = (n - 1 - 1) / 2; i >= 0; --i)
     {
         AdjustDown(arr, n, i);
     }  
 }

时间复杂度计算

        用满二叉树来看最坏情况:

image-20220808102635321

        计算得到时间复杂度仅为:O(n)。

        层数越深,结点越多,单个结点需要调整次数越少;层数越浅,结点越少,单个结点需要调整次数越多。

向上调整算法建堆

        从上向下进行向上调整算法,初始状态下数组第一个元素可以看成堆,根据要建成大根堆还是小根堆确定向上调整函数的比较逻辑,然后从第二个元素开始进行向上调整,接着到第三个......依次类推,每次向上调整都是一次元素插入,并且满足大根堆或小根堆的要求。

代码实现

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

void AdjustUp(HPDataType* arr, int child)
{
	int parent = (child - 1) / 2;

	while (child > 0)
	{
		if (arr[child] < arr[parent])
			Swap(&arr[child], &arr[parent]);

		else
			break;

		child = parent;
		parent = (child - 1) / 2;
	}
}

void HeapCreateUp(HPDataType* arr, size_t n)
 {
     for(int i = 1; i < n; i++)
     {
         AdjustUp(arr, i);
     }
 }

时间复杂度计算

        用满二叉树来看最坏情况:

image-20220808171828718

        计算得到时间复杂度为O(n*logn)。

        层数越深,结点越多,单个结点需要调整次数越多;层数越浅,结点越少,单个结点需要调整次数越少。

堆的应用

堆排序

        本质上是一种选择排序,依次选数,从后往前排。

        不建议新建一个堆来实现堆排序,没有必要再手搓一个堆出来,一是麻烦,二是空间复杂度会变成O(n)。

堆排序即利用堆的思想来进行排序,总共分为两个步骤:

  1. 原地建堆

    升序:建大堆

    降序:建小堆

  2. 利用堆删除思想来进行排序

    建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。

        比如:要求升序的话,每次都把堆顶元素和最后的元素互换,然后最后一个元素不看作堆里的,堆顶向下调整,选出次大的,后续依次类似处理(要求降序的话思路类同,就是每次把最小的换到后面去顺便选出次小的)。实际上就是从后向前排序。

如图所示:

image-20220808112340229

代码实现

        前面写的AdjustDown( )用的是针对小根堆的逻辑,适合降序使用,如果想要升序,就在函数里把arr[minChild + 1] < arr[minChild]改成arr[minChild + 1] > arr[minChild],并且要把if (arr[minChild] < arr[parent])改成if (arr[minChild] > arr[parent])

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

void AdjustDown(HPDataType* arr, int sz, int parent)
{
	int minChild = parent * 2 + 1;

	while (minChild < sz)
	{
		if (minChild + 1 < sz && arr[minChild + 1] < arr[minChild])
			++minChild;

		if (arr[minChild] < arr[parent])
		{
			Swap(&arr[minChild], &arr[parent]);
			parent = minChild;
			minChild = parent * 2 + 1;
		}
		else
			break;
	}

}

 void HeapSort(int* arr, int n)
 {
     //建堆
     for (int i = (n - 2) / 2; i >= 0; --i)
     {
         AdjustDown(arr, n, i);
     }
     //排序
     for (int i = 1; i < n; ++i)
     {
         Swap(&arr[0], & arr[n - i]);
         AdjustDown(arr, n - i, 0);
     }
 }

时间复杂度计算

        向下调整算法的时间复杂度为O(logn),外面套个循环,那就是O(n*logn)

Top-K问题

        即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。 ​ 对于Top-K问题,能想到的最简单直接的方式就是排序,但是,如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中,而存在磁盘文件中)。

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

  1. 用数据集合中前K个元素来新建堆 前k个最大的元素,则建小堆(建大堆popK次遇到大量数据就不行了,因为数据很可能不在内存而在磁盘文件中,而建堆必须要在内存中搞) 前k个最小的元素,则建大堆(建大堆popK次遇到大量数据就不行了,因为数据很可能不在内存而在磁盘文件中,而建堆必须要在内存中搞)
  2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足条件则替换堆顶元素并调整堆。将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。

思路分析

        在分析之前,我们先约定一下,将所有元素分为已访问的元素和未访问的元素,一开始把前K个元素建堆(称为K堆),此时已访问元素就是这K个,那为什么要找前K大元素要建小堆而找前K小元素要建大堆呢?我们是从第K+1个元素开始向后访问的吧,是在不断地扩大访问范围,每次都把访问的元素和K堆堆顶元素比较,要是访问的元素更大就让它入堆(找前K大元素),实际上就是不断把当前访问范围内前K大的元素集中到K堆中,再不断扩大访问范围直到访问完了所有元素,最终就能找出所有元素前K大元素了。

比如说我要找前K个最大元素:

 void Swap(HPDataType* x, HPDataType* y)
 {
     HPDataType tmp = *x;
     *x = *y;
     *y = tmp;
 }
 ​
 void AdjustDown(HPDataType* arr, int sz, int parent)
 {
     int minChild = parent * 2 + 1;
 ​
     while (minChild < sz)
     {
         if (minChild + 1 < sz && arr[minChild + 1] < arr[minChild])
             ++minChild;
 ​
         if (arr[minChild] < arr[parent])
         {
             Swap(&arr[minChild], &arr[parent]);
             parent = minChild;
             minChild = parent * 2 + 1;
         }
         else
             break;
     }
 ​
 }
 HPDataType* GetTopK(HPDataType* arr, int n, int k)
 {
     HPDataType* heap_k = (HPDataType*)malloc(k * sizeof(HPDataType));
     if (heap_k == NULL)
     {
         perror("malloc fail");
         exit(-1);
     }
     for (int j = 0; j < k; j++)
     {
         heap_k[j] = arr[j];
     }
     //建小根堆
     for (int i = (n - 1 - 1) / 2; i >= 0; --i)
     {
         AdjustDown(heap_k, k, i);
     }
     //比较剩余元素和调整堆
     for (int i = 0; i < n - k; i++)
     {
         if (arr[k + i] > heap_k[0])
         {
             Swap(&heap_k[0], &arr[k+i]);
             AdjustDown(heap_k, k, 0);
         }
     }
 ​
     return heap_k;
 }

        这种是针对数据全部在内存中的写法,写完后可以用下面这一函数自测一下:

 void TestTopK()
 {
     srand((unsigned)time(0));//记得要包含<stdlib.h>和<time.h>两个头文件
 ​
     int arr[100];
     
     for (int i = 0; i < 100; ++i)
     {
         arr[i] = rand() / 1000;
     }
 ​
     arr[5] = 1001;
     arr[10] = 1002;
     arr[15] = 1003;
     arr[20] = 1004;
     arr[25] = 1005;
     arr[30] = 1006;
     arr[35] = 1007;
     arr[40] = 1008;
     arr[45] = 1009;
     arr[50] = 1010;
 ​
     HPDataType* heap_k = GetTopK(arr, 100, 10);
 ​
     for (int i = 0; i < 10; ++i)
     {
         printf("%d\n", heap_k[i]);
     }
 }

优点分析

        由K + (n-K)log2 K 推得时间复杂度:O(n),确实嘎嘎快~

        空间复杂度:O(K)其实就是常数级O(1)

        就复杂度而言都能薄纱其他方法了。

        即使数据量很大,数据存储在硬盘上也无大碍,照样给你找出前K个最大或最小的元素。

u=2269813557,1572468495&fm=253&fmt=auto&app=138&f=JPEG.webp


以上就是本文全部内容了,感谢观看,你的支持就是对我最大的鼓励~

src=http___c-ssl.duitang.com_uploads_item_201708_07_20170807082850_kGsQF.thumb.400_0.gif&refer=http___c-ssl.duitang.gif