【数据结构】八大排序算法详解(C语言实现)|插入排序、希尔排序、选择排序、堆排序、冒泡排序、快速排序、归并排序、计数排序

🎬 博主名称键盘敲碎了雾霭
🔥 个人专栏 : 《C语言》《数据结构》

⛺️指尖敲代码,雾霭皆可破


文章目录

    • 引言
    • 一、插入排序
      • [1.1 基本思想](#1.1 基本思想)
      • [1.2 代码实现](#1.2 代码实现)
      • [1.3 特性总结](#1.3 特性总结)
    • 二、希尔排序
      • [2.1 基本思想](#2.1 基本思想)
      • [2.2 代码实现](#2.2 代码实现)
      • [2.3 特性总结](#2.3 特性总结)
    • 三、选择排序
      • [3.1 基本思想](#3.1 基本思想)
      • [3.2 代码实现](#3.2 代码实现)
      • [3.3 特性总结](#3.3 特性总结)
    • 四、堆排序
      • [4.1 基本思想](#4.1 基本思想)
      • [4.2 代码实现](#4.2 代码实现)
      • [4.3 特性总结](#4.3 特性总结)
    • 五、冒泡排序
      • [5.1 基本思想](#5.1 基本思想)
      • [5.2 代码实现](#5.2 代码实现)
      • [5.3 特性总结](#5.3 特性总结)
    • 六、快速排序
      • [6.1 基本思想](#6.1 基本思想)
      • [6.2 三种常见分区方法](#6.2 三种常见分区方法)
      • [6.3 快速排序优化](#6.3 快速排序优化)
      • [6.4 快速排序非递归实现(借助栈)](#6.4 快速排序非递归实现(借助栈))
      • [6.5 特性总结](#6.5 特性总结)
    • 七、归并排序
    • 八、计数排序
      • [8.1 基本思想](#8.1 基本思想)
      • [8.2 代码实现](#8.2 代码实现)
      • [8.3 特性总结](#8.3 特性总结)
    • 总结与对比

引言

排序是计算机科学中最基础的操作之一,几乎所有的应用程序都会涉及数据的排序。本文将详细介绍八种经典的排序算法:插入排序、希尔排序、选择排序、堆排序、冒泡排序、快速排序、归并排序、计数排序 。每种算法都包含基本思想、C语言代码实现、特性总结(时间复杂度、空间复杂度、稳定性),并辅以必要的文字说明,帮助读者深入理解其原理与适用场景。

文章中的所有代码均经过测试,可直接在C语言环境中运行。


一、插入排序

1.1 基本思想

插入排序的工作方式类似于我们整理扑克牌:将待排序的元素逐个插入到已经有序的序列中的适当位置,直到全部插入完毕。

初始时,认为第一个元素已经有序,然后取下一个元素,在已排序序列中从后向前扫描,找到相应位置并插入。

1.2 代码实现

c 复制代码
void InsertSort(int* arr, int n)
{
    for (int i = 0; i < n - 1; i++)
    {
        int end = i;
        int tmp = arr[end + 1];
        while (end >= 0)
        {
            if (arr[end] > tmp)
            {
                arr[end + 1] = arr[end];
                end--;
            }
            else
            {
                break;
            }
        }
        arr[end + 1] = tmp;
    }
}

1.3 特性总结

  • 元素集合越接近有序,直接插入排序的时间效率越高
  • 时间复杂度:最坏情况(逆序)O(N²),最好情况(有序)O(N)。
  • 空间复杂度:O(1),原地排序。
  • 稳定性:稳定(因为当待插入元素与有序序列中的元素相等时,插入到其后面,相对顺序不变)。

二、希尔排序

2.1 基本思想

希尔排序是对插入排序的改进,也称"缩小增量排序"。它通过将待排序数组按下标的一定增量分组,对每组使用插入排序;随着增量逐渐减少,每组包含的元素越来越多,当增量减至1时,整个数组基本有序,最后再进行一次插入排序。

2.2 代码实现

这里给出两种常见的写法,本质上都是对每组进行插入排序,只是循环层数不同。

版本一(三层循环,按组处理)

c 复制代码
void ShellSort(int* arr, int n)
{
    int gap = n;
    while (gap > 1)
    {
        gap = gap / 3 + 1;          // 保证最后 gap=1
        for (int j = 0; j < gap; j++)               // 对每组进行插入排序
        {
            for (int i = j; i < n - gap; i += gap)
            {
                int end = i;
                int tmp = arr[end + gap];
                while (end >= 0)
                {
                    if (arr[end] > tmp)
                    {
                        arr[end + gap] = arr[end];
                        end -= gap;
                    }
                    else
                    {
                        break;
                    }
                }
                arr[end + gap] = tmp;
            }
        }
    }
}

版本二(两层循环,多组交替进行)

c 复制代码
void ShellSort(int* arr, int n)
{
    int gap = n;
    while (gap > 1)
    {
        gap = gap / 3 + 1;
        for (int i = 0; i < n - gap; i++)    // 多组交替进行插入排序
        {
            int end = i;
            int tmp = arr[end + gap];
            while (end >= 0)
            {
                if (arr[end] > tmp)
                {
                    arr[end + gap] = arr[end];
                    end -= gap;
                }
                else
                {
                    break;
                }
            }
            arr[end + gap] = tmp;
        }
    }
}

2.3 特性总结

  • 希尔排序是对直接插入排序的优化,通过预排序使数组接近有序,最后一步插入排序效率很高。
  • 时间复杂度 :依赖于增量序列的选取,一般可认为在 O(N^1.3) ~ O(N^1.5) 之间。
  • 空间复杂度:O(1)。
  • 稳定性:不稳定(因为相同元素可能分到不同组,导致相对顺序改变)。

三、选择排序

3.1 基本思想

每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。

3.2 代码实现

版本一(每次选一个最小值)

c 复制代码
void SelectSort(int* arr, int n)
{
    for (int j = 0; j < n; j++)
    {
        int min = j;
        for (int i = j + 1; i < n; i++)
        {
            if (arr[min] > arr[i])
            {
                min = i;
            }
        }
        int tmp = arr[min];
        arr[min] = arr[j];
        arr[j] = tmp;
    }
}

版本二(同时选出最小值和最大值,减少循环次数)

c 复制代码
void Swap(int* a, int* b)
{
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

void SelectSort(int* arr, int n)
{
    int begin = 0;
    int end = n - 1;
    while (begin < end)
    {
        int min = begin;
        int max = end;
        for (int i = begin + 1; i <= end; i++)
        {
            if (arr[min] > arr[i])
            {
                min = i;
            }
            if (arr[max] < arr[i])
            {
                max = i;
            }
        }
        Swap(&arr[begin], &arr[min]);
        // 如果最大值在 begin 位置,修正 max
        if (max == begin)
            max = min;
        Swap(&arr[end], &arr[max]);
        begin++;
        end--;
    }
}

3.3 特性总结

  • 选择排序的思路非常简单,但效率较低,实际中很少使用。
  • 时间复杂度:始终为 O(N²)。
  • 空间复杂度:O(1)。
  • 稳定性:不稳定(例如,交换时可能把前面的相同元素换到后面)。

四、堆排序

4.1 基本思想

堆排序是利用堆这种数据结构设计的排序算法,属于选择排序的一种。它通过建堆(升序建大堆,降序建小堆)将最大(最小)元素放到堆顶,然后与堆尾元素交换,再对剩余元素向下调整,重复此过程。

4.2 代码实现

c 复制代码
void Swap(int* a, int* b)
{
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

void AdjustDown(int* arr, int n, int parent)
{
    int child = parent * 2 + 1;
    while (child < n)
    {
        if (child + 1 < n && arr[child] < arr[child + 1])
        {
            child++;
        }
        if (arr[child] > arr[parent])
        {
            Swap(&arr[child], &arr[parent]);
        }
        parent = child;
        child = parent * 2 + 1;
    }
}

void HeapSort(int* arr, int n)
{
    // 建大堆
    for (int i = (n - 1 - 1) / 2; i >= 0; i--)
    {
        AdjustDown(arr, n, i);
    }
    int end = n - 1;
    while (end > 0)
    {
        Swap(&arr[0], &arr[end]);
        AdjustDown(arr, end, 0);
        end--;
    }
}

4.3 特性总结

  • 堆排序使用堆来选数,效率较高。
  • 时间复杂度:O(N log N)。
  • 空间复杂度:O(1)。
  • 稳定性:不稳定(向下调整可能破坏相同元素的相对顺序)。

五、冒泡排序

5.1 基本思想

冒泡排序通过重复遍历待排序序列,比较相邻元素,如果顺序错误就交换,直到没有需要交换的元素为止。每一趟都会将最大(最小)元素"冒泡"到末尾。

5.2 代码实现

c 复制代码
void BubbleSort(int* arr, int n)
{
    for (int j = 0; j < n; j++)
    {
        for (int i = 0; i < n - 1 - j; i++)
        {
            if (arr[i] > arr[i + 1])
            {
                Swap(&arr[i], &arr[i + 1]);
            }
        }
    }
}

5.3 特性总结

  • 冒泡排序非常容易理解和实现。
  • 时间复杂度:最坏 O(N²),最好 O(N)(优化后可检测是否已经有序)。
  • 空间复杂度:O(1)。
  • 稳定性:稳定(相邻元素相等时不交换)。

六、快速排序

6.1 基本思想

快速排序采用分治思想:选择一个基准值(key),将待排序序列分成两部分,左部分所有元素 ≤ 基准值,右部分所有元素 ≥ 基准值,然后递归地对左右两部分进行快速排序。

6.2 三种常见分区方法

hoare版本
c 复制代码
void QuickSort(int* arr, int left, int right)
{
    if (left >= right)
        return;
    int begin = left, end = right;
    int keyi = begin;
    while (begin < end)
    {
        while (begin < end && arr[end] >= arr[keyi])
            end--;
        while (begin < end && arr[begin] <= arr[keyi])
            begin++;
        Swap(&arr[begin], &arr[end]);
    }
    Swap(&arr[keyi], &arr[begin]);
    keyi = begin;
    QuickSort(arr, left, keyi - 1);
    QuickSort(arr, keyi + 1, right);
}
挖坑法
c 复制代码
void QuickSort(int* arr, int left, int right)
{
    if (left >= right)
        return;
    int begin = left, end = right;
    int tmp = arr[left];
    int hole = left;
    while (begin < end)
    {
        while (begin < end && arr[end] >= tmp)
            end--;
        arr[hole] = arr[end];
        hole = end;
        while (begin < end && arr[begin] <= tmp)
            begin++;
        arr[hole] = arr[begin];
        hole = begin;
    }
    arr[hole] = tmp;
    QuickSort(arr, left, hole - 1);
    QuickSort(arr, hole + 1, right);
}
前后指针法
c 复制代码
void QuickSort(int* arr, int left, int right)
{
    if (left >= right)
        return;
    int keyi = left;
    int prev = keyi;
    int cur = prev + 1;
    while (cur <= right)
    {
        if (arr[cur] <= arr[keyi])
        {
            prev++;
            Swap(&arr[prev], &arr[cur]);
        }
        cur++;
    }
    Swap(&arr[prev], &arr[keyi]);
    keyi = prev;
    QuickSort(arr, left, keyi - 1);
    QuickSort(arr, keyi + 1, right);
}

6.3 快速排序优化

三数取中法选key

为了避免最坏情况(例如数组已经有序),可以使用三数取中法选取基准值。

c 复制代码
int Mid(int* arr, int left, int right)
{
    int mid = (left + right) / 2;
    if (arr[left] < arr[right])
    {
        if (arr[right] < arr[mid]) return right;
        else if (arr[mid] > arr[left]) return mid;
        else return left;
    }
    else
    {
        if (arr[right] > arr[mid]) return right;
        else if (arr[left] > arr[mid]) return mid;
        else return left;
    }
}

在分区前,将选出的中间值与最左边交换即可。

小区间优化

递归到小的子区间(例如元素个数 ≤ 10)时,使用插入排序代替递归,可以减少递归深度,提高效率。

c 复制代码
if ((right - left + 1) <= 10)
{
    InsertSort(arr + left, right - left + 1);
    return;
}

6.4 快速排序非递归实现(借助栈)

c 复制代码
int PartSort(int* arr, int left, int right)
{
    int begin = left, end = right;
    int keyi = begin;
    while (begin < end)
    {
        while (begin < end && arr[end] >= arr[keyi]) end--;
        while (begin < end && arr[begin] <= arr[keyi]) begin++;
        Swap(&arr[begin], &arr[end]);
    }
    Swap(&arr[keyi], &arr[begin]);
    return begin;
}

void QuickSortNonR(int* arr, int left, int right)
{
    ST st;            // 假设栈已实现
    STInit(&st);
    STPush(&st, right);
    STPush(&st, left);
    while (!STEmpty(&st))
    {
        int left1 = STTop(&st); STPop(&st);
        int right1 = STTop(&st); STPop(&st);
        int keyi = PartSort(arr, left1, right1);
        if (keyi + 1 <= right1)
        {
            STPush(&st, right1);
            STPush(&st, keyi + 1);
        }
        if (left1 <= keyi - 1)
        {
            STPush(&st, keyi - 1);
            STPush(&st, left1);
        }
    }
    STDestroy(&st);
}

6.5 特性总结

  • 快速排序综合性能很好,是实际中最常用的排序算法之一。
  • 时间复杂度:平均 O(N log N),最坏 O(N²)(可通过三数取中优化)。
  • 空间复杂度:递归栈平均 O(log N),最坏 O(N)。
  • 稳定性:不稳定(分区过程中可能交换相同元素)。

七、归并排序

7.1 基本思想

归并排序采用经典的分治策略:将序列分成两个子序列,分别排序后,再将两个有序子序列合并成一个有序序列。通常使用递归实现,也可以用非递归(迭代)实现。

7.2 代码实现

递归版本
c 复制代码
void _MergeSort(int* arr, int* tmp, int left, int right)
{
    if (left == right)
        return;
    int mid = (left + right) / 2;
    _MergeSort(arr, tmp, left, mid);
    _MergeSort(arr, tmp, mid + 1, right);
    int begin1 = left, end1 = mid;
    int begin2 = mid + 1, end2 = right;
    int i = left;
    while (begin1 <= end1 && begin2 <= end2)
    {
        if (arr[begin1] < arr[begin2])
            tmp[i++] = arr[begin1++];
        else
            tmp[i++] = arr[begin2++];
    }
    while (begin1 <= end1)
        tmp[i++] = arr[begin1++];
    while (begin2 <= end2)
        tmp[i++] = arr[begin2++];
    memcpy(arr + left, tmp + left, sizeof(int) * (right - left + 1));
}

void MergeSort(int* arr, int n)
{
    int* tmp = (int*)malloc(sizeof(int) * n);
    _MergeSort(arr, tmp, 0, n - 1);
    free(tmp);
}
非递归(迭代)版本
c 复制代码
void MergeNonR(int* arr, 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;
            int j = i;
            if (begin2 >= n)      // 第二组不存在,无需合并
                break;
            if (end2 >= n)         // 第二组右边界越界,修正
                end2 = n - 1;
            while (begin1 <= end1 && begin2 <= end2)
            {
                if (arr[begin1] < arr[begin2])
                    tmp[j++] = arr[begin1++];
                else
                    tmp[j++] = arr[begin2++];
            }
            while (begin1 <= end1)
                tmp[j++] = arr[begin1++];
            while (begin2 <= end2)
                tmp[j++] = arr[begin2++];
            memcpy(arr + i, tmp + i, sizeof(int) * (end2 - i + 1));
        }
        gap *= 2;
    }
    free(tmp);
}

7.3 特性总结

  • 归并排序需要额外的空间,但它是稳定的,适合外部排序。
  • 时间复杂度:始终为 O(N log N)。
  • 空间复杂度:O(N)。
  • 稳定性:稳定(合并时若相等,先取左半部分,保证顺序不变)。

八、计数排序

8.1 基本思想

计数排序是一种非比较排序,适用于数据范围集中的情况。它通过统计每个元素出现的次数,然后根据统计结果将元素放回原数组。

步骤:

  1. 找出待排序数组的最大值和最小值,确定计数数组的范围。
  2. 统计每个值出现的次数。
  3. 根据次数,将元素依次放回原数组。

8.2 代码实现

c 复制代码
void CountSort(int* arr, int n)
{
    int min = arr[0], max = arr[0];
    for (int i = 0; i < n; i++)
    {
        if (min > arr[i]) min = arr[i];
        if (max < arr[i]) max = arr[i];
    }
    int range = max - min + 1;
    int* count = (int*)calloc(range, sizeof(int));
    if (count == NULL)
    {
        perror("calloc");
        return;
    }
    // 统计次数
    for (int i = 0; i < n; i++)
    {
        count[arr[i] - min]++;
    }
    // 回写
    int j = 0;
    for (int i = 0; i < range; i++)
    {
        while (count[i]--)
        {
            arr[j++] = i + min;
        }
    }
    free(count);
}

8.3 特性总结

  • 计数排序在数据范围集中时效率极高,但适用范围有限(只能处理整数或可映射的数据)。
  • 时间复杂度:O(MAX(N, range)),range为数据范围。
  • 空间复杂度:O(range)。
  • 稳定性:稳定(但上述实现是稳定的吗?实际上,如果从后向前遍历原数组并放入结果数组可以保持稳定,但上面的实现是直接按次数顺序填充,对于相同元素,相对顺序无法保证------但因为计数排序通常用于整数,稳定性意义不大;严格来说,上述写法不稳定,若要稳定需采用累加位置并反向填充的方法)。

总结与对比

排序算法 平均时间复杂度 最坏时间复杂度 空间复杂度 稳定性
插入排序 O(N²) O(N²) O(1) 稳定
希尔排序 O(N^1.3) O(N²) O(1) 不稳定
选择排序 O(N²) O(N²) O(1) 不稳定
堆排序 O(N log N) O(N log N) O(1) 不稳定
冒泡排序 O(N²) O(N²) O(1) 稳定
快速排序 O(N log N) O(N²) O(log N)~O(N) 不稳定
归并排序 O(N log N) O(N log N) O(N) 稳定
计数排序 O(N+range) O(N+range) O(range) 稳定*

选择建议:

  • 数据量小、基本有序:插入排序。
  • 一般情况、对稳定性无要求:快速排序(最快)。
  • 要求稳定、内存充足:归并排序。
  • 数据范围小、整数:计数排序。
  • 内存紧张、需稳定:冒泡排序(但效率低)或归并排序(但需要额外内存),实际上内存紧张时可采用堆排序或希尔排序。

希望本文能帮助你全面理解这八大排序算法。如果有任何疑问或建议,欢迎在评论区留言讨论!

(完)

相关推荐
2501_940315261 小时前
98验证二叉搜索树
java·数据结构·算法
像污秽一样2 小时前
算法设计与分析-算法效率分析基础-分治法
算法·排序算法
香水5只用六神2 小时前
【TIM】基本定时器定时实验(2)
c语言·开发语言·stm32·单片机·嵌入式硬件·mcu·学习
TrueDei2 小时前
linux-C/C++主子进程同时占用主进程文件描述符问题
linux·c语言·c++
仰泳的熊猫2 小时前
题目2266:蓝桥杯2015年第六届真题-打印大X
数据结构·c++·算法·蓝桥杯
cui_ruicheng2 小时前
C++ 数据结构:AVL树原理与实现
数据结构·c++
小龙报2 小时前
【数据结构与算法】环与相遇:链表带环问题的底层逻辑与工程实现
c语言·数据结构·c++·物联网·算法·链表·visualstudio
噜啦噜啦嘞好2 小时前
算法:双指针
数据结构
仟濹2 小时前
【算法打卡day20(2026-03-12 周四)算法:前缀和,二维前缀和,快慢指针,哈希表set使用技巧,哈希表map使用技巧】7个题
数据结构·算法