引言
在上一篇博文中,嘗試實現了二叉堆的結構。在本篇博文中,將建立在堆的基礎之上,討論如何用堆實現排序。二叉堆的代碼直接引用昨天的實現源碼,在代碼的基礎上做一些修改使其變成堆排序。筆者目前整理的一些blog針對面試都是超高頻出現的。大家可以點擊鏈接:http://blog.csdn.net/u012403290
二叉堆源碼
如果對二叉堆不是很瞭解,可以查閱相關資料,或者閱讀一下我寫的這篇文章《java實現-堆》,地址是:http://blog.csdn.net/u012403290/article/details/71126768
下面是上篇博文二叉堆的實現源碼:
package com.brickworkers;
public class Heap<T extends Comparable<? super T>> {
private static final int DEFAULT_CAPACITY = 10; //默認容量
private T[] table; //用數組存儲二叉堆
private int size; //表示當前二叉堆中有多少數據
public Heap(int capactiy){
this.size = 0;//初始化二叉堆數據量
table = (T[]) new Comparable[capactiy + 1];//+1是因爲我們要空出下標爲0的元素不存儲
}
public Heap() {//顯得專業些,你就要定義好構造器
this(DEFAULT_CAPACITY);
}
//插入
public void insert(T t){
//先判斷是否需要擴容
if(size == table.length - 1){
resize();
}
//開始插入
//定義一個預插入位置下標
int target = ++size;
//循環比較父節點進行位置交換
for(table[ 0 ] = t; t.compareTo(table[target/2]) < 0; target /= 2){
table[target] = table[target/2];//如果滿足條件,那麼兩者交換,知道找到合適位置(上濾)
}
//插入數據
table[target] = t;
print();
}
//刪除最小
//刪除過程中,需要重新調整二叉堆(下濾)
public void deleteMin(){
if(size == 0){
throw new IllegalAccessError("二叉堆爲空");
}
//刪除元素
table[1] = table[size--];
int target = 1;//從頂部開始重新調整二叉堆
int child;//要處理的節點下標
T tmp = table[ target ];
for( ; target * 2 <= size; target = child )
{
child = target * 2;
if( child != size &&table[ child + 1 ].compareTo( table[ child ] ) < 0 ){//如果右孩子比左孩子小
child++;
}
if( table[ child ].compareTo( tmp ) < 0 ){
table[ target ] = table[ child ];
table[child] = null;
}
else{
break;
}
}
table[ target ] = tmp;
print();
}
//如果插入數據導致達到數組上限,那麼就需要擴容
private void resize(){
T [] old = table;
table = (T []) new Comparable[old.length*2 + 1];//把原來的數組擴大兩倍
for( int i = 0; i < old.length; i++ )
table[ i ] = old[ i ]; //數組進行拷貝
}
//打印數組
private void print(){
System.out.println();
for (int i = 1; i <= size; i++) {
System.out.print(table[i] + " ");
}
System.out.println("二叉堆大小:"+size);
}
public static void main(String[] args) {
Heap<Integer> heap = new Heap<>();
//循環插入0~9的數據
for (int i = 0; i < 10; i++) {
heap.insert(i);
}
//循環刪除3次,理論上是刪除0,1,2
for (int i = 0; i < 3; i++) {
heap.deleteMin();
}
}
}
思考
因爲二叉堆中在堆頂,要麼是最大值,要麼是最小值。我們每次進行一次deleteMin(deleteMax)就可以把這個數據存儲起來,一直到二叉堆刪除結束。那麼,我們考慮兩個方面:①如何把數據放入二叉堆;②如何把排好序的數據提取出來。
對於第一個問題,如何把數據放入二叉堆,最簡單的就是可以調用二叉堆的insert方法,把要排序的數據一個個放入其中,或者優雅一些,我們在源碼中新增一個構造函數,直接入參一個數組,直接轉化成二叉樹。那或許有的小夥伴又要問了,這個優雅不優雅有必要嗎?構造函數裏面進行insert操作?不!我們不在構造函數中調用insert操作(但是在源碼中,我們還是要進行比較),我們可以直接把所有數據全部插入,然後在把它整個調整成二叉堆。在後面的代碼中,我們將比較這兩者的優劣。
對於第二個問題,其實也有兩種解決辦法,第一種是我們創立一個新的數組,這個數組核心功能就是接收二叉堆刪除的數據,這樣一來,直到二叉堆刪除結束,那麼這個數組就是排序好了的數據,實現了數據存儲和提取。第二種,就顯得要高明一點點,我們試想,在二叉堆的實現過程中,我們本身就是用數組來實現的。而且,在上篇博文中,我們討論它的刪除的時候,最後一個元素的位置肯定會變動,所以,這種情況下,我們完全可以在二叉堆刪除堆頂元素的時候,把刪除的數據存儲在最後一個元素的位置。這麼一來在二叉堆刪除結束的時候,那麼這個原本表示二叉堆的數組已經是有序的了。
新增一個接收數組的構造函數
以下的所有代碼,都是建立在上述的二叉堆源碼上進行修改的。
1、接收數組的第一種方式:
//實現方式一,直接用insert來實現數據放入二叉堆中
public Heap(T[] array){
super();
for (T t : array) {
insert(t);//直接調用二叉堆的insert方法進行插入
}
}
第二種方式,我們先把數據全部隨意插入到數組中,然後把數組調整成二叉堆。如何調整呢?在上一篇博文中,我們在刪除的情況下,我們需要通過下濾的方式從新調整二叉堆,其實思想是一樣的,只是刪除的時候調整從最上層開始,而在這裏我們從最下層(標準來說是倒數第二層)開始。下圖表達了操作過程:
在源碼中,我把刪除節點後面的重新調整二叉堆抽離了出來:
private void dealHeap(int target) {
int child;//要處理的節點下標
T tmp = table[ target ];
for( ; target * 2 <= size; target = child )
{
child = target * 2;
if( child != size &&table[ child + 1 ].compareTo( table[ child ] ) < 0 ){//如果右孩子比左孩子小
child++;
}
if( table[ child ].compareTo( tmp ) < 0 ){
table[ target ] = table[ child ];
table[child] = null;
}
else{
break;
}
}
table[ target ] = tmp;
}
2、下面就是第二種實現放置,把數據全部放進去然後調整二叉堆:
public Heap( T [ ] array )
{
size = array.length;
array = (T[]) new Comparable[ size + 2 ];//存儲的數組要略微長
for (int i = 0; i < array.length; i++) {
table[ i++ ] = array[i];
}
//到這裏爲止,數據已經全部都放入了table中
//接下來要做的就是要把這些數據轉變成二叉堆
for(int i = size/2; i > 0; i--){//從倒數第一個有兒子的節點開始處理
dealHeap(i);
}
}
新增有序數據存儲
1、第一種實現,我們在二叉堆中新增一個resultTable,接收每次刪除的數據:
//成員變量
private T[] resultTable;//用於存儲有序數據(每次刪除的數據就放入其中)
//修改構造函數,確認好存儲數組的長度
resultTable = (T[]) new Comparable[ size ];//指定存儲數組大小
//在resultTable末端添加數據
//把數據插入到resultTable末端
private void insertResult(T t){
resultTable[resultTable.length - size] = t;
}
//在delateMin方法中,把insertResult方法加入其中,使得每次刪除的時候,就把數據移除出來
2、不新增數組直接把要刪除的數據放到二叉堆數組的最末端,爲什麼可以如此做,在思考模塊已經表述過了。
//刪除最小
//刪除過程中,需要重新調整二叉堆(下濾)
public void deleteMin(){
if(size == 0){
throw new IllegalAccessError("二叉堆爲空");
}
// //把刪除的元素放入resultTable中
// insertResult(table[1]);
//把要刪除的數據先存儲起來
T temp = table[1];
//刪除元素
table[1] = table[size--];
//把要刪除的數據放到末尾
table[size -- ] = temp;
int target = 1;//從頂部開始重新調整二叉堆
dealHeap(target);
print();
}
這樣一來,最後打印的時候,就可以直接打印二叉堆數組,不過這個時候的二叉堆不存在了。
效率比較
在上面所有的描述中,存在許多情況,接下來我們對他們進行比較,看看哪種效率會更高:
1、比較是把目標排序數組一個個insert塊還是直接放入數組在調整二叉堆快
在測試過程中,我們保證存儲數據一致,都採用額外的數組存儲。
下面是我比較的源碼和結果:
package com.brickworkers;
public class Heap<T extends Comparable<? super T>> {
private static final int DEFAULT_CAPACITY = 10; //默認容量
private T[] table; //用數組存儲二叉堆
private T[] resultTable;//用於存儲有序數據(每次刪除的數據就放入其中)
private int size; //表示當前二叉堆中有多少數據
public Heap(int capactiy){
this.size = 0;//初始化二叉堆數據量
table = (T[]) new Comparable[capactiy + 1];//+1是因爲我們要空出下標爲0的元素不存儲
}
public Heap() {//顯得專業些,你就要定義好構造器
this(DEFAULT_CAPACITY);
}
//實現方式二,直接先插入,然後對數據進行整理成二叉堆
public Heap( T [ ] array , boolean methodType){
if(methodType){
size = 0;
table = (T[]) new Comparable[DEFAULT_CAPACITY + 1];
//實現方式一,直接用insert來實現數據放入二叉堆中
long insertStratTime = System.currentTimeMillis();
for (T t : array) {
insert(t);
}
System.out.println("直接插入的方式耗時:"+(System.currentTimeMillis() - insertStratTime));
}else{
size = array.length;
table = (T[]) new Comparable[ size + 2 ];//存儲的數組要略微長
long dealHeapStratTime = System.currentTimeMillis();
for (int i = 0; i < array.length; i++) {
table[ i+1] = array[i];//arr[0]位置不存放數據
}
//到這裏爲止,數據已經全部都放入了table中
//接下來要做的就是要把這些數據轉變成二叉堆
for(int i = size/2; i > 0; i--){//從倒數第二層開始處理
dealHeap(i);
}
System.out.println("隨機插入重新調整的方式耗時:"+(System.currentTimeMillis() - dealHeapStratTime));
}
resultTable = (T[]) new Comparable[ size ];//指定存儲數組大小
}
//插入
public void insert(T t){
//先判斷是否需要擴容
if(size == table.length - 1){
resize();
}
//開始插入
//定義一個預插入位置下標
int target = ++size;
//循環比較父節點進行位置交換
for(table[ 0 ] = t; t.compareTo(table[target/2]) < 0; target /= 2){
table[target] = table[target/2];//如果滿足條件,那麼兩者交換,知道找到合適位置(上濾)
}
//插入數據
table[target] = t;
print();
}
//刪除最小
//刪除過程中,需要重新調整二叉堆(下濾)
public void deleteMin(){
if(size == 0){
throw new IllegalAccessError("二叉堆爲空");
}
// //把刪除的元素放入resultTable中
insertResult(table[1]);
//把刪除的元素放到末尾
// T temp = table[1];
//刪除元素
table[1] = table[size--];
// table[size -- ] = temp;
int target = 1;//從頂部開始重新調整二叉堆
dealHeap(target);
print();
}
private void dealHeap(int target) {
int child;//要處理的節點下標
T tmp = table[ target ];
for( ; target * 2 <= size; target = child )
{
child = target * 2;
if( child != size &&table[ child + 1 ].compareTo( table[ child ] ) < 0 ){//如果右孩子比左孩子小
child++;
}
if( table[ child ].compareTo( tmp ) < 0 ){
table[ target ] = table[ child ];
table[child] = null;
}
else{
break;
}
}
table[ target ] = tmp;
}
//如果插入數據導致達到數組上限,那麼就需要擴容
private void resize(){
T [] old = table;
table = (T []) new Comparable[old.length*2 + 1];//把原來的數組擴大兩倍
for( int i = 0; i < old.length; i++ )
table[ i ] = old[ i ]; //數組進行拷貝
}
//打印數組
private void print(){
/* System.out.println();
for (int i = 1; i <= size; i++) {
System.out.print(table[i] + " ");
}
System.out.println("二叉堆大小:"+size);*/
}
//把數據插入到resultTable末端
private void insertResult(T t){
resultTable[resultTable.length - size] = t;
}
public static void main(String[] args) {
Integer[] target = new Integer[100000];
for (int i = 0; i < 100000; i++) {
target[i] = i;
}
new Heap<Integer>(target, true);
new Heap<Integer>(target, false);
}
}
//運行結果:
//直接插入的方式耗時:5
//隨機插入重新調整的方式耗時:3
//
從試驗的結果來看,先隨機插入,後調整二叉堆的效率會高很多。當然了,不排除擴容機制佔時的影響,至於爲什麼會如此,我只能告訴你第一種方式的時間複雜度爲O(N),而第二種方式的時間複雜度爲O(NlogN),所以第二種是優先的。
2、我們比較是新數組存儲排序數據還是直接用二叉堆數組存儲排序數據。
package com.brickworkers;
public class Heap<T extends Comparable<? super T>> {
private static final int DEFAULT_CAPACITY = 10; //默認容量
private T[] table; //用數組存儲二叉堆
private T[] resultTable;//用於存儲有序數據(每次刪除的數據就放入其中)
private int size; //表示當前二叉堆中有多少數據
public Heap(int capactiy){
this.size = 0;//初始化二叉堆數據量
table = (T[]) new Comparable[capactiy + 1];//+1是因爲我們要空出下標爲0的元素不存儲
}
public Heap() {//顯得專業些,你就要定義好構造器
this(DEFAULT_CAPACITY);
}
//實現方式二,直接先插入,然後對數據進行整理成二叉堆
public Heap( T [ ] array , boolean methodType){
if(methodType){
size = 0;
table = (T[]) new Comparable[DEFAULT_CAPACITY + 1];
//實現方式一,直接用insert來實現數據放入二叉堆中
for (T t : array) {
insert(t);
}
}else{
size = array.length;
table = (T[]) new Comparable[ size + 2 ];//存儲的數組要略微長
for (int i = 0; i < array.length; i++) {
table[ i+1] = array[i];//arr[0]位置不存放數據
}
//到這裏爲止,數據已經全部都放入了table中
//接下來要做的就是要把這些數據轉變成二叉堆
for(int i = size/2; i > 0; i--){//從倒數第二層開始處理
dealHeap(i);
}
}
resultTable = (T[]) new Comparable[ size ];//指定存儲數組大小
}
//插入
public void insert(T t){
//先判斷是否需要擴容
if(size == table.length - 1){
resize();
}
//開始插入
//定義一個預插入位置下標
int target = ++size;
//循環比較父節點進行位置交換
for(table[ 0 ] = t; t.compareTo(table[target/2]) < 0; target /= 2){
table[target] = table[target/2];//如果滿足條件,那麼兩者交換,知道找到合適位置(上濾)
}
//插入數據
table[target] = t;
print();
}
//刪除最小
//刪除過程中,需要重新調整二叉堆(下濾)
public void deleteMin(boolean store){
if(size == 0){
throw new IllegalAccessError("二叉堆爲空");
}
if(store){
// //把刪除的元素放入resultTable中
insertResult(table[1]);
//刪除元素
table[1] = table[size--];
}else{
//把刪除的元素放到末尾
T temp = table[1];
//刪除元素
table[1] = table[size--];
table[size + 1] = temp;
}
int target = 1;//從頂部開始重新調整二叉堆
dealHeap(target);
print();
}
private void dealHeap(int target) {
int child;//要處理的節點下標
T tmp = table[ target ];
for( ; target * 2 <= size; target = child )
{
child = target * 2;
if( child != size &&table[ child + 1 ].compareTo( table[ child ] ) < 0 ){//如果右孩子比左孩子小
child++;
}
if( table[ child ].compareTo( tmp ) < 0 ){
table[ target ] = table[ child ];
table[child] = null;
}
else{
break;
}
}
table[ target ] = tmp;
}
//如果插入數據導致達到數組上限,那麼就需要擴容
private void resize(){
T [] old = table;
table = (T []) new Comparable[old.length*2 + 1];//把原來的數組擴大兩倍
for( int i = 0; i < old.length; i++ )
table[ i ] = old[ i ]; //數組進行拷貝
}
//打印數組
private void print(){
/*System.out.println();
for (int i = 1; i < table.length; i++) {
System.out.print(table[i] + " ");
}
System.out.println("二叉堆大小:"+size);*/
}
//把數據插入到resultTable末端
private void insertResult(T t){
resultTable[resultTable.length - size] = t;
}
public static void main(String[] args) {
Integer[] target = new Integer[100000];
for (int i = 0; i < 100000; i++) {
target[i] = i;
}
Heap<Integer> heap1 = new Heap<>(target, true);
long copyArrStartTime = System.currentTimeMillis();
//循環刪除
for (int i = 0; i < 100000; i++) {
heap1.deleteMin(true);
}
System.out.println("拷貝數組耗時:"+(System.currentTimeMillis() - copyArrStartTime));
Heap<Integer> heap2 = new Heap<>(target, true);
long ArrStartTime = System.currentTimeMillis();
//循環刪除
for (int i = 0; i < 100000; i++) {
heap2.deleteMin(false);
}
System.out.println("直接存儲耗時:"+(System.currentTimeMillis() - ArrStartTime));
}
}
//輸出結果:
//拷貝數組耗時:23
//直接存儲耗時:14
//
其實這個效率問題是顯而易見的,少一個數組之間的拷貝,當然會顯得高效很多。
希望對你有所幫助。