4-排序之优先队列与堆排序

优先队列

  • 堆有序定义:当一颗二叉树的每个节点都大于等于(或小于等于)它的两个子节点时,它被称为堆有序。
  • 堆有序原理:根节点是堆有序的二叉树的最大节点(最小节点)。
  • 二叉堆表示法:堆有序的二叉树使用完全二叉树来表示,由于使用了完全二叉树,使得我们可以用数组来存储二叉堆,而数组的索引代表节点的位置,即按照层级顺序放入数组,根节点在索引1(第一个位置不使用),两个子节点在索引2,3…
  • 二叉堆定义:二叉堆是一组能够用堆有序的完全二叉树排序的元素,并在数组中按照层级存储(不使用数组中的第一个元素)。
  • 优先队列:基于数据结构——二叉堆(简称为堆)实现。
  • 定理:一颗具有N个节点的完全二叉树的高度为$\lfloor lgN \rfloor$(规定:只有根节点的树高度为0)。
  • 复杂度:对于一个含有N个元素的基于堆的优先队列,插入元素操作只需不超过(lgN+1)次比较,删除最大(最小)元素的操作不超过2lgN次比较。

Show me the code:

/**
 * 可以动态扩容的优先队列
 * @param <T>
 */
public class PriorityQueue<T extends Comparable<T>> {
    private T[] pQueue; // 基于堆的完全二叉树,后面实现了数组的动态扩容,使数组的内存空间既不浪费又不至于紧张
    private int N = 0;  // N为当前索引值-1,存储于pQueue[1,N]之中,pQueue[0]没有使用,也代表数组中现存的元素个数
    private int maxN;   // maxN为给定数组的有效长度(数组实际长度-1)

    public PriorityQueue(int n) {
        this.maxN = n;
        pQueue = (T[]) new Comparable[maxN+1]; // 不允许创建泛型数组,必须要转型
    }

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

    public int size() {
        return N;
    }

    // 插入
    public void insert(T value) {
        int usedLength = N+1;
        if (usedLength >= maxN*3/4) // 大于数组长度的3/4,扩容成两倍
            resize(maxN*2);
        pQueue[++N] = value; // 执行插入操作;同时更新N,注意这里不能用N++
        swim(N); // 从堆底加入
    }

    // 删除最大值
    public T delMax() {
        if (pQueue[1] == null) // 已为空,不执行任何操作
            return null;
        T max = pQueue[1];
        exch(1,N--);      // 把最后一个元素放在顶端,然后N--(堆的大小-1)
        sink(1);         // 让“最后”一个元素下沉
        pQueue[N+1] = null; // 将垃圾(删除的最大值)清空
        if (N <= maxN/4)   // 小于数组长度的1/4,缩容成原来的一半
            resize(maxN/2);
        return max;
    }

    // 扩容 或者 缩容
    private void resize(int newLength) {
        this.maxN = newLength;
        T[] newPQueue = (T[]) new Comparable[maxN+1];
        for (int i = 1; i <= N; i++) // 注意这里是 1-N
            newPQueue[i] = pQueue[i];
        pQueue = newPQueue;
    }

    // 用于堆实现的比较方法:返回pQueue[i]是否小于pQueue[j]
    private boolean less(int i, int j) {
        return pQueue[i].compareTo(pQueue[j]) < 0;
    }

    // 用于堆实现的交换方法:交换pQueue[i] 和 pQueue[j]
    private void exch(int i, int j) {
        T temp = pQueue[i];
        pQueue[i] = pQueue[j];
        pQueue[j] = temp;
    }

    // 上浮:当某个节点变大(或在堆底加入了一个新的元素),主要用于插入
    private void swim(int k) {
        while(k > 1 && less(k/2,k)) {
            exch(k/2,k); // k/2默认向下取整
            k = k/2;
        }
    }

    // 下沉:当某个节点变小(例如,将根节点替换为一个较小的元素),主要用于删除最大值
    private void sink(int k) {
        while (2*k <= N) {
            int j = 2*k;
            if (j < N && less(j,j+1)) // 找到较大的子节点,并将j指向它
                j++;
            if (!less(k,j)) // 此时j一定指向较大的子节点,如果pQueue[k] >= pQueue[j],则下沉结束
                break;
            exch(k,j); // 如果没有break则说明pQueue[k] < pQueue[j],交换pQueue[k]、pQueue[j]
            k = j;     // 交换k、j,让k始终指向下沉的元素
        }
    }


