最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

C語(yǔ)言實(shí)現(xiàn)八大排序算法的代碼詳解

 更新時(shí)間:2026年05月14日 09:16:45   作者:代碼地平線(xiàn)  
排序,是計(jì)算機(jī)程序設(shè)計(jì)中最為基礎(chǔ)且重要的算法之一,無(wú)論是面試題還是實(shí)際工程,排序算法總是高頻出現(xiàn),本文從 冒泡排序 到 計(jì)數(shù)排序,逐一分析每種排序的核心思路、代碼實(shí)現(xiàn)、時(shí)間與空間復(fù)雜度,需要的朋友可以參考下

引言

排序,是計(jì)算機(jī)程序設(shè)計(jì)中最為基礎(chǔ)且重要的算法之一。無(wú)論是面試題還是實(shí)際工程,排序算法總是高頻出現(xiàn)。本文從 冒泡排序計(jì)數(shù)排序,逐一分析每種排序的核心思路、代碼實(shí)現(xiàn)、時(shí)間與空間復(fù)雜度,并給出 10 萬(wàn)級(jí)數(shù)據(jù)的實(shí)測(cè)對(duì)比,幫你建立完整的排序知識(shí)體系。

排序,籠統(tǒng)來(lái)說(shuō)就是將一串記錄按照關(guān)鍵字的大小,遞增或遞減地排列起來(lái)。生活中處處都有排序的影子——購(gòu)物按價(jià)格篩選、成績(jī)排行榜、考試成績(jī)排名等。

本文基于 C 語(yǔ)言,實(shí)現(xiàn)以下八大排序算法(全部以遞增為例):

序號(hào)排序名稱(chēng)類(lèi)別
1冒泡排序交換排序
2堆排序選擇排序
3直接插入排序插入排序
4希爾排序插入排序
5直接選擇排序選擇排序
6快速排序交換排序
7歸并排序歸并排序
8計(jì)數(shù)排序非比較排序

一、冒泡排序

冒泡排序是我們接觸的第一個(gè)排序,雖然效率不高,但教學(xué)意義重大,是打開(kāi)排序算法世界大門(mén)的第一把鑰匙。

圖解過(guò)程

初始數(shù)組:[5, 3, 8, 4, 2]

第一趟(i=0):

[5, 3, 8, 4, 2]
  ↓比較
[3, 5, 8, 4, 2]  交換 5>3
  ↓比較
[3, 5, 8, 4, 2]  不交換 5<8
  ↓比較
[3, 4, 8, 5, 2]  交換 8>4
  ↓比較
[3, 4, 2, 8, 5]  交換 8>2
第一趟結(jié)束,最大值8沉到最右

第二趟(i=1):

[3, 4, 2, 8, 5]
  ↓比較
[3, 4, 2, 8, 5]  不交換 3<4
  ↓比較
[2, 4, 3, 8, 5]  交換 4>2
  ↓比較
[2, 3, 4, 8, 5]  交換 4>3
第二趟結(jié)束,次大值5在8左邊

第三趟(i=2):

[2, 3, 4, 8, 5]
  ↓比較
[2, 3, 4, 8, 5]  不交換 2<3
  ↓比較
[2, 3, 4, 8, 5]  不交換 3<4
第三趟結(jié)束,無(wú)需交換,數(shù)組已有序

代碼實(shí)現(xiàn)

void Swap(int* a, int* b)
{
    int c = *a;
    *a = *b;
    *b = c;
}

void maopao(int* arr, int r)
{
    for (int i = 0; i < r - 1; i++)           // 趟數(shù)
    {
        int flag = 1;
        for (int j = 0; j < r - 1 - i; j++)   // 兩兩比較
        {
            if (arr[j] > arr[j + 1])
            {
                flag = 0;
                Swap(&arr[j], &arr[j + 1]);
            }
        }
        if (flag == 1) return;               // 提前結(jié)束優(yōu)化
    }
}

過(guò)程分析

  • 外層循環(huán)控制總的趟數(shù),對(duì)于 n 個(gè)元素,只需要 n-1 趟,因?yàn)樽詈笠惶酥皇R粋€(gè)元素?zé)o需比較。
  • 內(nèi)層循環(huán)負(fù)責(zé)兩兩比較,將大的元素逐步"冒泡"到右側(cè)。
  • 每經(jīng)過(guò)一趟排序,未排序部分就會(huì)少一個(gè)元素,因此內(nèi)層 j 的上限要減去 i。
  • flag 優(yōu)化:當(dāng)某一趟沒(méi)有任何交換時(shí),說(shuō)明數(shù)組已經(jīng)有序,直接 return。

時(shí)空復(fù)雜度

  • 空間復(fù)雜度:O(1),僅使用了常量級(jí)輔助變量
  • 時(shí)間復(fù)雜度:O(N²),最差情況為逆序

性能驗(yàn)證

10 萬(wàn)個(gè)隨機(jī)數(shù),冒泡排序耗時(shí)約 3000+ ms,效率最低,但邏輯最為簡(jiǎn)單。

二、堆排序

堆排序利用 這一完全二叉樹(shù)結(jié)構(gòu)的特點(diǎn)——堆頂元素要么最大(大堆),要么最?。ㄐ《眩?,不斷交換堆頂與末尾元素并重新調(diào)整堆,最終得到有序序列。

圖解過(guò)程

以數(shù)組 [4, 10, 3, 5, 1] 為例,構(gòu)建大堆:

原始完全二叉樹(shù):
        4
      /   \
    10      3
   /  \
  5    1

建堆過(guò)程(從最后一個(gè)非葉子節(jié)點(diǎn)開(kāi)始向下調(diào)整):
節(jié)點(diǎn)(10)是最后一個(gè)非葉子節(jié)點(diǎn),比較10與孩子5、1,10最大無(wú)需交換
節(jié)點(diǎn)(4)與孩子10、3比較,4<10,交換
        10
      /    \
     4      3
    / \
   5   1

