數據結構以內外排序

1、內排序    算法

排序類別 排序方法 最好時間複雜度 平均時間複雜度 最壞時間複雜度 輔助空間 穩定性 備註
插入類 插入 O(n) O(n2) O(n2) O(1) 穩定 大部分已排序時較好
希爾排序 - O(ns),1<s<2 - O(1) 不穩定 s是所選分組
交換類 冒泡排序 O(n) O(n2) O(n2) O(1) 穩定 n小時較好
快速排序 O(nlogn) O(nlogn) O(n2) O(logn) 不穩定 n大時較好
選擇類 選擇 O(n2) O(n2) O(n2) O(1) 不穩定 n小時較好
堆排序 O(nlogn) O(nlogn) O(nlogn) O(1) 不穩定 n大時較好
  歸併排序 O(nlogn) O(nlogn) O(nlogn) O(n) 穩定 n大時較好
基數排序 O(d(n+rd)) O(d(n+rd)) O(d(n+rd)) O(rd) 穩定 見下文

1.插入排序(InsertSort)shell

    插入排序是在一個已經有序的小序列的基礎上,一次插入一個元素。固然,剛開始這個有序的小序列只有1個元素,就是第一個元素。比較是從有序序列的末尾開始,也就是想要插入的元素和已經有序的最大者開始比起,若是比它大則直接插入在其後面,不然一直往前找直到找到它該插入的位置。若是遇見一個和插入元素相等的,那麼插入元素把想插入的元素放在相等元素的後面。這樣,相等元素的先後順序沒有改變,從原無序序列出去的順序就是排好序後的順序,因此插入排序是穩定的。api

    插入排序是對冒泡排序的改進。它比冒泡排序快2倍。通常不用在數據大於1000的場合下使用插入排序,或者重複排序超過200數據項的序列。數組

void insertSort(int a[],int n)   //插入排序  
{  
    int i,j;  
    int t;  
    for(i=1;i<n;i++)  
    {  
        t=a[i];   //保存當前無序表中的第一個數據  
        j=i-1;  
        while(j>=0 && a[j]>t)  
        {  
            a[j+1]=a[j];  
            j--;  
        }  
        a[j+1]=t;   //將數據插入有序表中  
    }  
}  

 

 

    2.希爾排序(ShellSor)ui

    希爾排序是按照不一樣步長對元素進行插入排序,當剛開始元素很無序的時候,步長最大,因此插入排序的元素個數不多,速度很快;當元素基本有序了,步長很小,插入排序對於有序的序列效率很高。因此,希爾排序的時間複雜度會比O(n2)好一些。因爲屢次插入排序,咱們知道一次插入排序是穩定的,不會改變相同元素的相對順序,但在不一樣的插入排序過程當中,相同的元素可能在各自的插入排序中移動,最後其穩定性就會被打亂,因此shell排序是不穩定的。spa

    Shell排序的分組合理性會對算法產生重要的影響。如今多用D.E.Knuth的分組方法。Shell排序比冒泡排序快5倍,比插入排序大體快2倍。Shell排序比起QuickSort,MergeSort,HeapSort慢不少。可是它相對比較簡單,它適合於數據量在5000如下而且速度並非特別重要的場合。它對於數據量較小的數列重複排序是很是好的。設計

void shellSort(int a[],int n)   //希爾排序  
{  
    int i,j,gap;  
    int t;  
    for(gap=n/2;gap>0;gap/=2)  
        for(i=gap;i<n;i++)  
        {  
            t=a[i];  
            j=i-gap;  
            while(j>=0 &&a[j]>t)  
            {  
                a[j+gap]=a[j];  
                j-=gap;  
            }  
            a[j+gap]=t;  
        }  
}  

 

 

    3.冒泡排序(BubbleSort)code

    冒泡排序就是把小的元素往前調或者把大的元素日後調。比較是相鄰的兩個元素比較,交換也發生在這兩個元素之間。因此,若是兩個元素相等,就不會再把它們倆交換一下的;若是兩個相等的元素沒有相鄰,那麼即便經過前面的兩兩交換把兩個相鄰起來,這時候也不會交換,相同元素的先後順序並無改變,因此冒泡排序是一種穩定排序算法。blog

    冒泡排序是最慢的排序算法,在實際運用中它是效率最低的算法。排序

