排序(七):堆排序

一. 堆是什么
堆树的定义如下:
(1)堆树是一颗完全二叉树;
完全二叉树:除了最后一层,其他层的节点个数都是最大值,即 2^N,N为层数,根节点为0;且最后一层自左向右的节点是连续的
(2)堆树中某个节点的值总是不大于或不小于其孩子节点的值;
(3)堆树中每个节点的子树都是堆树。
当父节点的键值总是大于或等于任何一个子节点的键值时为最大堆。
当父节点的键值总是小于或等于任何一个子节点的键值时为最小堆。

    堆可以用数组来表示,因为堆是完全二叉树,而完全二叉树很容易就存储在数组中。位置 k 的节点的父节点位置为 k/2,而它的两个子节点的位置分别为 2k 和 2k+1。这里不使用数组索引为 0 的位置,是为了更清晰地描述节点的位置关系。

package com.hong.heap;

import com.hong.arrays.Array;

public class MaxHeap<E extends Comparable<E>> {

    private Array<E> data;

    public MaxHeap(int capacity) {
        data = new Array<>(capacity);
    }

    public MaxHeap() {
        data = new Array<>();
    }

    /**
     * 将任意数组转化成堆的形式 heapify
     * 两种方式:
     * 1.向堆中一个一个的添加元素,时间复杂度O(NlongN) ,N为元素个数
     * 2.从第一个非叶子节点开始,进行siftDown,时间复杂度 O(N)
     *
     * @param arr
     */
    public MaxHeap(E[] arr) {
        /**
         *  由最后一个元素的索引推导出第一个非叶子节点的索引,然后开始向前一个一个的进行siftDown()
         *  这样,就不需要像第一种方式一样,需要遍历每个元素
         */
        data = new Array<>(arr);
        for (int i = parent(arr.length - 1); i >= 0; i--) {
            siftDown(i);
        }
    }

    /**
     * 返回堆中元素的个数
     *
     * @return
     */
    public int size() {
        return data.getSize();
    }

    public boolean isEmpty() {
        return data.isEmpty();
    }

    /**
     * 返回完全二叉树的数组表示中,index索引位置的元素的父元素在数组中的索引
     *
     * @param index
     * @return
     */
    private int parent(int index) {
        if (index == 0) {
            throw new IllegalArgumentException("index-0 doesn't have parent.");
        }
        return (index - 1) / 2;
    }

    /**
     * 返回完全二叉树的数组表示中,index索引位置的元素的左孩子在数组中的索引
     *
     * @param index
     * @return
     */
    private int leftChild(int index) {
        return index * 2 + 1;
    }

    /**
     * 返回完全二叉树的数组表示中,index索引位置的元素的右孩子在数组中的索引
     *
     * @param index
     * @return
     */
    private int rightChild(int index) {
        return index * 2 + 2;
    }

    /**
     * 向堆中添加元素
     * 在堆的最后增加一个结点,然后沿这堆树上升.
     * 将新元素放到数组末尾,然后上浮到合适的位置。
     * @param e
     */
    public void add(E e) {
        //尾插法
        data.addLast(e);
        siftUp(data.getSize() - 1);
    }

    /**
     * 向上筛选,找到新插入元素的正确位置
     * 在堆中,当一个节点比父节点大,那么需要交换这个两个节点。
     * 交换后还可能比它新的父节点大,因此需要不断地进行比较和交换操作,把这种操作称为上浮(ShiftUp)。
     *
     * @param k
     */
    private void siftUp(int k) {
       /* while (k > 0 && data.get(parent(k)).compareTo(data.get(k)) < 0) {
            data.swap(k, parent(k));
            k = parent(k);
        }*/
        /**
         * siftUp优化:
         * 在上面的代码中,每次比较当前节点和其父节点的值,符合条件则交换
         * 这中间的交换过程是可以优化的,我们的目的是最终将k位置的元素与沿着
         * 其父节点的路径上找到第一个比k位置元素大的节点时或者到达根节点时则停止比较.
         * 参考Java的PriorityQueue的实现。
         */
        E cur = data.get(k);
        while (k > 0){
            int parentIndex = parent(k);
            E parent = data.get(parentIndex);
            if (cur.compareTo(parent) <= 0){
                break;
            }
            data.set(k,parent);
            k = parentIndex;
        }
        data.set(k,cur);
    }