再調(diào)整節(jié)點(diǎn)(4),與孩子5比較,4<5,交換
        10
      /    \
     5      3
    / \
   4   1

代碼實(shí)現(xiàn)

void Swap(int* a, int* b)
{
    int c = *a;
    *a = *b;
    *b = c;
}

// 向下調(diào)整算法 —— 構(gòu)建大堆
void AdjustDown(int* arr, int r, int parent)
{
    int child = parent * 2 + 1;
    while (child < r)
    {
        if (child + 1 < r && arr[child + 1] > arr[child])
        {
            child++;
        }
        if (arr[parent] < arr[child])
        {
            Swap(&arr[parent], &arr[child]);
            parent = child;
            child = parent * 2 + 1;
        }
        else
        {
            break;
        }
    }
}

void Heappai(int* arr, int r)
{
    // 建堆 —— 從第一個(gè)非葉子節(jié)點(diǎn)開(kāi)始向下調(diào)整
    for (int i = (r - 1 - 1) / 2; i >= 0; i--)
    {
        AdjustDown(arr, r, i);
    }

    // 排序:不斷將堆頂(最大值)與末尾交換,再調(diào)整堆
    int end = r;
    while (end)
    {
        Swap(&arr[0], &arr[--end]);
        AdjustDown(arr, end, 0);
    }
}

過(guò)程分析

  • 建堆:從最后一個(gè)非葉子節(jié)點(diǎn) (n-1-1)/2 開(kāi)始往前,對(duì)每個(gè)節(jié)點(diǎn)執(zhí)行向下調(diào)整,最終得到一個(gè)大堆。
  • 排序:將堆頂最大值與數(shù)組末尾交換,此時(shí)末尾就是最大元素;然后對(duì)剩余的 n-1 個(gè)元素重新調(diào)整為堆,循環(huán)直至堆為空。
  • 核心就在于 向下調(diào)整算法 —— 讓父節(jié)點(diǎn)與孩子節(jié)點(diǎn)比較,若孩子比父大(大堆),則交換,并繼續(xù)向下調(diào)整。

時(shí)空復(fù)雜度

  • 空間復(fù)雜度:O(1),原地排序
  • 時(shí)間復(fù)雜度:O(N log N),建堆 O(N),每次調(diào)整 O(log N),共 N 次

性能驗(yàn)證

10 萬(wàn)數(shù)據(jù)僅需 6 ms 左右,遠(yuǎn)超冒泡排序。

三、直接插入排序

想象一下打撲克牌時(shí),每摸一張牌都會(huì)按順序插入到手牌中——直接插入排序就是這個(gè)思路。

圖解過(guò)程

數(shù)組 [4, 5, 2, 7, 1],逐步將元素插入已排序部分:

初始:已排序[4],待插入[5, 2, 7, 1]

插入5:tep=5,end=0,4<5 不挪,插入到位置1
已排序[4, 5],待插入[2, 7, 1]

插入2:tep=2,end=1,5>2 挪到位置2,end=0
              4>2 挪到位置1,end=-1
              插入到位置0
已排序[2, 4, 5],待插入[7, 1]

插入7:tep=7,end=2,5<7 不挪,插入到位置3
已排序[2, 4, 5, 7],待插入[1]

插入1:tep=1,end=3,7>1 挪,end=2
              5>1 挪,end=1
              4>1 挪,end=0
              2>1 挪,end=-1
              插入到位置0
最終:[1, 2, 4, 5, 7]

代碼實(shí)現(xiàn)

void insertsort(int a[], int n)
{
    for (int i = 0; i < n - 1; i++)
    {
        int end = i;
        int tep = a[end + 1];          // 保存待插入元素
        while (end >= 0)
        {
            if (a[end] > tep)          // 比待插入元素大,往后挪
            {
                a[end + 1] = a[end];
                end--;
            }
            else
            {
                break;
            }
        }
        a[end + 1] = tep;              // 插入到正確位置
    }
}

過(guò)程分析

  • 外層循環(huán)控制要插入的元素,用 end 指向已排序部分的最后一個(gè)位置,tep 保存待插入元素。
  • 內(nèi)層 while 循環(huán)中,如果已排序元素比 tep 大,就把它往后挪一位;否則找到插入位置。
  • 簡(jiǎn)單說(shuō):移動(dòng)排序,像整理?yè)淇伺埔粯?,比新牌大的牌就往前挪?/li>

時(shí)空復(fù)雜度

  • 空間復(fù)雜度:O(1)
  • 時(shí)間復(fù)雜度:O(N²),最差情況為逆序;但實(shí)際很難遇到最差情況,所以實(shí)際效率比冒泡高不少

性能驗(yàn)證

10 萬(wàn)數(shù)據(jù)約 幾十毫秒,明顯優(yōu)于冒泡排序。

四、希爾排序

希爾排序是直接插入排序的升級(jí)版,核心思想是預(yù)排序——先讓數(shù)據(jù)基本有序,最后再做一次直接插入排序。

希爾排序法又稱(chēng)縮小增量法。先選定一個(gè)整數(shù)(通常是 gap = n/3+1),把待排序記錄分成各組,所有距離相等的記錄分在同一組內(nèi),對(duì)每一組進(jìn)行排序,然后 gap = gap/3+1 得到下一個(gè)整數(shù),再次分組排序……當(dāng) gap=1 時(shí),就相當(dāng)于直接插入排序。

圖解過(guò)程

數(shù)組 [9, 5, 1, 7, 3, 6, 4, 8],gap 從 3 遞減到 1:

gap = 3 時(shí),分組情況:
索引:  0  1  2  3  4  5  6  7
數(shù)據(jù): 9  5  1  7  3  6  4  8
組別:  A  B  A  B  A  B  A  B

A組 [9, 1, 3, 4] 排序后:[1, 3, 4, 9]
B組 [5, 7, 6, 8] 排序后:[5, 6, 7, 8]

gap = 1 時(shí),整體插入排序,此時(shí)數(shù)組已接近有序,效率極高

代碼實(shí)現(xiàn)

