堆结构及堆排序详解

一、物理结构和概念结构 

  学习堆必须明确,堆有两个结构,一个是真实存在的物理结构,一个是有助于理解的概念结构。

  1. 堆一般由数组实现,但是我们平时在理解堆的时候,会把他构建成一个完全二叉树结构。堆分为大根堆和小根堆:大根堆,就是这颗树里的每一个结点都是以它为根结点的树中的最大值;小根堆则与之相反。

  

   (注意一定要是完全二叉树)

   2. 物理结构:从 0 开始的数组。

   怎么将数组和二叉树联系起来呢?

  当一个结点在数组中的下标为 index,那么这个结点对应的父节点的下标为 ( index-1 ) / 2左孩子的下标为  2 * index +1 右孩子的下标为  2 * index +2 。

  上面是以 0 开始的数组中各结点对应的关系,数组也可以以 1 开始,此时父节点下标为  index / 2,左孩子下标为 2 * index右孩子下标为 2 * index + 1。

  有一个物理数组下标从 0 - 8

  

   树结构为:

  

 二、heapInsert

  当数组中 0 ~ index -1 的位置已经是大根堆,现在添加一个元素到下标为 index ,需要怎么做才能继续保持大根堆的结构呢?

  1. 将新增元素index 与 父节点 ( index-1 ) / 2  比较,若比父节点大,则与父节点交换位置;

  2. 交换位置后,新增元素下标变为 父节点的下标,再与现在这个节点的父节点比较,周而复始;

  3. 直至 新增节点不再比父节点大或者已经到达了根结点,则新增节点的插入位置确定

  例子:现在有一个已经在 0 ~ 7 形成大根堆的数组 [ 24, 18, 20, 10, 9, 17, 8, 5 ] ,在下标为 8 的位置插入元素 22.

  

   JAVA 实现:

    public static void heapInsert(int[] arr, int index) {
        // 停止条件1:新增结点不再比父节点大
        // 停止条件2:已经到达了整棵树的 根结点 0 ,当 index = 0,( 0-1)/2 =0,所以arr[index] 和 arr[(index - 1) / 2] 相等
        while (arr[index] > arr[(index - 1) / 2]) {
            swap(arr, index, (index - 1) / 2);
            index = (index - 1) / 2;
        }
    }
    public static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

 三、heapify

  当将最大值 pop 出去之后,需要对这个堆进行调整,最常用的就是,将堆结构中最后一个的数提到 0 下标,然后将这个数从 0 开始下沉。

  某个数在 index 位置,看是否可以往下沉。这就是 heapify。

  当index 还有孩子节点时,比较左右两个节点的大小,选取节点值较大的一个,与index进行比较 ,若 子节点的值较大,父节点下沉,较大孩子上来。直至比孩子节点大或者没有孩子节点。

  

   JAVA 实现:

    public static void heapify(int[] arr, int index, int heapSize) {
        int left = index * 2 + 1;// 左孩子的下标
        while (left < heapSize) {// 下方还有孩子的时候
            // 两个孩子中,谁的值大,把下标给 largest变量
            int largest = (left + 1 < heapSize) && (arr[left + 1] > arr[left]) ? left + 1 : left;
            // 父与较大的孩子之间,谁的值大,吧下标给 largest
            largest = arr[largest] > arr[index] ? largest : index;
            if (index == largest) {
                break;
            }
            swap(arr, index, largest);
            index = largest;
            left = index * 2 + 1;
        }
    }

 

四、堆排序(非递减)

  堆排序一共分为两步,第一步是将数组构建成一个大根堆,第二步将堆结构中的根结点,也就是最大值,移到堆的最后,然后将这个结点从堆中移除。

  1. 将数组构建成一个大根堆。有两种方法,但是这两种方法会有不同的事件复杂度。

  (1)使用 heapInsert。index 从 1 开始(因为从 0 开始的话,只有一个元素,没有必要),直至最后一个元素 arr.length -1 ,每个元素都使用 heapInsert 的方式,逐个形成 [ 0,..., index] 的大根堆.

for (int index = 1; index < arr.length; index++) {// O(N)
    heapInsert(arr, index);// O(logN)
}

  每个元素最多向上进行 log2(N)次比较和交换,一共有 N 各元素,所以需要 O(NlogN) 的事件复杂度。

  这种方法比较适合逐个向数组中添加元素,但是此时排序,传进来的是整个数组,所以我们有一种可以将事件复杂度降低为 O(N) 的方式。

  (2)使用 heapify。从最后一个元素开始,不断下沉,使得以这个元素为根结点的数形成堆结构。

  

   为什么说事件复杂度为 O(N) 呢?

  

   Java 实现:

for (int index = arr.length - 1; index >= 0; index--) {
    heapify(arr, index, arr.length);
}

 

  2. 将堆结构中的根结点,也就是最大值,移到堆的最后,然后将这个结点从堆中移除。

  将堆中最大值逐个放在 arr.length-1,arr.length-2,...,1 的位置。使用 heapSize 记录堆中元素得个数,将堆中最大值往后,其实就是和堆中最后一个元素(下标为 heapSIze-1)交换位置,此时最后一个元素到达根结点 0 。交换完成后 heapSize--,意味着这个元素已经排序完成并将它从堆中移除,再调用 heapify 将提到根结点 0 得元素下沉。事件复杂度为 O(logN)

  int heapSize = arr.length;// 记录堆中元素的个数,如果一个数排序完成(放在堆数组的最后),就将他从堆数组中除去(heapSize--);
  swap(arr, 0, --heapSize);// 将堆中的最大值放在数组的最后,此时最大值排序完成,堆数组的个数减一
  while (heapSize > 0) {// O(N)
        heapify(arr, 0, heapSize);// O(logN)
        swap(arr, 0, --heapSize);// O(1)
    }

  形成堆结构,我们采用第二种方法,所以最后堆排序的代码为:

    // 堆排序
    public static void heapSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        // 形成大根堆:
        // O(N)
        for (int index = arr.length - 1; index >= 0; index--) {
            heapify(arr, index, arr.length);
        }
// 将堆中最大值逐个放在 arr.length-1,arr.length-2,...,1 的位置 // O(N*logN) int heapSize = arr.length; swap(arr, 0, --heapSize); while (heapSize > 0) {// O(N) heapify(arr, 0, heapSize);// O(logN) swap(arr, 0, --heapSize);// O(1) } }

 

posted @ 2020-04-23 18:22  葡萄籽pp  阅读(1462)  评论(0编辑  收藏  举报