堆结构与堆排序

堆的相关概念

1.堆结构就是用数组实现的完全二叉树结构,逻辑上是一棵完全二叉树,但物理上是保存在数组中。

2.完全二叉树中如果每棵子树的最大值都在顶部就是大根堆

3.完全二叉树中如果每棵子树的最小值都在顶部就是小根堆

顺序存储中父子节点的关系
数组模拟,从0位置开始,i为下标

父节点:(i-1)/2

左子节点:2*i+1

右子节点:2*i+2

通过i下标,和公式,就可以得出当前下标节点的父节点,与左右子节点的位置。

数组模拟,从1位置开始,i为下标

左:2*i (i << 1)

右:2*i+1 (i << 1 |1)

父:i/2 (i >> 1)

数组模拟堆,第一个是从0位置开始,第二个是从1位置开始。

因为开始位置不同,所以计算的公式也不同。但第从1位置开始,可以优化成位运算。

Java中的堆实现

public static class MyMaxHeap {
        private int[] heap;
        private final int limit;
        private int heapSize;

        public MyMaxHeap(int limit) {
            heap = new int[limit];
            this.limit = limit;
            heapSize = 0;
        }

        public boolean isEmpty() {
            return heapSize == 0;
        }

        public boolean isFull() {
            return heapSize == limit;
        }

        public void push(int value) {
            if (heapSize == limit) {
                throw new RuntimeException("heap is full");
            }
            heap[heapSize] = value;
            heapInsert(heap, heapSize++);
        }

        public int pop() {
            int ans = heap[0];
            swap(heap, 0, --heapSize);
            heapify(heap, 0, heapSize);
            return ans;
        }

        private void heapInsert(int[] arr, int index) {

            while (arr[index] > arr[(index - 1) / 2]) {
                swap(arr, index, (index - 1) / 2);
                index = (index - 1) / 2;
            }
        }

        private void heapify(int[] arr, int index, int heapSize) {
            int left = index * 2 + 1;
            while (left < heapSize) { 

                int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
                largest = arr[largest] > arr[index] ? largest : index;
                if (largest == index) {
                    break;
                }
                swap(arr, largest, index);
                index = largest;
                left = index * 2 + 1;
            }
        }

        private void swap(int[] arr, int i, int j) {
            int tmp = arr[i];
            arr[i] = arr[j];
            arr[j] = tmp;
        }

    }
堆结构两个重要方法,heapInsert与heapify
heapInsert用于构建堆

push添加操作就主要调用的heapInsert方法

public void push(int value) {
            if (heapSize == limit) {
                throw new RuntimeException("heap is full");
            }
            //既是size,也可以用作新来元素存放的位置
            heap[heapSize] = value;
            heapInsert(heap, heapSize++);
        }

因为用的数组实现堆,当数组添加了一个元素时,就会执行heapInsert操作,调整元素位置,以构建成逻辑上的堆。

heapInsert方法
private void heapInsert(int[] arr, int index) {
            // (i-1)/2 为父节点位置
            // 循环交换,直到不比父大
            // index=0
            while (arr[index] > arr[(index - 1) / 2]) {
                swap(arr, index, (index - 1) / 2);
                index = (index - 1) / 2;
            }
        }

新加进来的数,现停在了index位置。然后进行通过公式,与当前父节点循环比较,如果当前index大于此时它的父节点,就与父节点进行交换。不仅值交换了,当然index也要进行更新,变成它之前的父节点的index值。然后继续进行比较交换。

添加一个数,数在堆的逻辑中,位于完全二叉树的最底层。然后通过比较值的大小,不断往上移动,移动到正确的位置,以构成最大堆。

heapify也用于维护堆结构

pop取出数据主要调用heapify方法

public int pop() {
            int ans = heap[0];
            swap(heap, 0, --heapSize);
            heapify(heap, 0, heapSize);
            return ans;
        }

