排 序

导图

各排序方法的比较

排序方法 平均情况 最好情况 最坏情况 辅助空间 稳定性
直接插入排序 O(n2) O(n) O(n2) O(1) 稳定
冒泡排序 O(n2) O(n) O(n2) O(1) 稳定
归并排序 O(nlog2n) O(n) 稳定
简单选择排序 O(n2) O(n2) O(n2) O(1) 不稳定
希尔排序 O(nlog2n)~O(n2) O(n1.25) O(n2) O(1) 不稳定
堆排序 O(nlog2n) O(1) 不稳定
快速排序 O(nlog2n) O(nlog2n) O(n2) O(nlog2n)~O(n2) 不稳定

插入排序

插入排序的基本思想是将待排序的数据元素逐步插入到一个已经排好序的序列中,当所有待排序的数据元素都已经插入后,排序操作完成。

1:直接插入排序

思想:

在直接插入排序中,当要插入第i个元素ai时,前面i-1个元素已经排好序。这时将ai依次与ai-1 ~ a1进行比较,并插入到相应位置,此时数据元素的个数加1。

算法实现:

void insertSort( int a[]; int n)
{
    int i, j, tmp;
    for ( i = 1; i < n; i++)
    {
        tmp = a[i]; // 监视哨
        j = i - 1;
        //移动
        while ( j >= 0 && tmp <a[j])
        {
            a[j+1] = a[j];
            j--;
        }
        a[j+1] = tmp; //插入
    }
}

注意:

直接插入排序是一种稳定的插入方法。

时间复杂度为O(n2)。

2:折半插入排序

思想:

折半插入排序采用折半查找法确定待插入数据元素的位置。前提条件是序列已经有序。

算法实现:

void binSort( int a[], int n)
{
    int i, j, tmp, mid, low, high;
    for( i = 1; i < n; i++)
    {
        tmp = a[i];
        low = 0;
        high = i-1;
        // 查找
        while( low <= high )
        {
            mid = ( low + high ) / 2;
            if( a[mid] > tmp )
            {
                high = mid-1;
            }
            else
            {
                low = mid + 1;
            }
        }
        // 移动
        for( j=i-1; j>=low;j--)
        {
            a[j+1]=a[j];
        }
        a[low] = tmp; // 插入
    }
}

注意:

折半插入排序是一种稳定的排序方法。

折半插入排序的时间复杂度为O(n2)。

3:希尔排序

希尔排序又称缩小增量排序

思想:

选择一个增量h1(h1 < n, n是排序数据元素的个数),把序列中的数据元素从第一个开始,按照相距h1的两个数据元素分为一组,共分为h1组,并采用直接插入排序进行排序。

在此基础上选定h2 (h2<h1),重复上步操作,直至增量h为1时再次排序以完成排序。

  • 希尔提出:取h2=n2, hi-1=⌊ hi 2⌋ (向下取整)

  • Knuth提出:取h2=n2, hi-1=⌊ hi 3⌋ (向下取整)

图示:

希尔排序

算法实现:

void shellSort(int a[], int n)
{
    int t = 0, *h, d, i, j, k, m, tmp;
    h = (int *)malloc(sizeof(int) * n);
    d = n / 2;
    while (d > 0) //计算每趟的间隔
    {
        h[t] = d; //存储每次的增量值
        t++;
        d /= 2;
    }
    for (k = 0; k < t; k++) //做t趟排序,最后一次为1
    {
        m = h[k];   //去本次排序间隔
        for (j = m; j < m; j++) //对各子表进行排序
        {
            tmp = a[j]; 
            i = j - m;
            //比较
            while (i >= 0 && a[i] > tmp)
            {
                a[i + m] = a[i];
                i = i - m;
            }
            //移动
            a[i + m] = tmp;//待插入元素进入有序子表
        }
    }
}

注意:

希尔排序是一种不稳定的排序方法。

希尔排序的性能取决于增量的序列选择。

希尔排序的时间复杂度为O(n1.25)

交换排序

1:冒泡排序

思想:

依次将相邻的两个元素进行比较,按升序(或降序)排列,直至把该趟最大的元素放至最后。每趟执行后,把最后位置记下,下次执行在此基础上减少一次比较,即已把最大(或最小)的元素放入每次执行后的最后一个元素,因此不再进行比较。重复执行,直到有序。

算法实现:

