排序算法(直接插入、折半插入、希尔排序、冒泡排序、快速排序、简单选择排序、堆排序

53 阅读8分钟

   注:

  • 稳定:当a与b相等时,如果a原本在b前面,排序之后a仍然在b的前面。
  • 不稳定:当a与b相等时,如果a原本在b的前面,排序之后 a 可能会出现在 b 的后面。
  • 时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
  • 空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。

三、插入类排序

       1.直接插入排序

    (1)基本思想

       将待插入元素依次跟前面已经排好的元素相比较,如果选择的待插入元素比已排序的元素小,则已排序元素后移,待插入元素继续与前一个元素比较,直到找到合适插入位置插入。

使用直接插入排序方法为一列数字进行排序过程

       (2)算法描述      

       具体算法描述如下:

       ①从第一个元素开始,该元素可以认为已经被排好序;

       ②取出下一个元素,在已经排好序的元素序列中从后向前扫描;

       ③如果该元素(已排好序中元素)大于新元素(待插入元素),则将该元素移到下一位置;

       ④重复步骤③,直到找到已排好序的元素小于或者等于新元素的位置;

       ⑤将新元素插入到该位置;

       ⑥重复步骤②~⑤。

直接插入排序演示

       (3)代码实现

//直接插入排序——C语言实现
//本算法中使用了监视哨,主要是为了避免数据在后移时丢失。
#include <stdio.h>
int insort(int s[], int n)    /* 自定义函数 insort()*/
{
    int i,j;
    for(i=2;i<=n;i++)    //数组下标从2开始,s[0]做监视哨,s[1]一个数据无可比性
    {
        s[0]=s[i];    //给监视哨陚值
        j=i-1;    //确定要比较元素的最右边位黄
        while(s[0]<s[j])
        {
            s[j+1]=s[j];    //数据右移
            j--;    //产移向左边一个未比较的数
        }
        s[j+1]=s[0];    //在确定的位置插入s[i]
    }
    return 0;
}
int main()
{
    int a[11],i;    //定义数组及变量为基木整甩
    printf("请输入10个数据:\n");
    for (i =1;i<=10;i++)
        scanf("%d",&a[i]);    //接收从键盘输入的10个数据到数组a中
    printf("原始顺序:\n");
    for(i=1;i<11;i++)
        printf("%5d",a[i]);    //将未排序前的顺序输出
    insort(a,10);    //调用自定义函数 insort()
    printf("\n 插入数据排序后顺序:\n");
    for(i=1;i<11;i++)
        printf("%5d",a[i]); //将排序后的数组输出
    printf("\n");
    return 0;
}

       运行结果:

请输入10个数据:
25 12 36 45 2 9 39 22 98 37
原始顺序:
   25   12   36   45    2    9   39   22   98   37
插入数据排序后顺序:
   2    9   12   22   25   36   37   39   45   98

       2.折半插入排序

(1)基本思想

       折半插入排序的基本思想和直接插入排序类似,区别是查找插入位置的方法不同,折半插入排序是采用折半查找法来查找插入位置的。

        (2)算法描述  

       举一趟排序为例: 

       现在序列为13  38  49  65  76  97       27 49

       将要插入27,此时序列在数组中的情况为:

                            已经排序         未排序
关键字1338496576972749
数组下标01234567

       ①low = 0,high = 5,m = (0+5)/2 = 2(向下取整),下标为2的关键字为49,27 < 49,所以27应该插入到49的低半区,改变high = m-1=1,low仍然为0。

       ②low = 0,high = 1,m = (0+1)/2 = 0(向下取整),下标为0的关键字为13,27 > 13,所以27应该插入到13的高半区,改变low = m+1=1,high仍然为1。

       ③low = 1,high = 1,m = (1+1)/2 = 1(向下取整),下标为1的关键字为38,27 < 38,所以27应该插入到38的低半区,改变high = m-1=0,low仍然为1。此时low > high,折半查找结束,27的插入位置为下标为high的关键字之后,即13之后。

       ④依次向后移动关键字97,76,65,49,38,然后将27插入,这一趟折半插入排序结束。执行完这一趟排序结果为:

13  27  38  49  65  76  97        49

        (3)代码实现

//折半插入排序算——C语言实现
#include <stdio.h>
void print(int a[], int n ,int i){
    printf("%d:",i);
    for(int j=0; j<n; j++){
        printf("%d",a[j]);
    }
    printf("\n");
}
void BInsertSort(int a[],int size){
    int i,j,low = 0,high = 0,mid;
    int temp = 0;
    for (i=1; i<size; i++) {
        low=0;
        high=i-1;
        temp=a[i];
        //采用折半查找法判断插入位置,最终变量 low 表示插入位置
        while (low<=high) {
            mid=(low+high)/2;
            if (a[mid]>temp) {
                high=mid-1;
            }else{
                low=mid+1;
            }
        }
        //有序表中插入位置后的元素统一后移
        for (j=i; j>low; j--) {
            a[j]=a[j-1];
        }
        a[low]=temp;//插入元素
        print(a, 8, i);
    }
   
}
int main(){
    int a[8] = {3,1,7,5,2,4,9,6};
    BInsertSort(a, 8);
    return 0;
}

        运行结果:

1:13752496
2:13752496
3:13572496
4:12357496
5:12345796
6:12345796
7:12345679

       3.希尔排序

(1)基本思想

希尔排序演示

       希尔排序又叫缩小增量排序,其本质还是插入排序,只不过将待排序列按某种规则分为几个子序列,分别对这几个子序列进行直接插入排序。这个规则的体现就是增量的选取,将待排序数组按照增量进行分组,然后将每组的元素利用直接插入排序的方法进行排序;每次再将增量折半减小,循环上述操作;当增量等于1时,利用直接插入,完成排序。

       可以看到增量的选择是希尔排序的重要部分。只要最终增量为1任何步长序列都可以工作。一般来说最简单的增量取值是初次取数组长度的一半为增量,之后每次再减半,直到增量为1。

(2)算法描述  

       具体算法描述如下:

       ①选择一个增量序列t1,t2,…ti,tj,...tk,其中ti>tj,tk=1;(一般初次取数组半长,之后每次再减半,直到增量为1

       ②按增量序列个数k,对序列进行k 趟排序;

       ③每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子序列进行直接插入排序。仅当增量为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

        (3)代码实现

//希尔排序——C语言实现
//自定义函数 shsort(),实现希尔排序。
#include <stdio.h>
int shsort(int s[], int n)    /* 自定义函数 shsort()*/
{
    int i,j,d;
    d=n/2;    /*确定固定增虽值*/
    while(d>=1)
    {
        for(i=d+1;i<=n;i++)    /*数组下标从d+1开始进行直接插入排序*/
        {
            s[0]=s[i];    /*设置监视哨*/
            j=i-d;    /*确定要进行比较的元素的最右边位置*/
            while((j>0)&&(s[0]<s[j]))
            {
                s[j+d]=s[j];    /*数据右移*/
                j=j-d;    /*向左移d个位置V*/
            }
            s[j + d]=s[0];    /*在确定的位罝插入s[i]*/
        }
        d = d/2;    /*增里变为原来的一半*/
    }
return 0;
}
int main()
{
    int a[11],i;    /*定义数组及变量为基本整型*/
    printf("请输入 10 个数据:\n");
    for(i=1;i<=10;i++)
    scanf("%d",&a[i]);    /*从键盘中输入10个数据*/
    shsort(a, 10);    /* 调用 shsort()函数*/
    printf("排序后的顺序是:\n");
    for(i=1;i<=10;i++)
    printf("%5d",a[i]);    /*输出排序后的数组*/
    printf("\n");
    return 0;
}

       运行结果:

请输入 10 个数据:
69 56 12 136 3 55 46 99 88 25
排序后的顺序是:
3   12   25   46   55   56   69   88   99  136

四、交换类排序

       1.冒泡排序

    (1)基本思想

       冒泡排序是通过一系列的“交换”动作完成的,首先第一个关键字和第二个关键字比较,如果第一个大,则二者交换,否则不交换;然后第二个关键字和第三个关键字比较,如果第二个大,则二者交换,否则不交换......一直按照这种方式进行下去,最终最大的那个关键字被交换到最后,一趟冒泡排序完成。经过多趟这样的排序,最终使得整个序列有序。

冒泡排序演示

(2)算法描述  

       具体算法描述如下:

       ①比较相邻的元素。如果第一个比第二个大,就交换他们两个。

       ②对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。

       ③针对所有的元素重复以上的步骤,除了最后一个。

       ④重复上面的步骤①~③对未排序元素排序,直到在一趟排序过程中没有发生关键字交换。

(3)代码实现

//冒泡排序——C语言实现
//通过两个 for 循环实现冒泡排序的全过程,外层 for 循环决定冒泡排序的趟数,内层 for 循环决定每趟所进行两两比较的次数。
/*冒泡法的基本思路是,如果要对 n 个数进行冒泡排序,那么要进行 n-1 趟比较,在第 1 趟比较中要进行 n-1 次两两比较,在第 j 趟比较中要进行 n-j 次两两比较。从这个基本思路中就会发现,趟数决定了两两比较的次数,这样就很容易将两个 for 循环联系起来了。*/

#include <stdio.h>
int main()
{
    int i,j,t,a[11];    //定义变量及数组为基本整型
    printf("请输入10个数:\n");
    for(i=1;i<11;i++)
        scanf("%d",&a[i]);    //从键盘中输入10个数
    for(i=1;i<10;i++)    //变量i代表比较的趟数
        for(j=1;j<11-i;j++)    //变最j代表每趟两两比较的次数
            if(a[j]>a[j+1])
            {
                t=a[j];    //产利用中间变童实现两值互换
                a[j]=a[j+1];
                a[j+1]=t;
            }
            printf("排序后的顺序是:\n");
            for(i=1;i<=10;i++)
                printf("%5d",a[i]);    //将胃泡排序后的顺序输出
        printf("\n");
    return 0;
}

       运行结果:

请输入10个数:
66 32 23 45 25 5 15 69 46 37
排序后的顺序是:
5   15   23   25   32   37   45   46   66   69

       2.快速排序

    (1)基本思想

       快速排序的基本思想:挖坑填数+分治法

       每一趟选择当前所有子序列中的一个关键字(通常是第一个)作为枢轴,将子序列中比枢轴小的移到枢轴前边,比枢轴大的移到枢轴后边;当本趟所有子序列都被枢轴以上述规则划分完毕后会得到新的一组更短的子序列,他们成为下一趟划分的初始序列集。分别对这两部分子序列继续进行排序,以达到整个序列有序。

快速排序演示

(2)算法描述  

       具体算法描述如下:

       ①i = L; j = R; 将基准数挖出形成第一个坑a[i]
j--,由后向前找比它小的数,找到后挖出此数填前一个坑a[i]中。
i++,由前向后找比它大的数,找到后也挖出此数填到前一个坑a[j]中。
④再重复执行②,③二步,直到i==j,将基准数填入a[i]中。

(3)代码实现

//快速排序——C语言实现
/*自定义一个函数 qusort(),实现快速排序。
快速排序是冒泡排序的一种改进,主要的算法思想是在待排序的 n 个数据中取第一个数据作为基准值,将所有记录分为 3 组,使第一组中各数据值均小于或等于基准值,第二组做基准值的数琚,第三组中各数据值均大于或等于基准值。这便实现了第一趟分割,然后再对第二组和第兰组分别重复上述方法,依次类推,直到每组中只有一个记录为止。*/
#include <stdio.h>
int qusort(int s[],int start,int end)    //自定义函数 qusort()
{
    int i,j;    //定义变量为基本整型
    i=start;    //将每组首个元素赋给i
    j = end;    //将每组末尾元素赋给j
    s[0]=s[start];    //设置基准值
    while(i<j)
    {
        while(i<j&&s[0]<s[j])
        j--;    //位置左移
        if(i<j)
        {
            s[i]=s[j];    //将s[j]放到s[i]的位置上
            i++;    //位置右移
        }
        while(i<j&&s[i]<=s[0])
            i++;    //位置左移
        if(i<j)
        {
            s[j]=s[i];    //将大于基准值的s[j]放到s[i]位置
            j--;    //位置左移
        }
    }
    s[i]=s[0];    //将基准值放入指定位置
    if (start<i)
        qusort(s,start,j-1);    //对分割出的部分递归调用qusort()函数
    if (i<end)
        qusort(s,j+1,end);
    return 0;
}
int main()
{
    int a[11], i;    //定义数组及变量为基本整型
    printf("请输入10个数:\n");
    for(i=1;i<=10;i++)
        scanf("%d",&a[i]);    //从键盘中输入10个要进行排序的数
    qusort(a,1,10);    //调用qusort()函数进行排序
    printf("排序后的顺序是:\n");
    for(i=1;i<=10;i++)
        printf("%5d",a[i]);    //输出排好序的数组
    printf("\n");
    return 0;
}

       运行结果:

请输入10个数:
99 45 12 36 69 22 62 796 4 696
排序后的顺序是:
4   12   22   36   45   62   69   99  696  796

五、选择类排序

       1.简单选择排序

    (1)基本思想

       选择排序的基本思想:比较 + 交换。

       简单选择排序采用最简单的选择方式,从头至尾顺序扫描序列,找出最小的一个关键字,和第一个关键字交换,接着从剩下的关键字中继续这种选择和交换,最终使得序列有序。

éæ©æåºç示ä¾å¨ç»ã红è²è¡¨ç¤ºå½åæå°å¼ï¼é»è²è¡¨ç¤ºå·²æåºåºåï¼èè²è¡¨ç¤ºå½åä½ç½®ã

简单选择排序演示,红色表示当前最小值,黄色表示已排序序列,蓝色表示当前位置。

(2)算法描述  

       具体算法描述如下:

       ①从待排序序列中,找到关键字最小的元素;
②如果最小元素不是待排序序列的第一个元素,将其和第一个元素互换;
③从余下的 N - 1 个元素中,找出关键字最小的元素,重复①、②步,直到排序结束。

(3)代码实现

//选择排序——C语言实现
/*选择排序的基本算法是从待排序的区间中经过选择和交换后选出最小的数值存放到 a[0] 中,再从剩余的未排序区间中经过选择和交换后选出最小的数值存放到 a[1] 中,a[1] 中的数字仅大于 a[0],依此类推,即可实现排序。
程序中用到T两个 for 循环语句。第一个 for 循环是确定位置的,该位置是存放每次从待排序数列中经选择和交换后所选出的最小数。第二个 for 循环是实现将确定位置上的数与后面待排序区间中的数进行比较的。*/
#include <stdio.h>
int main()
{
    int i,j,t,a[11];    //定义变量及数组为基本整型
    printf("请输入10个数:\n");
    for(i=1;i<11;i++)
        scanf("%d",&a[i]);    //从键盘中输入要排序的10个数字
    for(i=1;i<=9;i++)
        for (j=i+1;j<=10;j++)
            if(a[i]>a[j])    //如果前一个数比后一个数大,则利用中间变量t实现两值互换
            {
                t=a[i];
                a[i]=a[j];
                a[j]=t;
            }
    printf("排序后的顺序是:\n");
    for(i=1;i<=10;i++)
        printf("%5d", a[i]);    //输出排序后的数组
    printf("\n");
    return 0;
}

       运行结果:

请输入10个数:
526 36 2 369 56 45 78 92 125 52
排序后的顺序是:
2   36   45   52   56   78   92  125  369  526

       2.堆排序

    (1)基本思想

       堆是一种数据结构,可以把堆看成一颗完全二叉树,这颗完全二叉树满足:任何一个非叶子结点的值都小于等于(或大于等于)其左右孩子结点的值。若父亲大孩子小,则这样的堆叫作大顶堆;若父亲小孩子大,则这样的堆叫作小顶堆

       根据堆的定义知道,代表堆的这棵完全二叉树的根节点的值是最大(或最小)的,因此将一个无序的序列调整为一个堆,就可以找出这个序列的最大(或最小)值,然后将找出的这个值交换到序列的最后(或最前),这样,有序序列的关键字增加1个,无序序列中关键字减少1个,对新的无序序列重复这样的操作,就实现了排序。这就是堆排序的思想。

       堆排序中最关键的操作就是将序列调整为堆。整个序列的过程就是通过不断的调整,使得不符合堆定义的完全二叉树变为符合堆定义的完全二叉树。

大顶堆排序演示

(2)算法描述  

       举一趟排序为例: 

       原始序列:49  38  65  97  76  13  27  49

       1)建堆

       现将这个序列调整为一个大顶堆,原始序列对应的完全二叉树如下图所示,在这个完全二叉树中,结点76、13、27、49是叶子结点,他们没有左右孩子,所以他们满足堆的定义。从97开始,按97、65、38、49的顺序依次调整。

       ①调整97。97>49,所以97和它的孩子49 满足堆的定义,不需要调整。
②调整65。65>13,65>27,所以65和它的孩子13、27满足堆的定义,不需要调整。
③调整38。97>38,76>38,不满足堆的定义,需要调整。在这里,38的两个孩子结点值都比38大,应该和哪个交换呢?显然应该是和两者中最大的交换啊。如果和76交换,则76<97仍然不满足堆的定义。因此,将38和97交换,交换后38成了49的根结点,49>38,仍然不满足,需要继续调整,将38和49交换,结果如下图

       ④调整49。49<97,49<65不满足定义,需要调整。因此和97交换,交换后49<76仍不满足堆的定义,继续调整,将49和76交换,结果如下图

       2)排序
可以看到,此时已经建立好了一个大顶堆。对应的序列为:97  76  65  49  49  13  27  38。将堆顶记录97和序列最后一个记录38交换。第一趟堆排序完成。97到达其最终的位置。将除97外的序列38  76  65  49  49  13  27重新调整为大顶堆。现在这个堆只有38不满足堆的定义,其他的记录都满足,所以只需要调整一个38就足够了。调整38,结果如下图

       现在的序列为76  49  65  38  49  13  27  97。将堆顶记录76和最后一个记录27交换,第二趟堆排序完成,76到达其最终位置,此时序列如下:27 49  65  38  49  13  76  97。然后对除76和97的序列依照上面的方法继续处理,直到树中只剩下1个结点时排序完成。

堆排序算法过程描述:

       ①从无序序列所确定的完全二叉树的第一个非叶子结点开始,从右至左,从下至上,对每个结点进行调整,最终将得到一个大顶堆;

       对结点的调整方法: 当前节点的值(假设为a)与其孩子结点进行比较,如果存在大于a的孩子结点,则从中选出最大的一个与a交换。当a来到下一层的时候重复上述操作,直到a的孩子结点值都小于a的值为止。

       ②将当前无序序列中第一个元素,反映在树中是根结点(假设为a)与无需序列中最后一个元素交换(假设为b)。a进入有序序列,到达最终位置。无序序列中元素减少1个,有序序列中元素增加1个。此时只有结点b可能不满足堆的定义,对其进行调整。

       ③重复②中过程,直到无序序列中的元素剩下1个时排序结束。

(3)代码实现

//堆排序——c++实现
#include <iostream>
#include <vector>
using namespace std;
 
/*该函数完成对在数组R[low]到R[high]范围内,对在low上的结点进行调整(大堆)*/
void heapAdjust(int R[], int low, int high)
{
	int i = low;
	int j = i *2+1;          //R[j]是R[i]的左孩子节点 (此时对应数组从0开始,若从1开始则 j=i*2)
	int temp = R[i];
	while (j < high)
	{
		if (j < high&&R[j] < R[j + 1])                 
			++j;                                   //若右孩子较大,则把 j 指向右孩子
		if (temp < R[j])
		{
			R[i] = R[j];                           //将 R[j] 调整到双亲节点的位置上
			i = j;                                 //修改 i 和 j 的值,以便继续向下调整
			j = 2 * i+1;
		}
		else
			break;
	}
	R[i] = temp;
}
 
/*堆排序函数*/
void heapSort(int R[], int n)
{
	int i;
	int temp;
	for (i = n / 2-1; i >= 0; --i)             //建立初始堆(大顶堆)(此时对应的数组下标从0开始,若从1开始第一个非叶子结点为 i=n/2)
		heapAdjust(R, i, n-1);
	for (i = n-1; i >= 1; --i)                  //进行 n-1 次循环,完成堆排序
	{
		/*此交换将根结点元素刚入最终的位置上*/
		temp = R[0];
		R[0] = R[i];
		R[i] = temp;
		heapAdjust(R, 0, i - 1);            //在较少了 1 个元素的无序序列中进行调整
	}
}
 
void main()
{
	int R[8] = {49,38,65,97,76,13,27,49 };
	heapSort(R, 8);
	for (int i = 0; i < 8; i++)
		cout << R[i] << " ";
	cout << endl;
}

       运行结果:

13 27 38 49 49 65 76 97

六、归并排序

       1.二路归并排序

(1)基本思想

       归并排序算法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。

img img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上物联网嵌入式知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、电子书籍、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取