    /**
     * 查看堆中的最大元素
     *
     * @return
     */
    public E findMax() {
        if (data.getSize() == 0) {
            throw new IllegalArgumentException("Can not findMax when heap is empty.");
        }

        return data.get(0);
    }

    /**
     * 取出堆中的最大元素
     *
     * @return
     */
    public E extractMax() {
        E max = findMax();
        //将最后一个元素先放到第一个位置,然后数据下沉筛选,找到在堆中的正确位置
        data.swap(0, data.getSize() - 1);
        data.removeLast();
        siftDown(0);
        return max;
    }

    /**
     * 向下调整k位置的元素
     * 当前k位置的元素与 k位置元素的左右孩子中较大的值比较,
     * 如果 > 较大值,则说明元素已经在正确的位置了,终止循环;
     * 否则互换,从较大值位置继续上面的逻辑
     *
     * 类似地,当一个节点比子节点来得小,也需要不断地向下进行比较和交换操作,把这种操作称为下沉(Shift Down)。
     * 一个节点如果有两个子节点,应当与两个子节点中最大那么节点进行交换。
     * @param k
     */
    private void siftDown(int k) {
        // 当前节点是非叶子节点
        while (leftChild(k) < data.getSize()) {
            int j = leftChild(k);
            // 如果也存在右孩子,则取出左右孩子较大值索引
            if ((j + 1) < data.getSize() &&
                    data.get(j + 1).compareTo(data.get(j)) > 0) {
                j = j + 1;
            }

            // data[j] 是 leftChild 和 rightChild 中的最大值
            if (data.get(k).compareTo(data.get(j)) >= 0) {
                break;
            }

            data.swap(j, k);
            k = j;
        }
    }

    /**
     * 从堆中取出最大元素,替换成元素e
     *
     * @param e
     * @return
     */
    public E replace(E e) {
        E ret = findMax();
        data.set(0, e);
        siftDown(0);
        return ret;
    }
}
package com.hong.arrays;

/**
 * @author wanghong
 * @date 2019/04/06 17:04
 * 封装自定义数组
 **/
public class Array<T> {
    private T[] data;
    private int size;

    public Array() {
        this(10);
    }

    public Array(int capacity) {
        data = (T[]) new Object[capacity];
        size = 0;
    }

    public Array(T[] arr) {
        data = (T[]) new Object[arr.length];
        for (int i = 0; i < arr.length; i++) {
            data[i] = arr[i];
        }
        size = arr.length;
    }

    public int getCapacity() {
        return data.length;
    }

    // 获取数组中的元素个数
    public int getSize() {
        return size;
    }

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

    /**
     * 头插法
     *
     * @param t
     */
    public void addFirst(T t) {
        add(0, t);
    }

    /**
     * 尾插法
     *
     * @param t
     */
    public void addLast(T t) {
        add(size, t);
    }

    /**
     * 向index索引处插入新元素
     *
     * @param index
     * @param t
     */
    public void add(int index, T t) {
        // 判断要插入的索引是否越界,即要插入的索引位置在[0,size]的区间内
        if (index < 0 || index > size) {
            throw new IllegalArgumentException("Add failed,Require index > 0 and index <= size");
        }

        // 判断当前数组容量是否已满,满了自动扩容
        if (size == data.length) {
            // throw new IllegalArgumentException("Add failed,Array is full");
            resize(2 * data.length);
        }

        // 将 索引 >= index 位置的元素依次向后挪一位,腾出index的位置放入新元素,这里从最后一个元素开始挪动
        for (int i = size - 1; i >= index; i--) {
            data[i + 1] = data[i];
        }

        data[index] = t;
        size++;
    }

    /**
     * 获取index索引处的元素
     *
     * @param index
     * @return
     */
    public T get(int index) {
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("Get failed. Index is illegal.");
        }