void shellsort(int a[], int n)
{
    int gap = n;
    while (gap > 1)
    {
        gap = gap / 3 + 1;            // 保證最后一次 gap=1
        for (int i = 0; i < n - gap; i++)
        {
            int end = i;
            int tep = a[end + gap];
            while (end >= 0)
            {
                if (a[end] > tep)
                {
                    a[end + gap] = a[end];
                    end -= gap;
                }
                else
                {
                    break;
                }
            }
            a[end + gap] = tep;
        }
    }
}

過(guò)程分析

  • gap 就是分組間隔,gap/3+1 使得 gap 逐步縮小,最終必為 1。
  • 每組內(nèi)進(jìn)行插入排序,當(dāng) gap=1 時(shí),整個(gè)數(shù)組已經(jīng)基本有序,直接插入排序效率最高。
  • 外層 for 循環(huán)用 i 來(lái)控制遍歷,而不是每組單獨(dú)排——優(yōu)化點(diǎn)在于 i 到了哪一組就排哪一組。

時(shí)空復(fù)雜度

  • 空間復(fù)雜度:O(1)
  • 時(shí)間復(fù)雜度:O(N^1.3) 左右,《數(shù)據(jù)結(jié)構(gòu)(C語(yǔ)言版)》— 嚴(yán)蔚敏

性能驗(yàn)證

10 萬(wàn)數(shù)據(jù)約 6 ms,相比直接插入排序提升顯著。

五、直接選擇排序

直接選擇排序的思路非常直接:每次在未排序部分選出最小值放到開(kāi)頭,選出最大值放到末尾,依次收縮邊界。

圖解過(guò)程

數(shù)組 [3, 5, 1, 4, 2],begin=0,end=4:

初始:[3, 5, 1, 4, 2]
       ↑          ↑
      mini       maxi

第一輪找最小最大:
遍歷 [3,5,1,4,2],發(fā)現(xiàn) mini=2(在索引4),maxi=5(在索引1)
交換 min 到開(kāi)頭,max 到末尾:
[2, 5, 1, 4, 3] ←→ [2, 3, 1, 4, 5]
  begin=1, end=3

第二輪:
遍歷 [5,1,4,3],發(fā)現(xiàn) mini=1(在索引2),maxi=5(在索引0,但開(kāi)頭已處理)
因?yàn)?maxi==begin,需要將 maxi 修正為 mini(索引2)
交換:
[1, 3, 5, 4, 2]
  begin=2, end=2,結(jié)束

代碼實(shí)現(xiàn)

void SelectSort(int* arr, int n)
{
    int begin = 0, end = n - 1;
    while (begin < end)
    {
        int mini = begin, maxi = begin;
        for (int i = begin + 1; i <= end; i++)
        {
            if (arr[i] < arr[mini]) mini = i;
            if (arr[i] > arr[maxi]) maxi = i;
        }
        // 注意:若最大值在開(kāi)頭,先交換會(huì)覆蓋 mini 的位置,需要修正
        if (maxi == begin) maxi = mini;
        Swap(&arr[mini], &arr[begin]);
        Swap(&arr[maxi], &arr[end]);
        begin++;
        end--;
    }
}

過(guò)程分析

  • 遍歷未排序區(qū)間 [begin, end],找出最小值下標(biāo) mini 和最大值下標(biāo) maxi。
  • 交換到兩端后,begin++,end–,縮小區(qū)間。
  • 特別注意:如果最大值恰好在開(kāi)頭,先交換 mini 和 begin 后,最大值的位置會(huì)被覆蓋,此時(shí)需要將 maxi 修正為 mini。

時(shí)空復(fù)雜度

  • 空間復(fù)雜度:O(1)
  • 時(shí)間復(fù)雜度:O(N²)

性能驗(yàn)證

與冒泡排序大差不差,10 萬(wàn)數(shù)據(jù)約 2000-3000 ms。

六、快速排序

快速排序簡(jiǎn)稱(chēng)"快排",相信即便沒(méi)學(xué)過(guò)也聽(tīng)過(guò)它的大名,是面試和工程中的高頻明星。

快速排序的基本思想:任取待排序元素序列中的某元素作為基準(zhǔn)值,按照該排序碼將待排序集合分割成兩子序列,左子序列所有元素均小于基準(zhǔn)值,右子序列所有元素均大于基準(zhǔn)值,然后遞歸左右子序列,直至所有元素排列在相應(yīng)位置上。

圖解過(guò)程 —— Lomuto 前后指針?lè)?/h3>

數(shù)組 [4, 2, 7, 1, 5, 3],選基準(zhǔn)值 key=4(左端):

初始:
[4, 2, 7, 1, 5, 3]
 ↑key
prev=0, cur=1

cur=1:a[1]=2 < 4,++prev=1,交換(自己和自己,換了等于沒(méi)換)
[4, 2, 7, 1, 5, 3]

cur=2:a[2]=7 > 4,cur++,不交換
cur=3:a[3]=1 < 4,++prev=3,交換 a[3]?a[3]
[4, 2, 1, 7, 5, 3]

cur=4:a[4]=5 > 4,cur++,不交換
cur=5:a[5]=3 < 4,++prev=4,交換 a[5]?a[4]
[4, 2, 1, 3, 5, 7]
           ↑
          prev

最后交換 a[prev] 和 a[key]:
[3, 2, 1, 4, 5, 7]
             ↑
           基準(zhǔn)值位置(已歸位)

左區(qū)間 [3,2,1],右區(qū)間 [5,7],遞歸繼續(xù)...

1. hoare 版本

int GetMid(int* a, int left, int right)
{
    int mid = (left + right) / 2;
    if (a[left] > a[right])
    {
        if (a[right] > a[mid]) return right;
        else if (a[mid] > a[left]) return left;
        else return mid;
    }
    else
    {
        if (a[mid] < a[left]) return left;
        else if (a[mid] > a[right]) return right;
        else return mid;
    }
}