void bubbleSort( int a[], int n)
{
    int tmp, i, j, flag;
    flag = n-1; //flag 记录一趟比较中最后一次发生比较的位置。
    while( flag > 0 )
    {
        j = flag-1; // j用于记录要比较的最后位置。
        flag = 0;   // 重置flag初值
        for( i=0; i<=j; i++) //进行一趟比较,使相邻元素正序。
        {
            if(a[i] > a[i+1])
            {
                tmp = a[i];
                a[i] = a[i+1];
                flag = i;
            }
        }
    } 
}

注意:

冒泡排序是一种稳定的排序方法。

时间复杂度为O(n2)。

冒泡排序的数据元素移动次数与序列的初始状态有关。

2:快速排序

思想: 递归实现

任意选取一个数据元素作为基准元素。做一次划分,将所有数据元素集合分为3部分。

中间部分只包含一个数据元素,即基准元素。基准元素前半部分办函所有关键字均小于基准元素的数据元素,后半部分与之相反。

在此基础上在此进行划分排序,直至得到一个有序序列。

动画演示:

算法实现:

// 换分函数
int partition(int a[], int low, int high)
{
    int i, j, k, tmp;
    tmp = a[low]; //基准元素保存在tmp
    i = low;
    j = high;
    while (i != j) //low 和 high 相遇时即完成划分
    {
        while (a[j] >= tmp && i < j)
        {
            j--;
        }
        if (i < j) //j指示的关键字<tmp的关键字
        {
            a[i] = a[j];
            i++;
            while (a[i] < tmp && i < j)
            {
                i++;
            }
            if (i < j) //i指示的关键字>tmp的关键字
            {
                a[j] = a[i]; // i指示的元素交换到j的位置
                j--;
            }
        }
        a[i] = tmp;
        return i;
    }
}

// 递归实现快速排序
void quickSort(int a[], int low, int high)
{
    int i;
    if (high > low)
    {
        i = partition(a, low, high); // 确定基准元素
        quickSort(a, low, i - 1);    //前半部分
        quickSort(a, i + 1, high);   //后半部分
    }
}

注意:

  1. 快速排序是不稳定的排序算法。
  2. 如果原队列有序,快排会退化成冒泡。

选择排序

1:简单选择排序

思想:

每一趟在后面n-i-1个待排序元素中选取关键字最小的数据元素,作为有序子序列的第i个元素,直到n-1趟做完,待排序的元素只剩下一个,即完成排序。

排序过程:

对n个数组元素a1an~进行排序。

初始时 i = 1;

  1. 在数组元素a1an中选择最小(或最大的数据元素)ak~。
  2. 如果k不等于i,则交换ai和aK
  3. i = i+1, 重复执行直至i等于n终止。

动画演示:

算法实现:

void selectSort(int a[], int n)
{
    int i, j, k, tmp;
    for (i = 0; i < n - 1; i++) // 每次循环的表长
    {
        k = i; //记录要遍历的起始位置
        // 遍历比较数据源元素
        for (j = i + 1; j < n; j++) 
        {
            // 将大于起始位置的数据元素的位置记录下来
            if (a[j] < a[k])  
            {
                k = j;
            }
        }
        
        // 判断,如果记录的数据位置不与遍历的起始位置相等,则进行数据元素交换
        if (k != i)
        {
            tmp = a[i]; // 记录遍历起始位置的元素,tmp作为交换元素媒介
            a[i] = a[k];
            a[k] = tmp;
        }
    }
}

注意:

  1. 简单选择排序是一种不稳定的排序算法。
  2. 选择排序不需要辅助的存储单元。
2:堆排序

思想:

将无序序列建成一个堆,得到关键字最大(或最小)的记录,输出去堆顶的最大(或最小)值后,使剩余的n-1个元素又将建成一个堆,则可得到n个元素的次大(小)值。重复执行,直至得到一个有序序列。

  1. 把用数据存储的待排序数据,转换成一棵完全二叉树。

  2. 将完全二叉树转换成堆树。

  3. 有了堆树后,便可以排序。

特征:

堆排序法是利用堆树来进行排序的方法,堆树是一种特殊的二叉树。具备:

  • 是一棵完全二叉树。

  • 每一个根结点的值均大于(小于)或等于它的两个子结点的值。

  • 树根的值是堆树中最大(最小)的。

过程如下:

对数据元素a1 ~ an做堆排序:

初始时,令j = n;

  1. 建立大堆根
  2. 将堆顶与堆尾元素交换(先下后上,先右后左), j = j-1;
  3. 将序列 a1 ~ aj重新调整为一个大根堆
  4. 重复1.2.3.直到当前堆为空