    @Override
    public String toString() {
        return "PriorityQueue " +
                Arrays.toString(pQueue) +
                ", UsedSize=" + N +
                ", ArraySize=" + maxN;
    }
}

class TestPriorityQueue {
    public static void main(String[] args) {
        PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(10);

        // test 扩容
        for (int i = 0; i < 10; i++)
            priorityQueue.insert(i);
        System.out.println(priorityQueue);

        // test 缩容
        priorityQueue.delMax();
        priorityQueue.delMax();
        priorityQueue.delMax();
        priorityQueue.delMax();
        priorityQueue.delMax();
        priorityQueue.delMax();
        System.out.println(priorityQueue);

    }
}

/* Output:
PriorityQueue [null, 9, 8, 5, 6, 7, 1, 4, 0, 3, 2, null, null, null, null, null, null, null, null, null, null], UsedSize=10, ArraySize=20
PriorityQueue [null, 3, 2, 1, 0, null, null, null, null, null, null], UsedSize=4, ArraySize=10
 */

索引优先队列

讲解参考:https://www.cnblogs.com/nullzx/p/6624731.html

Show me the code:

import java.util.Arrays;

/**
 * 索引优先队列:优先队列只能操作队头和队尾的元素,而索引优先队列可以更新队列中任意位置的值,实现原理就是通过两个索引,一个正向索引,一个反向索引。
 */

public class IndexPriorityQueue<T extends Comparable<T>> {
    /*示例:按照字母排序
     Index     0       1     2        3     4     5        6     7        8     9        10    11
    indexPq    null    10    3        6     1     4        8     null     null  null     null  null
    reIndexQp  null    4     null     2     5     null     3     null     6     null     1     null
    elements   null    k     null     f     n     null     c     null     h     null     b     null
     */
     
    // 存储有优先级之分的元素(对象引用),不一定连续存放
    private T[] elements;
    // 索引二叉堆,从索引1开始按优先级(大小顺序)存储elements元素的下标,连续存放,即indexPq里存储的下标所对应的elements数组的元素才是真正有序的
    private int[] indexPq;
    // 反向索引:reIndexQp[indexPq[i]] = indexPq[reIndexQp[i]] = i,
    // 作用是存储元素在elements数组中的索引值在index数组中的下标,这个数组也不是连续存放的,和elements数组对齐
    private int[] reIndexQp;
    private int N = 0;       // elements数组中现存的元素个数

    public IndexPriorityQueue(int maxN) {
        elements = (T[]) new Comparable[maxN + 1]; // 不允许创建泛型数组,必须要转型
        indexPq = new int[maxN + 1];
        reIndexQp = new int[maxN +1];
        for (int i = 0; i <= maxN; i++)
            reIndexQp[i] = -1;
    }

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

    public int size() {
        return N;
    }

    public boolean contains(int k) {
        return reIndexQp[k] != -1;
    }

    // 插入:在k位置插入元素,位置k并不代表任何含义,只是存储在elements数组的索引位置
    public void insert(int k, T value) {
        N++;
        elements[k] = value; // 放入索引k
        indexPq[N] = k;      // 记录此元素所在索引位置(k)
        reIndexQp[k] = N;    // 记录indexPq数组中哪个位置(N)存储着此元素的索引
        swim(N);             // 从堆底加入并上浮,维护indexPq 和 reIndexQp
    }

    public T max() {
        return elements[indexPq[1]];
    }

    public int maxIndex() {
        return indexPq[1];
    }

    // 删除最大值,并返回其索引
    public int delMax() {
        int indexOfMax = indexPq[1];
        if (elements[indexOfMax] == null) // 已为空,返回-1
            return -1;
        exch(1,N--);                 // 把最后一个元素(最小元素)放在顶端,然后N--(堆的大小-1)
        sink(1);                    // 让“最后”一个元素下沉
        elements[indexPq[N+1]] = null; // 将垃圾(删除的最大值)清空
        reIndexQp[indexPq[N+1]] = -1;  // 更新对应reIndexQp为-1
        indexPq[N+1] = 0;              // 更新最后一位删除的indexPq为0
        return indexOfMax;
    }