int Quicksort(int* a, int left, int right)
{
    int mid = GetMid(a, left, right);
    Swap(&a[left], &a[mid]);           // 三數(shù)取中優(yōu)化
    int key = left;
    int begin = left, end = right;
    while (begin < end)
    {
        while (begin < end && a[end] >= a[key]) end--;
        while (begin < end && a[begin] <= a[key]) begin++;
        Swap(&a[begin], &a[end]);
    }
    Swap(&a[key], &a[begin]);
    return begin;
}

void QuickSort(int* a, int left, int right)
{
    if (left >= right) return;
    int key = Quicksort(a, left, right);
    QuickSort(a, left, key - 1);
    QuickSort(a, key + 1, right);
}

2. Lomuto 前后指針?lè)?/h3>
int partQuickSort(int* a, int left, int right)
{
    int mid = GetMid(a, left, right);
    Swap(&a[left], &a[mid]);
    int key = left;
    int prev = left, cur = left + 1;
    while (cur <= right)
    {
        if (a[cur] < a[key] && ++prev != cur)
            Swap(&a[prev], &a[cur]);
        cur++;
    }
    Swap(&a[prev], &a[key]);
    return prev;
}

3. 小區(qū)間優(yōu)化 + 三數(shù)取中

void Quicksort2(int* a, int left, int right)
{
    if (left >= right) return;
    if (right - left + 1 < 10)         // 小區(qū)間優(yōu)化
    {
        insertsort(a + left, right - left + 1);
        return;
    }
    int key = Quicksort(a, left, right);
    Quicksort2(a, left, key - 1);
    Quicksort2(a, key + 1, right);
}

4. 非遞歸版本(借助棧)

void QuickSortNonR(int* a, int left, int right)
{
    ST st;
    STInit(&st);
    STPush(&st, right);
    STPush(&st, left);
    while (!STEmpty(&st))
    {
        int begin = STTop(&st); STPop(&st);
        int end = STTop(&st);   STPop(&st);
        int key = partQuickSort(a, begin, end);
        if (key + 1 < end) { STPush(&st, end); STPush(&st, key + 1); }
        if (begin < key - 1) { STPush(&st, key - 1); STPush(&st, begin); }
    }
    STDestory(&st);
}

時(shí)空復(fù)雜度

  • 空間復(fù)雜度:O(log N)(遞歸棧幀)
  • 時(shí)間復(fù)雜度:O(N log N),最差 O(N²)(有序數(shù)組可通過(guò)三數(shù)取中避免)

性能驗(yàn)證

10 萬(wàn)數(shù)據(jù)約 4 ms,綜合性能最強(qiáng)。

七、歸并排序

歸并排序采用 分治法 的思想,先遞歸拆分?jǐn)?shù)組至單個(gè)元素,再有序合并兩個(gè)子數(shù)組,最終得到完全有序的序列。

圖解過(guò)程

數(shù)組 [6, 5, 3, 1, 8, 7, 2, 4] 的遞歸拆分與合并:

遞歸拆分:
[6, 5, 3, 1, 8, 7, 2, 4]
[6, 5, 3, 1]    [8, 7, 2, 4]
[6, 5]  [3, 1]  [8, 7]  [2, 4]
[6] [5] [3] [1] [8] [7] [2] [4]   ← 單個(gè)元素,遞歸終止

兩兩合并(歸并):
[5, 6]  [1, 3]  [7, 8]  [2, 4]
[1, 3, 5, 6]    [2, 4, 7, 8]
[1, 2, 3, 4, 5, 6, 7, 8]   ← 完全有序

遞歸版本

void _MergeSort(int* a, int* tmp, int begin, int end)
{
    if (begin == end) return;
    int mid = (begin + end) / 2;
    _MergeSort(a, tmp, begin, mid);
    _MergeSort(a, tmp, mid + 1, end);

    // 歸并
    int begin1 = begin, end1 = mid;
    int begin2 = mid + 1, end2 = end;
    int i = begin;
    while (begin1 <= end1 && begin2 <= end2)
    {
        if (a[begin1] <= a[begin2]) tmp[i++] = a[begin1++];
        else                        tmp[i++] = a[begin2++];
    }
    while (begin1 <= end1) tmp[i++] = a[begin1++];
    while (begin2 <= end2) tmp[i++] = a[begin2++];
    memcpy(a + begin, tmp + begin, (end - begin + 1) * sizeof(int));
}

void MergeSort(int* a, int n)
{
    int* tmp = (int*)malloc(sizeof(int) * n);
    _MergeSort(a, tmp, 0, n - 1);
    free(tmp);
}

這里時(shí)間復(fù)雜度未來(lái)介紹一下為什么是O(logN)
這里拿N等于8舉例
因?yàn)檫@里用的是遞歸思想第一次是處理一個(gè)數(shù)組長(zhǎng)度為的8進(jìn)行歸并排序所以是O(N);
第二個(gè)進(jìn)入遞歸,是處理二個(gè)長(zhǎng)度為【4】【4】的二個(gè)數(shù)組分別進(jìn)行歸并排序
第三次是【2】【2】【2】【2】進(jìn)行歸并。
第四次是【1】【1】【1】【1】【1】【1】【1】【1】歸并
我們可以看見(jiàn)每一層都是O(N)
那一共有幾層呢
我們可以算一下

  • 初始數(shù)組長(zhǎng)度:n
  • 第1次拆分:得到2個(gè)長(zhǎng)度為n/2的子數(shù)組
  • 第2次拆分:得到4個(gè)長(zhǎng)度為n/4的子數(shù)組
  • 第k次拆分:得到2^k個(gè)長(zhǎng)度為n/2的k次方的子數(shù)組

當(dāng)子數(shù)組長(zhǎng)度為1時(shí),停止拆分:

n/z^k= 1

兩邊取以2為底的對(duì)數(shù):

k = log2 n

所以總層數(shù)約為log2 n層(向上取整,因?yàn)閿?shù)組長(zhǎng)度不一定是2的整數(shù)次冪)。
所以他的時(shí)間復(fù)雜度是O(NlogN)

非遞歸版本(循環(huán)實(shí)現(xiàn))

