常用資料結構與排序演算法實現、適用場景及優缺點(Java)
1.下壓棧(後進先出)(能夠動態調整陣列大小的實現):
package Chapter1_3Text; import java.util.Iterator; public class ResizingArrayStack<Item> implements Iterable<Item> { private Item[] a=(Item[]) new Object[1]; //棧元素 private int N=0; //元素數量 public boolean isEmpty(){return N==0;} public int size(){return N;} private void resize(int max){ //將棧移動到一個大小為max的新陣列 Item[] temp=(Item[]) new Object[max]; for(int i=0;i<N;i++) temp[i]=a[i]; a=temp; } public void push(Item item){ //將元素新增到棧頂 if(N==a.length) resize(2*a.length); a[N++]=item; } public Item pop(){ //從棧頂刪除元素 Item item=a[--N]; a[N]=null; //避免物件遊離 if(N>0 && N==a.length/4) resize(a.length/2); return item; } public Iterator<Item> iterator(){ return new ReverseArrayIterator(); } private class ReverseArrayIterator implements Iterator<Item>{ //支援後進先出的迭代 private int i=N; public boolean hasNext(){return i>0;} public Item next(){return a[--i];} public void remove(){ } } }
2.下壓堆疊(連結串列實現):
package Chapter1_3Text; import edu.princeton.cs.algs4.StdIn; import edu.princeton.cs.algs4.StdOut; import java.util.Iterator; public class Stack<Item> implements Iterable<Item> { private Node first; //棧頂(最近新增的元素) private int N; //元素數量 private class Node{ //定義了節點的巢狀類 Item item; Node next; } public boolean isEmpty(){return first==null;} //或:N==0 public int size(){return N;} public void push(Item item){ //向棧頂新增元素 Node oldfirst=first; first=new Node(); first.item=item; first.next=oldfirst; N++; } public Item pop(){ //從棧頂刪除元素 Item item=first.item; first=first.next; N--; return item; } //iterator()的實現在1.4節 public Iterator<Item> iterator(){ return new ListIterator(); } private class ListIterator implements Iterator<Item>{ private Node current=first; public boolean hasNext(){return current!=null;} public void remove(){} public Item next(){ Item item=current.item; current=current.next; return item; } } public static void main(String[] args){ //建立一個棧並根據StdIn中的指示壓入或彈出字串 Stack<String> s=new Stack<String>(); while(!StdIn.isEmpty()){ String item=StdIn.readString(); if(!item.equals("-")) s.push(item); else if(!s.isEmpty()) StdOut.print(s.pop()+" "); } StdOut.println("("+s.size()+" left on stack)"); } }
3.先進先出佇列:
package Chapter1_3Text; import edu.princeton.cs.algs4.StdIn; import edu.princeton.cs.algs4.StdOut; import java.util.Iterator; public class Queue<Item> implements Iterable<Item> { private Node first; //指向最早新增的節點的連結 private Node last; //指向最近新增的節點的連結 private int N; //佇列中的元素數量 private class Node{ //定義了節點的巢狀類 Item item; Node next; } public boolean isEmpty(){return first==null;} //或:N==0 public int size(){return N;} public void enqueue(Item item){ //向表尾新增元素 Node oldlast=last; last=new Node(); last.item=item; last.next=null; if(isEmpty()) first=last; else oldlast.next=last; N++; } public Item dequeue(){ //從表頭刪除元素 Item item=first.item; first=first.next; if(isEmpty()) last=null; N--; return item; } //iterator()的實現要到1.4節 public Iterator<Item> iterator(){ return new ListIterator(); } private class ListIterator implements Iterator<Item>{ private Node current=first; public boolean hasNext(){return current!=null;} public void remove(){} public Item next(){ Item item=current.item; current=current.next; return item; } } public static void main(String[] args){ //建立一個佇列並操作字串入列或出列 Queue<String> q=new Queue<String>(); while(!StdIn.isEmpty()){ String item=StdIn.readString(); if(!item.equals("-")) q.enqueue(item); else if(!q.isEmpty()) StdOut.print(q.dequeue()+" "); } StdOut.println("("+q.size()+" left on queue)"); } }
4.揹包:
package Chapter1_3Text;
import java.util.Iterator;
public class Bag<Item> implements Iterable<Item> {
private Node first; //連結串列的首節點
private class Node{
Item item;
Node next;
}
public void add(Item item){
//和Stack的push()方法完全相同
Node oldfirst=first;
first=new Node();
first.item=item;
first.next=oldfirst;
}
public Iterator<Item> iterator(){
return new ListIterator();
}
private class ListIterator implements Iterator<Item>{
private Node current=first;
public boolean hasNext(){return current!=null;}
public void remove(){}
public Item next(){
Item item=current.item;
current=current.next;
return item;
}
}
}
5.比較兩種排序演算法執行時間快慢的方法:
import edu.princeton.cs.algs4.*;
public class SoftCompare {
public static double time(String alg,Double[] a){
Stopwatch timer=new Stopwatch();
if(alg.equals("Insertion")) Insertion.sort(a);
if(alg.equals("Selection")) Selection.sort(a);
if(alg.equals("Shell")) Shell.sort(a);
if(alg.equals("Merge")) Merge.sort(a);
if(alg.equals("Quick")) Quick.sort(a);
if(alg.equals("Heap")) Heap.sort(a);
return timer.elapsedTime();
}
public static double timeRandomInput(String alg,int N,int T){
//使用演算法alg將T個長度為N的陣列排序
double total=0.0;
Double[] a=new Double[N];
for(int t=0;t<T;t++){
//進行一次測試(生成一個數組並排序)
for(int i=0;i<N;i++)
a[i]= StdRandom.uniform();
total+=time(alg,a);
}
return total;
}
public static void main(String[] args){
String alg1=args[0];
String alg2=args[1];
int N=Integer.parseInt(args[2]);
int T=Integer.parseInt(args[3]);
double t1=timeRandomInput(alg1,1000,100); //演算法1的總時間
double t2=timeRandomInput(alg2,1000,100); //演算法2的總時間
StdOut.printf("For %d random Doubles\n %s is",N,alg1);
StdOut.printf("%.1f times faster than %s\n",t2/t1,alg2);
}
}
6.選擇排序,演算法的時間效率取決於比較的次數:
public class Selection {
public static void sort(Comparable[] a){
//將a[]按升序排列
int N=a.length; //陣列長度
for(int i=0;i<N;i++){
//將a[i]和a[i+1..N]中最小的元素交換
int min=i; //最小元素的索引
for(int j=i+1;j<N;j++)
if(less(a[j],a[min])) min=j;
exch(a,i,min);
}
}
private static boolean less(Comparable v,Comparable w){return v.compareTo(w)<0;}
private static void exch(Comparable[] a,int i,int j){
Comparable t=a[i];
a[i]=a[j];
a[j]=t;
}
}
對於長度為N的陣列,選擇排序需要大約(N^2)/2次比較和N次交換,陣列元素交換位置的次數和陣列的大小是線性關係。
7.插入排序,給要插入的元素騰出空間,將其餘所有元素在插入之前都向右移動一位:
import edu.princeton.cs.algs4.In;
public class Insertion {
public static void sort(Comparable[] a){
//將a[]按升序排列
int N=a.length;
for(int i=1;i<N;i++){
//將a[i]插入到a[i-1]、a[i-2]、a[i-3]...之中
for(int j=i;j>0 && less(a[j],a[j-1]);j--)
exch(a,j,j-1);
}
}
private static boolean less(Comparable v,Comparable w){return v.compareTo(w)<0;}
private static void exch(Comparable[] a,int i,int j){
Comparable t=a[i];
a[i]=a[j];
a[j]=t;
}
public static boolean isSorted(Comparable[] a){
//測試陣列元素是否有序
for(int i=1;i<a.length;i++)
if(less(a[i],a[i-1])) return false;
return true;
}
public static void main(String[] args){
String[] a= In.readStrings();
sort(a);
assert isSorted(a);
}
}
與選擇排序不同,插入排序所需的時間取決於輸入的元素的初始順序。對於隨機排列的長度為N且主鍵不重複的陣列,平均情況下插入排序需要約(N^2)/4次比較以及大約(N^2)/4次交換。最壞情況下需要約(N^2)/2次比較和約(N^2)/2次交換,最好情況下需要N-1次比較和0次交換。
8.插入排序對部分有序的陣列很有效,但是選擇排序不然。當順序倒置的元素數量很少時,插入排序比其他排序演算法都要快。插入排序需要的交換操作和陣列中元素大小順序倒置的數量相同,需要的比較次數大於等於倒置的數量,小於等於倒置的數量加上陣列的大小再減一。由於插入排序不會移動比插入的元素更小的元素,所需的比較次數平均只有選擇排序的一半。
9.希爾排序,在插入排序中加入一個外迴圈來以h為間隔,按照遞增序列遞減得到:
import edu.princeton.cs.algs4.In;
import edu.princeton.cs.algs4.StdOut;
public class Shell {
public static void sort(Comparable[] a){
//將a[]按升序排列
int N=a.length;
int h=1;
while(h<N/3) h=3*h+1; //1,4,13,40,121,364,1093...
while(h>=1){
//將陣列變為h有序
for(int i=h;i<N;i++){
//將a[i]插入到a[i-h],a[i-3*h],a[i-3h]...之中
for(int j=i;i>=h && less(a[j],a[j-h]);j-=h)
exch(a,j,j-h);
}
h=h/3;
}
}
private static boolean less(Comparable v,Comparable w){return v.compareTo(w)<0;}
private static void exch(Comparable[] a,int i,int j){
Comparable t=a[i];
a[i]=a[j];
a[j]=t;
}
public static boolean isSorted(Comparable[] a){
//測試陣列元素是否有序
for(int i=1;i<a.length;i++)
if(less(a[i],a[i-1])) return false;
return true;
}
private static void show(Comparable[] a){
//在單行中列印陣列
for(int i=0;i<a.length;i++)
StdOut.print(a[i]+" ");
StdOut.println();
}
public static void main(String[] args){
//從標準輸入讀取字串,將它們排序並輸出
String[] a= In.readStrings();
sort(a);
assert isSorted(a);
show(a);
}
}
該演算法的效能不僅取決於每個排序子陣列的間隔h,還取決於h之間的數學性質,如公因子等。希爾排序比插入排序和選擇排序要快得多,並且陣列越大,優勢越大。它的執行時間複雜度達不到平方級別。在最壞的情況下,上面的實現(h以三倍來變化)的比較次數和N^(3/2)成正比。
10.對於中等大小的陣列,希爾排序的執行時間是可以接受的。它的程式碼量不大,也不需要使用額外的記憶體空間。除非對於很大的N,其他更高效的演算法可能只會比希爾排序快兩倍甚至不到,而且程式碼更復雜,如果需要解決一個排序問題而又沒有系統排序函式可用(例如直接接觸硬體或者運行於嵌入式系統中的程式碼),可以先用希爾排序,再考慮是否替換為更復雜的排序演算法。而對於部分有序和小規模的陣列,應使用插入排序。
11.自頂向下的歸併排序,先將所有元素複製到aux[]中,然後再歸併回a[]中:
public class Merge { //自頂向下的歸併排序,先將所有元素複製到aux[]中,然後再歸併回a[]中
private static Comparable[] aux; //歸併所需的輔助陣列
public static void sort(Comparable[] a){
aux=new Comparable[a.length]; //一次性分配空間,這裡必須要初始化容量為a.length,否則下面for迴圈k<=hi情況下遞增到最後會報錯NullPointerException
sort(a,0,a.length-1);
}
private static void sort(Comparable[] a,int lo,int hi){
//將陣列a[lo..hi]排序
if(hi<=lo) return;
int mid=lo+(hi-lo)/2;
sort(a,lo,mid); //將左半邊排序
sort(a,mid+1,hi); //將右半邊排序
merge(a,lo,mid,hi); //歸併結果
}
public static void merge(Comparable[] a,int lo,int mid,int hi){
//將a[lo..mid]和a[mid+1..hi]歸併
int i=lo,j=mid+1;
for(int k=lo;k<=hi;k++) //將a[lo..hi]複製到aux[lo..hi]
aux[k]=a[k];
for(int k=lo;k<=hi;k++){
//歸併回到a[lo..hi]
if(i>mid) a[k]=aux[j++]; //左半邊用盡,取右半邊的元素
else if(j>hi) a[k]=aux[i++]; //右半邊用盡,取左半邊的元素
else if(less(aux[j],aux[i])) a[k]=aux[j++]; //右半邊的當前元素小於左半邊的當前元素,取右半邊的元素
else a[k]=aux[i++]; //右半邊的當前元素大於左半邊的當前元素,取左半邊的元素
}
}
private static boolean less(Comparable v,Comparable w){return v.compareTo(w)<0;}
}
歸併排序是演算法設計中分治思想的典型應用,對於長度為N的任意陣列,自頂向下的歸併排序需要1/2*NlgN至NlgN次比較,最多需要訪問陣列6NlgN次。所以,歸併排序所需的時間和NlgN成正比,可以用歸併排序處理數百萬甚至更大規模的陣列,但是插入排序和選擇排序做不到。歸併排序的主要缺點是輔助陣列所使用的額外空間和N的大小成正比。
12.自底向上的歸併排序,多次遍歷整個陣列,根據子陣列大小進行兩兩歸併,四四歸併,八八歸併一直下去。每下一輪中子陣列的大小會翻倍:
public class MergeBU {
private static Comparable[] aux; //歸併所需的輔助陣列
public static void merge(Comparable[] a,int lo,int mid,int hi){
//將a[lo..mid]和a[mid+1..hi]歸併
int i=lo,j=mid+1;
for(int k=lo;k<=hi;k++) //將a[lo..hi]複製到aux[lo..hi]
aux[k]=a[k];
for(int k=lo;k<=hi;k++){
//歸併回到a[lo..hi]
if(i>mid) a[k]=aux[j++]; //左半邊用盡,取右半邊的元素
else if(j>hi) a[k]=aux[i++]; //右半邊用盡,取左半邊的元素
else if(less(aux[j],aux[i])) a[k]=aux[j++]; //右半邊的當前元素小於左半邊的當前元素,取右半邊的元素
else a[k]=aux[i++]; //右半邊的當前元素大於左半邊的當前元素,取左半邊的元素
}
}
private static boolean less(Comparable v,Comparable w){return v.compareTo(w)<0;}
public static void sort(Comparable[] a){
//進行lgN次兩兩歸併
int N=a.length;
aux=new Comparable[N];
for(int sz=1;sz<N;sz=sz+sz) //sz子陣列大小
for(int lo=0;lo<N-sz;lo+=sz+sz) //lo:子陣列索引
merge(a,lo,lo+sz-1,Math.min(lo+sz+sz-1,N-1));
}
}
對於長度為N的任意陣列,自底向上的歸併排序需要1/2NlgN至NlgN次比較,最多訪問陣列6NlgN次。自底向上的歸併排序比較適合用連結串列組織的資料。這種方法只需要重新組織連結串列連結就能將連結串列原地排序,不需要建立任何新的連結串列節點,不像前面幾種排序需要額外建立輔助陣列。自頂向下的歸併排序是化整為零,然後遞迴解決的方式,而自底向上的歸併排序是用循序漸進的方式來解決。
13.任何基於比較的演算法將長度為N的陣列排序需要lg(N!)~NlgN次比較,在二叉樹中,高度為h的樹,葉子節點的數量為N!~2^h個。所以,歸併排序在最壞情況下的比較次數約為NlgN。歸併排序是一種漸近最優的基於比較排序的演算法。
14.快速排序的步驟:
(1)先將輸入的陣列隨機打亂。
(2)將一個數組切分成兩個子陣列,左邊子陣列元素都不大於中間元素,右邊子陣列元素都不小於中間元素。即切分位置a[j]的條件是,a[lo]到a[j-1]中所有元素都不大於a[j],而且a[j+1]到a[hi]中的所有元素都不小於a[j],切分的具體實現是:將a[lo]作為初始切分元素,從陣列的左端向右掃描直到找到一個大於等於a[lo]的元素a[i],再從陣列右端向左掃描到一個小於等於a[lo]的元素a[j],然後將a[i]和a[j]交換位置,如此繼續,直到i和j兩個指標相遇(兩個指標還未交錯),在兩指標交錯後就將a[lo]與a[j]交換位置,一開始的a[lo]現在變成了a[j],在兩個指標相遇交錯前的位置i上,這個元素就是中間的切分元素。
(3)繼續遞迴,將上一輪的中間切分元素a[j](值是一開始的a[lo])的前一個值a[j-1]作為新一輪切分的a[hi],再如(2)的規則切分,如此迴圈直到第一輪切分的中間切分元素a[j]的左邊都已經被排序。
(4)接著從第一輪切分後的中間切分元素a[j]的右邊開始快速排序,也就是將當時的a[j+1]作為右邊第一次切分的a[lo],繼續切分,過程和(3)相同,最後第一輪切分的a[j]的左邊和右邊都已經有序,快速排序完成。
具體程式碼實現如下:
import edu.princeton.cs.algs4.StdRandom;
public class Quick {
public static void sort(Comparable[] a){
StdRandom.shuffle(a); //把陣列打亂,消除對輸入的依賴
sort(a,0,a.length-1);
}
private static void sort(Comparable[] a,int lo,int hi){
if(hi<=lo) return;
int j=partition(a,lo,hi); //切分成兩個子陣列
sort(a,lo,j-1); //將左半部分a[lo.j-1]排序
sort(a,j+1,hi); //將右半部分a[j+1..hi]排序
}
private static int partition(Comparable[] a,int lo,int hi){
//將陣列切分為a[lo..i-1],a[i],a[i+1..hi]
int i=lo,j=hi+1; //向右向左的掃描指標,j為hi+1,這樣從右到左掃描會先從hi開始掃描
Comparable v=a[lo]; //切分元素初始化為第一個元素,且該v以後為定值,就是最初的a[lo],以後a[lo]的值如何改變都與v無關
while(true){
//掃描左右兩側,檢查掃描是否結束並交換元素
while(less(a[++i],v)) if(i==hi) break;
while(less(v,a[--j])) if(j==lo) break;
if(i>=j) break;
exch(a,i,j);
}
exch(a,lo,j); //將v=a[j]放入正確的位置,當掃描指標i和j相遇時,將a[j]與a[lo]交換
return j; //a[lo..j-1]<=a[j]<=a[j+1..hi]達成
}
private static boolean less(Comparable v,Comparable w){return v.compareTo(w)<0;}
private static void exch(Comparable[] a,int i,int j){
Comparable t=a[i];
a[i]=a[j];
a[j]=t;
}
}
在迴圈中,a[i]小於一開始的a[lo]的值v時增大i,a[j]大於v時減小j(注意v是定值不再變化),然後交換a[i]和a[j]來保證i左側的元素都不大於v,j右側的元素都不小於v,當掃描指標相遇或交錯後交換a[lo]和a[j],切分結束,這樣切分的中間值就是a[j]。快速排序的通俗理解圖例如下:
15.快速排序與歸併排序的比較:快速排序的優點是原地排序(只需要一個很小的輔助棧),且將長度為N的陣列排序所需的時間和NlgN成正比。快速排序也是一種分治的排序演算法,將一個數組切分成兩個子陣列,將兩部分獨立的排序,與歸併排序彼此互補。歸併排序將陣列分成兩個子陣列分別排序,並將有序的子陣列歸併以將整個陣列排序;而快速排序將陣列排序的方式則是當兩個子陣列都有序時整個陣列也就自然有序了。在歸併排序中,遞迴呼叫發生在處理整個陣列之前,而快速排序中,遞迴呼叫發生在處理整個陣列之後。在歸併排序中,一個數組被等分為兩半,而在快速排序中,切分的位置取決於陣列的內容。對於含有以任意概率分佈的重複元素的輸入,歸併排序無法保證最佳效能。
16.快速排序的優缺點:原地排序,只需要一個很小的輔助棧,將長度為N的陣列排序所需的時間和NlgN成正比。歸併排序和希爾排序一般都比快速排序慢,因為前兩者還在內迴圈中移動資料。快速排序的另一個速度優勢在於比較次數較少。但是,該實現有個潛在的缺點:在切分不平衡時該程式會極為低效,可能會使效能達到平方級別。快速排序最多需要約(N^2)/2次比較,但一開始的隨機打亂陣列可以預防這種情況。但是,對於小陣列,快速排序比插入排序慢。
17.三向切分的快速排序,是從左到右遍歷陣列一次,維持一個指標lt使a[lo..lt-1]中的元素都小於v=a[lo],一個指標gt使a[gt+1..hi]中的元素都大於v,一個指標i使a[lt..i-1]中的元素都等於v,而a[i..gt]中的元素還未確定。一開始i和lo相等,對a[i]進行以下比較及處理:
(1)a[i]小於v,將a[lt]和a[i]交換,將lt和i加一。
(2)a[i]大於v,將a[gt]和a[i]交換,將gt減一。
(3)a[i]等於v,將i加一。
這些操作會不斷縮小gt-i的值,因此除非和切分元素相等,其他元素都會被交換。
實現如下:
import edu.princeton.cs.algs4.StdRandom;
public class Quick3way {
public static void sort(Comparable[] a){
StdRandom.shuffle(a);
sort(a,0,a.length-1);
}
private static void sort(Comparable[] a,int lo,int hi){
if(hi<=lo) return;
int lt=lo,i=lo+1,gt=hi;
Comparable v=a[lo]; //這邊v為定值,不再隨迴圈中的a[lo]變化
while(i<=gt){
int cmp=a[i].compareTo(v);
if(cmp<0) exch(a,lt++,i++);
else if(cmp>0) exch(a,i,gt--);
else i++;
} //現在a[lo..lt-1]<v=a[lt..gt]<a[gt+1..hi]成立
sort(a,lo,lt-1);
sort(a,gt+1,hi);
}
private static void exch(Comparable[] a,int i,int j){
Comparable t=a[i];
a[i]=a[j];
a[j]=t;
}
}
對於存在大量重複元素的陣列,這種方法比標準快速排序的效率高得多。
18.最小優先佇列,優先刪除最小的元素,留下最大的多個元素,例如在銀行交易中,顯示交易數額最大的使用者資訊:
import edu.princeton.cs.algs4.*;
public class TopM { //最小優先佇列,留下最大的,刪掉最小的
public static void main(String[] args){
//列印輸入流中最大的M行
int M=Integer.parseInt(args[0]);
MinPQ<Transaction> pq=new MinPQ<Transaction>(M+1); //優先佇列的容量為M+1,只存最大的這麼多個元素
while(StdIn.hasNextLine()){
//為下一行輸入建立一個元素並放入優先佇列中
pq.insert(new Transaction(StdIn.readLine()));
if(pq.size()>M)
pq.delMin(); //如果優先佇列中存在M+1個元素則刪除其中最小的元素,如果新加入的元素最小也會被刪掉
} //最大的M個元素都在優先佇列中
Stack<Transaction> stack=new Stack<Transaction>();
while(!pq.isEmpty()) stack.push(pq.delMin()); //棧中存放順序是最小優先佇列中,較小的先push進來放在棧底,最大的在棧頂
for(Transaction t:stack) StdOut.println(t); //因為上一行的存放方式,較大元素在棧頂所以先被輸出
}
}
優先佇列適合總資料量太大,無法排序甚至無法全部裝進記憶體的場合,例如10億個元素中選出最大的10個,只需要一個能儲存10個元素的佇列即可。優先佇列不是執行緒安全的,入隊和出隊的時間複雜度是 O(log(n)) 。
19.二叉樹/二叉堆。在二叉堆陣列中,每個元素都大於等於另兩個特定位置的元素,當一棵二叉樹的每個節點都大於等於它的兩個子節點時,被稱為堆有序。根節點是堆有序的二叉樹中的最大節點。堆有序的二叉樹稱為完全二叉樹,可以通過陣列來表示,但不使用陣列的第一個位置(即0),根節點從第二個位置,也就是1開始,具體方法就是將二叉樹的節點按照層級順序放入陣列中,根節點的子節點在位置2和3,子節點的子節點分別在位置4,5,6,7,以此類推,如下所示:
因此,位置k的節點的父節點的位置為k/2,而它兩個子節點的位置為2k和2k+1。基於二叉堆的優先佇列實現如下:
public class MaxPQ<Key extends Comparable<Key>> {
private Key[] pq; //基於堆的完全二叉樹
private int N=0; //儲存於pq[1..N]中,pq[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; //刪除交換到最後一個元素的原根節點,並防止物件遊離,這裡的N+1其實就是上一行的N,只不過上一行已經將N的大小減一,將引用指向原來最後一個元素的前一個元素
sink(1); //原來最後一個元素插入到了樹的根節點處,根據元素大小下沉至合適的層級
return max;
}
private boolean less(int i,int j){return pq[i].compareTo(pq[j])<0;}
private void exch(int i,int j){
Key t=pq[i];
pq[i]=pq[j];
pq[j]=t;
}
private void swim(int k){
while(k>1 && less(k/2,k)){
exch(k/2,k);
k=k/2;
}
}
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;
}
}
}
對於一個含有N個元素的基於二叉堆的優先佇列,插入元素操作只需不超過(lgN+1)次比較,刪除最大元素操作需要不超過2lgN次比較。用二叉堆實現的優先佇列在現代應用程式中越來越重要,因為它能在插入操作和刪除最大元素操作混合的動態場景中保證對數級別的執行時間。
20.堆排序,一開始將原始陣列重新組織安排進一個二叉堆中,然後進行下沉排序,從二叉堆中按遞減順序不斷重複取出並刪除最大元素,該排序方法的優點是在排序時可以將需要排序的陣列本身作為堆,無需任何額外空間:
import edu.princeton.cs.algs4.StdIn;
import edu.princeton.cs.algs4.StdOut;
public class HeapSort {
//堆排序
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--); //將當前N減小,當前的N後面的最大元素都已有序並被排除出二叉堆,因此只排序N前面的元素
sink(a,1,N);
}
}
private static boolean less(Comparable[] a,int i,int j){return a[i-1].compareTo(a[j-1])<0;}
private static void exch(Comparable[] a,int i,int j){
Comparable t=a[i-1]; //因為二叉堆的索引是1到N,而一般陣列是0到N-1,為了與其他排序演算法實現一致而減一,即將a[0]至a[N-1]排序
a[i-1]=a[j-1];
a[j-1]=t;
}
private static void sink(Comparable[] a,int k,int N){
while(2*k<=N){
int j=2*k;
if(j<N && less(a,j,j+1)) j++;
if(!less(a,k,j)) break;
exch(a,k,j);
k=j;
}
}
private static boolean isSorted(Comparable[] a){
for(int i=1;i<a.length;i++)
if(less(a,i,i-1)) return false;
return true;
}
private static void show(Comparable[] a){
for(int i=0;i<a.length;i++){
StdOut.println(a[i]);
}
}
public static void main(String[] args){
String[] a= StdIn.readStrings();
HeapSort.sort(a);
show(a);
}
}
通俗理解的過程圖如下:
堆排序的過程與選擇排序有些類似(按照降序而非升序取出所有元素),但所需的比較要少得多,因為二叉堆提供了一種從未排序部分找到最大元素的有效方法。將N個元素排序,堆排序只需少於(2NlgN+2N)次比較,以及一半次數的交換。堆排序適合例如嵌入式系統或低成本移動裝置中容量有限的場景,但很少應用於現代系統的很多應用中,因為它無法利用快取,陣列元素很少和相鄰其他元素比較,因此快取未命中的次數遠遠高於大多數比較都在相鄰元素間進行的演算法,如快速排序,歸併排序,希爾排序。
21.在很多應用中都會需要將一組物件根據含有的幾個不同屬性,來進行對應的排序,例如Transaction物件中含有客戶名稱,日期和交易金額,有時需要按金額大小排序,有時可能需要按照另一個屬性來排序,一個元素(物件)的多種屬性都可能被用作排序的鍵。要實現這種根據不同屬性各自排序的靈活性,Comparator介面正合適,可以通過定義多種比較器來完成,例如:
import edu.princeton.cs.algs4.Date;
import java.util.Comparator;
public class Transaction {
private String who;
private Date when;
private double amount;
public static void sort(Object[] a,Comparator c){ //在引數c中傳入多種Comparator可以實現針對物件的不同屬性進行排序的方法,例如傳入下方的new Transaction.WhenOrder()
int N=a.length;
for(int i=1;i<N;i++)
for(int j=i;j>0 && less(c,a[j],a[j-1]);j--)
exch(a,j,j-1);
}
private static boolean less(Comparator c,Object v,Object w){return c.compare(v,w)<0;}
private static void exch(Object[] a,int i,int j){
Object t=a[i];
a[i]=a[j];
a[j]=t;
}
public static class WhoOrder implements Comparator<Transaction>{ //根據日期進行排序的比較器
public int compare(Transaction v,Transaction w){return v.when.compareTo(w.when);}
}
public static class HowMuchOrder implements Comparator<Transaction>{ //根據金額大小進行排序的比較器
public int compare(Transaction v,Transaction w){
if(v.amount<w.amount) return -1;
else if(v.amount>w.amount) return +1;
else return 0;
}
}
}
這樣定義之後,需要將Transaction物件的陣列按照時間排序可以呼叫Transaction.sort(a, new Transaction.WhenOrder()),或者按照金額排序可以使用Transaction.sort(a, new Transaction.HowMuchOrder())。
22.如果一個排序演算法能夠保持陣列中重複元素的相對順序位置則可以認為是穩定的,例如相同地名下時間依然按照先後順序排列而不是打亂。上述演算法中,插入排序和歸併排序是穩定的,但是選擇排序、希爾排序、快速排序和堆排序不是。穩定性的通俗描述如下圖所示:
23.除了希爾排序(複雜度只是一個近似值)、插入排序(複雜度取決於輸入元素的排列情況)和上面快速排序的兩個版本(複雜度和概率有關,取決於輸入元素的分佈狀況)之外,將其他排序演算法的執行時間的增長數量級乘以適當的常數就能大致估計出執行時間。各種排序演算法的效能特點如下所示:
24.在大多數實際情況中,快速排序是最佳選擇。快速排序之所以是最快的通用排序演算法是因為它的內迴圈中指令很少,而且還能利用快取,因為它總是順序地訪問資料。所以它的執行時間增長數量級為約cNlgN,這裡的c比其他線性對數級別的排序演算法的相應常數都要小。在使用三向切分快速排序後,對於實際應用中可能出現的某些分佈的輸入就變成線性級別的了,而其他排序演算法依然需要線性對數時間。但是,如果穩定性很重要而空間充足,歸併排序是最好的選擇。而在執行時間至關重要的任何排序應用中,應考慮使用快速排序。對於Java來說,會對原始資料型別使用三向切分的快速排序,而對引用型別使用歸併排序。這些選擇實際上也表示了用速度和空間(對於原始資料型別)來換取穩定性(對於引用型別)。
25.找到一組數中第k小的元素:
import edu.princeton.cs.algs4.StdRandom;
public class KthSmallest {
//找到一組數中的第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]; //切分之後,a[j]左邊的數小於等於a[j],右邊的數大於等於a[j],如果j碰巧等於k,則該a[j]就是第k小的數
else if(j>k) hi=j-1;
else if(j<k) lo=j+1;
}
return a[k];
}
private static int partition(Comparable[] a,int lo,int hi){ //快速排序中的切分操作
//將陣列切分為a[lo..i-1],a[i],a[i+1..hi]
int i=lo,j=hi+1; //向右向左的掃描指標,j為hi+1,這樣從右到左掃描會先從hi開始掃描
Comparable v=a[lo]; //切分元素初始化為第一個元素,且該v以後為定值,就是最初的a[lo],以後a[lo]的值如何改變都與v無關
while(true){
//掃描左右,檢查掃描是否結束並交換元素
while(less(a[++i],v)) if(i==hi) break;
while(less(v,a[--j])) if(j==lo) break;
if(i>=j) break;
exch(a,i,j);
}
exch(a,lo,j); //將v=a[j]放入正確的位置,當掃描指標i和j相遇時,將a[j]與a[lo]交換
return j; //a[lo..j-1]<=a[j]<=a[j+1..hi]達成
}
private static boolean less(Comparable v,Comparable w){return v.compareTo(w)<0;}
private static void exch(Comparable[] a,int i,int j){
Comparable t=a[i];
a[i]=a[j];
a[j]=t;
}
}
在快速排序的切分操作中,如果k<j,就需要切分左子陣列(令hi=j-1),如果k>j,就需要切分右子陣列(令lo=j+1)。這個迴圈保證了陣列中lo左邊的元素都小於等於a[lo..hi],而hi右邊的元素都大於等於a[lo..hi]。這個演算法是線性級別的,比較次數的上界比快速排序略高,該演算法與快速排序的一個共同點就是同樣依賴隨機的切分元素,因此它的效能保證也來自於概率。