void bubbleSort(int a[],int n)   //冒泡排序  
{  
    int i,j;  
    int t;  
    for(i=0;i<n;i++)  
        for(j=0;j<n-i-1;j++)  
            if(a[j]>a[j+1])  
            {  
                t=a[j];  
                a[j]=a[j+1];  
                a[j+1]=t;  
            }  
}  

 

 

    4.快速排序(QuickSort)

    快速排序有兩個方向,左邊的i下標一直往右走,當a[i]<= a[center_inde],其中center_index是中樞元素的數組下標,通常取爲數組第0個元素。而右邊的j下標一直往左走,當a[j]> a[center_index]。若是i和j都走不動了,i<= j, 和a[j],重複上面的過程,直到i>j。交換a[j]和a[center_index],完成一趟快速排序。在中樞元素和a[j]交換的時候,頗有可能把前面的元素的穩定性打亂,好比序列爲{5,3,3,4,3,8,9,10,11},如今中樞元素5和3(第5個元素,下標從1開始計)交換就會把元素3的穩定性打亂,因此快速排序是一個不穩定的排序算法,不穩定發生在中樞元素和a[j]交換的時刻。

    快速排序是一個就地排序,分而治之,大規模遞歸的算法。從本質上來講,它是歸併排序的就地版本。快速排序能夠由下面四步組成。
    ⑴若是很少於1個數據,直接返回。
    ⑵通常選擇序列最左邊的值做爲支點數據。
    ⑶將序列分紅2部分,一部分都大於支點數據,另一部分都小於支點數據。
    ⑷對兩邊利用遞歸排序數列。

    快速排序比大部分排序算法都要快。儘管咱們能夠在某些特殊的狀況下寫出比快速排序快的算法,可是就一般狀況而言,沒有比它更快的了。快速排序是遞歸的,對於內存很是有限的機器來講,它不是一個好的選擇。

void quickSort(int a[],int s,int e)  //對a[s]至a[e]的元素進行快速排序  
{  
    int i=s,j=e;  
    int t;  
    if(s<e)  
    {  
        t=a[s];  
        while(i!=j)  
        {  
            while(j>i && a[j]>t) j--;  //從右向左掃描,找第一個小於t的a[j]  
            if(i<j)  //表示找到這樣的a[j]  
            {  
                a[i]=a[j];  
                i++;  
            }  
            while(i<j && a[i]<=t) i++;   //從左向右掃描,找第一個大於t的a[i]  
            if(i<j)   //表示找到這樣的a[i]  
            {  
                a[j]=a[i];  
                j--;  
            }  
        }  
        a[i]=t;    //將a[s]放到a[s]至a[e]的恰當位置i處,使得其左邊的元素都不大於它,其右邊的元素都不小於它。  
        quickSort(a,s,i-1);    //對左區間遞歸排序  
        quickSort(a,i+1,e);    //對右區間遞歸排序  
    }  
}  

 

 

    5.選擇排序(SelectSort)

    選擇排序是給每一個位置選擇當前元素最小的,好比給第一個位置選擇最小的,在剩餘元素裏面給第二個元素選擇第二小的,依次類推,直到第n-1個元素,第n個元素不用選擇了,由於只剩下它一個最大的元素了。那麼,在一趟選擇,若是當前元素比一個元素小,而該小的元素又出如今一個和當前元素相等的元素後面,那麼交換後穩定性就被破壞了。例如,序列{5,8,5,2,9},第一趟選擇第1個元素5會和2交換,那麼原序列中2個5的相對先後順序就被破壞了,因此選擇排序不是一個穩定的排序算法。

    在實際應用中處於和冒泡排序基本相同的地位。它們只是排序算法發展的初級階段,在實際中使用較少。

void selectSort1(int a[],int n)   //選擇排序  
{  
    int i,j;  
    int t;  
    for(i=0;i<n-1;i++)   //數據起始位置,從0到倒數第二個數據  
        for(j=i+1;j<n;j++)   ////在剩下的數據中循環  
        {  
            if(a[i]>a[j])    // //若是有比它小的,交換二者  
            {  
                t=a[i];  
                a[i]=a[j];  
                a[j]=t;  
            }  
        }  
}  
  