void _MergeSortNonR(int* a, int n)
{
    int* tmp = (int*)malloc(sizeof(int) * n);
    int gap = 1;
    while (gap < n)
    {
        for (int i = 0; i < n; i += 2 * gap)
        {
            int begin1 = i, end1 = i + gap - 1;
            int begin2 = i + gap, end2 = i + 2 * gap - 1;
            if (begin2 >= n) break;
            if (end2 >= n) end2 = n - 1;
            int j = begin1;
            while (begin1 <= end1 && begin2 <= end2)
            {
                if (a[begin1] <= a[begin2]) tmp[j++] = a[begin1++];
                else                        tmp[j++] = a[begin2++];
            }
            while (begin1 <= end1) tmp[j++] = a[begin1++];
            while (begin2 <= end2) tmp[j++] = a[begin2++];
            memcpy(a + i, tmp + i, (end2 - begin1 + 1) * sizeof(int));
        }
        gap *= 2;
    }
    free(tmp);
}

這里我們可以通過(guò)圖看見(jiàn)里面的循環(huán)執(zhí)行次數(shù)每一次都是O(N)
外面的次數(shù)是logN
所以他的時(shí)間復(fù)雜度也是O(NlogN)

時(shí)空復(fù)雜度

  • 空間復(fù)雜度:O(N),需要額外輔助數(shù)組
  • 時(shí)間復(fù)雜度:O(N log N)

性能驗(yàn)證

10 萬(wàn)數(shù)據(jù)約 4 ms,與快排持平,且性能穩(wěn)定。

八、計(jì)數(shù)排序

前面七種排序都需要兩兩比較元素大小,計(jì)數(shù)排序則另辟蹊徑,通過(guò)統(tǒng)計(jì)每個(gè)元素出現(xiàn)的次數(shù),按下標(biāo)天然有序的特性來(lái)完成排序。

圖解過(guò)程

數(shù)組 [4, 2, 4, 1, 3]

Step 1:找最小最大值
min=1, max=4,range = 4-1+1 = 4

Step 2:創(chuàng)建計(jì)數(shù)數(shù)組(長(zhǎng)度4,全0)
count: [0, 0, 0, 0]
        ↓
       index

Step 3:遍歷原數(shù)組,統(tǒng)計(jì)并映射
a[0]=4 → count[4-1]=count[3]++
a[1]=2 → count[2-1]=count[1]++
a[2]=4 → count[3]++
a[3]=1 → count[1-1]=count[0]++
a[4]=3 → count[3-1]=count[2]++

count: [1, 1, 1, 2]
         ↑  ↑  ↑  ↑
        1   2   3   4  (+min還原)

Step 4:遍歷計(jì)數(shù)數(shù)組,回寫(xiě)原數(shù)組
count[0]=1 → a[0]=1+1=2
count[1]=1 → a[1]=2+1=3
count[2]=1 → a[2]=3+1=4
count[3]=2 → a[3]=4+1=5, a[4]=5+1=6
最終:[2, 3, 4, 5, 6]

代碼實(shí)現(xiàn)

void contsort(int* a, int n)
{
    int min = a[0], max = a[0];
    for (int i = 1; i < n; i++)
    {
        if (a[i] < min) min = a[i];
        if (a[i] > max) max = a[i];
    }
    int range = max - min + 1;
    int* cout = (int*)calloc(range, sizeof(int));

    // 統(tǒng)計(jì)次數(shù)
    for (int i = 0; i < n; i++)
    {
        cout[a[i] - min]++;           // 映射到計(jì)數(shù)數(shù)組下標(biāo)
    }

    // 回寫(xiě)
    int j = 0;
    for (int i = 0; i < range; i++)
    {
        while (cout[i]--)
        {
            a[j++] = i + min;
        }
    }
    free(cout);
}

過(guò)程分析

  1. 先遍歷數(shù)組找出最小值和最大值,確定計(jì)數(shù)數(shù)組的大小 range = max - min + 1。
  2. 創(chuàng)建計(jì)數(shù)數(shù)組 cout,用 calloc 初始化為 0。
  3. 遍歷原數(shù)組,通過(guò) a[i] - min 映射到計(jì)數(shù)數(shù)組下標(biāo)并累加計(jì)數(shù)。
  4. 最后遍歷計(jì)數(shù)數(shù)組,按下標(biāo)(加上最小值還原原值)依次填回原數(shù)組。

特性分析

  • 計(jì)數(shù)排序不是比較排序,利用下標(biāo)天然有序的特性完成排序。
  • 適用場(chǎng)景:數(shù)據(jù)范圍集中時(shí)效率極高;數(shù)據(jù)分散時(shí)空間浪費(fèi)嚴(yán)重。

時(shí)空復(fù)雜度

  • 空間復(fù)雜度:O(range)
  • 時(shí)間復(fù)雜度:O(N + range)

性能驗(yàn)證

10 萬(wàn)數(shù)據(jù) 0 ms,在適用場(chǎng)景下堪稱(chēng)恐怖,但適用范圍有限。

九、完整測(cè)試代碼

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>