pop取出大根堆中最大的值,即数组中的第一个数,相当于完全二叉树中的根节点。

pop数据,我们需要维护堆的结构。

所以上面代码大致思路为

记录heap[0]的值,然后将根节点与最后的节点进行交换,堆的heapSize减1。

这样根节点的值不是删除,而是使用最后的根节点进行补充,而原来的根节点因为交换到了最后,而heapSize也减1,所以相当于,将其排除出了堆中。

但因为交换后根节点的值,不满足大根堆的结构,所以我们需要进行heapify,维护堆结构。

heapify方法

因为根节点的值为之前最小节点的值,所以我们可以将其,与子节点进行循环比较。

往下看,不断的下沉,循环完成,则为一个正常的堆结构。

private void heapify(int[] arr, int index, int heapSize) {
            //通过index根节点,计算出左子节点位置
            int left = index * 2 + 1;
            //有左子节点,就进入循环,进行比较。
            // 如果有左孩子,可能有或没有右孩子。所以具体的在循环内部操作
            while (left < heapSize) { 
                // 把较大孩子的下标,给largest
                // 左右子节点的比较,如果存在右子节点left + 1 < heapSize,
                // 并且右子节点大于左子节点,largest就为右子节点,否则是左
                int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;

                //最大值节点与父节点比较,最大为largest
                largest = arr[largest] > arr[index] ? largest : index;
                //largest等于index,即在上一行比较中,子节点没干过父节点index,index为最大值,堆结构正常,直接break
                if (largest == index) {
                    break;
                }
                //子节点大于父节点index,index和较大子节点,要互换
                swap(arr, largest, index);
                //更新index与left,继续比较。
                index = largest;
                left = index * 2 + 1;
            }
        }

注:优先级队列结构,就是堆结构

堆排序

堆排序时间复杂度

一个数组,进行堆排序。

数组所有元素,循环heapinsert构建堆。时间复杂度O(N*logN)

因为一共N个数,想象中的树高度为logN ,所以一次操作O(logN),循环了N次。即O(N * logN)

然后不断swap,heapSize减一,heapify。

相当于将最大数放到数组末尾,heapSize减一将其排除出堆结构,heapify维护堆结构

        int heapSize = arr.length;
        swap(arr, 0, --heapSize);
        // O(N*logN)
        while (heapSize > 0) { // O(N)
            heapify(arr, 0, heapSize); // O(logN)
            swap(arr, 0, --heapSize); // O(1)
        }

此操作也是,O(N * logN)

所有整个sort操作有2次O(N * logN),所以堆排序的时间复杂度为O(N * logN)

堆排序的优化

一个需求,只需要将数组形成堆结构,不需要排序,可以有O(N)的时间复杂度。

上面构建堆,是循环heapInsert,时间复杂度O(N*logN)

而我们可以从数组尾部倒序开始,每个节点heapify。

此操作只能用于一次得到所有数据,然后构建堆。如果是一个一个得到数据,则不适用

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

==为什么是O(N)复杂度?==

可能有人会疑惑,为什么堆排序中,我的一次heapify是 O(logN)。循环N次O(N * logN)

但构建堆时,同样的heapify,进行N次,怎么变成O(N)了。

因为堆排序中,每次的heapify都是完整都走完树的高度,logN。循环N次,即O(N * logN)

但我构建堆时

从最后一层(N/2个节点)开始heapify,但无法往下沉,进行一次操作,复杂度N/2*1

倒数第二层(N/4个节点),heapify,最多往下沉1次,所以2次操作,复杂度N/4*2

倒数第三层(N/8个节点),heapify,最大往下沉2次,所以3次操作,复杂度N/8*3

依次类推

可以参考知乎的回答

堆排序中建堆过程时间复杂度O(n)怎么来的? - TwoFrogs的回答 - 知乎 https://www.zhihu.com/question/20729324/answer/16025846

堆排序代码与对数器
public class Code03_HeapSort {