void selectSort2(int a[],int n)   //選擇排序的改進,減小了交換的次數  
{  
    int i,j,small;  
    int t;  
    for(i=0;i<n-1;i++)  //數據起始位置,從0到倒數第二個數據  
    {  
        small=i;    //記錄最小數據的下標  
        for(j=i+1;j<n;j++)   //在剩下的數據中尋找最小數據  
        {  
            if(a[j]<a[small])   //若是有比它更小的,記錄下標  
                small=j;  
        }  
        t=a[small];    //將最小數據和未排序的第一個數據交換  
        a[small]=a[i];  
        a[i]=t;  
    }  
}  

 

 

 

    6.堆排序(HeapSort)

    堆的結構是結點i的孩子爲2i和2i+1節點,大頂堆要求父結點大於等於其2個子結點,小頂堆要求父結點小於等於其2個子結點。在一個長爲n的序列,堆排序的過程是從第n/2開始和其子結點共3個值選擇最大(大頂堆)或者最小(小頂堆),這3個元素之間的選擇固然不會破壞穩定性。但當爲n/2-1,n/2-2, ...1這些個父結點選擇元素時,就會破壞穩定性。有可能第n/2個父節點交換把後面一個元素交換過去了,而第n/2-1個父結點把後面一個相同的元素沒有交換,那麼這2個相同的元素之間的穩定性就被破壞了。因此,堆排序不是穩定的排序算法。

    堆排序適合於數據量很是大的場合(百萬數據)。堆排序不須要大量的遞歸或者多維的暫存數組。這對於數據量很是巨大的序列是合適的。好比超過數百萬條記錄,因爲快速排序,歸併排序都使用遞歸來設計算法,在數據量很是大的時候,可能會發生堆棧溢出錯誤。堆排序會將全部的數據建成一個堆,最大的數據在堆頂,而後將堆頂數據和序列的最後一個數據交換。接下來再次重建堆,交換數據,依次下去,就能夠排序全部的數據。

void max_heapify(int a[], int start, int end)   //調整爲大頂堆  
{  
    //父結點和子結點下標  
    int dad = start, son = start * 2 + 1;  
    while(son <= end)   //子結點下標在數組範圍內才能比較  
    {  
        //先比較左右孩子的大小,選擇大孩子的下標  
        if(son+1 <= end && a[son+1] > a[son]) son++;   
  
        if(a[son] > a[dad])  
        {  
            int t = a[son];  
            a[son] = a[dad];  
            a[dad] = t;  
            dad = son;  
            son = dad * 2 + 1;  
        }  
        else  
            break;  
    }  
}  
  
void heapSort(int a[], int n)   //堆排序  
{  
    int i;  
    //初始化數組爲大頂堆,i=n/2-1表示最後一個父結點的下標  
    for(i=n/2-1; i>=0; i--) max_heapify(a, i, n-1);  
  
    for(i=n-1; i>0; i--)    //根做爲最大值調整到當前序列的最後  
    {  
        int t = a[0];  
        a[0] = a[i];  
        a[i] = t;  
        max_heapify(a, 0, i-1);  
    }  
}  

 

 

    7.歸併排序(MergeSort)

    歸併排序是把序列遞歸地分紅短序列,遞歸出口是短序列只有1個元素(認爲直接有序)或者2個序列(1次比較和交換),而後把各個有序的段序列合併成一個有序的長序列,不斷合併直到原序列所有排好序。能夠發現,在1個或2個元素時,1個元素不會交換,2個元素若是大小相等也沒有人故意交換,這不會破壞穩定性。那麼,在短的有序序列合併的過程當中,穩定是是否受到破壞?沒有,合併過程當中咱們能夠保證若是兩個當前元素相等時,咱們把處在前面的序列的元素保存在結果序列的前面,這樣就保證了穩定性。因此,歸併排序也是穩定的排序算法。

    歸併排序比堆排序稍微快一點,可是須要比堆排序多一倍的內存空間,由於它須要一個額外的數組。

void mergearray(int a[], int first, int mid, int last, int temp[])  //將有二個有序數列a[first...mid]和a[mid+1...last]合併。  
{  
    int i=first, j=mid+1;  
    int m=mid, n=last;  
    int k=0;  
    while (i<=m && j<=n)  
    {  
        if (a[i]<a[j])  
            temp[k++]=a[i++];  
        else  
            temp[k++]=a[j++];  
    }  
    while (i<=m)  
        temp[k++]=a[i++];  
    while (j<=n)  
        temp[k++]=a[j++];  
    for (i=0;i<k;i++)  
        a[first+i]=temp[i];  
}  
  
void mergesort(int a[], int first, int last, int temp[])  
{  
    if (first<last)  
    {  
        int mid=(first+last)/2;  
        mergesort(a, first, mid, temp);    //左邊有序  
        mergesort(a, mid+1, last, temp);  //右邊有序  
        mergearray(a, first, mid, last, temp);   //再將二個有序數列合併  
    }  
}  
  
void MergeSort(int a[], int n)  
{  
    int *p=(int *)malloc(n*sizeof(int));  
    mergesort(a, 0, n - 1, p);  
    free(p);  
}  

 

 

    8.基數排序(RadixSort)

    基數排序是按照低位先排序,而後收集;再按照高位排序,而後再收集;依次類推,直到最高位。有時候有些屬性是有優先級順序的,先按低優先級排序,再按高優先級排序,最後的次序就是高優先級高的在前,高優先級相同的低優先級高的在前。基數排序基於分別排序,分別收集,因此其是穩定的排序算法。

    基數排序和一般的排序算法並不走一樣的路線。它是一種比較新穎的算法,可是它只能用於整數的排序,若是咱們要把一樣的辦法運用到浮點數上,咱們必須瞭解浮點數的存儲格式,並經過特殊的方式將浮點數映射到整數上,而後再映射回去,這是很是麻煩的事情,所以,它的使用一樣也很少。並且,最重要的是,這樣算法也須要較多的存儲空間。

    時間效率:設待排序列爲n個記錄,d個關鍵碼,關鍵碼的取值範圍爲radix,則基數排序的時間複雜度爲O(d(n+radix)),其中,一趟分配時間複雜度爲O(n),一趟收集時間複雜度爲O(radix),共進行d趟分配和收集。