int main()
{
    srand((unsigned int)time(NULL));
    const int N = 100000;
    int* a1 = (int*)malloc(sizeof(int) * N);
    int* a2 = (int*)malloc(sizeof(int) * N);
    int* a3 = (int*)malloc(sizeof(int) * N);
    int* a4 = (int*)malloc(sizeof(int) * N);
    int* a5 = (int*)malloc(sizeof(int) * N);
    int* a6 = (int*)malloc(sizeof(int) * N);
    int* a7 = (int*)malloc(sizeof(int) * N);

    for (int i = 0; i < N; ++i)
    {
        a1[i] = rand() + i;
        a2[i] = a1[i];
        a3[i] = a1[i];
        a4[i] = a1[i];
        a5[i] = a1[i];
        a6[i] = a1[i];
        a7[i] = a1[i];
    }

    int begin7 = clock(); MergeSort(a7, N);        int end7 = clock();
    int begin6 = clock(); QuickSortNonR(a6, 0, N - 1); int end6 = clock();
    int begin5 = clock(); shellsort(a5, N);         int end5 = clock();
    int begin4 = clock(); Heappai(a4, N);          int end4 = clock();
    int begin3 = clock(); maopao(a3, N);           int end3 = clock();
    int begin2 = clock(); qsort(a2, N, sizeof(int), paixu); int end2 = clock();
    int begin1 = clock(); Quicksort2(a1, 0, N - 1); int end1 = clock();

    printf("排序 %d 個(gè)隨機(jī)數(shù),各算法用時(shí)(毫秒):\n", N);
    printf("MergeSort:       %d\n", end7 - begin7);
    printf("QuickSortNonR:   %d\n", end6 - begin6);
    printf("shellsort:       %d\n", end5 - begin5);
    printf("Heapsort:        %d\n", end4 - begin4);
    printf("bubblesort:      %d\n", end3 - begin3);
    printf("qsort:           %d\n", end2 - begin2);
    printf("Quicksort2:      %d\n", end1 - begin1);

    free(a1); free(a2); free(a3); free(a4); free(a5); free(a6); free(a7);
    return 0;
}

測(cè)試結(jié)果

排序 100000 個(gè)隨機(jī)數(shù),各算法用時(shí)(毫秒):
MergeSort:       4
QuickSortNonR:   4
shellsort:       6
Heapsort:        6
bubblesort:      3000+
qsort:           8
Quicksort2:      4

十、排序穩(wěn)定性詳解

什么是穩(wěn)定性?

穩(wěn)定性是指:待排序序列中存在值相等的元素,排序后這些相等元素的相對(duì)前后順序保持不變,則稱(chēng)該排序算法是穩(wěn)定的;否則稱(chēng)為不穩(wěn)定的。

舉例說(shuō)明

有一個(gè)數(shù)組,每個(gè)元素不僅有值,還有原始下標(biāo)(用于區(qū)分相同值):

初始狀態(tài):  [5a, 3, 5b, 1, 3]
          a在b前,都是5

排升序后:

  • 穩(wěn)定排序結(jié)果: [1, 3a, 3, 5a, 5b] → 兩個(gè) 3 的相對(duì)順序沒(méi)變,兩個(gè) 5 的相對(duì)順序也沒(méi)變
  • 不穩(wěn)定排序結(jié)果: [1, 3, 3, 5b, 5a] → 兩個(gè) 5 的相對(duì)順序被顛倒

為什么有的穩(wěn)定,有的不穩(wěn)定?

穩(wěn)定排序:只在必須交換時(shí)才交換,相同值之間靠比較判斷 (> / <),相等時(shí)保持原位置不動(dòng)。

排序穩(wěn)定原因
冒泡排序if(arr[j] > arr[j+1])> 而非 ,相等時(shí)不交換 ?
直接插入排序從后往前找,遇到 a[end] > tep 才挪,等于時(shí) break,相同值保持原有順序 ?
歸并排序合并時(shí) a[begin1] <= a[begin2],用 <= 保證相等時(shí)左區(qū)間優(yōu)先 ?
計(jì)數(shù)排序用下標(biāo)映射,下標(biāo)天然有序,相同值不會(huì)被調(diào)換位置 ?

不穩(wěn)定排序:排序過(guò)程中,相同值的元素可能被交換到另一個(gè)相同值的前面或后面,破壞了原始相對(duì)順序。

排序不穩(wěn)定原因
直接選擇排序每次同時(shí)交換 min 和 max 到兩端,相同值的兩個(gè)元素可能被換位 ?
希爾排序gap > 1 預(yù)排序階段,跨組比較時(shí)相同值可能產(chǎn)生相對(duì)位移 ?
堆排序建堆和調(diào)整過(guò)程中,堆頂與末尾交換時(shí),相同值可能被打亂 ?
快速排序hoare/挖坑法中,right 找小 left 找大交換,相同值可能在兩區(qū)間之間被換位 ?

穩(wěn)定性的意義

如果排序?qū)ο笫?strong>多字段結(jié)構(gòu)體(比如先按成績(jī)排序,相同成績(jī)時(shí)保持姓名先后順序),穩(wěn)定性就有重要價(jià)值。

簡(jiǎn)單說(shuō):穩(wěn)定排序保護(hù)"相等"元素的相對(duì)位置,不穩(wěn)定排序不保證這一點(diǎn)。

十一、排序總結(jié)

排序名稱(chēng)最好時(shí)間平均時(shí)間最壞時(shí)間空間復(fù)雜度穩(wěn)定性
冒泡排序O(N)O(N²)O(N²)O(1)? 穩(wěn)定
堆排序O(N log N)O(N log N)O(N log N)O(1)? 不穩(wěn)定
直接插入排序O(N)O(N²)O(N²)O(1)? 穩(wěn)定
希爾排序O(N log N)O(N^1.3)O(N²)O(1)? 不穩(wěn)定
直接選擇排序O(N²)O(N²)O(N²)O(1)? 不穩(wěn)定
快速排序O(N log N)O(N log N)O(N²)O(log N)? 不穩(wěn)定
歸并排序O(N log N)O(N log N)O(N log N)O(N)? 穩(wěn)定
計(jì)數(shù)排序O(N + range)O(N + range)O(N + range)O(range)? 穩(wěn)定

十二、內(nèi)部排序與外部排序

前面講的八大排序算法,全部屬于內(nèi)部排序,因?yàn)樗鼈兗僭O(shè)數(shù)據(jù)在內(nèi)存中,可以隨機(jī)訪(fǎng)問(wèn)。但實(shí)際工程中,數(shù)據(jù)量往往遠(yuǎn)大于內(nèi)存容量,這就需要外部排序。

什么是內(nèi)部排序?什么是外部排序?