动画演示:

算法实现:

void createHeap( int a[], int n)
{
    int i,j,k, tmp;
    for( k = 1; k<n; k++)
    {
        tmp = a[k];
        while(i != 0)
        {
            j= (i-1)/2;
            if( tmp <= a[j])
            {
                break;
            }
            a[i] = a[j];
            i = j;
        }
        a[i] = tmp;
    }
}

void siftDown( int a[], int n)
{
    int tmp = a[i];
    int j = 2*i + 1;
    while( j<=n )
    {
        if( j<n-1 && a[j] < a[j+1])
        {
            j++;
        }
        if ( tmp > a[j])
        {
            break;
        }
        a[i] = a[j];
        i = j;
        j = 2*i + 1;
    }
    a[i] = tmp;
}

void heapSort( int a[],int n)
{
    createHeap( a, n);
    while( n>0 )
    {
        int tmp;
        tmp = a[0];
        a[0] = a[n-1];
        a[n-1] = tmp;
        siftDown(a,n-1,0);
        n--;
    }
}

注意:

  1. 每一个根结点的值均大于或等于它的两个子结点的值
  2. 堆树是一种特殊的二叉树,即完全二叉树
  3. 堆是一种不稳定的排序

归并排序

思想:

归并排序是将两个或多个有序序列合并起来,并得到一个新的有序序列。

将两个有序序列合并成一个有序系列成二路归并。二路归并最简单,多路归并可由二路归并为基础实现。

动画演示:

算法实现:

// 归并算法
void merge(int a[], int low, int mid, int high)
{
    int i, j, k, m, *work;
    i = low, j = mid + 1, k = 0;
    m = high - low + 1;
    work = (int *)malloc(sizeof(int) * m);

    while (i <= mid && j <= high)
    {
        if (a[i] <= a[j])
        {
            work[k] = a[i];
            i++;
        }
        else
        {
            work[k] = a[j];
            j++;
        }
        k++;
    }

    if (i <= mid) // 处理第一个子表未归并元素
    {
        for (j = i; j <= mid; j++)
        {
            work[k] = a[j];
            k++;
        }
    }
    else if (j <= high) // 处理第一个子表未归并元素
    {
        for (i = j; i <= high; i++)
        {
            work[k] = a[i];
            k++;
        }
    }

    for (i = low; i <= high; i++)
    {
        a[i] = work[i - low]; //把排序结果存储到1中
    }
}

// 实现
void mergeSort(int a[], int n)
{
    int s, t, low, high, mid, m = 1;
    while (m < n)
    {
        s = 2 * m;
        for (t = 0; t < n; t = t + s)
        {
            low = t;
            high = t + s - 1;
            mid = t + m - 1;
            
            if (high > n - 1)
            {
                high = n - 1;
            }

            if (high > mid)
            {
                merge(a, low, mid, high);
            }
        }
        m = s;
    }
}
  • 递归实现
void merge(int arr[], int low, int mid, int high)
{
    int i, j, k;
    int n1 = mid - low + 1;
    int n2 = high - mid;
    int L[n1], R[n2];
    for (i = 0; i < n1; i++)
    {
        L[i] = arr[low + i];
    }
    for (j = 0; j < n2; j++)
    {
        R[j] = arr[mid + 1 + j];
    }
    i = 0;
    j = 0;
    k = low;
    while (i < n1 && j < n2)
    {
        if (L[i] <= R[j])
        {
            arr[k] = L[i++];
        }
        else
        {
            arr[k] = R[j++];
        }
        k++;
    }

    while (i < n1)
    {
        arr[k++] = L[i++];
    }

    while (j < n2)
    {

        arr[k++] = R[j++];
    }
}

// 实现
void mergeSort(int arr[], int low, int high)
{
    if (low < high)
    {
        int mid = (low + high) / 2;
        mergeSort(arr, low, mid);
        mergeSort(a, mid + 1, high);
        merge(arr, low, mid, high);
    }
}

注意:

归并排序算法是稳定的排序算法。

基数排序

思想:

将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。在改变位数比较时,在‘桶’中取出元素是按照‘’先进先出‘’的原则。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。

动画演示:

算法实现:

#include <stdio.h>
#include <stdlib.h>
#define MAX_NUM_OF_KEY 8 //构成关键字的组成部分的最大个数
#define RADIX 10        //基数,例如关键字是数字,无疑由0~9组成,基数就是10;如果关键字是字符串(字母组成),基数就是 26
#define MAX_SPACE 10000

