排序算法

分類:

一、選擇排序 (不穩定)

1. 基本思想:

  每一趟從待排序的數據元素中選出最小(或最大)的一個元素,順序放在已排好序的數列的最後,直到全部待排序的數據元素排完。

2. 排序過程:

【示例】:

   初始關鍵字 [49 38 65 97 76 13 27 49]

第一趟排序後 13 [38 65 97 76 49 27 49]

第二趟排序後 13 27 [65 97 76 49 38 49]

第三趟排序後 13 27 38 [97 76 49 65 49]

第四趟排序後 13 27 38 49 [49 97 65 76]

第五趟排序後 13 27 38 49 49 [97 97 76]

第六趟排序後 13 27 38 49 49 76 [76 97]

第七趟排序後 13 27 38 49 49 76 76 [ 97]

最後排序結果 13 27 38 49 49 76 76 97

3.

void selectionSort(int* arr,long len)

{

       for ( int i = 0; i < len - 1; ++i )

        {

            int index = i;

            for ( int j = i + 1; j < len; ++j)

            {

                if ( arr[index] > arr[j];

                index = j;

            }

           if (index ! = i)

               swap( arr[index], arr[i]);

        }  

}

選擇排序法的第一層循環從起始元素開始選到倒數第二個元素,主要是在每次進入的第二層循環之 前,將外層循環的下標賦值給臨時變量,接下來的第二層循環中,如果發現有比這個最小位置處的元素更小的元素,則將那個更小的元素的下標賦給臨時變量,最 後,在二層循環退出後,如果臨時變量改變,則說明,有比當前外層循環位置更小的元素,需要將這兩個元素交換.

二.直接插入排序(穩定)

插入排序(Insertion Sort)的基本思想是:每次將一個待排序的記錄,按其關鍵字大小插入到前面已經排好序的子文件中的適當位置,直到全部記錄插入完成爲止。

直接插入排序

  直接插入排序(Straight Insertion Sort):將一個記錄插入到排好序的有序表中,從而得到一個新的、記錄數增1的有序表。

直接插入排序算法

staticvoid insertion_sort(int[] unsorted)       
(
	for (int i =1; i < unsorted.Length; i++)           
	{       
		if (unsorted[i -1] > unsorted[i])               
		{                   
			int temp = unsorted[i];                   
			int j = i;                   
			while (j >0 && unsorted[j -1] > temp)                   
			{                
				unsorted[j]= unsorted[j -1];                       
				j--;                   
			}                   
			unsorted[j]= temp;               
		}           
	}       
}
 
 

  哨兵(監視哨)有兩個作用:一是作爲臨變量存放R[i](當前要進行比較的關鍵字)的副本;二是在查找循環中用來監視下標變量j是否越界。

 

  當文件的初始狀態不同時,直接插入排序所耗費的時間是有很大差異的。最好情況是文件初態 爲正序,此時算法的時間複雜度爲O(n),最壞情況是文件初態爲反序,相應的時間複雜度爲O(n2),算法的平均時間複雜度是O(n2)。算法的輔助空間 複雜度是O(1),是一個就地排序。

直接插入排序是穩定的排序方法。

三. 冒泡排序(穩定)

[算法思想]:將被排序的記錄數組R[1..n]垂直排列,每個記錄R[i]看作是重量爲 R[i].key的氣泡。根據輕氣泡不能在重氣泡之下的原則,從下往上掃描數組R:凡掃描到違反本原則的輕氣泡,就使其向上"飄浮"。如此反覆進行,直到 最後任何兩個氣泡都是輕者在上,重者在下爲止。

  1. /******************************************************** 
  2. *函數名稱:BubbleSort 
  3. *參數說明:pDataArray 無序數組; 
  4. *          iDataNum爲無序數據個數 
  5. *說明:    冒泡排序 
  6. *********************************************************/  
  7. void BubbleSort(int* pDataArray, int iDataNum)  
  8. {  
  9.     BOOL flag = FALSE;    //記錄是否存在交換  
  10.     for (int i = 0; i < iDataNum - 1; i++)    //走iDataNum-1趟  
  11.     {  
  12.         flag = FALSE;  
  13.         for (int j = 0; j < iDataNum - i - 1; j++)      
  14.             if (pDataArray[j] > pDataArray[j + 1])  
  15.             {  
  16.                 flag = TRUE;  
  17.                 DataSwap(&pDataArray[j], &pDataArray[j + 1]);  
  18.             }  
  19.           
  20.         if (!flag)    //上一趟比較中不存在交換,則退出排序  
  21.             break;  
  22.     }  
  23. }  

平均時間複雜度:O(n2);空間複雜度:O(1)  (用於交換)

 

四. 希爾排序

基本思想:

      先取一個小於n的整數d1作爲第一個增量,把文件的全部記錄分成d1個組。所有距離爲dl的倍數的記錄放在同一個組中。先在各組內進行直接插人排序;然 後,取第二個增量d2<d1重複上述的分組和排序,直至所取的增量dt=1(dt<dt-l<…<d2<d1),即所有記 錄放在同一組中進行直接插入排序爲止。

     該方法實質上是一種分組插入方法。

給定實例的shell排序的排序過程

     假設待排序文件有10個記錄,其關鍵字分別是:

        49,38,65,97,76,13,27,49,55,04。

     增量序列的取值依次爲:

        5,3,1

Shell排序的算法實現

1. 不設監視哨的算法描述

void ShellPass(SeqList R,int d)

   {//希爾排序中的一趟排序,d爲當前增量

     for(i=d+1;i<=n;i++) //將R[d+1..n]分別插入各組當前的有序區

       if(R[i].key<R[i-d].key){

         R[0]=R[i];j=i-d; //R[0]只是暫存單元,不是哨兵

         do {//查找R[i]的插入位置

            R[j+d];=R[j]; //後移記錄

            j=j-d; //查找前一記錄

         }while(j>0&&R[0].key<R[j].key);

         R[j+d]=R[0]; //插入R[i]到正確的位置上

       } //endif

   } //ShellPass

void ShellSort(SeqList R)

   {

    int increment=n; //增量初值,不妨設n>0

    do {

          increment=increment/3+1; //求下一增量

          ShellPass(R,increment); //一趟增量爲increment的Shell插入排序

       }while(increment>1)

    } //ShellSort

注意:

     當增量d=1時,ShellPass和InsertSort基本一致,只是由於沒有哨兵而在內循環中增加了一個循環判定條件"j>0",以防下標越界。

2.設監視哨的shell排序算法

算法分析

1.增量序列的選擇

     Shell排序的執行時間依賴於增量序列。

     好的增量序列的共同特徵:

  ① 最後一個增量必須爲1;

  ② 應該儘量避免序列中的值(尤其是相鄰的值)互爲倍數的情況。

     有人通過大量的實驗,給出了目前較好的結果:當n較大時,比較和移動的次數約在nl.25到1.6n1.25之間。

2.Shell排序的時間性能優於直接插入排序

     希爾排序的時間性能優於直接插入排序的原因:

  ①當文件初態基本有序時直接插入排序所需的比較和移動次數均較少。

  ②當n值較小時,n和n2的差別也較小,即直接插入排序的最好時間複雜度O(n)和最壞時間複雜度0(n2)差別不大。

  ③在希爾排序開始時增量較大,分組較多,每組的記錄數目少,故各組內直接插入較快,後來增量di逐漸縮小,分組數逐漸減少,而各組的記錄數目逐漸增多,但由於已經按di-1作爲距離排過序,使文件較接近於有序狀態,所以新的一趟排序過程也較快。

     因此,希爾排序在效率上較直接插人排序有較大的改進。

3.穩定性

     希爾排序是不穩定的。參見上述實例,該例中兩個相同關鍵字49在排序前後的相對次序發生了變化。

五. 堆排序

1、 堆排序定義

     n個關鍵字序列Kl,K2,…,Kn稱爲堆,當且僅當該序列滿足如下性質(簡稱爲堆性質):

     (1) ki≤K2i且ki≤K2i+1 或(2)Ki≥K2i且ki≥K2i+1(1≤i≤ )

     若將此序列所存儲的向量R[1..n]看做是一棵完全二叉樹的存儲結構,則堆實質上是滿足如下性質的完全二叉樹:樹中任一非葉結點的關鍵字均不大於(或不小於)其左右孩子(若存在)結點的關鍵字。

【例】關鍵字序列(10,15,56,25,30,70)和(70,56,30,25,15,10)分別滿足堆性質(1)和(2),故它們均是堆,其對應的完全二叉樹分別如小根堆示例和大根堆示例所示。

2、大根堆和小根堆

     根結點(亦稱爲堆頂)的關鍵字是堆裏所有結點關鍵字中最小者的堆稱爲小根堆。

     根結點(亦稱爲堆頂)的關鍵字是堆裏所有結點關鍵字中最大者,稱爲大根堆。

注意:

     ①堆中任一子樹亦是堆。

      ②以上討論的堆實際上是二叉堆(Binary Heap),類似地可定義k叉堆。

3、堆排序特點

     堆排序(HeapSort)是一樹形選擇排序。

     堆排序的特點是:在排序過程中,將R[l..n]看成是一棵完全二叉樹的順序存儲結構,利用完全二叉樹中雙親結點和孩子結點之間的內在關係【參見二叉樹的順序存儲結構】,在當前無序區中選擇關鍵字最大(或最小)的記錄。

4、堆排序與直接插入排序的區別

      直接選擇排序中,爲了從R[1..n]中選出關鍵字最小的記錄,必須進行n-1次比較,然後在R[2..n]中選出關鍵字最小的記錄,又需要做n-2次 比較。事實上,後面的n-2次比較中,有許多比較可能在前面的n-1次比較中已經做過,但由於前一趟排序時未保留這些比較結果,所以後一趟排序時又重複執 行了這些比較操作。

     堆排序可通過樹形結構保存部分比較結果,可減少比較次數。

 

5、堆排序

    堆排序利用了大根堆(或小根堆)堆頂記錄的關鍵字最大(或最小)這一特徵,使得在當前無序區中選取最大(或最小)關鍵字的記錄變得簡單。

(1)用大根堆排序的基本思想

① 先將初始文件R[1..n]建成一個大根堆,此堆爲初始的無序區

② 再將關鍵字最大的記錄R[1](即堆頂)和無序區的最後一個記錄R[n]交換,由此得到新的無序區R[1..n-1]和有序區R[n],且滿足R[1..n-1].keys≤R[n].key

③  由於交換後新的根R[1]可能違反堆性質,故應將當前無序區R[1..n-1]調整爲堆。然後再次將R[1..n-1]中關鍵字最大的記錄R[1]和該區 間的最後一個記錄R[n-1]交換,由此得到新的無序區R[1..n-2]和有序區R[n-1..n],且仍滿足關係R[1..n- 2].keys≤R[n-1..n].keys,同樣要將R[1..n-2]調整爲堆。

    ……

直到無序區只有一個元素爲止。

(2)大根堆排序算法的基本操作:

① 初始化操作:將R[1..n]構造爲初始堆;

② 每一趟排序的基本操作:將當前無序區的堆頂記錄R[1]和該區間的最後一個記錄交換,然後將新的無序區調整爲堆(亦稱重建堆)。

注意:

①只需做n-1趟排序,選出較大的n-1個關鍵字即可以使得文件遞增有序。

②用小根堆排序與利用大根堆類似,只不過其排序結果是遞減有序的。堆排序和直接選擇排序相反:在任何時刻,堆排序中無序區總是在有序區之前,且有序區是在原向量的尾部由後往前逐步擴大至整個向量爲止。

(3)堆排序的算法:

void HeapSort(SeqIAst R)

   { //對R[1..n]進行堆排序,不妨用R[0]做暫存單元

    int i;

    BuildHeap(R); //將R[1-n]建成初始堆

    for(i=n;i>1;i--){ //對當前無序區R[1..i]進行堆排序,共做n-1趟。

      R[0]=R[1];R[1]=R[i];R[i]=R[0]; //將堆頂和堆中最後一個記錄交換

     Heapify(R,1,i-1); //將R[1..i-1]重新調整爲堆,僅有R[1]可能違反堆性質

     } //endfor

   } //HeapSort

(4) BuildHeap和Heapify函數的實現

 因爲構造初始堆必須使用到調整堆的操作,先討論Heapify的實現。

① Heapify函數思想方法

 每趟排序開始前R[l..i]是以R[1]爲根的堆,在R[1]與R[i]交換後,新的無 序區R[1..i-1]中只有R[1]的值發生了變化,故除R[1]可能違反堆性質外,其餘任何結點爲根的子樹均是堆。因此,當被調整區間是 R[low..high]時,只須調整以R[low]爲根的樹即可。

"篩選法"調整堆

   R[low]的左、右子樹(若存在)均已是堆,這兩棵子樹的根R[2low]和R[2low+1]分別是各自子樹中關鍵字最大的結點。若 R[low].key不小於這兩個孩子結點的關鍵字,則R[low]未違反堆性質,以R[low]爲根的樹已是堆,無須調整;否則必須將R[low]和它 的兩個孩子結點中關鍵字較大者進行交換,即R[low]與R[large] (R[large].key=max(R[2low].key,R[2low+1].key))交換。交換後又可能使結點R[large]違反堆性質,同 樣由於該結點的兩棵子樹(若存在)仍然是堆,故可重複上述的調整過程,對以R[large]爲根的樹進行調整。此過程直至當前被調整的結點已滿足堆性質, 或者該結點已是葉子爲止。上述過程就象過篩子一樣,把較小的關鍵字逐層篩下去,而將較大的關鍵字逐層選上來。因此,有人將此方法稱爲"篩選法"。

②BuildHeap的實現

  要將初始文件R[l..n]調整爲一個大根堆,就必須將它所對應的完全二叉樹中以每一結點爲根的子樹都調整爲堆。

  顯然只有一個結點的樹是堆,而在完全二叉樹中,所有序號 的結點都是葉子,因此以這些結點爲根的子樹均已是堆。這樣,我們只需依次將以序號爲 -1,…,1的結點作爲根的子樹都調整爲堆即可。

      具體算法【參見教材】。

5、大根堆排序實例

     對於關鍵字序列(42,13,24,91,23,16,05,88),在建堆過程中完全二叉樹及其存儲結構的變化情況參見。

6、 算法分析

     堆排序的時間,主要由建立初始堆和反覆重建堆這兩部分的時間開銷構成,它們均是通過調用Heapify實現的。

     堆排序的最壞時間複雜度爲O(nlgn)。堆排序的平均性能較接近於最壞性能。

     由於建初始堆所需的比較次數較多,所以堆排序不適宜於記錄數較少的文件。

     堆排序是就地排序,輔助空間爲O(1),

     它是不穩定的排序方法。

六. 快速排序

快速排序的基本思路是:首先我們選擇一箇中間值middle(程序中我們可使用數組中間值),把比中間值小的放在其左邊,比中間值大的放在其右邊。由於這個排序算法較複雜,我們先給出其進行一次排序的程序框架(從各類數據結構教材中可得):

void quick_sort(ints[], int l, int r)

{

    if (l < r)

    {

             //Swap(s[l], s[(l + r) / 2]); //將中間的這個數和第一個數交換參見注1

        int i = l,j = r,x = s[l];

        while (i < j)

        {

           while(i <j && s[j] >=x) //從右向左找第一個小於x的數

                           j--; 

           if(i < j)

                           s[i++] =s[j];

                    

           while(i <j && s[i] <x) //從左向右找第一個大於等於x的數

                           i++; 

           if(i < j)

                           s[j--] =s[i];

        }

        s[i] =x;

        quick_sort(s, l, i - 1);// 遞歸調用

        quick_sort(s, i + 1, r);

    }

對於n個成員,快速排序法的比較次數大約爲n*logn 次,交換次數大約爲(n*logn)/6次。如果n爲100,冒泡法需要進行4950 次比較,而快速排序法僅需要200  次,快速排序法的效率的確很高。快速排序法的性能與中間值的選定關係密切,如果每一次選擇的中間值都是最大值(或最小值),該算法的速度就會大大下降。快 速排序算法最壞情況下的時間複雜度爲O(n2),而平均時間複雜度爲O(n*logn)。

七. 合併排序

說明

之前所介紹的排序法都是在同一個陣列中的排序,考慮今日有兩筆或兩筆以上的資料,它可能是不同陣列中的資料,或是不同檔案中的資料,如何為它們進行排序?

解法

可以使用合併排序法,合併排序法基本是將兩筆已排序的資料合併並進行排序,如果所讀入的資料尚未排序,可以先利用其它的排序方式來處理這兩筆資料,然後再將排序好的這兩筆資料合併。

有人問道,如果兩筆資料本身就無排序順序,何不將所有的資料讀入,再一次進行排序?排序的精 神是儘量利用資料已排序的部份,來加快排序的效率,小筆資料的排序較為快速,如果小筆資料排序完成之後,再合併處理時,因為兩筆資料都有排序了,所有在合 併排序時會比單純讀入所有的資料再一次排序來的有效率。

那麼可不可以直接使用合併排序法本身來處理整個排序的動作?而不動用到其它的排序方式?答案是肯定的,只要將所有的數字不斷的分為兩個等分,直到最後剩一個數字為止,然後再反過來不斷的合併,就如下圖所示:

 

 

不過基本上分割又會花去額外的時間,不如使用其它較好的排序法來排序小筆資料,再使用合併排序來的有效率。

下面這個程式範例,我們使用快速排序法來處理小筆資料排序,然後再使用合併排序法處理合併的動作。

例子

 

  • C

 

#include <stdio.h>

#include <stdlib.h>

#include <time.h>

#define MAX1 10

#define MAX2 10

#define SWAP(x,y) {int t; t = x; x = y; y = t;}

int partition(int[], int, int);

void quicksort(int[], int, int);

void mergesort(int[], int, int[], int, int[]);

int main(void) {

int number1[MAX1] = {0};

int number2[MAX1] = {0};

int number3[MAX1+MAX2] = {0};

int i, num;

srand(time(NULL));

printf("排序前:");

printf("\nnumber1[]:");

for(i = 0; i < MAX1; i++) {

number1[i] = rand() % 100;

printf("%d ", number1[i]);

}

printf("\nnumber2[]:");

for(i = 0; i < MAX2; i++) {

number2[i] = rand() % 100;

printf("%d ", number2[i]);

}

// 先排序兩筆資料

quicksort(number1, 0, MAX1-1);

quicksort(number2, 0, MAX2-1);

printf("\n排序後:");

printf("\nnumber1[]:");

for(i = 0; i < MAX1; i++)

printf("%d ", number1[i]);

printf("\nnumber2[]:");

for(i = 0; i < MAX2; i++)

printf("%d ", number2[i]);

// 合併排序

mergesort(number1, MAX1, number2, MAX2, number3);

printf("\n合併後:");

for(i = 0; i < MAX1+MAX2; i++)

printf("%d ", number3[i]);

printf("\n");

return 0;

}

int partition(int number[], int left, int right) {

int i, j, s;

s = number[right];

i = left - 1;

for(j = left; j < right; j++) {

if(number[j] <= s) {

i++;

SWAP(number[i], number[j]);

}

}

SWAP(number[i+1], number[right]);

return i+1;

}

void quicksort(int number[], int left, int right) {

int q;

if(left < right) {

q = partition(number, left, right);

quicksort(number, left, q-1);

quicksort(number, q+1, right);

}

}

void mergesort(int number1[], int M, int number2[],

int N, int number3[]) {

int i = 0, j = 0, k = 0;

while(i < M && j < N) {

if(number1[i] <= number2[j])

number3[k++] = number1[i++];

else

number3[k++] = number2[j++];

}

while(i < M)

number3[k++] = number1[i++];

while(j < N)

number3[k++] = number2[j++];

}

 

  • Java

 

public class MergeSort {

public static int[] sort(int[] number1,

int[] number2) {

int[] number3 =

new int[number1.length + number2.length];

int i = 0, j = 0, k = 0;

while(i < number1.length && j < number2.length) {

if(number1[i] <= number2[j])

number3[k++] = number1[i++];

else

number3[k++] = number2[j++];

}

while(i < number1.length)

number3[k++] = number1[i++];

while(j < number2.length)

number3[k++] = number2[j++];

return number3;

}

}

八。基數排序

基數排序是根據組成關鍵字的各位值,用"分配"和"收集"的方法進行排序。例如,把撲克牌的排序看成由花色和麪值兩個數據項組成的主關鍵字排序。

  花色:梅花<方塊<紅心<黑桃

  面值:2<3<4<...<10<J<Q<K<A

  若要將一副撲克牌排成下列次序:

  梅花2,...,梅花A,方塊2,...,方塊A,紅心2,...,紅心A,黑桃2,...,黑桃A。

  有兩種排序方法:

  一、先按花色分成四堆,把各堆收集起來;然後對每堆按面值由小到大排列,再按花色從小到大按堆收疊起來。----稱爲"最高位優先"(MSD)法。

  二、先按面值由小到大排列成13堆,然後從小到大收集起來;再按花色不同分成四堆,最後順序收集起來。----稱爲"最低位優先"(LSD)法。

  [例] 設記錄鍵值序列爲{88,71,60,31,87,35,56,18},用基數排序(LSD)。如圖所示:其中f[i]、e[i]爲按位分配面值爲i的隊列的隊頭和隊尾指針。

   #define D 3

   typedef struct

   { int key;

     float data;

     int link;

   } JD

 

key data link

int jspx(JD r[],int n)

{ /*鏈式存儲表示的基數排序*/

   int i,j,k,t,p,rd,rg,f[10],e[10];

   /*p爲r[]的下標,rd,rg爲比例因子,f[j],e[j]是代碼爲j的隊的首尾指針*/

   for(i=1;i<n;i++) r[i].link=i+1;

   r[n].link=0;

   p=1;rd=1;rg=10;

   for(i=1;i<=D;i++)

   { for(j=0;j<10;j++) { f[j]=0;e[j]=0; } /*各隊列初始化*/

     do /*按位分配--分到各隊列中*/

     { k=(r[p].key%rg)/rd; /*取鍵值的某一位*/

       if(f[k]==0) f[k]=p;

       else r[e[k]].link=p; /*有重複值--修改鏈接*/

       e[k]=p;

       p=r[p].link; /*取下一個結點的地址*/

     }while(p>0);

     j=0; /*按位收集--調整分配後的鏈接*/

     while(f[j]==0) j=j+1;

     p=f[j];t=e[j];

     for(k=j+1;k<10;k++)

       if(f[k]>0){ r[t].link=f[k];t=e[k]; }/*調整鏈接*/

     r[t].link=0; /*鏈尾爲0*/

     rg=rg*10;rd=rd*10; /*提高一位*/

   }

   return(p); /*返回有序鏈表的首地址*/

九 枚舉排序

將每個記錄項與其他諸項比較計算出小於該項的記錄個數,以確定該項的位置。


十 總結


    1. 選取排序方法需要考慮的因素:
    (1) 待排序的元素數目n;
    (2) 元素本身信息量的大小;
    (3) 關鍵字的結構及其分佈情況;
    (4) 語言工具的條件,輔助空間的大小等。

2. 排序的選擇:
   (1) 若n較小(n <= 50),則可以採用直接插入排序或直接選擇排序。由於直接插入排序所需的 記錄移動操作較直接選擇排序多,因而當記錄本身信息量較大時,用直接選擇排序較好。
   (2) 若文件的初始狀態已按關鍵字基本有序,則選用直接插入或冒泡排序爲宜。
   (3) 若n較大,則應採用時間複雜度爲O(nlog2n)的排序方法:快速排序、堆排序或歸併排序。
快速排序是目前基於比較的內部排序法中被認爲是最好的方法。
   (4) 在基於比較排序方法中,每次比較兩個關鍵字的大小之後,僅僅出現兩種可能的轉移,因此可以用一棵二叉樹來描述比較判定過程,由此可以證明:當文件的n個關鍵字隨機分佈時,任何藉助於"比較"的排序算法,至少需要O(nlog2n)的時間。

   這句話很重要 它告訴我們自己寫的算法 是有改進到最優 當然沒有必要一直追求最優

(5) 當記錄本身信息量較大時,爲避免耗費大量時間移動記錄,可以用鏈表作爲存儲結構。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章