類(lèi)型定義適用場(chǎng)景
內(nèi)部排序(內(nèi)排)數(shù)據(jù)全部加載到內(nèi)存中,可以隨機(jī)訪(fǎng)問(wèn)任意元素數(shù)據(jù)量小,能完整裝進(jìn)內(nèi)存
外部排序(外排)數(shù)據(jù)太大無(wú)法一次性裝進(jìn)內(nèi)存,需要分塊讀寫(xiě)磁盤(pán)/文件超大文件、百萬(wàn)/千萬(wàn)級(jí)數(shù)據(jù)量

什么時(shí)候用外排?

當(dāng)數(shù)據(jù)量達(dá)到內(nèi)存裝不下的程度時(shí),就必須用外排:

  • 排序 10 億個(gè)整數(shù)(~40GB),機(jī)器只有 16GB 內(nèi)存
  • 排序一個(gè) 100GB 的日志文件
  • 數(shù)據(jù)庫(kù)對(duì)超大型表進(jìn)行排序輸出

外排的核心思想:分而治之 + 多路歸并

外排分為兩個(gè)階段

階段一:分段內(nèi)排

原始大文件(太大,無(wú)法一次讀入內(nèi)存)
        ↓
每次讀一塊數(shù)據(jù)進(jìn)內(nèi)存(比如 1GB)
        ↓
用內(nèi)排算法(快排/歸并排)排好這一塊
        ↓
寫(xiě)回磁盤(pán),得到若干有序的小文件(稱(chēng)為"歸并段")

階段二:多路歸并

多個(gè)有序小文件
        ↓
每次從各文件讀一個(gè)數(shù)(或一小批),選最小/最大的輸出
        ↓
繼續(xù)讀、繼續(xù)選,直到所有文件處理完畢
        ↓
最終得到完全有序的大文件

圖解過(guò)程

假設(shè)內(nèi)存每次最多容納 3 個(gè)整數(shù),待排序數(shù)據(jù)為 [8, 3, 9, 2, 7, 1, 5, 4, 6]

【階段一:分段內(nèi)排】
內(nèi)存每次最多3個(gè)數(shù),分3次讀入:

讀入 [8, 3, 9] → 內(nèi)排 → [3, 8, 9] → 寫(xiě)回 temp1
讀入 [2, 7, 1] → 內(nèi)排 → [1, 2, 7] → 寫(xiě)回 temp2
讀入 [5, 4, 6] → 內(nèi)排 → [4, 5, 6] → 寫(xiě)回 temp3

得到三個(gè)有序文件:temp1=[3,8,9]  temp2=[1,2,7]  temp3=[4,5,6]

【階段二:多路歸并】
三路歸并:每次從 temp1/temp2/temp3 各讀一個(gè)數(shù)出來(lái)比較

第一輪:
比較 3(t1)、1(t2)、4(t3) → 選 1 → 輸出 [1],從 temp2 補(bǔ)一個(gè)數(shù)
比較 3(t1)、2(t2)、4(t3) → 選 2 → 輸出 [1,2],從 temp2 補(bǔ)一個(gè)數(shù)
比較 3(t1)、7(t2)、4(t3) → 選 3 → 輸出 [1,2,3],從 temp1 補(bǔ)一個(gè)數(shù)
比較 8(t1)、7(t2)、4(t3) → 選 4 → 輸出 [1,2,3,4],從 temp3 補(bǔ)一個(gè)數(shù)
比較 8(t1)、7(t2)、5(t3) → 選 5 → 輸出 [1,2,3,4,5],從 temp3 補(bǔ)一個(gè)數(shù)
比較 8(t1)、7(t2)、6(t3) → 選 6 → 輸出 [1,2,3,4,5,6],從 temp3 補(bǔ)一個(gè)數(shù)
...繼續(xù),最終輸出 [1,2,3,4,5,6,7,8,9]

關(guān)鍵點(diǎn):多路歸并的效率

多路歸并的復(fù)雜度為 O(N logK),其中:

  • N 為總數(shù)據(jù)量
  • K 為歸并路數(shù)(文件數(shù)量)

路數(shù) K 越大,層數(shù)越少,讀寫(xiě)磁盤(pán)次數(shù)越少。理想情況下 K 越大越好,但受限于內(nèi)存中同時(shí)打開(kāi)的文件描述符數(shù)量。

實(shí)際優(yōu)化手段:

  • 增加歸并路數(shù)(K 從 2 增大到 8、16……)
  • 敗者樹(shù)/勝者樹(shù):減少比較次數(shù)
  • 置換-選擇排序:生成長(zhǎng)度更大的有序歸并段,減少歸并段數(shù)量

內(nèi)排 vs 外排的選擇

數(shù)據(jù)量內(nèi)存夠用?推薦方案
幾千~幾萬(wàn)?直接內(nèi)排,快排/歸并排隨便選
幾十萬(wàn)~幾百萬(wàn)?內(nèi)排,可用優(yōu)化快排或歸并排
數(shù)千萬(wàn)~數(shù)億?外排,分塊內(nèi)排 + 多路歸并
TB 級(jí)數(shù)據(jù)?外排 + 加大歸并路數(shù)/多線(xiàn)程/分布式

一句話(huà)總結(jié)

  • 內(nèi)排:數(shù)據(jù)裝得下內(nèi)存,所有元素隨意訪(fǎng)問(wèn),快排/歸并排/堆排隨便用
  • 外排:數(shù)據(jù)太大裝不下,分塊讀進(jìn)內(nèi)存排好序,再通過(guò)歸并思想把各有序塊合并成整體有序

這里舉一個(gè)例子說(shuō)一下

假如我們有10億個(gè)整數(shù),那他就要10億乘以4個(gè)比特位,而內(nèi)存就只有1G,這里我們見(jiàn)一下?lián)Q算單位
1G=1024MB
1MB=1024KB
1KB=1024byte

所以1G大約等于10的9次方byte也就是1億
所以這時(shí)候我們用內(nèi)存,直接直接排序根本排不了,這時(shí)候我們就要使用外排序了。
這里我們可以先把4G的大文件存在磁盤(pán)里面,在分別取1G存進(jìn)內(nèi)存里面進(jìn)行排序,內(nèi)存里面現(xiàn)在也不能用歸并排序,因?yàn)樗€要開(kāi)辟一個(gè)O(N)的空間,所以我們就用快排,排內(nèi)存的,然后依次排4個(gè)文件,在內(nèi)存里面排好了再取出來(lái),進(jìn)行歸并排序,下面我用一個(gè)圖來(lái)說(shuō)一下。

