常用排序算法的C语言实现方式

新手上路,请多指教。如果有写的不对的地方,还请指出,非常感谢。

参考书目:数据结构(C语言版)(第2版),殷人昆编著, 清华大学出版社

目录

排序

¶ 前置代码

一、插入排序

1.1 直接插入排序

¶ 代码 

1.2 折半插入排序

¶ 代码

1.3 希尔排序

¶ 代码

二、交换排序

2.1 冒泡排序

¶ 代码

2.2 快速排序

¶ 基本代码 

2.2.1 Hoare划分

¶ Partition_Hoare

2.2.2 Rowe划分

¶ Partition_Rowe

2.2.3 改进一 cutoff

2.2.4 改进二 median3

¶ median3

三、选择排序

3.1 简单选择排序

¶ 代码

3.2 堆排序

¶ 代码

四、归并排序

4.1 二路归并排序

¶ 代码

五、基数排序

¶ 前置代码 

5.1 MSD基数排序

5.2 LSD基数排序

 六、其他排序


排序

排序是计算机内经常进行的一种操作,

其目的是将一 组“无序”的记录序列调整为“有序”的记录序列。

假设含n个记录的序列为{ R1, R2, …, Rn } , 其相应的关键字序列为 { K1, K2, …,Kn } ,这些关键字相互之间可以进行比较,即在它们之间 存在着这样一个关系 : Kp1≤Kp2≤…≤Kpn , 按此固有关系将上式记录序列重新排列为 { Rp1, Rp2, …,Rpn } 的操作称作排序。 

 ¶ 排序方法的分类

  • 有序区增长。将数据表分为有序区和无序区,在排序过程中逐步扩大有序区,直到有序区扩大到整个数据表
  • 有序程度增长。数据不能明确分为有序区和无序区,随着排序过程执行,数据表有程度逐渐提高,直到完全有序。 

¶ 排序算法的稳定性

        在待排记录序列中,任何两个关键字相同的记录,用某种排序方法排序后相对位置不变,则称这种排序方法是稳定的,否则称为不稳定的。

        并非稳定算法优于不稳定算法,各有各的适合场合。在很一些情况下,数据原本的相对次序是没有意义或者不重要的,不稳定的算法完全可以满足要求。

        要排序的内容是一个复杂对象的多个数字属性,其原本的初始顺序存在意义,且二次排序时保持原有排序的意义。此时必须使用稳定的算法。

        一般来说,出现大跨度移动的算法是不稳定的。当然具体情况具体分析。比如将冒泡算法的比较条件改为 >= ,就不稳定了。

排序方法的比较

常用排序算法的C语言实现方式_第1张图片

排序方法的选择

不同的排序方法适应不同的应用环境和要求 

1. 若n较小,可采用直接插入或简单选择排序

        ¶ 当记录规模较小时,直接插入排序较好,它会 比选择更少的比较次数,且是稳定的;

        ¶ 当记录规模稍大时,因为简单选择移动的记录 数少于直接插入,宜用简单选择排序。

2. 若初始状态基本有序,则应选用直接插入、冒泡或随机的快速排序为宜;

3. 若n较大,则应采用时间复杂度为O(nlog2n)的排序

        ¶ 快速排序、堆排序、归并排序。

4. 特殊的基数排序

常用排序算法的C语言实现方式_第2张图片

三种平均时间复杂度为O(nlog2n)的算法 

  • 快速排序的平均效率高,但是最坏时间复杂度 为O(n2),且空间复杂度为O(log2n) 
  • 堆排序的最坏时间复杂度为O(nlog2n),且空间 复杂度仅为O(1),但是不稳定 
  • 归并排序的最坏时间复杂度为O(nlog2n),且是稳定算法,但是空间复杂度为O(n)

 ¶ 内部排序和外部排序

排序算法可以分为内部排序外部排序

  • 内部排序是数据记录在内存中进行排序,不需要访问外存便能完成. 
  • 外部排序是因排序数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存 

本文中介绍的方法均为内部排序,使用数组实现,从小到大排列。并且添加了一些中间过程的输出。本人在自行测试的时候是没有问题。如果有朋友发现有误,还请提出,我尽快改正。请多多谅解。


¶ 前置代码

