实现优先队列的基本数据结构是使用二叉堆
一、二叉堆
二叉堆即用一颗完全二叉树(底层元素从左向右填入)。
如果所示,因此我们可以使用一个数组来表示二叉堆,而不使用链表:
0 1 2 3 4 5 6 7 8 9 10
A B C D E F G H I
可以发现,对于任意节点 i,其 左二子在 2i 位置,右儿子在 2i+1位置
关于优先队列,我们可以规定最大元优先或最小元优先,对应的堆就是大顶堆和小顶堆。
大顶堆: 即树上每个节点都小于等于其父亲节点,除了根节点,因为根节点没有父亲
小顶堆:即树上每个节点都大于等于其父亲节点。同上
二、基本的堆操作(以小顶堆为例)
1.插入
1.可以在下一个可用位置处创建一个空节点,将待插入节点x放在0位置(0位置为空)
2.然后循环比较待加入节点和空节点的父亲比较,如果父节点大于x,则将让父节点置于空节点,空节点上冒到原父节点;如果父节点小于x,则直接将x放在空位置,此时不会改变堆序
若创建大顶堆,则和父亲节点比较时,同小顶堆相反即可。
实现如下:
先看基本的数据结构:
public class BinaryHeap<T extends Comparable<? super T>> {
private static final int DEFAULT_CAPACITY = 10;
//当前堆大小(元素个数)
private int currentSize;
//数组表示二叉堆
private T[] array;
public BinaryHeap() {
this(DEFAULT_CAPACITY);
}
public BinaryHeap(int capacity) {
this.currentSize = 0;
array = (T[]) new Comparable[capacity + 1];
}
}
插入操作:
/**
* 先建立一个空位置 hole,上滤直到找到满足堆序的位置,将x插入到该位置
* array[0]暂存待插入元素x
* @param x
*/
public void insert(T x) {
//扩容操作
if (currentSize == array.length - 1) {
enlargeArray(array.length * 2 + 1);
}
//让下一个节点为空节点
int hole=++currentSize;
//将待插入节点放在0处
array[0]=x;
//从空节点开始上冒
percolateUp(hole,array[0]);
}
/**
* 上冒
* @param hole
*/
private void percolateUp(int hole,T x){
for ( ; x.compareTo(array[hole/2])<0 ;hole /= 2) {
array[hole]=array[hole/2];
}
array[hole]=x;
}
2. 删除最小值(出队)
在二叉堆中,最小值即第arr[1]元素,但是删除它后,根节点变为空,因此我们需要把最后一个节点X放在这个空元素上,同时让当前堆大小减一,如果整个堆有序,则直接返回最小值,但是显然不可能保持堆序。
因此我们就需要从该空节点开始进行下滤操作:
下滤与上冒相反,就是沿着空节点开始包含最小儿子的路径上找到一个位置可以放入X。
图例:删除最小节点A
B比I小,因此将空节点下滤:
D比I小,因此空节点移动到D位置:
此时I放在当前的空节点位置可以保持堆序:
实现如下:
/**
* 删除最小元素,下滤
* 1.找到最小元素,移除(currentSize--,并将最后一个元素放在最小元素位置)
* 2.对该元素进行下滤
* @return
*/
public T deleteMin(){
if (isEmpty()){
System.out.println("堆为空");
}
T min = findMin();
array[1]=array[currentSize--];
percolateDown(1);
return min;
}
/**
* 从hole开始下滤
* @param hole
*/
private void percolateDown(int hole) {
int child;
T temp = array[hole];
for ( ; hole*2<=currentSize ;hole=child ) {
child=hole*2;
if (child!=currentSize&& //偶数个节点的情况下,最后一个hole只有一个左儿子,如果没有此条件array[child+1]会出现空指针异常
array[child+1].compareTo(array[child])<0){//右儿子比左儿子还小
child++;//child指向右儿子
}
//如果temp大于两个儿子中最小的一个节点,则让空节点下移到child
if (array[child].compareTo(temp)<0){
array[hole]=array[child];
}else
break;
}
array[hole]=temp;
}
public T findMin(){
if (isEmpty()){
System.out.println("堆为空");
}
return array[1];
}
完整代码:
/**
* 堆(优先队列)
*
* @author MaoLin Wang
* @date 2020/2/1611:26
*/
public class BinaryHeap<T extends Comparable<? super T>> {
private static final int DEFAULT_CAPACITY = 10;
private int currentSize;
private T[] array;
public BinaryHeap() {
this(DEFAULT_CAPACITY);
}
public BinaryHeap(int capacity) {
this.currentSize = 0;
array = (T[]) new Comparable[capacity + 1];
}
public BinaryHeap(T[] array) {
currentSize = array.length;
this.array = (T[]) new Comparable[(currentSize + 2) * 11 / 10];
int i = 1;
for (T arr : array) {
array[i++] = arr;
}
buildHead();
}
private void buildHead() {
for (int i = currentSize/2; i >0 ; i--) {
percolateDown(i);
}
}
private void enlargeArray(int newSize) {
T[] old = array;
array = (T[]) new Comparable[newSize];
for (int i = 0; i < old.length; i++) {
array[i] = old[i];
}
}
/**
* 先建立一个空位置 hole,上滤直到找到满足堆序的位置,将x插入到该位置
* array[0]暂存待插入元素x
* @param x
*/
public void insert(T x) {
if (currentSize == array.length - 1) {
enlargeArray(array.length * 2 + 1);
}
int hole=++currentSize;
array[0]=x;
percolateUp(hole,array[0]);
}
/**
* 上滤
* @param hole
*/
private void percolateUp(int hole,T x){
for ( ; x.compareTo(array[hole/2])<0 ;hole /= 2) {
array[hole]=array[hole/2];
}
array[hole]=x;
}
/**
* 删除最小元素,下滤
* 1.找到最小元素,移除(currentSize--,并将最后一个元素放在最小元素位置)
* 2.对该元素进行下滤
* @return
*/
public T deleteMin(){
if (isEmpty()){
System.out.println("堆为空");
}
T min = findMin();
array[1]=array[currentSize--];
percolateDown(1);
return min;
}
/**
* 从hole开始下滤
* @param hole
*/
private void percolateDown(int hole) {
int child;
T temp = array[hole];
for ( ; hole*2<=currentSize ;hole=child ) {
child=hole*2;
if (child!=currentSize&& //保证偶数个节点的情况下,最后一个hole只有一个左儿子
array[child+1].compareTo(array[child])<0){
child++;
}
if (array[child].compareTo(temp)<0){
array[hole]=array[child];
}else
break;
}
array[hole]=temp;
}
public T findMin(){
if (isEmpty()){
System.out.println("堆为空");
}
return array[1];
}
public void print(){
for (int i = 1; i <currentSize ; i++) {
System.out.println(array[i]);
}
}
private boolean isEmpty() {
return currentSize==0;
}
// Test program
public static void main( String [ ] args )
{
BinaryHeap<Integer> heap = new BinaryHeap<>();
heap.insert(13);
heap.insert(14);
heap.insert(16);
heap.insert(19);
heap.insert(21);
heap.insert(19);
heap.insert(68);
heap.insert(65);
heap.insert(26);
heap.insert(32);
heap.insert(31);
System.out.println("-----");
while (!heap.isEmpty()){
//依此出队
System.out.println(heap.deleteMin());
}
}
}
结果:
13
14
16
19
19
21
26
31
32
65
68
大顶堆的下滤操作也基本相同,只是改变一个比较的符号。
三、堆排序
讲完了优先队列的实现后,再实现堆排序就很容易了。
进行优先队列时,我们构造了大顶堆或小顶堆。这里依然以小顶堆为例:
1.如果我们想让一个小顶堆数组变成有序数组,只需从最后一个元素开始,与根节点交换(相当于在优先队列中删除最小值,但是这里把最小值保存到了最后一个元素的位置),同时让currentSize减一。
2.对新的根节点进行下滤操作,直到currentSize
3.这样循环直到交换完所有元素,就得到了一个从大到小的有序数组
实现如下:
/**
* 使用小顶堆 --->从大到小排序
* @param arr
*/
public static void heapSort(int[] arr){
for (int i = arr.length/2-1; i >=0 ; i--) {
//从 arr.length/2-1 即倒数第一个非叶子节点开始,逐一下滤,形成小顶堆
perDownMin(arr,i,arr.length);
}
for (int i = arr.length-1; i >0 ; i--) {
//将根节点,即最小的节点与最后一个节点交换,让最小值逐一放在最后
swap(arr,i,0);
//从根节点开始重新生成小顶堆
perDownMin(arr,0,i);
}
}
/**
* 小顶堆下滤
* @param arr
* @param i
* @param length
*/
private static void perDownMin(int[]arr, int i, int length){
int child;
int temp;
for (temp=arr[i];leftChild(i)<length ;i=child) {
child=leftChild(i);
if (leftChild(i)!=length-1&&arr[child+1]<arr[child]){
child++;
}
if (temp>arr[child]){
arr[i]=arr[child];
}else {
break;
}
}
arr[i]=temp;
}
private static void swap(int[] arr, int index1, int index2){
int temp=arr[index1];
arr[index1]=arr[index2];
arr[index2]=temp;
}
需要注意的是,这里不需要像优先队列进行删除(出队)和插入(入队)操作,因此数组是从0开始而不是从1开始,在个别判断有所不同。
同样的方法以大顶堆实现堆排序:
/**
* 使用大顶堆 ----> 从小到大排序
* @param arr
* @param <T>
*/
public static <T extends Comparable<? super T>>void heapSort(T[] arr){
for (int i = arr.length/2-1; i >=0 ; i--) {
perDownBig(arr,i,arr.length);
}
for (int i = arr.length-1; i >0 ; i--) {
swap(arr,0,i);
perDownBig(arr,0,i);
}
}
/**
* 大顶堆下滤
* @param arr
* @param i
* @param length
* @param <T>
*/
private static <T extends Comparable<? super T>>void perDownBig(T[] arr, int i, int length){
int child;
T temp;
for(temp=arr[i];leftChild(i)<length;i=child){
child=leftChild(i);
if (child!= length-1 && arr[child].compareTo(arr[child+1])<0){
child++;
}
if (temp.compareTo(arr[child])<0){
arr[i]=arr[child];
}else {
break;
}
}
arr[i]=temp;
}
leftChild
是一个私有的计算孩子节点位置的方法:
private static int leftChild(int i){
return 2*i+1;
}
测试:
public static void main(String[] args) {
/*
int[] array =new int[8000000];
for (int i=0;i<8000000;i++){
array[i]=(int)(Math.random()*8000000);
}
long begintime=System.currentTimeMillis();
heapSort(array);
long endtime=System.currentTimeMillis();
System.out.println("用时:"+(endtime-begintime)+"ms");*/
int[] array =new int[10];
for (int i=0;i<10;i++){
array[i]=(int)(Math.random()*100);
}
heapSort(array);
for (int i:array){
System.out.println(i);
}
}
97
77
72
69
65
43
32
19
17
5
堆排序的时间复杂度为 NlogN
空间复杂度为 1