堆(Heap)是一种数据结构,我们研究数据结构还是从三个部分了解,第一是逻辑结构,第二是存储结构,第三则是基础操作,堆应用很广泛,例如堆排序、Top-k问题等
逻辑结构:一棵完全二叉树,分为小根堆和大根堆两种,一个是要求其父亲始终小于孩子结点,一个要求父亲永远大于孩子节点。
存储结构:使用顺序存储来存储这个结构,完全可以用一个数组,但是为了扩容和一些后面的操作,采用顺序表会比较好一些,结构体里面有数据域指针、size和capacity三个部分。
基础操作:1、初始化,与顺序表的初始化完全相同,这里不过多赘述
2、Push:插入前首先要保证这是一个大堆或小堆,然后将数据插入到size指向的位置后size++,最后向上调整算法恢复成一个堆。
在这里要提一下向上调整算法,此算法使用前一定是一个堆才能使用,算法是直接针对数组使用的(这里后面堆排序会用到),在这里以小堆为例,我们插入的元素放在后面,它是作为孩子进来的,首先去找它的父亲节点,(找父亲节点和孩子节点这里也要注意,我们这里是从数组的0下标处开始存储的,因此找父亲节点是(i-1)/2,找孩子是2*i+1和2*i+2,若是从1开始存储,找的时候就不用-1或+1,这里做题的时候认真读题并验证一下再写后面),然后进行比较,若插入的这个数据比它的父亲数据还小就需要交换,因为此时的父亲一定小于此数据的兄弟,所以将这个数据调整上去那会符合小堆父亲<孩子的特点,然后再找到这个数据和此数据的父亲节点比较,直至此数据>父亲或数据调整到下标为0的位置为止。
3、Pop:删除操作我们要删除哪个元素呢?删除哪个比较有用?删除堆顶最有用,能找出小堆中最小的数据和大堆中最大的元素,也就是找出一个数组中最大或最小的数据,这时候就能稍微体现出堆的用处了,一定会应用于排序中,但是现在如何删除呢?不能直接覆盖,因为覆盖之后将会打乱所有的父亲兄弟之间的关系,就不是一个堆了,如何删除?size此时指向的是最后一个数据的后一个位置,我们首先要将首元素和尾部的元素交换,然后删除即可,删除后我们得到的是一个没有堆顶的堆,父亲和兄弟之间的关系没有变,也就是左子树和右子树都是堆,这就是我们向下调整算法使用前的条件,向下调整算法是为了将堆顶的这个数据调整到合适位置,使整体成为一个堆。
向下调整算法:也可以直接应用于数组,此时要将某一个父亲节点向下调整到合适位置,不一定是要调整整个树的根节点,例如此时左右子树为小堆,堆顶交换过来的数据比较大,我们就要将最小的元素拿上去,从左右小堆中选更小的那个孩子与之交换,然后继续找左右子树中小的那个与之交换,直至孩子的下标超出整个数组的长度,也就是刚刚删除的数组尾部元素的下标的数值,比如数组长度为10,下标是0-9共10个数据,删除的是下标为9的数据,此时数组中还剩9个数据,那下标一定<9,也就是<数组内元素个数,这时候就越界了,没有这个孩子,我们还要注意在选左右子树中大的那个数据时要防止右孩子为空,避免使用空指针的错误。
GetTop:直接return size-1指向的那个元素即可,千万别用--,这样我们传入的是结构体的指针可以直接找到整个结构体,若用--就会对size的值修改。
还有一个操作是判空,即size==0的时候堆为空返回1,否则返回0.
这就是堆的基础操作,实现代码在下面的链接中,大家可以去看一下2023.5.23数据结构堆的基础操作的实现 · 9130a9a · 胡浩琰/class108 - Gitee.com
我们说完了基础操作后再仔细研究一下两个算法,向上调整算法和向下调整算法,这两个算法可以直接应用于数组,但是各有各的前提,向上调整算法的前提是插入前一定是一个堆,向下调整算法的前提是左右子树一定是堆,假设一棵二叉树的高度是h,则某一个结点最多调整h-1次,将h与总结点数n等价替换一下log以2为底n+1的对数,这个计算大家可以参考一下完全二叉树的高度的计算,也就是向上调整一个算法的时间复杂度为O(log以2为底n的对数),向下调整算法调整一个数据最坏情况下的时间复杂度也是一样的,复杂度主要体现在交换的次数中。
下面我们来说两个堆的应用,堆这种数据结构可以应用于排序中,而且时间复杂度很可观,比之前学习的冒泡排序、暴力排序等要优化了太多太多,这个当数据很大的时候体现的很清楚。
堆排序:首先有一个大小为n的数组,如何排序?有一种思路是建一个堆,将数组中的元素依次Push进去,若降序排列则建大堆,若升序排列则建小堆,全部Push进去后依次GetTop的元素并将其放入数组中,以小堆为例,获取最小的放入数组第一个位置中,下一步将其删除并向下调整找出第二小的数据,以此类推直至堆为空,这种思路的时间复杂度暂时不计算,但是可以看到要排序n个元素就要进行n-1次删除,即向下调整算法,时间复杂度相对于其他排序好很多,但缺点是显而易见的,需要创建一个堆麻烦,而且要开辟额外的空间,会有空间复杂度,我们能不能直接对数组进行操作,避免了使用堆,留下了优点。完全可行,每一个数组都可以看作一个完全二叉树,首先我们要将这个数组调整为一个堆,第一个建堆的方式就是向上调整算法,使用前提是每次插入一个元素前一定是一个堆,那我们看这个数组就这样看,看作里面一开始没有数据,依次遍历,假插入,从数组开头开始遍历,假插入第一个元素,然后向上调整,假插入第二个元素,向上调整,直至到数组尾部为止,这样保证了每次假插入前都是堆,可以使用向上调整算法,但是这个算法在时间复杂度上稍微有瑕疵,这个后面再说,还有一种建堆方式就是向下调整建堆,我们使用向下调整的前提是左右子树都为堆,因此叶子节点都不需要调整,第一个分支节点有一个或两个孩子节点,此时需要也可以开始调整了,将这个分支节点对应的数据调到合适的地方使之成为一个堆,方便后面结点的调整,从最后一个孩子节点的父亲结点开始调整,也就是第一个分支节点开始,一直--,直到数组第一个元素为止,这时需要调整的元素的个数几乎少了一半,全部的叶子节点都不需要调整了,这就是建堆的方式,但是建什么堆如何决定?数据是从后向前依次找出来的,若排降序,先找的就是最大的元素,也就需要排大堆,因为数据从后向前放不会打乱前面所有数据的兄弟父亲关系,若直接找最小的元素将其安在第一个位置,则后面的次小的元素怎么找,没什么办法找了,因此排降序我们建立小堆,排升序我们创建大堆,倒着找,先找最大的,再找次大的,这就是一个假删除,开始我们建好堆了,直接将堆顶最大的数据与最后一个交换,然后end--,这就是一个假删除过程,然后向下调整算法恢复成堆,再找次大的元素,向下调整和交换找元素是一组,要找n个元素我们就要向下调整n-1次,若数组长度为n,我们就要找n-1个元素,因为排好前n-1个最后一个也一定排好了。这就是堆排序的过程,建堆------假删除并向下调整。
堆排序的细节和过程详细的看完后可以看一下这段代码2023.5.24堆的基本操作以及堆排序和利用文件中的数据玩Top-k · 3c377a8 · 胡浩琰/class108 - Gitee.com向上调整建堆的时间复杂度是O(N*log以2为底N的对数),向下调整算法的时间复杂度甚至可以估算为O(N),很节省时间,假删除和向下调整的时间复杂度是O(N*log以2为底N的对数),因此堆排序的时间复杂度为O(N*log以2为底N的对数),比冒泡排序中的O(N^2)的时间复杂度小了太多。
Top-k问题:在10000000个数中找最大的5个数这就是Top-k问题,看到这种问题的思路只有将其全部放入堆中然后排序找处前五个,但是这么多数据能全部放入内存吗?如果能,100亿个int类型的数据呢?因此不可能采用这种思路,我们换一种思路,可以将这些数据放入到文件中保存下来,然后可以遍历了,但是要将其全部放入堆中不可能。我们换一种利用堆的思路,我们要找的是前5个最大的数据,那我们就将前五个数据放入到堆中,这很容易放进去,采用一个数组,然后将这个数组调整为小堆,记住一定是小堆,因为此时堆顶是这五个数据中最小的那个数据,然后从第六个数据开始遍历所有数字,这个遍历是不可避免的,有文件我们可以随意遍历,遇到比堆顶元素大的元素就直接覆盖堆顶元素,然后向下调整算法将其重新调整为小堆,始终保持堆顶是最小的数据,当遍历到最后,这个堆中存的这五个数据一定是最大的五个数据,因为比堆顶大的全部进来了,外面没有比这个堆中最小的还要大的数据了,非常巧妙地一种算法,解决了大数据的问题。建堆---不满足则覆盖并向下调整建堆。
这就是堆的基础操作和堆的应用,堆很重要,在很多方面都能用到堆,因此基础操作和堆排序以及Top-k问题很重要,需要好好掌握。