//静态链表的结点结构
typedef struct{
    int data;//存储的关键字
    int keys[MAX_NUM_OF_KEY];//存储关键字的数组(此时是一位一位的存储在数组中)
    int next;//做指针用,用于是静态链表,所以每个结点中存储着下一个结点所在数组中的位置下标
}SLCell;

//静态链表结构
typedef struct{
    SLCell r[MAX_SPACE];//静态链表的可利用空间,其中r[0]为头结点
    int keynum;//当前所有的关键字中最大的关键字所包含的位数,例如最大关键字是百,说明所有keynum=3
    int recnum;//静态链表的当前长度
} SLList;

typedef int  ArrType[RADIX];//指针数组,用于记录各子序列的首尾位置

//排序的分配算法,i表示按照分配的位次(是个位,十位还是百位),f表示各子序列中第一个记录和最后一个记录的位置
void Distribute(SLCell *r,int i,ArrType f,ArrType e){
    //初始化指针数组
    for (int j=0; j<RADIX; j++) {
        f[j]=0;
    }
    //遍历各个关键字
    for (int p=r[0].next; p; p=r[p].next) {
        int j=r[p].keys[i];//取出每个关键字的第 i 位,由于采用的是最低位优先法,所以,例如,第 1 位指的就是每个关键字的个位
        if (!f[j]) {//如果只想该位数字的指针不存在,说明这是第一个关键字,直接记录该关键字的位置即可
            f[j]=p;
        }else{//如果存在,说明之前已经有同该关键字相同位的记录,所以需要将其进行连接,将最后一个相同的关键字的next指针指向该关键字所在的位置,同时最后移动尾指针的位置。
            r[e[j]].next=p;
        }
        e[j]=p;//移动尾指针的位置
    }
}
//基数排序的收集算法,即重新设置链表中各结点的尾指针
void Collect(SLCell *r,int i,ArrType f,ArrType e){
    int j;
    //从 0 开始遍历,查找头指针不为空的情况,为空表明该位没有该类型的关键字
    for (j=0;!f[j]; j++);
    r[0].next=f[j];//重新设置头结点
    int t=e[j];//找到尾指针的位置
    while (j<RADIX) {
        for (j++; j<RADIX; j++) {
            if (f[j]) {
                r[t].next=f[j];//重新连接下一个位次的首个关键字
                t=e[j];//t代表下一个位次的尾指针所在的位置
            }
        }
    }
    r[t].next=0;//0表示链表结束
}
void RadixSort(SLList *L){
    ArrType f,e;
    //根据记录中所包含的关键字的最大位数,一位一位的进行分配与收集
    for (int i=0; i<L->keynum; i++) {
        //秉着先分配后收集的顺序
        Distribute(L->r, i, f, e);
        Collect(L->r, i, f, e);
    }
}
//创建静态链表
void creatList(SLList * L){
    int key,i=1,j;
    scanf("%d",&key);
    while (key!=-1) {
        L->r[i].data=key;
        for (j=0; j<=L->keynum; j++) {
            L->r[i].keys[j]=key%10;
            key/=10;
        }
        L->r[i-1].next=i;
        i++;
        scanf("%d",&key);
    }
    L->recnum=i-1;
    L->r[L->recnum].next=0;
}
//输出静态链表
void print(SLList*L){
    for (int p=L->r[0].next; p; p=L->r[p].next) {
        printf("%d ",L->r[p].data);
    }
    printf("\n");
}
int main(int argc, const char * argv[]) {
    SLList *L=(SLList*)malloc(sizeof(SLList));
    L->keynum=3;
    L->recnum=0;
    creatList(L);//创建静态链表
    printf("排序前:");
    print(L);
   
    RadixSort(L);//对静态链表中的记录进行基数排序
   
    printf("排序后:");
    print(L);
    return 0;
}

注意:

  1. 由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
  2. 基数排序是一种稳定的排序算法。
  3. 在某些时候,基数排序法的效率高于其它的稳定性排序法。
  4. 排序过程无须比较关键字,而是通过“分配”和“收集”过程来实现排序。
  5. 基数排序分为最高位优先I(MSD)和最低位优先(LSD)。
posted @   Gonfei  阅读(113)  评论(0编辑  收藏  举报
(评论功能已被禁用)
编辑推荐:
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
点击右上角即可分享
微信分享提示