堆的应用一:优先级队列
优先级队列首先应该是一个队列。队列最大的特性就是先进先出。但是在优先级队列中,出队顺序不是先进先出,而是按照优先级来,优先级最高的,最先出队。
用堆来实现优先级队列是最直接、最高效的。这是因为,堆和优先级队列非常相似。一个堆就可以看作一个优先级队列。很多时候,它们只是概念上的区分而已。
往优先级队列中插入一个元素,就相当于往堆中插入一个元素。从优先级队列中取出优先级最高的元素,就相当于取出堆顶元素。
很多数据结构和算法都要依赖它。比如,赫夫曼编码、图的最短路径、最小生成树算法等等。
合并有序小文件
假设我们有 100 个小文件,每个文件的大小都一样,每个文件中存储的都是有序的字符串。
我们希望将这些100 个小文件合并成一个有序的大文件。这里就会用到优先级队列。
我们从这 100 个文件中,各取第一个字符串,入到小顶堆中,那堆顶的元素,也就是优先级队列队首的元素,就是最小的字符串。我们将这个字符串放入到大文件中,并将其从堆中删除。然后再从小文件中取出下一个字符串,放入到堆中。循环这个过程,就可以将 100 个小文件中的数据依次放入到大文件中。
高性能定时器
假设定时器中维护了很多定时任务,每个任务都设定了一个要触发执行的时间点。
定时器每过一个很小的单位时间,就扫描一遍任务,看是否有任务到达设定的执行时间。
这样的做法比较低效,任务的时间离当前时间可能还有很久,很多次扫描其实都是徒劳的。每次都要扫描整个任务列表,比较耗时。
使用优先级队列来,我们按照任务设定的执行时间,将这些任务存储在优先级队列中,也小顶堆的堆顶存储的是最先执行的任务。
拿队首任务的执行时间点,与当前时间点相减,得到一个时间间隔T。这个时间间隔T就是指从当前时间开始需要等待多久才会有第一个任务需要被执行。
这样定时器就可以设定在 T 秒之后再来执行任务。从当前时间点到(T-1)秒这段时间里,定时器都不需要做任何事情。
堆的应用二:利用堆求 Top K
求Top K的问题可以抽象成两类。一类是针对静态数据集合,也就是说数据集合事先确定,不会再变。另一类是针对动态数据集合,也就是说数据集合事先并不确定,有数据动态地加入到集合中。
静态数据
维护一个大小为K的小顶堆,顺序遍历数组,从数组中取出数据与堆顶元素比较。
如果比堆顶元素大,我们就把堆顶元素删除,并且将这个元素插入到堆中。如果比堆顶元素小,则不做处理,继续遍历数组。
这样等数组中的数据都遍历完之后,堆的数据就是前 K 大数据了。
动态数据
一个数据集合中有两个操作,一个是添加数据,另一个询问当前的前K 大数据。如果每次询问前K大的数据,我们都基于当前的数据重新计算的话就浪费了性能。
可以一直都维护一个K大小的小顶堆,当有数据被添加到集合中时,就拿它与堆顶的元素对比。
如果比堆顶元素大就把堆顶元素删除,并且将这个元素插入到堆中。如果比堆顶元素小,则不做处理。
这样无论任何时候需要查询当前的前 K 大数据,都可以立刻返回。
堆的应用三:利用堆求中位数
如果数据的个数是奇数,那第n/2+1个数据就是中位数。如果数据的个数是偶数的话,第n/2个和n/2+1个这两个数据就是中位数。可以随意取一个作为中位数,如第n/2个。
静态数据
中位数是固定的,我们可以先排序,第n/2个数据就是中位数。
每次询问中位数的时候,直接返回这个固定的值。排序的代价比较大,但是边际成本很小。
动态数据
中位数在不停地变动,静态数据的方法每次询问中位数的时候,都要先进行排序的话效率不会高。
借助堆这种数据结构,不用排序就可以非常高效地实现求中位数。
新加入的数据小于等于大顶堆的堆顶元素,就将这个新数据插入到大顶堆。新加入的数据大于等于小顶堆的堆顶元素,我们就将这个新数据插入到小顶堆。
这个时候可能出现两个堆中的数据个数不符合前面约定的情况,这时可以从一个堆中不停地将堆顶元素移动到另一个堆,来让两个堆中的数据满足上面的约定。
利用两个堆,一个大顶堆一个小顶堆就可以实现在动态数据集合中求中位数的操作。
原文地址:https://www.cnblogs.com/errornull/p/10054750.html