堆,这是经常听到的一个数据结构,其非常的重要,重要性就不闲扯了,这次主要说明堆的数据结构,堆一般可以分为大顶堆和小顶堆,顾名思义,也就是最大值或者是最小值在第一个位置,需要说明的是这不是我们自己规定了,而是由于堆的性质决定了,在堆中,子节点的值一定要小于父节点(当堆为小顶堆时),堆包括堆的变形以及使用都紧紧围绕这个核心来展开。
堆可以存在数组中,一般的在数组中,若某个i节点为父节点,则2*i为左子节点,2*i+1则为右子节点,牢记这个性质,这个性质理解了将会让我们理解二叉堆,以及堆排序较少障碍。
1. 二叉堆
话不多说,先来一个二叉堆的建立,二叉堆的实现有这个几个核心,第一个,如何使堆保持刚才提到的性质,第二个则是如何保证堆在插入的时候不会破坏原来的性质。我们以建立一个最小堆为例说明一下:
public class BinaryHeap<AnyType extends Comparable <? super AnyType>> {
private static final int DEFAULT_CAPACITY = 10;
private int currentSize;
private AnyType[] array;
/**
* Construct the binary heap
*/
public BinaryHeap(){
this(DEFAULT_CAPACITY);
}
/**
* Construct the binary heap
* @Param capacity the capacity of the binary heap
*/
public BinaryHeap(int capacity){
currentSize = 0;
array = (AnyType[]) new Comparable[capacity + 1];
}
/**
* Construct the binary heap given an array of items
*/
public BinaryHeap(AnyType[] items){
currentSize = items.length;
array = (AnyType[]) new Comparable[(currentSize + 2) * 11 / 10];
int i = 1;
for (AnyType item : items){
array[i++] = item;
}
buildHeap(currentSize);
}
/**
* Insert into the priority queue, maintaining heap order
* Duplicates are allowed
* @Param x the item to insert
*/
public void insert(AnyType x){
if(currentSize == array.length - 1)
enlargeArray(array.length * 2 + 1);
int hole = ++currentSize;
//perlocatUp to find the true position for the new inserted element
for(array[0] = x; x.compareTo(array[hole / 2]) < 0; hole /= 2){
array[hole] = array[hole / 2];
}
array[hole] = x;
}
private void enlargeArray(int newSize) {
AnyType[] old = array;
array = (AnyType[]) new Comparable[newSize];
for(int i = 0; i < old.length; i++){
array[i] = old[i];
}
}
/**
* Find the smallest item in the priority queue
* @return the smallest item, or throw an UnderflowException if empty
*/
public AnyType findMin(){
if(isEmpty()){
throw new UnderflowException();
}
return array[1];
}
/**
* Remove the smallest item form priority queue
* @Param the samllest item, or throw an UnderflowException if empty
*/
public AnyType deleteMin(){
if(isEmpty()){
throw new UnderflowException();
}
AnyType minItem = findMin();
array[1] = array[currentSize--];
percolateDown(1);
return minItem;
}
/**
* Test if the priority queue is logically empty
* @reutrn true is empty,false otherwise
*/
public boolean isEmpty(){
return currentSize == 0;
}
/**
* Make the priority empty logically .
*/
public void makeEmpty(){
currentSize = 0;
}
/**
* Establish heap order property from an arbitrary
* arrangement of items Runs in linear time
*/
private void buildHeap(int currentSize) {
for(int i = currentSize / 2; i > 0; i--){
percolateDown(i);
}
}
/**
* Internal method to percolate down in the heap
* @Param hole the index at which the percolate begins
*/
private void percolateDown(int hole) {
int child;
AnyType temp = array[hole];
for(; 2 * hole <= currentSize; hole = child){
child = 2 * hole;
if(child != currentSize && array[child + 1].compareTo(array[child]) < 0){
child++;
}
if(array[child].compareTo(temp) < 0){
array[hole] = array[child];
}else
break;
}
array[hole] = temp;
}
public void percolateDown1(int hole){
int left = 2 * hole;
int right = 2 * hole + 1;
int min = hole;
if(min <= currentSize / 2){
if(left <= currentSize && array[left].compareTo(array[min]) < 0){
min = left;
}
if(right <= currentSize && array[right].compareTo(array[min]) < 0){
min = right;
}
if(min != hole){
AnyType temp = array[hole];
array[hole] = array[min];
array[min] = temp;
percolateDown1(min);
}
}
}
}
首先说明插入的问题,我们看到有个insert函数,此函数功能为插入元素,可以发现我们定义了一个currentSize字段,其为类中的一个变量,当我们插入第一个将元素的时候,会将其放置在array[0]的位置,然而hole则已经进行了自增1的操作,其实可以发现堆的真正的根节点在array[1]而不是在array[0],无论是在for循环中,还是最后的赋值,其hole的值都是在hole自增1之后进行的,所以,当第一个插入的时候,我们可以确定个节点的位置,这个很位置很重要,其存放了最大值(大顶堆)或者是最小值(小顶堆)。有人会对insert中循环部分产生疑问,会考虑右子节点是不是没考虑,其是根据上面子节点的性质,当为右子节点时,(2 * i + 1)/ 2取整数的话仍然是i,所以,此时右子节点还是和父节点比较,来保证堆的性质,所以但我们按照此insert方法插入后可以保证array[1]到currentSize范围内的数据符合二叉堆的性质,此时array[1]位置即为min或max。
其次说明的就是当我们输入一个指定的数组(形参列表为 AnyType[] items的构造函数)时,或者是当我们每次删除最小值(最大值)的时候,都需要保证堆序性质。当我们输入一个items时,此时需要我们对整个数组进行调整,所以此时我们有了perlocateDown函数,此函数时保证堆序性质的精华,单独拿出来说:
private void percolateDown(int hole) {
int child;
AnyType temp = array[hole];
for(; 2 * hole <= currentSize; hole = child){
child = 2 * hole;
if(child != currentSize && array[child + 1].compareTo(array[child]) < 0){
child++;
}
if(array[child].compareTo(temp) < 0){
array[hole] = array[child];
}else
break;
}
array[hole] = temp;
}
public void percolateDown1(int hole){
int left = 2 * hole;
int right = 2 * hole + 1;
int min = hole;
if(min <= currentSize / 2){
if(left <= currentSize && array[left].compareTo(array[min]) < 0){
min = left;
}
if(right <= currentSize && array[right].compareTo(array[min]) < 0){
min = right;
}
if(min != hole){
AnyType temp = array[hole];
array[hole] = array[min];
array[min] = temp;
percolateDown1(min);
}
}
}
对于perlocateDown函数,其hole值为开始调整的根节点,因为当我们输入好以后,我们对于堆的调整就是一个不断的向下到达子节点的过程,需要不断的检调整以符合堆序性质,所以我们在调用此函数的函数内都写为此形式perlocateDown(root);所以我们需要做的就是比较子节点和父节点之间谁最小的问题,若某个子节点小于其父节点,则将其和其父节点交换,然后再进行下一层的检验。而下面的perlocateDown1则是另一种写法,一种递归写法,其原理相同,都是交换不符合性质的子节点和父节点,不同点在于递归便于理解一些,方法中指明了左右子节点以及判断的关系,但是最后的递归可能会迷糊,其是递归则是递归的改动过的地方,如果此位置有改动则需要检查此位置以及其所下属的子节点(如果有的话)是否符合堆(小顶堆)的性质。同样的在每次deleteMin后,都会破坏堆序性质,所以需要重新调整。
2.D叉堆
说白了D叉堆直观上就是多了几个叉,而D叉堆的树形结构要比二叉堆的高度要低,这是正常的,因为其由D个叉了,每个节点对应下面多个子节点。然而,让自己实现D叉堆可能会有些一蒙,不过仔细分析的话还是能找到“七寸”的,其核心点在于父节点子节点的表示以及子节点和父节点元素不符合堆的性质时如何交换。对于第一点,我们还是建设秉承上一个的思想,0位置放置插入的元素,则此时,父节点和子节点可以表示如下,对于子节点,重要的是找到子节点最开始的位置,数量就是D,因为时D叉树嘛。
//获取父节点
public int Parent(int hole){
return (hole - 2 + D_ary) / D_ary;
}
//获取子节点
public int Child(int parent, int index_child){
return (D_ary * (parent - 1) + 2 + index_child);
}
假设D_ary为叉的个数,hole则为输入的节点,Parent则是获取父节点,Child则是获取子节点,index_child为子节点中的索引位置(因为由D_ary个叉)。所以我们可以在原有的二叉堆的insert代码上改动一下,说白了,D叉堆退化以后就能得到二叉堆。
public void insert(AnyType x){
if(currentSize == mArray.length - 1){
enlargeArray(mArray.length * 2 + 1);
}
int hole = ++currentSize;
for(mArray[0] = x;x.compareTo(mArray[Parent(hole)]) < 0; hole = Parent(hole)){
mArray[hole] = mArray[Parent(hole)];
}
mArray[hole] = x;
}
//扩充容量
private void enlargeArray(int newSize) {
AnyType[] old = mArray;
mArray = (AnyType []) new Comparable[newSize];
for(int i = 0; i < old.length; i++){
mArray[i] = old[i];
}
}
可以看到,改变的部分为原来的 hole/2 全部变为了Parent(hole),其是前者就是寻找父亲节点,只不过我们用后者将其一般化了,其是带入以下,检验一下,思想是一样的,所以一般的我们及使用这个一般化形式表示,这也就和说的二叉堆的性质吻合了。而对于perlocateDown函数,其主要改变的部分为子节点和父节点之间不符合性质交换元素的部分,因为元素个数不确定了就不能用原来的东西在判断了:
public void perlocateDown(int hole){
int largest = hole;
for(int k = 0; k < D_ary; k++){
int child = Child(hole,k);
if(child < currentSize && mArray[child].compareTo(mArray[largest]) < 0){
largest = child;
}
}
if(largest != hole){
AnyType temp = mArray[hole];
mArray[hole] = mArray[largest];
mArray[largest] = temp;
perlocateDown(largest);
}
}
可以发现和二叉堆的perlocateDown1函数相似度很高,其是将对应位置的代码替换理解一下,就能够明白了,完整的代码如下:
public class DHeap <AnyType extends Comparable<? super AnyType>>{
private AnyType[] mArray;
private int currentSize;
private static final int DEFAULT_CAPACITY = 10;
private final int D_ary;
public DHeap(int D_ary){
this.D_ary = D_ary;
mArray = (AnyType[]) new Comparable[DEFAULT_CAPACITY];
currentSize = 0;
}
//获取父节点
public int Parent(int hole){
return (hole - 2 + D_ary) / D_ary;
}
//获取子节点
public int Child(int parent, int index_child){
return (D_ary * (parent - 1) + 2 + index_child);
}
//下滤过程,注意,这个为确定好父节点,子节点之后
//通过寻找子节点中最小的下标,然后进行交换(小顶堆)
public void perlocateDown(int hole){
int largest = hole;
for(int k = 0; k < D_ary; k++){
int child = Child(hole,k);
if(child < currentSize && mArray[child].compareTo(mArray[largest]) < 0){
largest = child;
}
}
if(largest != hole){
AnyType temp = mArray[hole];
mArray[hole] = mArray[largest];
mArray[largest] = temp;
perlocateDown(largest);
}
}
//在插入的时候不使用mArray[0],此位置作为插入元素缓冲区
public void insert(AnyType x){
if(currentSize == mArray.length - 1){
enlargeArray(mArray.length * 2 + 1);
}
int hole = ++currentSize;
for(mArray[0] = x;x.compareTo(mArray[Parent(hole)]) < 0; hole = Parent(hole)){
mArray[hole] = mArray[Parent(hole)];
}
mArray[hole] = x;
}
//扩充容量
private void enlargeArray(int newSize) {
AnyType[] old = mArray;
mArray = (AnyType []) new Comparable[newSize];
for(int i = 0; i < old.length; i++){
mArray[i] = old[i];
}
}
//获取小顶堆的最小值
public AnyType findMin(){
if (isEmpty())
throw new UnderflowException();
return mArray[1];
}
public boolean isEmpty(){
return currentSize == 0;
}
//删除小顶堆的最小值
public AnyType deleteMin(){
if(isEmpty())
throw new UnderflowException();
AnyType maxItem = findMin();
//此处相当于将currentSize大小的最后一个元素移动到mArray[0]
//位置覆盖掉原来的最小值,然后将currentSize大小减一,将尾部的
//最后一个元素舍弃,然后重新进行下滤
mArray[1] = mArray[currentSize -- ];
//重新调整,使得符合堆的性质
perlocateDown(1);
return maxItem;
}
}
3.堆排序
如果看过前面的内容,对于堆排序就相当于换了个写法,只不过我们把每次取出来的最大值或者是最小值和最后一个元素交换,然后对1到n-1个元素再次进行堆序性质调整,再取出来,再调整,循环下去。是的,你可能马上想到上面的某些函数,如perlocateDown,以及当输入items时,我们调用的biuldHeap方法,是滴,核心内容就这些。测试代码如下:
public class HeapSort {
/**
* 调整,使得保持堆序性质
* @param array 堆序所存在的数组
* @param hole 根节点,即调整开始的为止,一般为下滤
* @param length 修改后堆序的长度,这里就是变化的部分,即n-1
* 当循环时,就相当于每次取出一个,对剩下的进行调整
*/
public static void HeapAdjust(int[] array,int hole, int length){
int left = 2 * hole;
int right = 2 * hole + 1;
//min
//int min = hole;
int max = hole;
if(max < length / 2){
if(left <= length && array[left] > array[max]){
max = left;
}
if (right <= length && array[right] > array[max]) {
max = right;
}
if(max != hole){
int temp = array[hole];
array[hole] = array[max];
array[max] = temp;
HeapAdjust(array,max,length);
}
}
}
/**
* 仿照二叉堆中perlocateDown1改写的调整堆序的方法,功能相同
* @param array
* @param hole
* @param length
*/
public static void HeapAdjust2(int[] array,int hole, int length){
int child;
int temp = array[hole];
for(; 2 * hole < length; hole = child){
child = 2 * hole;
//控制右边节点不要越界
//可以修改为MaxHeap > -----> <
if(child + 1 != length && array[child + 1] > array[child]){
child++;
}
if(array[child] > temp){
array[hole] = array[child];
}else
break;
}
array[hole] = temp;
}
public static void heapSort (int[] array){
for(int i = array.length / 2; i >= 0; i--){
HeapAdjust2(array,i,array.length);
}
for(int i = array.length - 1; i > 0; i--){
int temp = array[i];
array[i] =array[0];
array[0] = temp;
HeapAdjust2(array,0,i);
}
}
public static void print(int[] array){
for(int i = 0; i< array.length; i++){
System.out.println("element : " + array[i]);
}
}
public static void main(String[] args){
int [] array={5,3,5,8,2,9,12,78,9,2};
heapSort(array);
print(array);
}
}
可以发现,如果把握好几个重点,如何插入,如何调整(perlocateDown)以及如何运用性质进行排序等都比较容易,所以多梳理一下,多练习就会好很多的。