結(jié)語(yǔ)

以上就是C語(yǔ)言實(shí)現(xiàn)八大排序算法的代碼詳解的詳細(xì)內(nèi)容,更多關(guān)于C語(yǔ)言八大排序算法的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • C語(yǔ)言 數(shù)組指針詳解及示例代碼

    C語(yǔ)言 數(shù)組指針詳解及示例代碼

    本文主要介紹C語(yǔ)言 數(shù)組指針,這里整理了相關(guān)資料并附示例待會(huì)及實(shí)現(xiàn)結(jié)果,幫助大家學(xué)習(xí)C語(yǔ)言中指針的知識(shí),有需要學(xué)習(xí)此部分內(nèi)容的朋友可以參考下
    2016-08-08
  • C語(yǔ)言超細(xì)致講解循環(huán)語(yǔ)句

    C語(yǔ)言超細(xì)致講解循環(huán)語(yǔ)句

    我們說(shuō)到當(dāng)滿(mǎn)足特定條件時(shí),就會(huì)執(zhí)行if語(yǔ)句或者switch語(yǔ)句后面的語(yǔ)句,否則不執(zhí)行,但是這只能執(zhí)行一次,在日常生活中,有些事情是需要重復(fù)去做的,C語(yǔ)句就為此引入了循環(huán)語(yǔ)句。所以今天繼續(xù)為大家分享C語(yǔ)言循環(huán)家族
    2022-05-05
  • C++?opencv圖像處理實(shí)現(xiàn)灰度變換示例

    C++?opencv圖像處理實(shí)現(xiàn)灰度變換示例

    這篇文章主要為大家介紹了C++?opencv圖像處理灰度變換的實(shí)現(xiàn)示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-05-05
  • C語(yǔ)言?超詳細(xì)梳理總結(jié)動(dòng)態(tài)內(nèi)存管理

    C語(yǔ)言?超詳細(xì)梳理總結(jié)動(dòng)態(tài)內(nèi)存管理

    動(dòng)態(tài)內(nèi)存是相對(duì)靜態(tài)內(nèi)存而言的。所謂動(dòng)態(tài)和靜態(tài)就是指內(nèi)存的分配方式。動(dòng)態(tài)內(nèi)存是指在堆上分配的內(nèi)存,而靜態(tài)內(nèi)存是指在棧上分配的內(nèi)存,本文帶你深入探究C語(yǔ)言中動(dòng)態(tài)內(nèi)存的管理
    2022-03-03
  • Cocos2d-x UI開(kāi)發(fā)之場(chǎng)景切換代碼實(shí)例

    Cocos2d-x UI開(kāi)發(fā)之場(chǎng)景切換代碼實(shí)例

    這篇文章主要介紹了Cocos2d-x UI開(kāi)發(fā)之場(chǎng)景切換代碼實(shí)例,cocos2d-x中的場(chǎng)景切換是通過(guò)導(dǎo)演類(lèi)調(diào)用相應(yīng)的方法完成的,本文通過(guò)代碼和詳細(xì)注釋來(lái)說(shuō)明,需要的朋友可以參考下
    2014-09-09
  • C++的虛析構(gòu)詳解及實(shí)例代碼

    C++的虛析構(gòu)詳解及實(shí)例代碼

    這篇文章主要介紹了C++的虛析構(gòu)詳解及實(shí)例代碼的相關(guān)資料,需要的朋友可以參考下
    2017-05-05
  • C/C++ chrono簡(jiǎn)單使用場(chǎng)景示例詳解

    C/C++ chrono簡(jiǎn)單使用場(chǎng)景示例詳解

    這篇文章主要介紹了C/C++ chrono簡(jiǎn)單使用場(chǎng)景示例詳解,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧
    2025-06-06
  • C++中的vector中erase用法實(shí)例代碼

    C++中的vector中erase用法實(shí)例代碼

    在vector數(shù)組中我們刪除數(shù)組經(jīng)常用的就是erase方法,但是earse的用法一不注意就會(huì)出錯(cuò),今天我就遇到了,所以在這里總結(jié)一下,避免大家用錯(cuò),對(duì)vector中erase用法感興趣的朋友跟隨小編一起看看吧
    2022-11-11
  • C++中重載、重寫(xiě)(覆蓋)和隱藏的區(qū)別實(shí)例分析

    C++中重載、重寫(xiě)(覆蓋)和隱藏的區(qū)別實(shí)例分析

    這篇文章主要介紹了C++中重載、重寫(xiě)(覆蓋)和隱藏的區(qū)別,是C++面向?qū)ο蟪绦蛟O(shè)計(jì)非常重要的概念,需要的朋友可以參考下
    2014-08-08
  • C語(yǔ)言算法--有序查找(折半查找/二分查找)

    C語(yǔ)言算法--有序查找(折半查找/二分查找)

    我們知道無(wú)序查找只能靠遍歷,如果有序查找我們還挨個(gè)去遍歷,未免太浪費(fèi)時(shí)間,所以這里我們會(huì)用到不一樣的方法,希望能給你帶來(lái)幫助
    2021-08-08

最新評(píng)論

湘潭市| 五河县| 富平县| 信丰县| 寿宁县| 云霄县| 千阳县| 兰溪市| 苍南县| 化德县| 电白县| 延吉市| 报价| 宜城市| 化隆| 抚远县| 兴山县| 永寿县| 马龙县| 阿荣旗| 外汇| 政和县| 纳雍县| 安顺市| 乌恰县| 内江市| 慈利县| 平阴县| 诸城市| 南皮县| 江西省| 安平县| 环江| 凤庆县| 盐亭县| 三门县| 虎林市| 盐山县| 玉溪市| 三河市| 丽水市|