    // 删除索引k位置的元素,与删除最大值类似
    public void delete(int k) {
        int indexOfPq = reIndexQp[k];
        exch(indexOfPq,N--);
        swim(indexOfPq);
        sink(indexOfPq);
        elements[k] = null;
        reIndexQp[k] = -1;
        indexPq[N+1] = 0;
    }

    // 更新值
    public void change(int k, T newValue) {
        elements[k] = newValue;
        // 更新值后,可能出现三种情况:
        //    1. 比父节点大:需要上浮
        //    2. 比子节点小:需要下沉
        //    3. 大小在父节点和子节点之间:不执行任何操作
        // 所以此处采取的策略是先上浮在下沉(或先下沉再上浮)
        swim(reIndexQp[k]); // 上浮
        sink(reIndexQp[k]); // 下沉
    }

    // 用于堆实现的比较方法:这里怎么设计关乎着是大堆顶(<0)还是小堆顶(>0)
    private boolean less(int i, int j) {
        return elements[indexPq[i]].compareTo(elements[indexPq[j]]) < 0;
    }

    // 用于堆实现的交换方法:交换indexPq[i]、indexPq[j] 和 reIndexPq[i]、reIndexPq[j]
    private void exch(int i, int j) {
        int tempPq = indexPq[i];
        indexPq[i] = indexPq[j];
        indexPq[j] = tempPq;
        reIndexQp[indexPq[i]] = i;
        reIndexQp[indexPq[j]] = j;
    }

    // 上浮:当某个节点变大(或在堆底加入了一个新的元素),主要用于插入
    private void swim(int k) {
        while(k > 1 && less(k/2,k)) {
            exch(k/2,k); // k/2默认向下取整
            k = k/2;
        }
    }

    // 下沉:当某个节点变小(例如,将根节点替换为一个较小的元素),主要用于删除最大值
    private void sink(int k) {
        while (2*k <= N) {
            int j = 2*k;
            if (j < N && less(j,j+1)) // 找到较大的子节点,并将j指向它
                j++;
            if (!less(k,j)) // 此时j一定指向较大的子节点,如果elements[indexPq[k]] >= elements[indexPq[j]],则下沉结束
                break;
            exch(k,j); // 如果没有break则说明elements[indexPq[k]] < elements[indexPq[j]],交换indexPq 和 reIndexQp
            k = j;     // 交换k、j,让k始终指向下沉的元素
        }
    }

    @Override
    public String toString() {
        return "      indexPq " +
                Arrays.toString(indexPq) + "\n" +
                "    reIndexQp " +
                Arrays.toString(reIndexQp) + "\n" +
                "PriorityQueue " +
                Arrays.toString(elements);
    }
}

class TestPriorityQueue {
    public static void main(String[] args) {
        IndexPriorityQueue<Integer> indexPq = new IndexPriorityQueue<>(10);
        indexPq.insert(1,1);
        indexPq.insert(3,3);
        indexPq.insert(4,4);
        indexPq.insert(6,6);
        indexPq.insert(8,8);
        indexPq.insert(10,10);

        System.out.println(indexPq);

        System.out.println();
        System.out.println(indexPq.max() + " " + indexPq.maxIndex());

        System.out.println();
        indexPq.delMax();
        System.out.println(indexPq);

        System.out.println();
        indexPq.change(3,11);
        System.out.println(indexPq);

        System.out.println();
        indexPq.delete(3);
        System.out.println(indexPq);
    }
}