#include 
#include 
typedef int DataType;

void printArray(DataType a[], int n)
{
    for (int i = 0; i < n; i++)
        printf("%d ", a[i]);
    printf("\n");
}

void swap(DataType *a,DataType *b)
{
    DataType t=*a;
    *a=*b;
    *b=t;
}
int main(int argc, char const *argv[])
{
    
    int n;
    scanf("%d",&n);
    DataType *array=(DataType*)malloc(n*sizeof(DataType));
    for (int i = 0; i < n; i++)
        scanf("%d",array[i]);
    printArray(array, n);

    /*调用排序算法*/
    
    printArray(array, n);
    free(array);
    return 0;
}

一、插入排序

基本方法:

每步将一个待排序的元素按其排序码大小插入到前面有序区的适当位置,直到元素全部插入

  1. 在R[1..i-1]中查找R[i]的插入位置: R[1..j].key ≤ R[i].key < R[j+1..i-1].key;
  2. 将R[j+1..i-1]中的所有记录均后移一个位置;
  3. 将R[i] 插入(复制)到R[j+1]的位置上

1.1 直接插入排序

基于顺序查找

从 i-1 往前 扫描查找待插入位置

直接插入排序
i (0) (1) (2) (3) (4) (5)
初始序列 21 25 49 25* 16 8
1 21 25 49 25* 16 8
2 21 25 49 25* 16 8
3 21 25 25* 49 16 8
4 16 21 25 25* 49 8
5 8 16 21 25 25* 49

灰色为有序区,紫色为待插入数据

¶ 直接插入排序特点

  • 算法简单
  • 存储结构:顺序、链式
  • 时间复杂度为O(n^2)
  • 空间复杂度为O(1)
  • 稳定
  • 适用于:
  1. 若待排序记录按关键码基本有序时,直接插入 排序的效率可以大大提高;
  2. 由于直接插入排序算法简单,则在待排序记录数量 n 较小时效率也很高 

¶ 代码 

void InsertSort(DataType a[], int n)
{
    int i, j;
    for (i = 1; i < n; ++i)
    {//0~i-1是有序的
        DataType tmp = a[i];
        /*查找与移动同时进行*/
        for (j = i - 1; j >= 0 && a[j] > tmp; --j)
            a[j + 1] = a[j];//后移元素
        a[j + 1] = tmp;
        printf("The %2dth insertion: ", i);
        printArray(a, n);
    }   
}

1.2 折半插入排序

基于二分查找

通过二分查找待插入位置,与直接插入相比减少了比较次数

¶ 代码

void BinaryInsertSort(DataType a[], int n)
{
    for (int i = 1; i < n; i++)
    {//0~i-1是有序的
        DataType tmp = a[i];
        int low = 0;
        int high = i - 1;
        while (low <= high)
        {//二分查找插入位置
            int mid = (low + high) / 2;
            if (tmp < a[mid])    high = mid - 1;//向左缩区间
            else    low = mid + 1;//向右缩区间
        }
        for (int j = i - 1; j >= low; j--)
            a[j + 1] = a[j];//后移元素
        a[low] = tmp;
        printf("The %2dth insertion: ",i);
        printArray(a,n);
    }
}

1.3 希尔排序

¶ 基于逐趟缩小增量

基本思想:对待排记录序列先作“宏观”调整,再作 “微观”调整。

  1. 对数据分组,在各组内进行直接插入排序;
  2. 作若干次使待排记录基本有序;
  3. 对全部记录进行一次顺序插入排序;

分组方式:将 n 个记录分成 d 个子序列:

  • { R[1],R[1+d],R[1+2d],…,R[1+kd] }
  • { R[2],R[2+d],R[2+2d],…,R[2+kd] }
  • { R[d],R[2d],R[3d],…,R[kd],R[(k+1)d] }

d 称为增量,它的值在排序过程中从大到小逐渐缩小, 直至最后一趟排序减为 1 

