sort簡單分析

排序算法分析(都以從小到大爲例):
1、Insertsort:(原地排序)
最壞情況:從大到小排列(逆序),6,5,4,3,2,1, 比較(移動次數要乘以3)次數=n*(n - 1) / 2;
最好情況:從小到大排列(正序),1,2,3,4,5,6,比較次數 = n;
平均情況:與最壞情況一樣,爲Θ(n^2).
算法穩定。

void insertsort(unsigned long *a,int num,unsigned long *res)
{
    unsigned long temp[num];
    temp[0] = a[0];
    //每次取一個數 插入temp數組中 
    for(int i = 1; i < num; i++)
        for(int j = i - 1; j >= 0; j--)
        {
            if(a[i] < temp[j])
            {
                temp[j + 1] = temp[j];
                if(j == 0)
                    temp[j] = a[i];
            }
            else
            {
                temp[j + 1] = a[i];
                break;
            }
        }
    for(int i = 0; i < num; i++)
        res[i] = temp[i];
}

//原地排序版本
void insertsort1(unsigned long *a,int num)
{
    unsigned long key;
    //每次取一個數 插入temp數組中 
    for(int i = 1; i < num; i++)
    {
        key = a[i];
        for(int j = i - 1; j >= 0; j--)
        {
            if(key < a[j])
            {
                a[j + 1] = a[j];
                if(j == 0)
                    a[j] = key;
            }
            else
            {
                a[j + 1] = key;
                break;
            }
        }
    }
}

2、Shellsort:(分組的插入排序)
開始無序的時候每組元素較少,最後差不多有序的時候元素纔多起來,所以效果比插入排序要好。
算法不穩定 14336,步長開始取2,則33會換。

void shellsort(unsigned long *a,int num)
{
    int step = num / 3;
    unsigned long temp;
    //每次插入排序的間隔步長 
    for(int i = step; i >= 1; i /= 2)
    //插入排序
        for(int j = 0; j  < i; j++) 
        //將每隔i的數分爲一組,並進行每組的插入排序 
            for(int k = j + i; k <= num - 1; k += i)
            {
                temp = a[k];
                for(int m = k - i; m >= j; m -= i)
                {
                    if(temp < a[m])
                    {
                        a[m + i] = a[m];
                        if(m == j)
                            a[m] = temp;
                    }
                    else
                    {
                        a[m + i] = temp;
                        break;
                    }
                }
            }
}

3、Bubblesort:
每次將最小的元素移到(冒泡到)最前面(i)的位置。
最壞情況:逆序,6,5,4,3,2,1,比較次數:n*(n - 1) / 2; 移動次數:3 * n*(n - 1) / 2
最好情況:正序,123456,比較次數n
平均情況:
算法穩定

void bubblesort(unsigned long *a,int num)
{
    for(int i = 0; i < num; i++)
        for(int j = num - 1; j >= i + 1; j--)
        {
            unsigned long temp;
            if(a[j] < a[j - 1])
            {
                temp = a[j - 1];
                a[j - 1] = a[j];
                a[j] = temp;
            }
        }
}

4、selectsort:
和冒泡排序不同的是每次都只找到後面s[i+1,end]中最小的,僅進行下標的記錄,而不進行相鄰位置的交換。
最好最壞情況和冒泡一樣,不過移動次數減少,比較次數一樣。

void selectsort(unsigned long *a,int num)
{
    int min_flag;
    for(int i = 0; i < num; i++)
    {
        unsigned long min = a[i];
        min_flag = i;
        for(int j = i; j < num; j++)
        {
            if(a[j] < min)
            {
                min = a[j];
                min_flag = j;
            }
        }
        unsigned long temp = a[min_flag];
        a[min_flag] = a[i];
        a[i] = temp;
    }
}

5、Qsort(挖坑、填數)
最壞情況:每次partition都取的是當前組最大或最小,62345,每次分治的時候都是一個數組中有一個,另外一個有m-1個,故需進行n次劃分,而每次進行的比較次數爲m-1,故複雜度爲n^2
最好情況:每次flag取中間大小的數,分治2邊數組大小差不多,則會劃分logn次,而進行比較次數就是每組的元素數,故爲nlogn。
平均情況:假設規模爲N的問題分爲一個規模爲9/10N的問題和規模爲1/10N的問題,即T(n) = T(9n/10) + T(n/10) + Θ(n),用遞歸樹分析可得T(n) = O(nlogn),而且比分區9:1要更平均(也就是情況更好)的概率爲80%,所以在絕大部分情況下快速排序算法的運行時間爲O(nlogn)。