/*
      indexPq [0, 10, 6, 8, 1, 4, 3, 0, 0, 0, 0]
    reIndexQp [-1, 4, -1, 6, 5, -1, 2, -1, 3, -1, 1]
PriorityQueue [null, 1, null, 3, 4, null, 6, null, 8, null, 10]

10 10

      indexPq [0, 8, 6, 3, 1, 4, 0, 0, 0, 0, 0]
    reIndexQp [-1, 4, -1, 3, 5, -1, 2, -1, 1, -1, -1]
PriorityQueue [null, 1, null, 3, 4, null, 6, null, 8, null, null]

      indexPq [0, 3, 6, 8, 1, 4, 0, 0, 0, 0, 0]
    reIndexQp [-1, 4, -1, 1, 5, -1, 2, -1, 3, -1, -1]
PriorityQueue [null, 1, null, 11, 4, null, 6, null, 8, null, null]

      indexPq [0, 8, 6, 4, 1, 0, 0, 0, 0, 0, 0]
    reIndexQp [-1, 4, -1, -1, 3, -1, 2, -1, 1, -1, -1]
PriorityQueue [null, 1, null, null, 4, null, 6, null, 8, null, null]
*/

堆排序

有了前面的基础,堆排序就非常简单了,思路就是先构造一个大顶堆,每次交换堆顶(数组头)和数组“最后”一个元素(最后一个元素的索引不断前移),然后让交换到堆顶的一个元素下沉,保持堆的有序性,最终输出从小到大排列有序的数组。

构造阶段 – sink 比 swim 更高效

高效的堆构造:因为数组的每个位置都已经是一个子堆的根节点了,所以以数组前半部分为根节点的子树一定包含了所有的元素,我们可以从N/2处自右至左扫描,即只扫描数组前半部分,然后sink(),只需少于2N次比较以及少于N次交换就可以构造出有序的堆。

排序阶段

数组从左到右不断将最大元素放到最后,然后让"最后"一个元素下沉,保持堆的有序性,最终输出从小到大排列有序的数组

复杂度

将N个元素排序,堆排序只需少于(2NlgN+2N)次比较以及一半次数的交换。其中2NlgN来自下沉阶段,2N来自构造阶段。

Show me the code:

import java.util.Arrays;

/**
 * 堆排序
 */
public class HeapSort {
    private Comparable[] toBeSortedArray; // 待排序的数组

    public HeapSort(Comparable[] toBeSortedArray) {
        this.toBeSortedArray = toBeSortedArray;
    }

    public void sort() {
        int N = toBeSortedArray.length;

        // 堆构造阶段:使用sink操作,从N/2处自右至左扫描,只需少于N次比较以及少于N次交换
        for (int k = N/2; k >= 1; k--)
            sink(k, N);

        // 堆排序阶段:数组从左到右不断将最大元素放到最后,然后让最后一个元素下沉,保持堆的有序性,最终输出从小到大排列有序的数组
        int n = N;
        while (n > 1) {
            exch(1, n--);
            sink(1, n);
        }
    }

    private void sink(int k, int N) {
        while (2*k <= N) {
            int j = 2*k;
            if (j < N && less(j, j+1))
                j++;  // 找到两个子节点中较大的那个
            if (!less(k, j)) // 这时j一定指向较大的子节点
                break;
            exch(k, j); // 如果没有break则说明pq[k-1] < pq[j-1],交换pq[k-1]、pq[j-1]
            k = j;
        }
    }

    // 注意:因为数组是从索引0开始存储的,而堆排序的索引为了计算方便从1开始,所以要 -1
    private boolean less(int i, int j) {
        return toBeSortedArray[i-1].compareTo(toBeSortedArray[j-1]) < 0;
    }

    private void exch(int i, int j) {
        Comparable swap = toBeSortedArray[i-1];
        toBeSortedArray[i-1] = toBeSortedArray[j-1];
        toBeSortedArray[j-1] = swap;
    }

    // 判断数组是否有序
    public boolean isSorted() {
        for (int i = 1; i < toBeSortedArray.length; i++)
            if (!less(i,i+1))
                return false;
        return true;
    }
}

class TestHeapSort {
    public static void main(String[] args) {
        Integer[] testArray = {5,8,4,1,2,3,7,9,6,0}; // 注意数组是从索引0开始存储的,而堆排序的索引为了计算方便从1开始
        HeapSort heapSort = new HeapSort(testArray);
        heapSort.sort();
        System.out.println(heapSort.isSorted());
        System.out.println(Arrays.toString(testArray));
    }

    /* Output:
    true
    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
     */
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值