14:堆和堆排序

堆Heap==》堆排序:原地、O(nlogn)

堆==》堆是一个完全二叉树;堆中每一个节点的值都必须大于等于(或小于等于)其子树中每个节点的值==》大顶堆(小顶堆)

实现一个堆==》完全二叉树比较适合用数组来存储

数组中下标为i的节点的左子节点,就是下标为i*2的节点,右子节点就是下标为i*2+1的节点,父节点就是下标为i/2的节点

往堆中插入一个元素==》heapify  堆化==》顺着节点所在的路径,向上或者向下,对比,然后交换

1、从下往上

 

public class Heap {
    private int[] a;  //数组,从下标1开始存储数据
    private int n;  //堆可以存储的最大数据个数
    private int count; //堆中已经存储的数据个数

    public Heap(int capacity) {
        a = new int[capacity + 1];
        n = capacity;
        count = 0;
    }

    public void insert(int data) {
        if (count >= n) return;  //堆满
        ++count;
        a[count] = data;
        int i = count;
        while (i/2 > 0 && a[i] > a[i/2]) {  //自下往上堆化
            swap(a, i, i/2);  //swap()函数作用:交换下标为i和i/2的两个元素
            i = i / 2;
        }
    }
}

 

2、从上往下

======================================

删除堆顶元素

 

public void removeMax() {
    if (count == 0) return -1;  //堆中没有数据
    a[1] = a[count];
    --count;
    heapify(a, count, 1);
}

private void heapify(int[] a, int n, int i) {  //自上往下堆化
    while (true) {
        int maxPos = i;
        if (i * 2 <= n && a[i] < a[i * 2])  maxPos = i*2;
        if (i * 2 + 1 <= n && a[maxPos] < a[i * 2 + 1])  maxPos = i * 2 + 1;
        if (maxPos == i)  break;
        swap(a, i, maxPos);
        i = maxPos;
    }
}

 

==》一个包含n个节点的完全二叉树,树的高度不会超过logn  堆化的过程是顺着节点所在路径比较交换的,所以堆化的时间复杂度跟树的高度成正比,也就是O(logn)==》插入数据和删除堆顶元素的主要逻辑就是堆化,所以,往堆中插入一个元素和删除堆顶元素的时间复杂度都是O(logn)

 

堆排序=建堆+排序

1、建堆==》将数组原地建成一个堆。“原地”就是不借助另一个数组,就在原数组上操作

思路一-->在堆中插入一个元素的思路:假设起初堆中只包含一个数据,就是下标为1的数据。然后调用插入操作,将下标从2到n的数据依次插入到堆中。这样就将包含n个数据的数组,组织成了堆

建堆是从前往后处理数组数据,在每个数据插入堆中时,从下往上堆化

思路二-->从后往前处理数组,每个数据都是从上往下堆化

 

private static void buildHeap(int[] a, int n) {
//对下标从n/2开始到1的数据进行堆化,下标是n/2+1到n的节点是叶子节点,不需要堆化
for (int i = n / 2; i >= 1; --i) { heapify(a, n, i); } } private static void heapify(int[] a, int n, int i) { while (true) { int maxPos = i; if (i * 2 <= n && a[i] < a[i * 2]) maxPos = i * 2; if (i * 2 + 1 <= n && a[maxPos] <a[i * 2 + 1] ) maxPos = i * 2 + 1; if (maxPos == i) break; swap(a, i, maxPos); i = maxPos; } }

 

堆排序的建堆过程的时间复杂度是O(n)==>因为叶子节点不需要堆化,所以需要堆化的节点从倒数第二层开始。每个节点堆化的过程中,需要比较和交换的节点个数,跟这个节点的高度 k 成正比。

 

2、排序

建堆结束之后,数组中的数据已经是按照大顶堆的特性来组织的。数组中的第一个元素就是堆顶,也就是最大的元素。我们把它跟最后一个元素交换,那最大元素就放到了下标为 n 的位置。

 

