zoukankan      html  css  js  c++  java
  • 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​ 个数据。

  • 相关阅读:
    .NET Core MVC下的TagHelper
    测试.NET core MiddleWare 运行逻辑
    中台
    VSCode 完美整合前后端框架(angular2+.NET core)
    三分钟热度并非贬义
    【算法】莫队算法粗略讲解
    【题解】洋溢着希望
    【三角学】三角恒等变换公式推导
    【题解】方差
    【数据结构】FHQ Treap 详解
  • 原文地址:https://www.cnblogs.com/liushoudong/p/13509497.html
Copyright © 2011-2022 走看看