        return data[index];
    }

    public T getLast() {
        return get(size - 1);
    }

    public T getFirst() {
        return get(0);
    }

    /**
     * 修改index索引位置的元素为t
     *
     * @param index
     * @param t
     */
    public void set(int index, T t) {
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("Set failed. Index is illegal.");
        }

        data[index] = t;
    }

    /**
     * 判断数组中是否包含元素t
     *
     * @param t
     * @return
     */
    public boolean contains(T t) {
        for (int i = 0; i < size; i++) {
            if (data[i].equals(t)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 查找数组中元素t的索引,无则返回-1
     *
     * @param t
     * @return
     */
    public int find(T t) {
        for (int i = 0; i < size; i++) {
            if (data[i].equals(t)) {
                return i;
            }
        }
        return -1;
    }

    /**
     * 删除第一个元素
     *
     * @return
     */
    public T removeFirst() {
        return remove(0);
    }

    /**
     * 删除最后一个元素
     *
     * @return
     */
    public T removeLast() {
        return remove(size - 1);
    }

    /**
     * 从数组中删除指定的元素
     *
     * @param t
     */
    public void removeElement(T t) {
        int index = find(t);
        if (index != -1) {
            remove(index);
        }
    }

    /**
     * 从数组中删除index位置处的元素并返回被删除的元素
     *
     * @param index
     * @return
     */
    public T remove(int index) {
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("Remove failed. Index is illegal.");
        }

        T t = data[index];
        for (int i = index + 1; i < size; i++) {
            data[i - 1] = data[i];
        }
        size--;
        data[size] = null; // loitering objects != memory leak

        /**
         *  删除元素后,如果剩下的容量到了一半则缩容
         *  为了防止复杂度震荡,即出现这样的操作:
         *   addLast -> 超过data.length -> resize -> removeLast -> resize
         *   反复几次,会造成复杂度突然上升,性能下降
         *   解决方案:Lazy
         */
        //  if (size == data.length / 2) {
        if (size == data.length / 4 && data.length / 2 != 0) {
            resize(data.length / 2);
        }

        return t;
    }

    @Override
    public String toString() {
        StringBuilder res = new StringBuilder();
        res.append(String.format("Array:size = %d,capacity = %d\n", size, data.length));
        res.append("[");
        for (int i = 0; i < size; i++) {
            res.append(data[i]);
            if (i != size - 1) {
                res.append(",");
            }
        }
        res.append("]");

        return res.toString();
    }

    /**
     * 将数组扩容为newCapacity大小
     *
     * @param newCapacity
     */
    private void resize(int newCapacity) {
        T[] newData = (T[]) new Object[newCapacity];
        for (int i = 0; i < size; i++) {
            newData[i] = data[i];
        }
        data = newData;
    }

    /**
     * 互换i,j位置的两个元素
     *
     * @param i
     * @param j
     */
    public void swap(int i, int j) {
        if (i < 0 || i >= size || j < 0 || j >= size)
            throw new IllegalArgumentException("Index is illegal.");

        T t = data[i];
        data[i] = data[j];
        data[j] = t;
    }
}

二. 堆排序
    由于堆可以很容易得到最大的元素并删除它,不断地进行这种操作可以得到一个递减序列。如果把最大元素和当前堆中数组的最后一个元素交换位置,并且不删除它,那么就可以得到一个从尾到头的递减序列,从正向来看就是一个递增序列。因此很容易使用堆来进行排序。并且堆排序是原地排序,不占用额外空间。

  • 版本一:原地堆排序
package com.hong.heap;

import com.hong.sort.SortTestHelper;

/**
 * @author wanghong
 * @date 2019/11/24 22:18
 * 不使用一个额外的最大堆, 直接在原数组上进行原地的堆排序
 **/
public class HeapSort {

    // 我们的算法类不允许产生任何实例
    private HeapSort(){}

    public static void sort(Integer[] arr){
        int n = arr.length;
        // 注意,此时我们的堆是从0开始索引的
        // 从(最后一个元素的索引-1)/2开始,即第一个 non-leaf node
        // 最后一个元素的索引 = n-1
        for( int i = (n-1-1)/2 ; i >= 0 ; i -- ){
            shiftDown2(arr, n, i);
        }

        for( int i = n-1; i > 0 ; i-- ){
            swap( arr, 0, i);
            shiftDown2(arr, i, 0);
        }
    }

    // 交换堆中索引为i和j的两个元素
    private static void swap(Object[] arr, int i, int j){
        Object t = arr[i];
        arr[i] = arr[j];
        arr[j] = t;
    }

    // 原始的shiftDown过程
    private static void shiftDown(Comparable[] arr, int n, int k){
        while( 2*k+1 < n ){
            int j = 2*k+1;
            if( j+1 < n && arr[j+1].compareTo(arr[j]) > 0 )
                j += 1;

            if( arr[k].compareTo(arr[j]) >= 0 )break;

            swap( arr, k, j);
            k = j;
        }
    }

    // 优化的shiftDown过程, 使用赋值的方式取代不断的swap,
    // 该优化思想和我们之前对插入排序进行优化的思路是一致的
    private static void shiftDown2(Comparable[] arr, int n, int k){
        Comparable e = arr[k];
        while( 2*k+1 < n ){
            int j = 2*k+1;
            if( j+1 < n && arr[j+1].compareTo(arr[j]) > 0 ){
                j += 1;
            }

            if( e.compareTo(arr[j]) >= 0 ){
                break;
            }

            arr[k] = arr[j];
            k = j;
        }

        arr[k] = e;
    }

    // 测试 HeapSort
    public static void main(String[] args) {
        int N = 1000000;
        Integer[] arr = SortTestHelper.generateRandomArray2(N, 0, 100000);
        SortTestHelper.testSort2("com.hong.heap.HeapSort", arr);
    }
}
  • 版本二
package com.hong.heap;

import com.hong.sort.SortTestHelper;

/**
 * @author wanghong
 * @date 2019/11/24 21:50
 *  基础堆排序和Heapify(堆化)
 **/
public class HeapSort1 {

    // 对整个arr数组使用HeapSort1排序
    // HeapSort1, 将所有的元素依次添加到堆中, 在将所有元素从堆中依次取出来, 即完成了排序
    // 无论是创建堆的过程, 还是从堆中依次取出元素的过程, 时间复杂度均为O(nlogn)
    // 整个堆排序的整体时间复杂度为O(nlogn)
    public static void sort(int[] arr){
        int n = arr.length;
        MaxHeap<Integer> maxHeap = new MaxHeap<>(n);
        for (int i = 0;i < n;i++){
            maxHeap.add(arr[i]);
        }
        for (int i = n-1;i>=0;i--){
            arr[i] = maxHeap.extractMax();
        }
    }

    // 测试 HeapSort1
    public static void main(String[] args) {
        int N = 1000000;
        int[] arr = SortTestHelper.generateRandomArray(N, 0, 100000);
        SortTestHelper.testSort("com.hong.heap.HeapSort1", arr);
    }
}
  • 版本三
package com.hong.heap;

import com.hong.sort.SortTestHelper;

/**
 * @author wanghong
 * @date 2019/11/24 21:57
 *  优化的堆排序
 **/
public class HeapSort2 {

    // 对整个arr数组使用HeapSort2排序
    // HeapSort2, 借助我们的heapify过程创建堆
    // 此时, 创建堆的过程时间复杂度为O(n), 将所有元素依次从堆中取出来, 时间复杂度为O(nlogn)
    // 堆排序的总体时间复杂度依然是O(nlogn), 但是比HeapSort1性能更优, 因为创建堆的性能更优
    public static void sort(Integer[] arr){
        MaxHeap<Integer>  maxHeap = new MaxHeap<>(arr);
        for (int i = arr.length-1;i >= 0;i--){
            arr[i] = maxHeap.extractMax();
        }
    }

    // 测试 HeapSort2
    public static void main(String[] args) {
        int N = 1000000;
        Integer[] arr = SortTestHelper.generateRandomArray2(N, 0, 100000);
        SortTestHelper.testSort2("com.hong.heap.HeapSort2", arr);
    }
}

三. 堆排序的应用——Top K问题
在N个元素中选出前M个元素
M远小于N
排序: NlongN
优先队列 NlogM 维护当前看到的前M个元素

四. 排序算法总结
在这里插入图片描述
稳定排序:对于相等的元素,在排序后,原来靠前的元素依然靠前。相等元素的相对位置没有发生变化。

/ 可以通过⾃自定义⽐比较函数,让排序算法不不存在稳定性的问题。
boolean operator<(const Student& otherStudent){
    return score != otherStudent.score ?
    score > otherStudent.score :
    name < otherStudent.name;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值