//i從左往右,j從右往左,flag爲第一個元素,將左邊(i所指)比flag大的放到右邊,把右邊(j所指)的放到左邊,i和j交替進行,挖坑、填數 
int partition(unsigned long *a,int start,int end)
{
    unsigned long flag = a[start];
    int i = start;
    int j = end;
    //每次先跳過不需要動的,然後對需要動的進行移動 
    while(i < j)
    {
        //跳過右邊比flag大的 ,這裏有(i< j)是爲了防止數組a後面值沒有初始化會是亂值!
        while((a[j] >= flag) && (i < j)) j--;
        if(i < j)
            a[i++] = a[j];
        //跳過左邊比flag小的 
        while((a[i] <= flag) && (i < j))i++;
            if(i < j)
                a[j--] = a[i];          
    }
    a[i] = flag;
    return i;
}

//不斷進行左右分治的partition,直到每個左右數組"僅有一個元素 "
void qsort(unsigned long *a,int start,int end)
{
    //start < end,保證僅有一個元素的時候停止 
    if(start < end)
    {
        int mid = partition(a,start,end);
        qsort(a,start,mid - 1);
        qsort(a,mid + 1,end);
    }
}

6、mergesort
算法裏面有一種分治策略:Divide(分解子問題的步驟) 、 Conquer(遞歸解決子問題的步驟)、 Combine(子問題解求出來後合併成原問題解的步驟)
使得複雜度爲nlogn,就是將問題分成2個同樣的子問題,然後再找一個O(n)複雜度的方法將2個子問題的結果合併成原問題的結果,複雜度計算式爲:

T(n)=2T(n/2)+O(n);

這樣的T(n) = nlogn,mergesort就是這種思路。
Merge的最壞情況,需要進行n次比較與n次移動。
最好情況,進行n次移動+比較。
最好情況:已排好序,不用移動,僅比較 nlogn
最壞情況:一直都是merge的最壞情況,分組每次都是平均分的,故分logn次,所以也是nlogn

//將2個已排好序的數組合併成一個有序數組
//[start:mid]是array1,[mid+1:end]是array2 
void merge(unsigned long *a,int start,int mid,int end,unsigned long *res)
{
    int frsize = mid - start;
    unsigned long temp[1000];
    int j = mid + 1;
    int i = start;
    int k = 0;
    while((i <= mid) && (j <= end))
    {
        if(a[i]  > a[j])
            temp[k++] = a[j++];
        else
            temp[k++] = a[i++];
    }
    if(i == mid + 1)//first array finishes first
        for(int m = j; m <= end; m++)
            temp[k++] = a[m];
    else//second finishes first
        for(int m = i; m <= mid; m++)
            temp[k++] = a[m];
    for(int i = 0; i <=  end; i++)
        res[i] = temp[i];
}

void mergesort(unsigned long *a ,int start,int end)
{
    if(start < end)
    {
        int i = (start + end) / 2;
        mergesort(a,start,i);
        mergesort(a,i + 1,end);
        merge(a,start,i,end,a);
    }
}

歸併排序與快速排序比較:歸併的merge要開額外的空間,而快排的partition可以原地進行!

7.堆排序
堆排序主要需要建立堆,通過heapify(int *a,int start,int num)操作將堆建立,然後每次取出根節點並將其移除後再將餘下num-1個數用heapify操作建立堆。heapify操作一次複雜度爲logn,一共n次heapify操作,複雜度O(nlogn)。其爲不穩定排序算法。是就地排序,只需要O(1)的空間。

#include "heap.h"

using namespace std;

//a數組有效元素爲a[1]-a[num].
void heap::min_heapify(int *a, int p,int num)
{
    int lchild = 2 * p, rchild = 2 * p + 1;
    int temp;
    if (lchild > num)//已經到葉子節點
        return;
    else if (rchild > num)
    {
        if (a[lchild] < a[p])
        {
            temp = a[p];
            a[p] = a[lchild];
            a[lchild] = temp;
        }
        return;
    }

    min_heapify(a, 2 * p, num);
    min_heapify(a, 2 * p + 1, num);

    int min;
    if (a[lchild] < a[rchild])
        min = lchild;
    else
        min = rchild;
    if (a[min] < a[p])
    {
        temp = a[p];
        a[p] = a[min];
        a[min] = temp;
    }
}


void heap::min_heapsort(int *a, int num)
{
    for (int i = num; i >= 1; i--)
    {
        min_heapify(a, 1, i);
        int temp = a[i];
        a[i] = a[1];
        a[1] = temp;    
    }
}

8、桶排序
此排序是爲了優化計數排序的空間太大問題,即要排序的數很多(如500萬個數),但是數的範圍不大(如0-900),通過將數組的數據全部歸一化到[0,1)之間,然後建立m個區間分別爲[0,1/m)、[1/m,2/m)、……[m-1/m,1)的bucket,每個桶都是一個鏈表,將歸一化的數據放入到對應的桶中,然後對每個桶進行排序(用qsort、insertsort.etc都行),然後按順序輸出即可。
這裏寫圖片描述

