Version:1.0 StartHTML:0000000168 EndHTML:0000122808 StartFragment:0000000499 EndFragment:0000122791
排序
索引
二次排序算法
選擇排序
插入排序
遞歸排序算法
歸併排序
快速排序
堆排序
附:模擬棧實現快速排序
另一種構建初始堆的方法
二次排序算法
選擇排序
先在數組找最小的元素,放在第一個位置.然後是下一個最小值,如此循環,直到最後一個元素,依次放到第二,第三...個位置
/**
* 選擇排序
* @param data 待排序的數組
* @param first 待排序序列開始元素的位置
* @param n 序列的長度
*/
public static void selectionsort(int []data,int first,int n){
int min;
int temp;
for(int i=first;i<first+n-1;i++){
min=i;
for(int j=i+1;j<first+n;j++){
if(data[min]>data[j])
min=j;
}
temp=data[i];
data[i]=data[min];
data[min]=temp;
}
}
任何基於元素交換的排序算法都稱爲交換排序.
選擇排序是最簡單的排序算法
選擇排序的效率分析
(n-1)"外層循環次數"*(循環控制動作"固定值"+尋找合適下標所需代價)<=
(n-1)*(一常數+5"內層循環的初始動作"+另一常數"交換變量"*n)
展開式子
n^2*常數+n*常數+常數”可能爲負”
選擇排序中的運行時間與數組的初值無關.算法都要進行相同次數的比較.所以無論最好,平均,最差選擇排序的時間複雜度都爲O(n^2)
插入排序
每次獲得一個元素,構建另一個有序列表
依次按順序將數組中的一個元素添加到該元素前面已排好序的序列中的適當位置
public static void insertsort(int []data,int first,int n){
int temp;
int i;
int j;
for(i=first+1;i<first+n;i++){
//將data[i]插入到data[i-1]到data[first]的序列中,使得data[first]到data[i]都有序
temp=data[i];
for(j=i;(j>first)&&(data[j-1]>temp);j--)
data[j]=data[j-1];
data[j]=temp;
}
}
插入排序—效率
最壞情況需要(n-1)+....+2+1次移動和比較
結果爲(n-1)(n-2)/2,O(n^2)
平均情況是最壞情況的一半也是O(n^2)
插入排序最好情況下效率是線性的
遞歸排序算法
遞歸分治
分治的思想是當問題較小時,直接解決.當問題較大時,將它劃分爲兩個較小的問題,每個問題約爲原問題的一半
分治的排序範式
1,將要排序的元素分成大小相等(或幾乎相等)的兩組
2,對這些較小的元素組排序(通過遞歸調用)
3,將兩個排好序的組合併成一個大的已經排好序的序列
歸併排序是分治排序的最簡單實現
當數組段只有一個元素時,停止遞歸調用.只有一個元素的數組自然有序
public static void mergesort(int []data,int first,int n){
int n1,n2;
//當n=1時,即數組中只有一個元素時,便自然有序
if(n>1){
//計算兩個半截數組的大小.
n1=n/2;
n2=n-n1;
//用遞歸對兩個子數組進行排序
mergesort(data,first,n1);
mergesort(data, first+n1, n2);
//將兩個有序(first~n1,first+ni~n2)序列合併爲一個有序序列,
merge(data,first,n1,n2);
}
}
private static void merge(int[] data,int first,int n1,int n2){
int cursor1=0;
int cursor2=0;
//需要額外的空間來保存正在排序的數組.
int temp[]=new int [n1+n2];
int items=0;
while(true){
if(cursor1==n1){
for(int j=items;j<n1+n2;j++){
temp[j]=data[first+n1+cursor2];
cursor2++;
}
break;
}
if(cursor2==n2){
for(int j=items;j<n1+n2;j++){
temp[j]=data[first+cursor1];
cursor1++;
}
break;
}
if(data[first+cursor1]>data[first+n1+cursor2]){
temp[items]=data[first+n1+cursor2];
cursor2++;
}else{
temp[items]=data[first+cursor1];
cursor1++;
}
items++;
}
for(int i=0;i<n1+n2;i++){
data[first+i]=temp[i];
}
}
歸併排序分析
當一個數組長度爲n時
遞歸到最後合併數組時,每個子數組的元素個數爲1,此時所需要的比較和交換爲1.整個數組被分爲n個元素爲1的子數組.所以總體上的比較次數爲n/1*1;
接着遞歸退到上一層,此時每個子數組的元素個數爲2.同樣的總體上的比較次數爲n/2*2;
由於數組是不斷折半的,所以歸併排序的時間複雜度爲O(n*logn);
用歸併排序對數組排序來說並不是一個好選擇,因爲它需要額外的臨時數組.
但歸併排序適合於對文件進行排序特別是文件大到數組無法存儲時,
通過遞歸調用不斷分割文件,直到可以用數組保存爲止.此時,就可以對數組中的文件進行快速排序或堆排序了.
最後再將有序段合併爲一個排好序的文件.
快速排序
與歸併排序一樣,都是基於分治思想.
在歸併排序中劃分是瑣碎的,組合是複雜的,在快速排序中,劃分是複雜的,組合是瑣碎的.
基本思想
首先在數組中,找到一箇中間元素,稱爲基準元素,或簡單的稱爲基準(pivot)
然後將所有小於基準的元素,移動到基準元素之前,大於基準的元素,便置於基準之後
再通過遞歸調用對基準元素前後的小數組進行排序.直到數組被劃分爲大小爲1的段爲止.
尋找基準元素
對於未排序的數組,每個元素都是中間元素的可能性都是一樣的.所以可以每次都將數組段中的第一個元素當作基準元素.
算法首先從數組的第二個元素開始向後歷遍,直至找到比基準大的元素爲止,保存該元素索引.稱爲”大”.再從數組段的後面繼續往前歷遍.直至找到比基準小的元素爲止,保存該索引.稱爲”小”.當小索引大於}大索引時,交換兩元素.再從大索引的位置繼續上述操作.否則.將基準元素與小索引所在元素交換.然後算法結束.
public static void quicksort(int []data,int first,int n){
int pivotIndex;
int n1,n2;
if(n>1){
pivotIndex=partition(data, first, n);
n1=pivotIndex-first;
n2=n-n1-1;
quicksort(data,first,n1);
quicksort(data,pivotIndex+1,n2);
}
}
//將第一個元素當作基準元素,並不是一個好的方法.對於完全有序的數組來說.需要n^2次操作,包括2*n次遞歸
private static int partition(int []data,int first,int n){
int pivot=data[first];
int tooBigIndex=first+1;
int tooSmallIndex=first+n-1;
while(tooBigIndex<=tooSmallIndex){
if(data[tooBigIndex]>pivot){
while(data[tooSmallIndex]>=pivot&&tooSmallIndex>first){
tooSmallIndex--;
}
if(tooBigIndex<tooSmallIndex){
data[tooBigIndex]^=data[tooSmallIndex];
data[tooSmallIndex]^=data[tooBigIndex];
data[tooBigIndex]^=data[tooSmallIndex];
}
}
tooBigIndex++;
}
data[first]=data[tooSmallIndex];
data[tooSmallIndex]=pivot;
return tooSmallIndex;
}
快速排序—分析
在理想的情況下,每次選取的基準元素,都爲最接近數組的中間位置.此時,數組平均分成兩個部分.這樣分的層次約爲logN.每層的操作爲O(n).
那麼在最好情況下快速排序的運行時間爲O(n*logn);
快速排序的最壞情況出現在數組有序時,這時遞歸的層數爲n.所以最壞時間爲O(n^2).
同時過多的遞歸調用會拋出StackOverFlowError.
堆排序
是一種交換排序算法
堆排序分兩步進行.
首先,將原始數組構建成堆.建堆的方法是首先將數組的第一個元素當成堆,再往堆裏添加第二個元素,需執行一次向上重排.繼續添加第三個,直至整個數組.堆仍保存在原數組中.
接着,將元素取出堆,堆的第一個元素是最大的.將該元素與堆的最後一個元素交換,將堆的大小減一,然後再對現有堆的根元素執行一次向下重排,使其恢復堆結構.
public static void heapsort(int []data,int n){
if(n>1){
makeHeap(data,n);
int i=n-1;
while(i>0){
data[0]^=data[i];
data[i]^=data[0];
data[0]^=data[i];
reheapifyDown(data,i);
i--;
}
}
}
/**
* 構建堆
* @param data
* @param n
*/
private static void makeHeap(int []data,int n){
for(int i=1;i<n;i++)
{int j=i;//新添加進堆的元素一開始放在堆的最後面
int p=(j-1)/2;//父節點.
while(j!=0&&data[j]>data[p]){
data[j]^=data[p];
data[p]^=data[j];
data[j]^=data[p];
j=p;
p=(j-1)/2;
}
}
}
/**
* 使只有根素在錯誤位置的堆,恢復堆結構
* @param data
* @param n
*/
private static void reheapifyDown(int data[],int n){
int i=0;
int b;
while(2*i+1<n){//執行向下重排元素的左孩子所在位置.如果2*i+1<n說明其還有子元素.
if(2*i+2<n){//執行向下重排元素的右孩子所在位置
b=data[2*i+1]<data[2*i+2]?2*i+2:2*i+1;//選出兩個孩子中較大的一個
}else{
b=2*i+1;
}
if(data[i]<data[b]){//如果父元素比孩子元素大,則結束循環.
data[i]^=data[b];
data[b]^=data[i];
data[i]^=data[b];
i=b;
}else{
break;
}
}
}
堆排序—分析
構建初始堆
構建堆時,每次增加一個元素,然後對該元素執行一次向上重排.每次向上重排時的運算次數爲:常量*堆的深度(logn).構建堆時,需要執行(n-1)次向上重排.所以
(n-1)*常量*(Logn)
O(n*logn)
將元素取出堆
每次都需要對另一元素執行向下重排,時間複雜度也爲O(n*logn)
所以堆排序的總時間爲O(2n*logn)=O(n*logn)
各種排序時間比較
元素個數 |
選擇排序(時間/毫秒) |
插入排序(時間/毫秒) |
歸併排序(時間/毫秒) |
快速排序(時間/毫秒) |
堆排序(時間/毫秒) |
5000(正序) |
47 |
0 |
0 |
31 |
0 |
5000(逆序) |
46 |
79 |
0 |
32 |
0 |
50000(正序) |
5171 |
0 |
16 |
StackOverflowError |
32 |
50000(逆序) |
5235 |
5422 |
15 |
31 |
|
5000(隨機) |
50 |
31 |
0 |
0 |
0 |
50000(隨機) |
5127 |
2450 |
16 |
16 |
31 |
100000(隨機) |
20812 |
10797 |
32 |
16 |
47 |
模擬棧實現快速排序
public static void quicksortbyStack(int []data ,int first,int n){
Stack<Integer> stack=new Stack<Integer>();
int pivotIndex;
int n1,n2;
stack.push(first);stack.push(n);
do{
n=stack.pop();first=stack.pop();
if(n>1){
pivotIndex=partition(data, first, n);
n1=pivotIndex-first;
n2=n-n1-1;
stack.push(first);stack.push(n1);
stack.push(pivotIndex+1);stack.push(n2);}
}while(!stack.isEmpty());
}
另一種構建初始堆的方法
/**
* 另一種生成堆的方法,把數組當成完成二叉樹去生成堆.
* @param data
* @param n
*/
private static void makeHeap2(int []data,int n){
for(int i=(n/2);i>0;i--){
heapifyDown(data, i, n);
}
}
/**
* 使子樹形成堆結構
* @param data
* @param rootOfSubtree 是某子樹的根節點.
* @param n
*/
private static void heapifyDown(int data[],int rootOfSubtree ,int n){
int i=rootOfSubtree;
int b;
while(2*i+1<n){//執行向下重排元素的左孩子所在位置.如果2*i+1<n說明其還有子元素.
if(2*i+2<n){//執行向下重排元素的右孩子所在位置
b=data[2*i+1]<data[2*i+2]?2*i+2:2*i+1;//選出兩個孩子中較大的一個
}else{
b=2*i+1;
}
if(data[i]<data[b]){//如果父元素比孩子元素大,則結束循環.
data[i]^=data[b];
data[b]^=data[i];
data[i]^=data[b];
i=b;
}else{
break;
}
}
}