算法導論-1.排序算法


排序算法是最基礎的一類算法。主要排序算法包括選擇排序、插入排序、冒泡排序、合並排序、堆排序和快速排序。把這些排序算法全部實現一邊,再把《算法導論》對應章節后面的習題做一遍,確實是系統學習算法的一個不錯的開端。

選擇排序

選擇排序的想法很簡單,把需要排序的數組看成一堆撲克牌:先查一遍,抽出最小的作為第一個張;在剩下的牌堆里再查一遍,選出最小的作為第二個元素……重復直到牌堆耗盡。想法簡單的其代價就是運行時間為Θ(n×n):在查詢A、2或3的時候,都要反復地比較其與K、Q的大小,直覺告訴我這樣做沒有意義。

void selectionSort(int* x, int length){
    for(int i=0;i<length-1;i++){
        int minValue = INT_MAX;
        int minPosition = 0;
        for(int j=i;j<length;j++){
            if(x[j]<=minValue){
                minValue=x[j];
                minPosition=j;
            }
        }
        int temp = x[minPosition];
        x[minPosition]=x[i];
        x[i]=temp;
    }
}

插入排序

插入排序的想法類似於從牌堆中摸牌,並插入到手中已經排序過的牌中。在最外層以i為自變量的循環中,0~i-1張牌就是手中已排序的牌(循環開始時就是第一張牌x[0]),i~length-1張牌就是牌堆,每次循環將第i張牌(也就是牌堆頂部的第一張牌)插入到牌堆中。因為手中的牌已經排好序,所以每次插入都會移動所有比這張待插入的牌大的手牌。這個排序算法的運行時間也很高,為Θ(n×n)。

void insertionSort(int* x, int length){
    for (int i=1;i<length;i++){
        for (int j=i;j>0;j--){
            if(x[j]<x[j-1]){
                int temp = x[j];
                x[j]=x[j-1];
                x[j-1]=temp;
            }
        }
    }
}

合並排序

合並算法是一種遞歸的算法。假設牌堆已經分成兩堆,每一堆都已經排好序,比如一堆是{A,3,4,7,J,K},另一堆是{2,5,6,8,9,10,Q},把這兩堆合並成一個牌堆的方法很簡單,只要依次比較兩個牌堆頂部的牌,選擇較小的一張加入已排序的牌堆,直到兩個牌堆都空了,這個工作的時間代價僅為Θ(n)。如何獲得已排序的兩個牌堆呢?答案是先直接均分為兩堆,再遞歸地調用自身去對着兩堆合並排序,直到問題的規模足夠小,比如只剩1張或2張牌。好在將問題分解到足夠小需要的遞歸次數是lgn而不是n,因此該算法的運行時間為Θ(n×lgn)。不過合並排序需要額外的空間開銷,換言之它不是一種原地排序算法,他的空間開銷為Θ(n×lgn),而原地排序算法僅僅為Θ(n)。

void mergeSort(int* x, int length){
    if(length==0 || length==1){
        return;
    }
    int mid = (length-1)/2;
    int child1Size = mid+1;
    int* child1 = new int[child1Size];
    int child2Size = length-mid-1;
    int* child2 = new int[child2Size];
    for(int i=0;i<mid+1;i++){
        child1[i]=x[i];
    }
    for(int i=mid+1;i<length;i++){
        child2[i-mid-1]=x[i];
    }
    mergeSort(child1,child1Size);
    mergeSort(child2,child2Size);
    {    
        int i=0;int j=0;int k=0;
        while(k<length){
            if(i==child1Size){
                x[k]=child2[j];
                j++;k++;continue;
            }
            if (j==child2Size){
                x[k]=child1[i];
                i++;k++;continue;
            }
            if(child1[i]<=child2[j]){
                x[k]=child1[i];
                i++;k++;continue;
            }
            if(child2[j]<child1[i]){
                x[k]=child2[j];
                j++;k++;continue;
            }
        }
    }
    return;
}

冒泡排序

冒泡排序是一個很簡單的原地排序方法:從數組的最后一個元素開始(這個元素就是當前的“泡”)向前遍歷,如果前一個元素比當前的“泡”小,那么當前的“泡”就變成了前一個元素;如果前一個元素比當前的“泡”大,那么交換“泡”和該元素的位置(這個“泡”冒上去了)。每一次冒泡到最前面一個元素,都能保證選擇了最小的“泡”。雖然該算法運行時間為Θ(n×n),但是在冒前面的“泡”的過程中,后面的相對較小的元素也一定程度地冒上來了(換言之,這次冒泡並不是“無用功”),使得冒比較大的“泡”時經常只需要檢查一下而不用作交換,所以常數因子比較小。

void bubbleSort(int* x, int length)
{
    for (int i=0;i<length;i++){
        for (int j=length-1;j>0;j--){
            if(x[j]<x[j-1]){
                int temp = x[j];
                x[j]=x[j-1];
                x[j-1]=temp;
            }
        }
    }
}

堆排序

堆排序是一種“漂亮”的原地排序方法,其運行時間為Θ(n×lgn)。熟悉了最大堆的特性,堆排序的思路就非常簡單了:最大堆的根節點最大,將根節點和堆中的最后一個元素互換位置,並且將堆的長度減去1;此時堆已經不是最大堆,但是根元素的兩個子堆仍然是最大堆,將這種堆轉化為最大堆的過程heapMax只需要Θ(lgn)時間,那么就轉化為最大堆再取次大的元素。由一個雜亂數組建成一個最大堆的過程heapMaxBuild也需要Θ(n×lgn)時間,這不影響總的運行時間量級。

void heapMax(int* xInput, int xSize, int i){
    int l=(i+1)*2-1;
    int r=(i+1)*2;
    if(i>=xSize){
        return;
    }
    if(l>=xSize && r>=xSize){
        return;
    }
    if(xInput[i]>=xInput[l] && (r>=xSize ? true : (xInput[i]>=xInput[r]))){
        return;
    }
    int maxI=(r>=xSize ? l : (xInput[l]>xInput[r]?l:r));
    int temp = xInput[i];
    xInput[i]=xInput[maxI];
    xInput[maxI]=temp;
    heapMax(xInput, xSize, maxI);
}
void heapMaxBuild(int* x, int length){
    for(int i=length/2+1;i>=1;i--){
        heapMax(x, length, i-1);
    }
}
void heapSort(int* x, int length){
    heapMaxBuild(x, length);
    for (int i=length;i>=2;i--)    {
        int temp = x[i-1];
        x[i-1]=x[0];
        x[0]=temp;
        heapMax(x, i-1, 0);
    }
}

快速排序

快速排序的運行時間為Θ(n×lgn),它是一種原地排序算法。取最后一個元素作為基准,遍歷一次數組,將小於基准的元素排在前面,大於基准的元素排在后面,而不在乎同小於或同大於基准的元素之間的順序。一次遍歷之后,將最后一個元素和大於基准的元素的第一個交換位置,則基准前方的都是小於基准的元素,后方都是大於基准的元素,再遞歸調用自身,對同小於或同大於基准的元素進行快速排序。

int partition(int* x, int p, int r){
    int mid = x[r];
    int i = p;
    for (int j = p; j <= r - 1; j++){
        if(x[j] <= mid){
            int tmp = x[i];
            x[i] = x[j];
            x[j] = tmp;
            i++;
        }
    }
    int tmp = x[i];
    x[i]=x[r];
    x[r]=tmp;
    return i;
}
void quickSort(int* x, int p, int r){
    if(p < r){
        int q = partition(x, p, r);
        quickSort(x, p, q-1);
        quickSort(x, q+1, r);
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM