16--排序算法

232 阅读11分钟

关于图的基础知识和图的存储相关实现,以及数据结构与算法的其他相关知识,请查看文章《数据结构与算法基础知识文章汇总》

一、定义

假设含有N个记录的序列为(R1,R2,...,Rn),其相应的关键字为(K1,K2,...,Kn)。需确定1~N的一种排序P1,P2,...,Pn,使其相应的关键字满足Kp1<=Kp2<=...<=Kpn递减(递增)关系。即使得序列成为一个按关键字有序的序列(Rp1,Rp2,...,Rpn),这样的操作称为排序

二、分类

在计算机算法中,排序分为以下两个分类:

  • 1.内排序,在排序的过程中,待排序的全部记录都被放置在内存中;
  • 2.外排序,由于排序的记录太多,不能同时都放置在内存中,整个排序过程需要在内外存之间多次交换数据才能进行。

三、常用排序算法

1.在开始实现排序算法前,为方便算法的实现,设计如下数据结构和状态值

#define TRUE 1

#define FALSE 0

//用于要排序数组个数最大值,可根据需要修改
#define MAXSIZE 10000

typedef int Status;
typedef struct
{
    //用于存储要排序数组,r[0]用作哨兵或临时变量
    int r[MAXSIZE+1];

    //用于记录顺序表的长度
    int length;
}SqList;

2.实现打印函数

//3.数组打印
void print(SqList L)
{
    int i;
    for(i=1;i<L.length;i++)
        printf("%d,",L.r[i]);
    printf("%d",L.r[i]);
    printf("\n");
}

3.实现交换算法

//2.排序常用交换函数实现
//交换L中数组r的下标为i和j的值
void swap(SqList *L,int i,int j)
{
    int temp=L->r[i];
    L->r[i]=L->r[j];
    L->r[j]=temp;
}

1.冒泡排序

1.简单冒泡排序

void BubbleSort0(SqList *L){
    int i,j;
    for(i = 1; i < L->length; i++) {
        for(j = i+1; j <= L->length; j++) {
            if(L->r[i] > L->r[j])
                swap(L, i, j);
        }
    }
}
  • 1.第0个位置是哨兵,所以从1开始;
  • 2.两个for循环的意思解释为:从第1个元素开始,将表中的每个元素都与后面的元素进行比较,如果后面的元素较小,则交换两者,将的数据放到前面的数据放到后面,最终得到一个升序的序列。

image.png

2.正宗冒泡排序

void BubbleSort(SqList *L){
    int i,j;
    for(i = 1; i < L->length; i++) {
        //注意:j是从后面往前循环
        for(j = L->length-1; j>=i; j--) {
            //若前者大于后者(注意与上一个算法区别所在)
            if(L->r[j]>L->r[j+1]){
                //交换L->r[j]与L->r[j+1]的值;
                swap(L, j, j+1);
            }   
        }
    }
}
  • 1.内层循环是从数组的倒数第二个元素开始的,因为需要将倒数第二倒数第一进行比较;
  • 2.内层循环第1次是倒数第二和倒数第一进行比较;第2次是倒数第三和倒数第二进行比较;第3次是倒数第四和倒数第三进行比较。每次比较都将较小的元素往前移;
  • 3.外层循环第1次将最小的元素放在第1个位置;第2次将第二小的元素放在第2个位置,依此类推。

image.png

3.冒泡排序的优化

void BubbleSort2(SqList *L){
    int i,j;

    //flag用作标记
    Status flag = TRUE;
    
    //i从[1,L->length) 遍历;

    //如果flag为False退出循环. 表示已经出现过一次j从L->Length-1 到 i的过程,都没有交换的状态;
    for(i = 1; i < L->length && flag; i++) {

        //flag 每次都初始化为FALSE
        flag = FALSE;

        for(j = L->length-1; j>=i; j--) {

            if(L->r[j] > L->r[j+1]){
               //交换L->r[j]和L->r[j+1];
               swap(L, j, j+1);
            
               //如果有任何数据的交换动作,则将flag改为true;
               flag = TRUE;
            }
        }
    }
}
  • 1.flag用来标记当后面的序列是否需要执行比较
  • 2.如果内层循环中执行了交换,说明后面的序列是无序的,下次还需要进行比较
  • 3.flag标记当前是否执行过交换,如果没有执行过交换,说明后面的序列都是有序的,也就无需再继续比较了。

image.png

如上图左边的序列,从第2个元素开始都是有序的,并且第一次执行,只有第1个和第2个执行了交换,此后的序列已经变成有序的序列了,没有必要再继续循环比较下去了。

2.选择排序

void SelectSort(SqList *L){
    int i,j,min;
    
    for(i = 1; i < L->length; i++) {
        //① 将当前下标假设为最小值的下标
        min = i;
        
        //② 循环比较i之后的所有数据
        for(j = i+1; j <= L->length; j++) {
            //③ 如果有小于当前最小值的关键字,将此关键字的下标赋值给min
            if(L->r[min] > L->r[j]) {
                min = j;
            }
        }

        //④ 如果min不等于i,说明找到了最小值,则交换2个位置下的关键字
        if(i!=min)
        {
            swap(L, i, min);
        }
            
    }
}
  • 1.选择排序就是选择出后面最小的元素与当前元素进行交换
  • 2.从第1个开始,依次和后面的元素进行比较,如果找到了比第1个元素小的,则进行交换

image.png

3.插入排序

void InsertSort(SqList *L){
    int i,j;

    //L->r[0] 哨兵 可以把temp改为L->r[0]
    int temp=0;

    //假设排序的序列集是{0,5,4,3,6,2};
    //i从2开始的意思是我们假设5已经放好了. 后面的牌(4,3,6,2)是插入到它的左侧或者右侧
    for(i=2;i<=L->length;i++)
    {
        //需将L->r[i]插入有序子表
        if(L->r[i]<L->r[i-1])
        {
            //设置哨兵 可以把temp改为L->r[0]
            temp = L->r[i];

            for(j=i-1;L->r[j]>temp;j--)
            {
                  //记录后移
                  L->r[j+1]=L->r[j];
            }      

            //插入到正确位置 可以把temp改为L->r[0]
            L->r[j+1]=temp;
        }
    }
}
  • 1.只有一个元素时,默认是有序的,所以for循环从2开始;
  • 2.当比较到第i个元素时,前i-1个元素一定是有序的;
  • 3.当比较到第i个元素时,如果前面的i-1个元素有比它大的,则先记录i,再次前面的元素后移1位,直到不比i小时,停止后移。然后将i放置到空出来的位置

image.png

2前面的序列都是有序的,将2与前面序列的元素比较,如果比2大,则后移一位,直到出现比2小的元素或比较到第1元素为止。最后将2放置在前后移位后空置出来的位置上。

4.希尔排序

1.希尔排序实现思路 希尔排序就是将数组按下标的一定增量进行分组,再对每组使用插入排序算法进行排序。随着增量的减下,每组的元素越来越多,当增量减至为1时,刚好整个数组被分为一组。

例如:

image.png

image.png

image.png

2.代码实现

void shellSort(SqList *L){
    int i,j;

    int increment = L->length;

    //0,9,1,5,8,3,7,4,6,2
    //① 当increment 为1时,表示希尔排序结束
    do{
        //② 增量序列
        increment = increment/3+1;

        //③ i的待插入序列数据 [increment+1 , length]
        for(i = increment+1; i <= L->length; i++) {
            //④ 如果r[i] 小于它的序列组元素则进行插入排序,例如3和9. 3比9小,所以需要将3与9的位置交换
            if(L->r[i] < L->r[i-increment]) {
                //⑤ 将需要插入的L->r[i]暂时存储在L->r[0].和插入排序的temp 是一个概念;
                L->r[0] = L->r[i];

                //⑥ 记录后移
                for(j = i-increment; j > 0 && L->r[0]<L->r[j]; j-=increment) {
                    L->r[j+increment] = L->r[j];
                }

                //⑦ 将L->r[0]插入到L->r[j+increment]的位置上;
                L->r[j+increment] = L->r[0];
            }
        }
    }while (increment > 1);
}
  • 1.数据的长度为length,将数据分为increment = length/3 + 1组,即间隔increment的下标元素为一组
  • 2.对间隔increment的下标的每组元素进行插入排序
  • 3.缩小分组,比如原来是4组,现在变成2组,再变成1组;依次对分为4组、2组和1组情况下的各小组进行插入排序
  • 4.每次对分组进行插入排序时,都会把相对较小的元素移动到前面去,经过几轮后,整个序列就变得相对有序了;
  • 5.所以在最后increment = 1时,要再进行一次插入排序,保证最终得到的序列是绝对有序的。
  • 6.希尔排序是对插入排序升级插入排序是从第2个元素开始比较平移插入;而希尔排序是先将数组分成下标间隔为increment的increment等份,并分别对increment等份执行插入排序。通过缩小increment,达到数组的相对有序,以实现最终数组的绝对有序。相比希尔排序,插入排序的increment = 1

5.堆排序

1.堆结构 堆是具有下面性质的完全二叉树:

  • 1.每个结点的值都大于等于左右孩子的值,称为大顶堆
  • 2.每个结点的值都小于等于左右孩子的值,称为小顶堆

如果按照层序遍历的方式给结点从1开始编号,则结点之间满足如下关系:

image.png

2.大顶堆

image.png

  • 1.根结点60的编号是4左孩子的编号是24=8右孩子的编号是24+1=9,即30和20结点;
  • 2.根结点60大于左孩子30,也大于右孩子20。

3.小顶堆

image.png

  • 1.30结点的编号是4,左孩子的编号是24=8,右孩子的编号是24+1=9,即60和40结点;
  • 2.30结点小于左孩子60,小于右孩子40。

也就是说,可以把数组看作是一个完成二叉树堆结构。于是我们就可以利用堆结构的特征,来实现排序算法。

4.实现思路

  • 1.根据需要构造大顶堆或小顶堆,如果是升序,则构造大顶堆;如果是降序,则构造小顶堆;以升序构造大顶堆为例。
  • 2.将符合完全二叉树n/2个非叶子结点和它们的左右孩子调整为大顶堆,即找到它们的最大值放置到根结点,则得到根结点都大于左右孩子的结点的堆结构
  • 3.交换堆顶元素末尾元素的值,原后再对交换后剩余元素构造大顶堆

(1)例如对下面这棵完成二叉树n/2个非叶子结点50、10、90、30构建大顶堆

image.png (2)构建的结果为: image.png

(3)构建后进行交换堆顶与末尾元素: image.png

最终,我们将最大的值90放在了堆顶位置。然后再将堆顶元素90未尾的元素20进行交换,即把最大值放到了最后面去。

(4)交换堆顶和末尾元素后将剩余的元素再构建大顶堆: image.png 交换后,最大值放到了数组未尾。但前面n-1个元素又不符合大顶堆结构了,于是我们将前面n-1个元素构建大顶堆。即找到n-1个元素中的最大值,然后再将这个最大值放到剩余元素末尾。如此循环下来,我们将剩于元素最大值都往后放了,直到全部放完为止,就得到了最终的有序序列

5.代码实现

(1)构建大顶堆

void HeapAjust(SqList *L,int s,int m){
    int temp,j;

    //记录根结点
    temp = L->r[s];

    //找到左右子树,并与根结点进行比较后交换,s为根结点,则2 * s为左孩子,
    for(j = 2 * s; j <=m; j*=2) {
        //j为左孩子,j+1为右孩子。如果右孩子大于左孩子,则保存右孩子,否则j保存左孩子
        if(j < m && L->r[j] < L->r[j+1])
        {
            ++j;//j是左右孩子中的较大者
        }
        
        //比较根结点与j的大小
        if(temp >= L->r[j])
        {
            break;
        }

        //j比根结点大,所以把j和根结点为进行交换
        L->r[s] = L->r[j];

        //记录被交换的结点
        s = j;
    }
    
    //将要根结点的值赋给被交换的结点
    L->r[s] = temp;
}

(2)堆排序实现

void HeapSort(SqList *L){
    int i;

    //将完全二叉树的n/2个非叶子结点构建大顶堆
    for(i=L->length/2; i>0;i--){
        HeapAjust(L, i, L->length);
    }

    //2. 将堆顶与末尾元素进行交换,再将剩余元素进行大顶堆的构建
    for(i = L->length; i > 1; i--){
        //① 将堆顶记录与当前未经排序子序列的最后一个记录进行交换;
        swap(L, 1, i);

        //② 将L->r[1...i-1]重新调整成大顶堆;
        HeapAjust(L, 1, i-1);
    }
}

四、总结

排序算法有:

  • 1.冒泡排序:从最后一个元素开始,依次与前一个元素进行比较,满足比较条件则交换的过程;
  • 2.选择排序:从第i个元素开始,依次找到后面比自己更大(或更小)的元素,找到则交换的过程;
  • 3.插入排序;从第1+i个元素开始,依次与前面的元素作比较,不满足比较,则将被比较的元素后移一位,直到满足比较时将当前元素插入空出来的位置;
  • 4.希尔排序:将数组元素间隔划分为若干组,每组进行插入排序。缩小分组后,再对每组进行插入排序的过程;
  • 5.堆排序:构建堆结构,将堆顶与末尾交换,再将剩余元素进行堆的构建,再交换堆顶与末尾元素的过程;