void bucketsort(unsigned long *a,int num,int m)
{
    double data[1000];
    unsigned long max = 0;
    vector<double> bucket[20];
    float seg = 1.0 / m;
    for(int i = 0; i < num; i++)
    {
        if(max < a[i])
            max = a[i];
    }
    max = max + 100;
    for(int i = 0; i < num; i++)
    {
        data[i] = (double)a[i] / max;
        for(int j = 0; j < m; j++)//放桶裏 
        {
            if(data[i] >= seg * j && data[i] <= seg * (j + 1))
            {
                bucket[j].push_back(data[i]);
                break;
            }
        }
    }
    for(int i = 0; i < m; i++)
        sort(bucket[i].begin(),bucket[i].end());
    int base = 0;

    for(int i = 0; i < m; i++)
    {
        for(int j = 0; j < bucket[i].size(); j++)
            a[base++] = (bucket[i])[j] * max;
    }
}

複雜度分析:
在做出數據落入每個bucket都是等可能的情況下,我們可以看到T() = O(n) + 每個桶排序時間總和,兩邊同時取期望,然後可以證明,排序時間的期望(用insertsort)爲:n * (2 - 1/n).最終桶排序複雜度爲O(n + c),其中C=N*(logN-logm),空間複雜度爲O(N+M)。

eg:

在一個文件中有10G個整數,亂序排列,要求找出中位數。內存限制爲2G。只寫出思路即可(內存限制爲2G意思是可以使用2G空間來運行程序,而不考慮本機上其他軟件內存佔用情況。) 

分析:既然要找中位數,很簡單就是排序的想法。那麼基於字節的桶排序是一個可行的方法。

思想:將整型的每1byte作爲一個關鍵字,也就是說一個整形可以拆成4個keys,而且最高位的keys越大,整數越大。如果高位keys相同,則比較次高位的keys。整個比較過程類似於字符串的字典序。

第一步: 把10G整數每2G讀入一次內存,然後一次遍歷這536,870,912即(1024*1024*1024)*2 /4個數據。每個數據用位運算">>"取出最高8位(31-24)。這8bits(0-255)最多表示256個桶,那麼可以根據8bit的值來確定丟入第幾個桶。最後把每個桶寫入一個磁盤文件中,同時在內存中統計每個桶內數據的數量NUM[256]。

代價:(1) 10G數據依次讀入內存的IO代價(這個是無法避免的,CPU不能直接在磁盤上運算)。(2)在內存中遍歷536,870,912個數據,這是一個O(n)的線性時間複雜度。(3)把256個桶寫回到256個磁盤文件空間中,這個代價是額外的,也就是多付出一倍的10G數據轉移的時間。

第二步:根據內存中256個桶內的數量NUM[256],計算中位數在第幾個桶中。很顯然,2,684,354,560個數中位數是第1,342,177,280個。假設前127個桶的數據量相加,發現少於1,342,177,280,把第128個桶數據量加上,大於1,342,177,280。說明,中位數必在磁盤的第128個桶中。而且在這個桶的第1,342,177,280-N(0-127)個數位上。N(0-127)表示前127個桶的數據量之和。然後把第128個文件中的整數讀入內存。(若數據大致是均勻分佈的,每個文件的大小估計在10G/256=40M左右,當然也不一定,但是超過2G的可能性很小)。注意,變態的情況下,這個需要讀入的第128號文件仍然大於2G,那麼整個讀入仍然可以按照第一步分批來進行讀取。

代價:(1)循環計算255個桶中的數據量累加,需要O(M)的代價,其中m<255。(2)讀入一個大概80M左右文件大小的IO代價。

第三步:繼續以內存中的某個桶內整數的次高8bit(他們的最高8bit是一樣的)進行桶排序(23-16)。過程和第一步相同,也是256個桶。

第四步:一直下去,直到最低字節(7-0bit)的桶排序結束。我相信這個時候完全可以在內存中使用一次快排就可以了。

整個過程的時間複雜度在O(n)的線性級別上(沒有任何循環嵌套)。但主要時間消耗在第一步的第二次內存-磁盤數據交換上,即10G數據分255個文件寫回磁盤上。一般而言,如果第二步過後,內存可以容納下存在中位數的某一個文件的話,直接快排就可以了(修改者注:我想,繼續桶排序但不寫回磁盤,效率會更高?).

9、基數排序

void radixsort(unsigned long *a,int num)
{
    vector<int> radix[10];//裝每次排序後的序列 
    int allzero = 0;
    unsigned long temp[1000];

    int step = 1;

    while(!allzero)
    {
        allzero = 1;
        for(int i = 0; i < num; i++)
        {
            int digit = a[i] % (int)pow(10,step);
            digit /= (int)pow(10,step - 1);
            radix[digit].push_back(i);
            if(digit > 0)
                allzero = 0;
        }
        int k = 0;
        for(int i = 0; i < 10; i++)//按照序列順序輸出即可 
        {
            for(int j = 0; j < radix[i].size(); j++)
                temp[k++] = a[(radix[i])[j]];
            radix[i].clear();
        }
        for(int i = 0; i < num; i++)
            a[i] = temp[i];
        step++;
    }
}

這裏寫圖片描述

發佈了34 篇原創文章 · 獲贊 12 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章