堆排序到底怎么排的呀?

315 阅读5分钟

堆排序

优先队列

许多应用程序都需要处理有序的元素,但不一定要求它们全部有序,或是不一定要一次就将它们排序。很多情况下我们会收集一些元素,处理当前键值最大的元素,然后再收集更多的元素,再处理当前键值最大的元素。在这种情况下,一个合适的数据结构应该支持两种操作:删除最大元素插入元素。这种数据类型叫做优先队列

优先队列的使用和队列(删除最老的元素)以及栈(删除最新的元素)类似。

image-20210615134415481.png

public class TopM {
	public static void main(String[] args) { // 打印输入流中最大的Af行
        int M = Integer.parseInt(args[0]) ;
        MinPQ<Transaction> pq = new MinPQ<Transaction>(M + l);
        while (StdIn.hasNextLine()) { //为下一行输入创建一个元素并放入优先队列中
            pq.insert(new Transaction(StdIn.readLine()));
            if (pq.sizeQ > M)
                pq.de1Min() ; // 如果优先队列中存在M+1个元素则删除其中最小的元素
		} // 最大的A/个元素都在优先队列中
		Stack<Transaction> stack = new Stack <Transaction>();
		while (!pq.isEmpty()) 
            stack.push(pq.delMin()) ;
		for(Transaction t : stack) 
            StdOut.println(t);
    }
}

从命令行输人一个整数M 以及一系列字符串,每一行表示一个事务。这段代码调用了MinPQ 并会打印数字最大的M行。它用到了 Transaction类,构造了一个用数字作为键的优先队列。当优先队列的大小超过M时就删掉其中最小的元素。所有事务输入完毕之后程序会从优先队列中按递减顺序打印出最大的M个事务。这段代码相当于将所有事务放入一个栈,遍 历栈以颠倒它们的顺序并按照增序将它们打印出来。

数组实现(无序)

insert() 方法的代码和桟的push()方法完全一样。要实现删除最大元素,我们可以添加一段类似于选择排序的内循环的代码,将最大元素和边界元素交换然后删除它,和我们对栈的pop ()方法的实现一样。和栈类似,我们也可以加人调整数组大小的代码来保证数据结构中至少含有四分之一的元素而又永远不会溢出。

数组实现(有序)

另一种方法就是在insert()方法中添加代码,将所有较大的元素向右边移动一格以使数组保持有序和插人排序一样。这样,最大的元素总会在数组的一边,优先队列的删除最大元素操作就和栈的pop()操作一样了。

链表表示法

和刚才类似,我们可以用基于链表的下压栈的代码作为基础,而后可以选择修改pop( ) 来找到并返回最大元素,或是修改push()来保证所有元素为逆序并用pop( ) 来删除并返回链表的首元素 ( 也就是最大的元素)

image-20210615143651348.png

堆的定义

数据结构二叉堆能够很好地实现优先队列的基本操作。

当一棵二叉树的每个结点都大于等于它的两个子结点时,它被称为堆有序。

二叉堆表示法

如果我们用指针来表示堆有序的二叉树,那么每个元素都需要三个指针来找到它的上下结点(父结点和两个子 结点各需要一个)如果我们使用完全二叉树,表达就会变得特别方便。完全二叉树只用数组而不需 要指针就可以表示。 具体方法就是将二叉树的结点按照层级顺序放人数组中,根结点在位置1,它的子结点在位置2 和 3,而子结点的子结点则分别在位置4、5、6 和 7,以此类推。

堆的算法

由下至上的堆有序化(上浮)

image-20210615150407347.png

插入元素。我们将新元素加到数组末尾, 增加堆的大小并让这个新元素上浮到合适的位置。

image-20210615151345945.png

private void swim(int k) {
    while (k > 1 && less(k / 2,k)) {
        exch(k / 2,k);
        k = k / 2;
    }
}

由上至下的堆有序化(下沉)

image-20210615150419615.png

删除最大元素。我们从数组顶端删去最大的元素并将数组的最后一个元素放到顶端,减小堆的大小并让这个元素下沉到合适的位置。

image-20210615151436589.png

private void sink(int k) {
    while (2 * k <= N) {
        int j = 2 * k;
        if (j < N && less(j,j + 1))
            j++;
        if (!less(k,j))
            break;
        exch(k,j);
        k = j;
    }
}

基于堆的优先队列

public class MaxPQ<Key extends Comparable<Key» { 
    private Key[] pq; // 基于堆的完全按二叉树
	private int N = 0; // 存储于p q [l. . N ]中,p q [ 0 ]没有使用
	public MaxPQ(int maxN) { 
        pq = (Key[]) new Comparable[maxN+1]; 
    }
    public boolean isEmpty() { 
        return N == 0; 
    }
	public int size() { 
        return N; 
    }
	public void insert(Key v) {
		pq[++N] = v;
		swim(N);
    }
	public Key delMax() {
		Key max = pq[1]; 	//从根结点得到最大元素
		exch(1, N--); 		//将其和最后一个结点交换
		pq[N+1] = null; 	//防止越界
         sink(1);			//恢复堆的有序性
		return max;
    }
//辅助方法的实现请见本节前面的代码框
	private boolean less (int i, int j)
	private void exch(int i, int j)
	private void swim (int k)
	private void sink (int k)
  }

对于一个含有#个元素的基于堆的优先队列,插入元素操作只需不超过(lgN + 1)次比较, 删除最大元素的操作需要不超过2lgN次比较。

堆排序算法

public static void sort(Comparable[] a) {
    int N = a.length;
    for (int k = N / 2; k >= 1; k--) {
        sink (a,k,N);  
    }
    while (N > 1) {
        exch(a,1,N--);
        sink(a,1,N);
    }
}

这段代码用sink ( ) 方法将a[1] 到a [N] 的元素排序(sink() 被修改过,以a和N作为参数)。for循环构造了堆,然后while 循环将最大的元素a[1] 和a[N] 交换并修复了堆,如此重复直到堆变空。

将N个元素排序,堆排序只需少于(2NlgN + 2N )次比较(以及一半次数的交换)。

各种排序算法的性能特点

image-20210615155957710.png

快速排序是最快的通用排序算法。在大多数实际情况中,快速排序是最佳选择。

Java系统库中的主要排序方法java.util.Arrays.sort()根据不同的参数类型,它实际上代表了一系列排序方法:

  • 每种原始数据类型都有一个不同的排序方法;
  • 一个适用于所有实现了 Comparable接口的数据类型的排序方法;
  • 一个适用于实现了比较器Comparator的数据类型的排序方法。

找到一组数中的第k小元素

public static Comparable select(COmparable[] a,int k) {
	StdRandom.shuffle(a);
    int lo = 0,hi = a.length - 1;
    while (hi > lo) {
        int j = partition(a,lo,hi);
        if (j == k) {
            return a[k];
        } else if (j > k) {
            hi = j - 1;
        } else if (j < k) {
            lo = j + 1;
        }
    }
    return a[k];
}