数据结构与算法—堆(heap)

目录

1、插入 

2、删除

建堆        

1、堆化

2、排序

与快速排序比较

堆的应用

一、优先级队列

1、合并有序小文件:

2、高性能定时器

二、求Top K和中位数

1、TopK问题

2、利用堆求中位数(动态数据)


1、堆是一个完全二叉树。(除最后一层,其他层的节点个数都是满的,最后一层的节点都靠左排列)

2、堆中一个节点的值都必须大于等于(或者小于等于)其子树中每个节点的值。对应的是大顶堆和小顶堆。

堆排序是一种原地的,时间复杂度为O(nlogn)的排序算法

大顶堆:每个节点的值都大于等于子树中每个节点值

小顶堆:每个节点的值都小于等于子树中每个节点值

堆的操作 插入与删除

1、插入 

        顺着节点所在的路径,向上或者向下对比(先跟最后一个数据比,依次向上),然后交换。让新插入的节点与父节点对比大小,如果满足子节点小于等于父节点,相互交换,一直重复。

2、删除

        大顶堆,堆顶元素是最大元素。当我们删除堆顶元素之后,就需要把第二大的元素放到堆顶,依次迭代。直到叶子节点被删除。

        问题:数组空洞,不满足完全二叉树定义。

        改进 :把最后一个节点放到堆顶,然后用同样的方法比较。这就是从上往下的堆化方法。

建堆        

1、堆化

         从最后一个非叶子节点i = heap->elements / 2开始,使得数据满足堆的两个特性。建堆时间复杂度O(n)

void heapify(struct heap *heap, int parent)
{
        struct element **elem = heap->elem;
        int elements = heap->elements;
        int left, right, max;

        while (true) {
                left = parent * 2;
                right = left + 1;

                max = parent;
                if (left <= elements && elem[max]->data < elem[left]->data)
                        max = left;
                if (right <= elements && elem[max]->data < elem[right]->data)
                        max = right;

                if (max == parent)
                        break;

                swap(heap, max, parent);
                parent = max;
        }
}


void build_heap(struct heap *heap)
{
        int i;

        for (i = heap->elements / 2; i >= 1; i--)
                heapify(heap, i);
}

2、排序

按照大顶堆:数组中的第一个元素就是堆顶,我们把它跟后一个元素交换,那么最大元素就放到了下标为n的位置。再次堆化,取堆顶元素,放在n-1位置上。这样交换完数据就排序好了

void swap(struct heap *heap, int i, int j)
{
        struct element *tmp;

        tmp = heap->elem[j];
        heap->elem[j] = heap->elem[i];
        heap->elem[i] = tmp;
}

int heap_sort(struct heap *heap)
{
        int elements = heap->elements;

        while (heap->elements) {
                swap(heap, 1, heap->elements);
                heap->elements--;
                heapify(heap, 1);
        }

        return elements;
}

排序时间复杂度 O(logn)

堆排序是不稳定的排序算法,在排序过程中,存在将堆的最后一个节点跟堆顶互换。有可能改变相同数据的原始相对顺序。

与快速排序比较

在实际开发中,为什么快速排序要比堆排序性能好

1、堆排序的数据访问方式没有快速排序友好

2、对于相同的数据,堆排序数据交换次数多余快速排序

堆的应用

一、优先级队列

队列最大特点先进先出,而优先级队列,数据的出队顺序不是先进先出,而是按照优先级来,优先级高的先出队。

堆和优先级队列非常相似,一个堆看作一个优先级队列。

1、合并有序小文件:

100个小文件,每个文件大小是100MB,每个文件中存储的都是有序的字符串。

使用优先级队列,从各文件拿出第一个string,将小文件中取出来的字符串放入到小顶堆中,100个文件的string组成的小顶堆,堆顶的元素,就是优先级队列首元素,就是最小的字符串。

将这个小字符串从堆中删除,再从小文件中取出下一个字符串,放入堆中。循环执行

时间复杂度:O(logn)

2、高性能定时器

定时器中维护很多定时任务,每个任务都设定了一个要触发执行的时间点。定时器定时扫描任务,查看任务设置的执行时间,如果到了,就拿出来执行。

每过1秒扫描一遍任务列表做法低效

1、任务定时可能很久,1s扫描很多都是徒劳

2、每次都扫描整个任务列表,如果任务表很大,必然会很耗时。

使用优先级队列

按照任务设定的执行时间,将这些任务存储在优先级队列中,队列首部存储的是最先执行的任务。

拿队首任务的执行时间点,与当前时间相减,得到时间间隔T;T就是需要等待多久。

时间到,从队首取出任务,然后再计算新的任务执行时间与当前时间点的差值。

二、求Top K和中位数

1、TopK问题

1、静态数据,数据事先确定,不会改变

维护一个大小为K的小顶堆,顺序遍历数组,从数组中取出数据与堆顶元素比较。如果比堆顶元素大,把堆顶元素删除,并且将这个元素插入到堆中;

如果比堆顶元素小,则不做处理。等数据都遍历完后,堆数据就是前K大数据。

时间复杂度:遍历数组O(n)时间复杂度,一次堆化操作需要O(logk)的时间复杂度,最坏情况下,n个元素都入堆一次,时间复杂度O(nlogk)

2、动态数据

两个操作:1、添加数据;2、询问当前的K大数据

一直维护一个K大小的小顶堆

当有数据添加到集合中,拿堆它与堆顶元素对比。如果比堆顶元素大,就把堆顶元素删除,并将这个元素加入到堆中;如果比堆顶元素小,不变。

无论任何时候都需要查询当前的K大数据,都可以得到。

时间复杂度:如果每次询问前K大数据,基于当前的数据重新计算的话,时间复杂度O(nlogk),n表示当前数据的大小。

2、利用堆求中位数(动态数据)

先将数据排序,再将数据一分为二,维护两个堆,一个大顶堆,一个小顶堆

当有数据插入时,如果数据小于等于大顶堆的元素,就将这个数据添加到大顶堆中;否则,小顶堆。

调整:数据插入,破坏了数据均分的特性;将一个堆顶元素,移动到另一个堆中。

插入数据涉及堆化,时间复杂度为O(logn),查找时间复杂度O(1)

猜你喜欢

转载自blog.csdn.net/WANGYONGZIXUE/article/details/129250981