希尔排序
初始序列 16 25 12 30 47 11 23 36 9 18 31
第一趟希尔排序,设 d=5
分组 16 25 12 30 47 11 23 36 9 18 31
排序 11 23 12 9 18 16 25 36 30 47 31
第二趟希尔排序,设 d=3
分组 11 23 12 9 18 16 25 36 30 47 31
排序 9 18 12 11 23 16 25 31 30 47 36
第三趟希尔排序,设 d=1
分组 9 18 12 11 23 16 25 31 30 47 36
排序 9 11 12 16 18 23 25 30 31 36 47

 ¶ 希尔排序特点

  • 不稳定
  • 性能依赖于增量的选择,介于O(nlog2n)~O(n2)
    • Hibbard:gap=2^k-1 , 2^(k-1)-1 , ……,7, 3, 1
    • Sedgewick: {1, 5, 9, 41, 109,……},交替取9*4^i-9*2^i+1 和 4^i-3*2^i+1
    • Knuth:gap=【gap/3】+1(【】向下取整

¶ 代码

void ShellSort(DataType a[], int n, int d[], int m)
{ // d[m]存放增量,d[0]=1
    for (int k = m - 1; k >= 0; k--)
    {
        int gap = d[k];//缩小增量
        for (int start = 0; start < gap; start++)
        { // 直接插入的变形,间隔从1改为gap
            for (int j, i = start + gap; i < n; i += gap)
            {
                DataType tmp = a[i];
                for (j = i - gap; j >= start && a[j] > tmp; j -= gap)
                    a[j + gap] = a[j];
                a[j + gap] = tmp;
            }
        }
        printf("The gap= %d: ", gap);
        printArray(a, n);
    }
}

二、交换排序

基本思想

将待排记录中两两记录关键字进行比较, 若逆序则交换位置。

2.1 冒泡排序

设计思路:

  1. 比较相邻的元素。如果逆序,则交换之。
  2. 经历一趟交换后,从后往前将无序区最小元素交换到有序区尾、无序区头,扩大有序区(也可从前往后,无序区在前,有序区在后)。
  3. 设置flag, 在该趟过程中无逆序,则提前终止。
  4. 最多进行 n-1 趟交换后,排列整齐。
冒泡排序(从后向前)
i (0) (1) (2) (3) (4) (5)
初始序列 21 25 49 25* 16 8
1 8 21 25 49 25* 16
2 8 16 21 25 49 25*
3 8 16 21 25 25* 49
4 8 16 21 25 25* 49

冒泡排序特点

  • 时间复杂度O(n2)
  • 空间复杂度O(1) 
  • 稳定性:稳定 

¶ 代码

void BubbleSort(DataType a[], int n)
{
    for (int i = 0; i < n ; i++)
    {
        int flag = 0;
        for (int j = n - 1; j > i ; j--)
        {//将第i小的元素交换到第i位
            if (a[j - 1] > a[j])
            {
                swap(&a[j - 1], &a[j]);
                flag = 1;
            }
        }
        printf("The %dth swap:", i + 1);
        printArray(a, n);
        if (!flag)    return; // 没有逆序,排列整齐
    }
}


2.2 快速排序

基本思想:

分治与递归

  • 通过一趟排序将待排序记录分割成两个部分,
  • 选择一个关键字作为分割标准,称为pivot,
  • 一部分记录的关键字比另一部分的小。

基本操作:

  1. 选定一记录R(pivot),将所有其他记录关键字k’ 与该记录关键字k比较,
  2. 若 k’k 则将记录换至R之后,
  3. 继续对R前后两部分记录进行快速排序,直至排序范围为1。
快速排序
初始 49 38 65 97 76 13 27 49
1 [27 38 13] 49 [76 97 65 49]
2 [13] 27 [38] 49 [49 65] 76 [97]
3 13 27 38 49 49 65 76 97

¶ 快速排序特点 

  1.  存储结构:顺序
  2. 时间复杂度
    • 最坏情况:每次划分选择pivot是最小或最大元素 O(n2)
    • 最好情况(每次划分折半): O(nlog2n)
    • 平均时间复杂度为O(nlog2n)
  3. 空间复杂度
    • 最坏情况:O(n)
    • 最好情况(每次划分折半): O(log2n)
    • 平均空间复杂度O(log2n)
  4. 稳定性 :不稳定

¶ 基本代码 

void QuickSort(DataType a[], int left, int right)
{
    if (left >= right)    return;
    int len = right - left + 1;
    printf("                 ");
    printArray(a + left, len);
//if(len < cutoff){Insert(a+left, len);return;}
//median3(a, left, right);
    int pivotpos = pivotpos = Partition(a, left, right);              
    printf("The pivot =%3d : ", a[pivotpos]);
    printArray(a + left, len);
    QuickSort(a, left, pivotpos - 1);
    QuickSort(a, pivotpos + 1, right,);
}

2.2.1 Hoare划分

  1.  将最左元素设置为pivot
  2.  从表的两端交替地向中间扫描,直到两个指针相遇
    1. 先从高端扫描
    2. 找到第一个比pivot小的记录
    3. 将该记录移动到low指针指向的地方
    4. 再从低端扫描
  3.  将pivot移动到low指针位置,并返回该位置

常用排序算法的C语言实现方式_第3张图片

¶ Partition_Hoare
int  Partition(DataType a[], int left, int right)
{
    DataType pivot = a[left]; // 以最左元素为基准
    int l = left, r = right;
    while (l < r)
    {
        while (a[r] >= pivot && l < r)
            r--; // 从右直到找到小于pivot
        a[l] = a[r]; // 交换到左边

        while (a[l] <= pivot && l < r)
            l++; // 从左直到找到大于pivot
        a[r] = a[l]; // 交换到右边
    }
    a[l] = pivot; // l=r,回放基准
    return l;//返回基准元素位置
}

2.2.2 Rowe划分

  1. 将最左元素设置为pivot
  2. 从左到右一趟扫描
  3. 凡是比pivot小的都交换到左边
  4. 最后再对pivot交换,并返回该位置

常用排序算法的C语言实现方式_第4张图片

¶ Partition_Rowe
int  Partition(DataType a[], int left, int right)
{

    int l = left;
    DataType pivot = a[l];//以最左元素为基准
    for (int i = left + 1; i <= right; i++)
    {//一趟扫描整个序列
        if (a[i] < pivot)
        {//left+1~l < pivot
            l++;            //小于基准,交换
            if (l != i)    swap(&a[l], &a[i]);
        }
    }
    a[left] = a[l];
    a[l] = pivot;
    return l;//返回基准元素位置
}

2.2.3 改进一 cutoff

当 n 很小时,快速排序往往慢于简单排序

当序列长度为5~25时,采用直接插入排序要比快速排序快至少 10%

设置cutoff,当长度小于设定值时采用直接插入排序

2.2.4 改进二 median3

快速排序在处理比较有序的序列时效率很低,容易形成单枝树。

尽量将pivot取在中间位置。

一种方法是left,mid,right 三者取中作为 pivot;也可以取五个甚至更多。

¶ median3
void median3(DataType a[],int left,int right)
{
    int k1, k2; // k1最小指针,k2次小指针
    int mid = (left + right) / 2;
    if (a[left] <= a[mid])  {k1 = left;k2 = mid;}
    else                    {k1 = mid;k2 = left;}
    if (a[right] < a[k1])      {k2 = k1;k1 = right;}
    else if (a[right] < a[k2])  k2 = right;
    if (k2 != left)         swap(&a[k2], &a[left]);
    printf("-----a[%d]=%d,a[%d]=%d,a[%d]=%d\n",
                left,a[left],mid,a[mid],right,a[right]);
}

三、选择排序

3.1 简单选择排序

设计思路:

  • 第 i 趟(i=0,1,…,n-2),在 i~n-1中选出最小元素,交换到a[i]位置,使之成为有序区的第 i 个元素。
  • 共执行 n-1 趟。 

基本步骤:

  1.  在一组元素 a[i]~a[n-1] 中选择最小元素
  2. 若它不是这组第一个元素(a[i]),将两者对调
  3. 在剩余序列 a[i+1]~a[n-1]中重复执行 1、2 ,直到剩下一个元素
简单选择排序
21 25 49 25* 16 8
8 25 49 25* 16 21
8 16 49 25* 25 21
8 16 21 25* 25 49
8 16 21 25* 25 49
8 16 21 25* 25 49

 不稳定

¶ 代码

void SelectSort(DataType a[], int n)
{
    int i, j, k;
    DataType tmp;
    for (i = 0; i < n - 1; i++)
    {//第i趟找i位置的元素
        k = i;
        for (j = i + 1; j < n; j++)//寻找最小
            if ((a[j] < a[k]))    k = j;
        if (k != i)    swap(&a[k], &a[i]);
        printf("The %2dth select: ", i+1);
        printArray(a, n);
    }
}

3.2 堆排序

设计思想:

  1. 建立大根堆,堆顶元素即最大值
  2. 将其放在末尾,成为有序区的首位。有序区从后往前扩大
  3. 重新调成堆,得到当前序列最大值

基本步骤:

  1. 建立初始大根堆,自下而上;
  2. 将堆顶元素与最后一个元素对换;
  3. 调整堆 (H.R[s..m]中记录的关键字除 R[s] 之外均满足堆的特征;
  4. 重复上述过程,共进行n-1次。

创建大根堆 

常用排序算法的C语言实现方式_第5张图片常用排序算法的C语言实现方式_第6张图片 

堆排序过程

常用排序算法的C语言实现方式_第7张图片常用排序算法的C语言实现方式_第8张图片常用排序算法的C语言实现方式_第9张图片常用排序算法的C语言实现方式_第10张图片

堆排序特点 

  • 堆排序的最坏时间复杂度为O(nlogn)。
  • 空间复杂度:1个记录空间,O(1)
  • 稳定性:不稳定
  • 适合于n较大的情况。 

¶ 代码

void siftDown(DataType a[], int start, int m)
{//从start到m自上而下建立大根堆
    int i = start, j = 2 * i + 1; // j是i的左子节点
    DataType tmp = a[i];
    while (j <= m)
    {
        if (j < m && a[j] < a[j + 1]) // 左右节点都存在
            j++;                      // j指向大的子节点
        if (tmp >= a[j])    break; // 找到原根节点应存放位置
        a[i] = a[j];
        i = j; // i下降到大的子节点
        j = 2 * j + 1;
    }
    a[i] = tmp;
}

void HeapSort(DataType a[], int n)
{
    for (int i = (n - 2) / 2; i >= 0; i--)
        siftDown(a, i, n - 1);//从倒数第二层向上扩大调成堆
    printf("Initial heap:");
    printArray(a,n);

    for (int i = n - 1; i > 0; i--)
    {
        swap(&a[0], &a[i]); // 交换最大元素至无序序列末尾
        printf("The %dth operation:", n - i);
        printArray(a, i);
        siftDown(a, 0, i - 1);
    }
}

四、归并排序

基本思想:分治

划分:将序列划为等长序列,为子序列排序

归并: 将子序列组合成一个新的有序表。

4.1 二路归并排序

¶ 设计思想:

  1. 设初始序列含有n个记录,则可看成 n 个有序的子序列,每个子序列长度为1。
  2. 两两合并,得到 /2 个长度为 2 或1的有序子序列。
  3. 再两两合并,……
  4. 如此重复,直至得到一个长度为 n 的有序序列为止。

常用排序算法的C语言实现方式_第11张图片

¶ 归并排序特点

  • 时间复杂度: O(nlogn)。共进行 log2 n趟归并,每趟对n个记录进行归并
  • 空间复杂度: O(n)
  • 稳定性: 稳定

¶ 代码

void MergeSort(DataType a[],int left,int right)
{
    if (left >= right)    return;
    /*划分*/
    int mid = (left + right) / 2;
    MergeSort(a, left, mid);
    MergeSort(a, mid + 1, right);
    /*归并*/
    int i = left, j = mid + 1;         // 左右子序列头指针
    int len = right - left + 1, k = 0; // 辅助数组长度、指针
    DataType *t = (DataType *)malloc(len * sizeof(DataType));
    while (i <= mid && j <= right)
    {
        if (a[i] <= a[j])    t[k++] = a[i++];
        else                 t[k++] = a[j++];
    }
    while (i <= mid)    t[k++] = a[i++];
    while (j <= right)  t[k++] = a[j++];
    
    for (int i = 0; i < len; i++)
        a[left + i] = t[i];
    printf("----");
    printArray(t,len);    
    free(t);
}

五、基数排序

基数排序:借助多关键字排序的方法对单关键字排序

将整数按位数切割成不同的数字,然后按每个位数分别比较

包含多位 k = k1,k2,…,kd 的单关键字

         多关键字排序

  • 最高位优先(MSD:Most Significant Digit first)
  • 最低位优先(LSD:Least Significant Digit first) 

¶ 基数排序的特点

  • 关键字包含d位 k = k1, k2, …, kd. 每个关键字最多包含r个不同关键字
  • 非比较型整数排序
  • 基本步骤:分配、收集
  • 时间复杂度 O( d(n+r) )
  • 空间复杂度 O( n+r )
  • 稳定 

¶ 前置代码 

#include 
#include 
#define MAX 20
#define BASE 10 // 基数
void printArray(int a[], int n)
{
    for (int i = 0; i < n; i++)
        printf("%d ", a[i]);
    printf("\n");
}
int geMaxtDigit(int a[], int n)
{ // 待排序序列数字的最大位数
    int digit = 1;
    int base = BASE;
    int max = a[0];
    for (int i = 1; i < n; i++)
        if (a[i] > max)    max = a[i];
    while (max >= base)
    {
        digit++;
        base *= 10;
    }
    printf("%d\n",digit);
    return digit;
}
int getFigure(int x, int k)
{ // 获取右起第k位数字
    for (int i = 1; i < k; i++)
        x /= 10;
    return x % 10;
}

5.1 MSD基数排序

高位优先  先通过一次分配将数据分成多个组,然后对各组数据分别排序

常用排序算法的C语言实现方式_第12张图片

void MSD(int a[],int left,int right,int k,int digit)
{
    if (left >= right || k > digit)    return;
    int i, j;
    
    int count[BASE] = {0}; // 统计某一位出现相同数字个数
    for (i = left; i <= right; i++)
    {
        int index = getFigure(a[i], digit - k+1);
        count[index]++; // 统计各桶元素个数
    }
    
    int start[BASE] = {0}; // 记录当前位上各个数字开始的位置
    for (j = 1; j < BASE; j++)
        start[j] = count[j - 1] + start[j - 1]; // 安排各桶元素位置

    int *atemp = (int *)malloc((right - left + 1) * sizeof(int));
    for (i = left; i <= right; i++)
    {
        int index = getFigure(a[i], digit - k+1);
        atemp[start[index]++] = a[i];
    }
    for (i = left, j = 0; i <= right; i++, j++)
        a[i] = atemp[j];
    free(atemp);

    int p1 = left,p2;//每个桶的头尾
    for (j = 0; j < BASE; j++)
    {//处理各桶里的数据
        p2 = p1 + count[j] - 1;
        MSD(a, p1, p2, k + 1,digit);
        p1 = p2 + 1;
    }  
}

void radixSort_MSD(int a[], int n)
{
    int digit = geMaxtDigit(a, n);
    MSD(a, 0, n - 1, 1, digit);
}

 接收器太小,递归层次增加,效率降低;太大会出现很多空接收器,同样影响效率

5.2 LSD基数排序

低位优先  通过多次对全体数据集的分配和收集实现排序

常用排序算法的C语言实现方式_第13张图片 常用排序算法的C语言实现方式_第14张图片

void radixSort_LSD(int a[], int n)
{
    int digit = geMaxtDigit(a, n);
    int base = 1;
    int *atemp = (int *)malloc(sizeof(int) * n);
    while (digit--)
    {
        int count[BASE] = {0}; // 统计某一位出现相同数字个数
        for (int i = 0; i < n; i++)
        {
            int index = a[i] / base % BASE;
            count[index]++;
        }

        int start[BASE] = {0}; // 记录当前位上各个数字开始的位置
        for (int i = 1; i < BASE; i++)
            start[i] = start[i - 1] + count[i - 1];
        
        for (int i = 0; i < n; i++)
        {
            int index = a[i] / base % BASE;
            atemp[start[index]++] = a[i];
        }
        printf("Collect the %d th digit: ", digit + 1);
        printArray(atemp, n);
        for (int i = 0; i < n; i++)
            a[i] = atemp[i];
        base *= BASE;
    }
    free(atemp);
}

 六、其他排序

除了上述排序还有一些也经常会用到比如计数排序、桶排序等等。

先留个坑,日后再写。

你可能感兴趣的:(数据结构与算法设计,排序算法,c语言)