zoukankan      html  css  js  c++  java
  • Java 数据结构

    Java 数据结构 - 堆和堆排序:为什么快排比堆排序性能好

    数据结构与算法目录(https://www.cnblogs.com/binarylei/p/10115867.html)

    关于二叉树这种数据结构在实现软件工程中的应用,前面我们已经介绍了红黑树,下面我们再介绍另一种常见的二叉树 - 堆。

    • 红黑树:基于平衡二叉查找树的动态数据结构,用于快速插入和查找数据,其时间复杂度都是 O(logn)。
    • 堆:按照结点大于等于(或小于等于)子结点,又分为大顶堆和小顶堆。和红黑树不同,椎只是部分有序,即 "左结点 < 父结点 && 右结点 < 父结点",而有序二叉树要求 "左结点 < 父结点 < 右结点"。椎插入和删除元素的时间复杂度都是 O(logn)。堆常见的应用有优先级队列和堆排序。

    1. 什么是堆

    1.1 堆的定义

    堆的严格定义如下,只要满足这两点,它就是一个堆:

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

    说明: 和红黑树不同,椎并是一个部分有序队列,尤其要注意以下两点。

    1. 堆是完全二叉树,因此堆这种数据都是用数组进行存储。
    2. 对于每个节点的值都大于等于子树中每个节点值的堆,叫作大顶堆。反之则是小顶堆

    1.2 堆的常见操作

    堆的常见操作两个,分别是插入元素和删除堆顶元素:

    • 堆化(heapify):往堆中插入元素叫做堆化。堆化分为从下往上和从上往下两种堆化方法。
    • 删除堆顶元素:我们知道,堆顶元素是最小或最大元素。删除堆顶元素后,需要通过从上往下的堆化,使其重新满足堆的定义。

    (1)堆化

    我们往一个小顶堆中添加新的结点,分析从下往上是如何进行堆化。当然,你也可以使用从下往上的堆化。

    如上图所示,插入结点时从下往上是堆化为两步:

    1. 将数组最后位置添加一个新的结点,也就是 arr[size] = value。
    2. 从这个新结点和父结点依次向上比较并交换,直接重新符合堆的定义。其时间复杂度为树的高度,也就是 O(logn)。
    private int[] arr;
    private int size;
    
    // arr[0] 不存储任何元素,当然你也可以将堆整体向前移动一位
    public void add(int value) {
        if (size >= capcity) return;
        ++size;
        arr[size] = value;
        int i = size;
        while (i > 0 && arr[i / 2] > arr[i]) {
            swap(arr, i, i / 2);
            i = i / 2;
        }
    }
    

    (2)删除堆顶元素

    与插入结点时相反,删除元素时需要从上至下堆化

    需要注意的是直接和子结点比较并交换位置,可能会出现数组空洞,不符合完全二叉树的定义,如右图所示出现的数据空洞。解决方案如左所示,先将数组最后一位的结点交换到堆顶,然后再从上至下比较交换。

    1. 将数组最后结点赋值给椎顶结点,也就是 arr[1] = arr[size]。
    2. 从这个新结点和子结点依次向下比较并交换,直接重新符合堆的定义。其时间复杂度为树的高度,也就是 O(logn)。
    public int poll() {
        if (size <= 0) return -1;
        int value = arr[1];
        int i = 1;
        arr[i] = arr[size];
        arr[size] = 0;
        size--;
    
        while (true) {
            int minPos = i;
            if (size >= 2 * i && arr[minPos] > arr[2 * i]) minPos = 2 * i;
            if (size >= 2 * i + 1 && arr[minPos] > arr[2 * i + 1]) minPos = 2 * i + 1;
            if (minPos == i) break;
    
            swap(arr, i, minPos);
            i = minPos;
        }
        return value;
    }
    

    2. 堆排序

    如果我们要堆数据结构实现从小到大的排序,该怎么实现呢?我们知道将数组堆化成大顶堆后,堆顶是最大值,然后我们依次取出堆顶元素,这样取出的元素就是按从大到小的顺序,我们每次取出元素时依次放到数组最后。这样当全部取出后,就实现了从小到大的排序。堆排序分为两步:

    1. 堆化:将数组原地建成一个堆。
    2. 排序:依次取出堆顶元素与数组最后一个元素交换位置。

    2.1 堆化

    原地堆化也有两种思路:

    1. 从下往上进行堆化。和插入排序一样,将数组分为两部为:已经堆化和未堆化。依次遍历未堆化部为,将其插入到已经堆化部分。
    2. 从上往下进行堆化。遍历所有的叶子结点,将其与堆顶结点交换后从上往下进行堆化。
    // 从下往上堆化
    private static void heavify(Integer[] arr) {
        for (int i = 1; i < arr.length; i++) {
            shiftUp(i, arr);
        }
    }
    // 大顶堆:从下往上堆化
    private static void shiftUp(int i, Integer[] arr) {
        while (i > 0 && arr[i] > arr[(i - 1) / 2]) {
            swap(arr, i, (i - 1) / 2);
            i = (i - 1) / 2;
        }
    }
    
    // 从上往下堆化,对于完全二叉树而言,号子结点起始位置: "arr.length / 2 + 1"
    public void heavify(int[] arr) {
        for (int i = arr.length / 2; i >= 1; --i) {
            // 随机数据的插入和删除,堆的长度会发生变化,需要第二个参数来控制向下堆化的最大位置
            shiftDown(i, arr.length, arr[i]);
        }
    }
    // 大顶堆:从上往下堆化
    private static void shiftDown(int i, int size, Integer[] arr) {
        while (true) {
            int maxPos = i;
            if (size > 2 * i + 1 && arr[maxPos] < arr[2 * i + 1]) {
                maxPos = 2 * i + 1;
            }
            if (size > 2 * i + 2 && arr[maxPos] < arr[2 * i + 2]) {
                maxPos = 2 * i + 2;
            }
            if (maxPos == i) {
                break;
            }
            swap(arr, i, maxPos);
            i = maxPos;
        }
    }
    

    说明: 最核心的方法是向上堆化 shiftUp 和 向下堆化 shiftDown 这两个方法,可以参考 PriorityQueue 小顶堆的实现。

    2.2 排序

    排序同样是将数据分为已经排序和未排序部分。其中未排序部分是一个大顶堆,依次从大顶堆中取出最大元素,插入已经排序部分。

    public void sort(Integer[] arr) {
        heavify(arr);
        // 从大顶堆中取出最大元素,依次插入已经排序部分
        doSort(arr);
    }
    
    private void doSort(Integer[] arr) {
        int n = arr.length;
        for (int i = 1; i < n; i++) {
            swap(arr, 0, n - i);
            shiftDown(0, n - i, arr);
        }
    }
    

    2.3 三大指标

    (1)时间复杂度

    • 堆化时间复杂度:O(n)

                             层数   元素个数  时间复杂度  总时间复杂度
                ○             1      1          h-1     h-1    
            ○       ○         2      2          h-2     2(h-2)
          ○   ○   ○   ○       3      n/8        2       n/2^3 * 2
         ○ ○ ○ ○ ○ ○ ○ ○      4      n/4        1       n/2^2 * 1
                ...           h      n/2        0       0
      
      S = n/2 + 2n/4 + 3n/16 + 4n/32 + 2(h-2) + (h-1) = O(n)
      
    • 排序时间复杂度:O(nlogn)

    如果数据原本就是有序的,堆排序在堆化过程会打乱原先的顺序,再进行排序,因此,即便是完全有序数组的时间复杂度是 O(nlogn)。但对完全逆序的数组,其时间复杂度也是 O(nlogn)。总的来说,堆排序的时间复杂度非常稳定。

    (2)空间复杂度

    堆排序是原地排序。

    (3)稳定性

    在堆化的过程中,会出现非相邻元素交换,因此堆排序是非稳定排序。

    2.4 堆排序 vs 快速排序

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

    1. 堆排序对 CPU 不友好。快速排序来说局部顺序访问,而堆排序则是逐层访问。 比如,堆排序会依次访问数组下标是 1,2,4,8 的元素,而不是像快速排序那样,局部顺序访问。
    2. 堆排序算法的数据交换次数要多于快速排序。比如,有序数组建堆后,数据反而变得更无序。

    3. 优先级队列

    如 Java 中的优先级队列 PriorityQueue 和 PriorityBlockingQueue 就是使用小顶堆实现的。

    • 合并有序小文件
    • 高性能定时器
    • Top N
    • 利用堆求中位数

    每天用心记录一点点。内容也许不重要,但习惯很重要!

  • 相关阅读:
    PDF太大怎么办?缩小PDF的4种常用方法
    电脑插上网线无法连接网络完美解决方案
    HTTP攻击与防范-跨站攻击-01简介
    HTTP攻击与防范-跨网站脚本攻击
    HTTP攻击与防范-PHP客户端脚本攻击
    HTTP攻击与防护-函数注入攻击
    HTTP攻击与防范-命令注入攻击
    重置NSX防火墙策略
    NBU8.1安装
    Vcenter由Win2008r2迁移到linux
  • 原文地址:https://www.cnblogs.com/binarylei/p/10115921.html
Copyright © 2011-2022 走看看