int maxbit(int a[],int n)    //求數組元素的最大位數  
{  
    int d=1,i=0;  //保存最大的位數  
    int p=10;  
    for(i=0;i<n;i++)  
    {  
        while(a[i]>=p)  
        {  
            p*=10;  
            d++;  
        }  
    }  
    return d;  
}  
  
void radixsort(int a[],int n)    //基數排序  
{  
    int d = maxbit(a,n);  
    long *tmp=(long *)malloc(n*sizeof(long));  
    long count[10];  //計數器,統計每位基數的個數  
    long i,j,k;  
    int radix=1;  
    for(i=1;i<=d;i++)  //進行d次排序  
    {  
        for(j=0;j<10;j++)    //每次分配前清空計數器  
            count[j]=0;  
        for(j=0;j<n;j++)   //統計基數出現的次數  
        {  
            k=(a[j]/radix)%10;  
            count[k]++;  
        }  
        for(j=1;j<10;j++)  //將tmp中的位置依次分配給每一個計數器  
            count[j]=count[j-1]+count[j];  
        for(j=n-1;j>=0;j--)  //根據計數器,將記錄依次收集到tmp中  
        {  
            k=(a[j]/radix)%10;  
            count[k]--;  
            tmp[count[k]]=a[j];  
        }  
        for(j=0;j<n;j++)  //將臨時數組的內容複製到數組a中  
            a[j] = tmp[j];  
        radix = radix*10;  
    }  
    free(tmp);  
}  

 

2、外排序

當待排序的文件比內存的可以使用容量還大時,文件沒法一次性放到內存中進行排序,須要藉助於外部存儲器(例如硬盤、U盤、光盤),這時就須要用外部排序算法來解決。

外部排序算法由兩個階段構成:
按照內存大小,將大文件分紅若干長度爲 l 的子文件(l 應小於內存的可以使用容量),而後將各個子文件依次讀入內存,使用適當的內部排序算法對其進行排序(排好序的子文件統稱爲「歸併段」或者「順段」),將排好序的歸併段從新寫入外存,爲下一個子文件排序騰出內存空間;
對獲得的順段進行合併,直至獲得整個有序的文件爲止。

例如,有一個含有 10000 個記錄的文件,可是內存的可以使用容量僅爲 1000 個記錄,毫無疑問須要使用外部排序算法,具體分爲兩步:
1. 將整個文件其等分爲 10 個臨時文件(每一個文件中含有 1000 個記錄),而後將這 10 個文件依次進入內存,採起適當的內存排序算法對其中的記錄進行排序,將獲得的有序文件(初始歸併段)移至外存。
2. 對獲得的 10 個初始歸併段進行如圖所示的兩路歸併,直至獲得一個完整的有序文件。

 

如圖所示有 10 個初始歸併段到一個有序文件,共進行了 4 次歸併,每次都由 m 個歸併段獲得 ⌈m/2⌉ 個歸併段,這種歸併方式被稱爲 2-路平衡歸併。

對於外部排序算法來講,影響總體排序效率的因素主要取決於讀寫外存的次數,即訪問外存的次數越多,算法花費的時間就越多,效率就越低。

對於同一個文件來講,對其進行外部排序時訪問外存的次數同歸並的次數成正比,即歸併操做的次數越多,訪問外存的次數就越多。使用2-路平衡歸併的方式,觸類旁通,還可使用 3-路歸併、4-路歸併甚至是 10-路歸併的方式,下圖爲 5-路歸併的方式:



對於 k-路平衡歸併中 k 值得選擇,增長 k 能夠減小歸併的次數,從而減小外存讀寫的次數,最終達到提升算法效率的目的。除此以外,通常狀況下對於具備 m 個初始歸併段進行 k-路平衡歸併時,歸併的次數爲:s=」logk⁡m」(其中 s 表示歸併次數)。

從公式上能夠判斷出,想要達到減小歸併次數從而提升算法效率的目的,能夠從兩個角度實現:1. 增長 k-路平衡歸併中的 k 值;2. 儘可能減小初始歸併段的數量 m,即增長每一個歸併段的容量。

相關文章
相關標籤/搜索