今天我们来看一下另一种著名的排序算法,叫堆排序。它可以达到O(nlogn)的时间复杂度。这比我们之前介绍的插入排序算法要来的高效,和归并排序一样。在这里我们讨论高效性其实忽略了算法的常数因子大小,看起来堆排序和归并排序的时间复杂度很低,但是我们在实际运用中仍然用的不多。在大多数需要排序的场合下,我们一般使用快速排序算法,虽然该算法的时间复杂度是O(n^2),但是它的常数因子很小。所以在常用的数据规模下,它所花费的时间是最小的。
在介绍堆排序之前,我们先来认识一下堆这个数据结构--heap。
1. 什么是堆?堆有哪些性质?
上图,先来看看堆的样子:
上图展示的是二叉堆,近似的可以看做是完全二叉树,可以用数组表示出来。我们约定左孩子节点的索引是父节点索引的2倍,右孩子索引是父节点索引的2倍加1。这样一个数组可以完整的表示出二叉堆。
堆分两种,一种是最大堆,一种是最小堆。如果用A数组来存放堆,我们可以表示出他们的性质。
最大堆的性质是:A[parent(i)] >= A[i]
最小堆的性质是:A[parent(i)] <= A[i]
parent(i)表示索引是i的孩子的父亲的索引。
这就是堆的所有的性质,简单吧!不过真因为简单,所以在堆中只包含很少的信息,以最大堆为例,我们只知道父节点一定不小于子节点,最顶端的节点最大,仅此而已。对于最小值在哪,我们根本无法确定,只知道它位于叶子节点上。
一般的堆类都包含下面几个功能:
public interface Heap<T extends Comparable<T>> {
void insert(T element);
T get();
T delete();
boolean isEmpty();
void makeEmpty();
}
我们可以向堆中insert元素,可以get最大或者最小的元素,可以delete最顶端的元素,也可以makeEmpty清空堆。
在insert元素的时候,我们需要上滤的策略,即先将要插入的元素放在数组的最后,然后为了保证堆的性质,我们要从底向上不断的更新过滤。
在delete元素的时候,我们需要下滤的策略,即将最后的元素放在顶端,从上往下更新过滤,为的也是保证堆的性质不变。
也许你会晕,什么上滤下滤啊,其实也没那么复杂,都是为了保证堆的性质不发生变化。
下面可以看看我用java写的最小二叉堆的代码:
package charp_1;
import java.util.ArrayList;
/**
* @author freestyle458
*/
public class MinBinaryHeap<T extends Comparable<T>> implements Heap<T> {
public static void main(String[] args)
{
/* MinBinaryHeap<Integer> minHeap = new MinBinaryHeap<>();
minHeap.insert(10);
minHeap.insert(3);
minHeap.insert(40);
minHeap.insert(100);
minHeap.insert(1);
minHeap.insert(5);
minHeap.insert(7);
minHeap.insert(2);
System.out.println(minHeap);
System.out.printf("the min element is : %d\n", minHeap.get());
System.out.println(minHeap.delete());
System.out.println(minHeap);
System.out.println(minHeap.delete());
System.out.println(minHeap);
ArrayList<Integer> items = new ArrayList<>();
items.add(8);
items.add(15);
items.add(7);
items.add(100);
items.add(2);
items.add(7);
MinBinaryHeap<Integer> minHeap2 = new MinBinaryHeap<>(items);
System.out.println(minHeap2);
System.out.println(minHeap2.delete());
System.out.println(minHeap2);
System.out.println(minHeap2.delete());
System.out.println(minHeap2);
System.out.println(minHeap2.delete());
System.out.println(minHeap2);*/
}
private ArrayList<T> array;
public MinBinaryHeap()
{
// TODO Auto-generated constructor stub
array = new ArrayList<>();
array.add(null);
}
public MinBinaryHeap(ArrayList<T> items)
{
this();
array.addAll(items);
for (int i = (array.size()-1)/2; i > 0; i--) {
precolateDownMinHeap(i);
}
}
@Override
public void insert(T element)
{
// TODO Auto-generated method stub
array.add(element);
percolateUpMinHeap(array.size()-1);
}
private void percolateUpMinHeap(int index)
{
int hole = index;
T element = array.get(hole);
while (hole > 1 && element.compareTo(array.get(hole/2)) < 0) {
array.set(hole, array.get(hole/2));
hole /= 2;
}
array.set(hole, element);
}
@Override
public T get()
{
// TODO Auto-generated method stub
if (isEmpty()) {
return null;
} else {
return array.get(1);
}
}
@Override
public T delete()
{
// TODO Auto-generated method stub
T minElement = get();
if (minElement == null)
return null;
array.set(1, array.get(array.size() - 1));
array.remove(array.size() - 1);
if (isEmpty())
return minElement;
precolateDownMinHeap(1);
return minElement;
}
private void precolateDownMinHeap(int index)
{
int lChild = 2*index;
int rChild = 2*index + 1;
int min = index;
if (lChild < array.size() && array.get(lChild).compareTo(array.get(index)) < 0) {
min = lChild;
}
if (rChild < array.size() && array.get(rChild).compareTo(array.get(min)) < 0)
min = rChild;
if (min != index) {
T element = array.get(index);
array.set(index, array.get(min));
array.set(min, element);
precolateDownMinHeap(min);
}
}
@Override
public boolean isEmpty()
{
// TODO Auto-generated method stub
if (array.size() == 1)
return true;
else
return false;
}
@Override
public void makeEmpty()
{
// TODO Auto-generated method stub
array.clear();
array.add(null);
}
@Override
public String toString()
{
// TODO Auto-generated method stub
String result = "the minHeap is :\n";
int height = (int)(Math.log(array.size() - 1) / Math.log(2));
for (int i = 0; i <= height; i++) {
int levelSize = (int)Math.pow(2, i);
for (int j = 0; j < levelSize; j++) {
int position = (int)Math.pow(2, i) + j;
if (position > array.size() - 1) {
break;
} else {
result += array.get(position) + " ";
}
}
result += "\n";
}
return result;
}
}
因为以堆以树形的方式比较直观,所以我在toString方法中用近似数的形式表示出来了。
运行的效果是:
可以看到堆的建立是正确的。insert和delete操作也是正常的!!!
在这基础上,我们可以将最大堆和最小堆合并起来,写出来一般性的堆类。其实所谓的最大堆和最小堆,无非是在上滤和下滤的过程中比较元素的方式不一样而已。
下面欣赏一下终极版的BinaryHeap类吧,可以很轻松的构造最大最小堆。
<span style="font-size:18px;">/**
*
*/
package charp_1;
import java.util.ArrayList;
/**
* @author freestyle458
*
*/
public class BinaryHeap<T extends Comparable<T>> implements Heap<T> {
public static void main(String[] args)
{
BinaryHeap<Integer> items1 = new BinaryHeap<>(true);
BinaryHeap<Integer> items2 = new BinaryHeap<>(false);
items1.insert(10);
items1.insert(8);
items1.insert(20);
items1.insert(30);
items1.insert(3);
items1.insert(50);
System.out.println(items1);
System.out.println("---------------------------");
items2.insert(10);
items2.insert(8);
items2.insert(20);
items2.insert(30);
items2.insert(3);
System.out.println(items2);
System.out.println("----------------------------");
ArrayList<Integer> nums = new ArrayList<>();
nums.add(3);
nums.add(4);
nums.add(1);
nums.add(10);
nums.add(11);
BinaryHeap<Integer> items3 = new BinaryHeap<>(true, nums);
System.out.println(items3);
System.out.println("---------------------------");
BinaryHeap<Integer> items4 = new BinaryHeap<>(false, nums);
System.out.println(items4);
System.out.println("---------------------------");
System.out.println("delete top element is : " + items3.delete());
System.out.println(items3);
System.out.println("delete top element is : " + items3.delete());
System.out.println(items3);
}
private ArrayList<T> array;
private boolean isMaxHeap;
public BinaryHeap()
{
//默认是以最大堆的形式出现
this(true);
}
public BinaryHeap(boolean isMaxHeap)
{
this.isMaxHeap = isMaxHeap;
array = new ArrayList<>();
array.add(null);
}
public BinaryHeap(boolean isMaxHeap, ArrayList<T> items)
{
this(isMaxHeap);
array.addAll(items);
//从每个节点(除叶子节点)开始下滤
for (int i = (array.size()-1)/2; i > 0; i--) {
percolateDown(isMaxHeap, i);
}
}
private void percolateDown(boolean isMaxHeap, int index)
{
int child;
int hole = index;
T element = array.get(hole);
if (isMaxHeap) {
while (2*hole < array.size()) {
child = 2*hole;
if (child != array.size()-1 && array.get(child).compareTo(array.get(child+1)) < 0)
child++;
if (array.get(child).compareTo(element) > 0) {
array.set(hole, array.get(child));
hole = child;
} else
break;
}
array.set(hole, element);
} else {
while (2*hole < array.size()) {
child = 2*hole;
if (child != array.size()-1 && array.get(child).compareTo(array.get(child+1)) > 0)
child++;
if (array.get(child).compareTo(element) < 0) {
array.set(hole, array.get(child));
hole = child;
} else
break;
}
array.set(hole, element);
}
}
@Override
public void insert(T element)
{
// TODO Auto-generated method stub
array.add(element);
percolateUp(isMaxHeap, array.size()-1);
}
private void percolateUp(boolean isMaxHeap, int index)
{
int hole = index;
T element = array.get(hole);
//如果是最大堆,那么将上层的小者换下, 如果是最小堆,则将上面的大者换下
if (isMaxHeap) {
while (hole > 1 && element.compareTo(array.get(hole/2)) > 0) {
array.set(hole, array.get(hole/2));
hole /= 2;
}
} else {
while (hole > 1 && element.compareTo(array.get(hole/2)) < 0) {
array.set(hole, array.get(hole/2));
hole /= 2;
}
}
array.set(hole, element);
}
@Override
public T get()
{
// TODO Auto-generated method stub
if (isEmpty())
return null;
else
return array.get(1);
}
@Override
public T delete()
{
// TODO Auto-generated method stub
T popitems = get();
if (popitems == null)
return null;
else {
array.set(1, array.get(array.size()-1));
array.remove(array.size()-1);
if (!isEmpty()) {
percolateDown(isMaxHeap, 1);
}
return popitems;
}
}
@Override
public boolean isEmpty()
{
// TODO Auto-generated method stub
if (array.size() == 1)
return true;
else
return false;
}
@Override
public void makeEmpty()
{
// TODO Auto-generated method stub
array.clear();
array.add(null);
}
@Override
public String toString()
{
String result = isMaxHeap ? "the maxHeap is:\n" : "the minHeap is:\n";
int height = (int)(Math.log(array.size() - 1) / Math.log(2));
for (int i = 0; i <= height; i++) {
int levelSize = (int)Math.pow(2, i);
for (int j = 0; j < levelSize; j++) {
int position = (int)Math.pow(2, i) + j;
if (position > array.size() - 1) {
break;
} else {
result += array.get(position) + " ";
}
}
result += "\n";
}
return result;
}
}
</span>
下面来看一下运行测试的结果,看看是否符合要求:
简单的介绍完了堆结构之后,我们下面来看一下堆排序。
如何用堆排序呢?其实你已经想到了吧!比如最大堆为例,我们知道最大的元素在最顶端,那么每次我们取出最顶端的元素放在数组的最后,不就可以很好的完成堆排序啦!
遍历每个元素用O(logN)的时间,总共有N个元素,所以估计一下时间应该是O(NlogN)。
说到时间复杂度,需要注意的是用乱序数组构造最大堆的过程是线性时间完成的,为O(N)。你也许会说为什么不是O(nlogn)呢,因为每个元素的高度和是O(n),所以在下滤时总共用了O(n)的时间。
好了,我们看看具体的实现代码吧!
<span style="font-size:18px;">/**
*
*/
package charp_1;
/**
* @author freestyle458
*
*/
public class HeapSort {
public static void main(String[] args)
{
int[] nums = new int[]{0, 12, 1, -78, 43, 78, 6 ,10};
Integer[] items = new Integer[nums.length];
for (int i = 0; i < nums.length; i++) {
items[i] = nums[i];
}
heapSort(items);
for (Integer i : items) {
System.out.printf("%d ", i);
}
System.out.println();
}
public static <T extends Comparable<T>> void percolateDown(T[] a, int i , int n)
{
int hole = i;
T element = a[hole];
int child;
while (2*hole + 1 < n ) {
child = 2 * hole + 1;
if (child != n - 1 && a[child].compareTo(a[child+1]) < 0) {
child++;
}
if (element.compareTo(a[child]) < 0) {
a[hole] = a[child];
hole = child;
} else
break;
}
a[hole] = element;
}
public static <T extends Comparable<T>> void swap(T[] a, int i, int j)
{
T tmp = a[i];
a[i] = a[j];
a[j] = tmp;
}
public static <T extends Comparable<T>> void heapSort(T[] a)
{
for (int i = a.length/2; i >= 0; i--)
percolateDown(a, i, a.length);
for (int i = a.length - 1; i > 0; i--) {
swap(a, 0, i);
percolateDown(a, 0, i);
}
}
}
</span>
运行结果:
介绍完了堆排序,我们来看一下优先队列,这个也是堆结构运用最多的地方!
什么是优先队列呢?我们知道一般的队列是先进先出,后进后出。而优先队列是依据元素的优先级来最先处理的。比如最大优先队列是优先级最大的元素先得到处理。
这个很符合最大堆的特点,值最大的永远在最顶端。优先队列在操作系统的任务管理中起到很重要的作用。那么用堆如何实现呢?在理解了最大最小堆的实现过程后可以很容易编写。
一般的优先队列具有:
insert(x); 插入元素
max(s); 取出最大元素
deleteMax(s); 删除并取出最大元素
increaseKey(s, x, key); 增加特定元素的值
这些方法在利用最大最小堆的基础上增加几行代码就可以了!这里就不贴出来了,有兴趣的朋友可以自己写一下哦!