//n表示数据的个数,数组a中的数据从下标1到n的位置
public static void sort(int[] a, int n) {
    buildHeap(a, n);
    int k = n;
    while (k > 1){
        swap(a, 1, k);
        --k;
        heapify(a, k, 1);
    }
}

 

堆排序包括建堆和排序两个操作,建堆过程的时间复杂度是 O(n),排序过程的时间复杂度是 O(nlogn),所以,堆排序整体的时间复杂度是 O(nlogn)。

 

那如果从 0 开始存储,实际上处理思路是没有任何变化的,唯一变化的,可能就是,代码实现的时候,计算子节点和父节点的下标的公式改变了。如果节点的下标是 i,那左子节点的下标就是 2∗i+1,右子节点的下标就是 2∗i+2,父节点的下标就是 (i−1)/2​。

 

堆排序为什么没有快速排序快?==》

堆排序数据访问的方式没有快速排序友好==》对于快速排序来说,数据是顺序访问的。而对于堆排序来说,数据是跳着访问的。对CPU缓存是不友好的

对于同样的数据,在排序过程中,堆排序算法的数据交换次数要多于快速排序==》快速排序数据交换的次数不会比逆序度多。但是堆排序的第一步是建堆,建堆的过程会打乱数据原有的相对先后顺序,导致原数据的有序度降低。

 

=================================

堆的应用==》优先级队列、top k、中位数

快速获取到TOP10最热门的搜索关键词==>分片+散列表+堆

 

优先级队列==》Java的PriorityQueue

合并有序小文件==》假设我们有 100 个小文件,每个文件的大小是 100MB,每个文件中存储的都是有序的字符串。我们希望将这些 100 个小文件合并成一个有序的大文件。

高性能定时器

 

Top k==》求 Top K 的问题抽象成两类。一类是针对静态数据集合,也就是说数据集合事先确定,不会再变。另一类是针对动态数据集合,也就是说数据集合事先并不确定,有数据动态地加入到集合中。

针对静态数据==》维护一个大小为 K 的小顶堆,顺序遍历数组,从数组中取出数据与堆顶元素比较。如果比堆顶元素大,就把堆顶元素删除,并且将这个元素插入到堆中;如果比堆顶元素小,则不做处理,继续遍历数组。这样等数组中的数据都遍历完之后,堆中的数据就是前 K 大数据了。遍历数组需要 O(n) 的时间复杂度,一次堆化操作需要 O(logK) 的时间复杂度,所以最坏情况下,n 个元素都入堆一次,时间复杂度就是 O(nlogK)。

针对动态数据求得 Top K 就是实时 Top K==》一个数据集合中有两个操作,一个是添加数据,另一个询问当前的前 K 大数据。如果每次询问前 K 大数据,都基于当前的数据重新计算的话,那时间复杂度就是 O(nlogK),n 表示当前的数据的大小。实际上,可以一直都维护一个 K 大小的小顶堆,当有数据被添加到集合中时,就拿它与堆顶的元素对比。如果比堆顶元素大,就把堆顶元素删除,并且将这个元素插入到堆中;如果比堆顶元素小,则不做处理。这样,无论任何时候需要查询当前的前 K 大数据,都可以立刻返回。

 

求中位数==》求动态数据集合中的中位数。需要维护两个堆,一个大顶堆,一个小顶堆。大顶堆中存储前半部分数据,小顶堆中存储后半部分数据,且小顶堆中的数据都大于大顶堆中的数据。如果有 n 个数据,n 是偶数,从小到大排序,那前 n/2​ 个数据存储在大顶堆中,后 n/2​ 个数据存储在小顶堆中。这样,大顶堆中的堆顶元素就是要找的中位数。如果 n 是奇数,情况是类似的,大顶堆就存储 n/2​+1 个数据,小顶堆中就存储 n/2​ 个数据。

 

posted @ 2020-08-17 10:41  LinBupt  阅读(153)  评论(0编辑  收藏  举报