    // 堆排序额外空间复杂度O(1)
    public static void heapSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        // O(N*logN)
        for (int i = 0; i < arr.length; i++) { // O(N)
            heapInsert(arr, i); // O(logN)
        }
        // O(N)

        // for (int i = arr.length - 1; i >= 0; i--) {
        //     heapify(arr, i, arr.length);
        // }
        int heapSize = arr.length;
        swap(arr, 0, --heapSize);
        // O(N*logN)
        while (heapSize > 0) { // O(N)
            heapify(arr, 0, heapSize); // O(logN)
            swap(arr, 0, --heapSize); // O(1)
        }
    }

    // arr[index]刚来的数,往上
    public static void heapInsert(int[] arr, int index) {
        while (arr[index] > arr[(index - 1) / 2]) {
            swap(arr, index, (index - 1) / 2);
            index = (index - 1) / 2;
        }
    }

    // arr[index]位置的数,能否往下移动
    public static void heapify(int[] arr, int index, int heapSize) {
        int left = index * 2 + 1; // 左孩子的下标
        while (left < heapSize) { // 下方还有孩子的时候
            // 两个孩子中,谁的值大,把下标给largest
            // 1)只有左孩子,left -> largest
            // 2) 同时有左孩子和右孩子,右孩子的值<= 左孩子的值,left -> largest
            // 3) 同时有左孩子和右孩子并且右孩子的值> 左孩子的值, right -> largest
            int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
            // 父和较大的孩子之间,谁的值大,把下标给largest
            largest = arr[largest] > arr[index] ? largest : index;
            if (largest == index) {
                break;
            }
            swap(arr, largest, index);
            index = largest;
            left = index * 2 + 1;
        }
    }

    public static void swap(int[] arr, int i, int j) {
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }

    // for test
    public static void comparator(int[] arr) {
        Arrays.sort(arr);
    }

    // for test
    public static int[] generateRandomArray(int maxSize, int maxValue) {
        int[] arr = new int[(int) ((maxSize + 1) * Math.random())];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random());
        }
        return arr;
    }

    // for test
    public static int[] copyArray(int[] arr) {
        if (arr == null) {
            return null;
        }
        int[] res = new int[arr.length];
        for (int i = 0; i < arr.length; i++) {
            res[i] = arr[i];
        }
        return res;
    }

    // for test
    public static boolean isEqual(int[] arr1, int[] arr2) {
        if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) {
            return false;
        }
        if (arr1 == null && arr2 == null) {
            return true;
        }
        if (arr1.length != arr2.length) {
            return false;
        }
        for (int i = 0; i < arr1.length; i++) {
            if (arr1[i] != arr2[i]) {
                return false;
            }
        }
        return true;
    }

    // for test
    public static void printArray(int[] arr) {
        if (arr == null) {
            return;
        }
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + " ");
        }
        System.out.println();
    }

    // for test
    public static void main(String[] args) {

        // 默认小根堆
        PriorityQueue<Integer> heap = new PriorityQueue<>();
        heap.add(6);
        heap.add(8);
        heap.add(0);
        heap.add(2);
        heap.add(9);
        heap.add(1);

        while (!heap.isEmpty()) {
            System.out.println(heap.poll());
        }

        int testTime = 500000;
        int maxSize = 100;
        int maxValue = 100;
        boolean succeed = true;
        for (int i = 0; i < testTime; i++) {
            int[] arr1 = generateRandomArray(maxSize, maxValue);
            int[] arr2 = copyArray(arr1);
            heapSort(arr1);
            comparator(arr2);
            if (!isEqual(arr1, arr2)) {
                succeed = false;
                break;
            }
        }
        System.out.println(succeed ? "Nice!" : "Fucking fucked!");

        int[] arr = generateRandomArray(maxSize, maxValue);
        printArray(arr);
        heapSort(arr);
        printArray(arr);
    }

}

本文由博客一文多发平台 OpenWrite 发布!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值