排序算法总结(一)

八大排序算法总结(一)

在这里插入图片描述
在这里插入图片描述

归并排序

在这里插入图片描述
归并算法采用分治策略
在这里插入图片描述
1、归并排序的基本思想

将待排序序列R[0…n-1]看成是n个长度为1的有序序列,将相邻的有序表成对归并,得到n/2个长度为2的有序表;将这些有序序列再次归并,得到n/4个长度为4的有序序列;如此反复进行下去,最后得到一个长度为n的有序序列

2、归并排序的算法描述

第一步:申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
第二步:设定两个指针,最初位置分别为两个已经排序序列的起始位置
第三步:比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
重复步骤3直到某一指针超出序列尾,将另一序列剩下的所有元素直接复制到合并序列尾

归并排序其实要做两件事:
(1)“分解”——将序列每次折半划分(递归实现)

(2)“合并”——将划分后的序列段两两合并后排序

如何合并?

在每次合并过程中,都是对两个有序的序列段进行合并,然后排序。

这两个有序序列段分别为 R[low, mid] 和 R[mid+1, high]。

先将他们合并到一个局部的暂存数组R2中,带合并完成后再将R2复制回R中。

我们称 R[low, mid] 第一段,R[mid+1, high] 为第二段。

每次从两个段中取出一个记录进行关键字的比较,将较小者放入R2中,最后将各段中余下的部分直接复制到R2中。

经过这样的过程,R2已经是一个有序的序列,再将其复制回R中,一次合并排序就完成了。

时间复杂度:O(nlogn)
(最好情况和最坏情况都是O(n
logn))
空间复杂度:O(n)
是否稳定: 稳定

#include<stdio.h>
#include<stdlib.h>

/*Merge函数将R[low,mid]和R[mid+1,high]这两排好序的小数组合并处排序大数组R[low,high]*/
void Merge(int *R,int low,int mid, int high)
{
    int i = low, j = mid+1, p = 0;
    //k指向合并后数组的头
    int *R1;  //局部变量,暂存数组
    R1=(int *)malloc((high-low+1)*sizeof(int)); 
    //分配出一块两个小数组合并后的数组的空间
    if(!R1)
    {
        printf("malloc error!\n");
        return;   //空间申请失败
    }
    while(i<=mid && j<=high)
    {
        if(R[i]<=R[j])
        {
            R1[p] = R[i];
            i++;
        }
        else
        {
            R1[p] = R[j];
            j++;
        }
        p++;
    }
    //然后肯定至少有一个数组是有剩余没排进去的,并且值很大
    while(i<=mid)  //若第1个子文件非空,则复制剩余记录到R1中  
    {  
        R1[p++]=R[i++];  
    }  
    while(j<=high) //若第2个子文件非空,则复制剩余记录到R1中  
    {  
        R1[p++]=R[j++];  
    }  
  
    for(p=0,i=low;i<=high;p++,i++)  
    {  
        R[i]=R1[p];    //归并完成后将结果复制回R[low..high]  
    }  
}
//分治法进行二路归并排序
void MergeSort(int *R, int low, int high)
{
    int mid;
    if(low<high)  //区间长度大于1
    {
        mid = (low+high)/2;
        MergeSort(R,low,mid);
        MergeSort(R,mid+1,high);
        Merge(R,low,mid,high);
    }
}

void main()  
{  
    int a[9]={49,38,65,97,76,13,27,1,19}; //这里对9个元素进行排序  
    int low=0,high=8;                   //初始化low和high的值  
  
    printf("排序前的序列: ");  
    for(int i=low;i<=high;i++)  
    {  
        printf("%d ",a[i]);             //输出测试  
    }  
    printf("\n");  
  
    MergeSort(a,low,high);  
  
    printf("排序后的序列:  ");  
    for( int i=low;i<=high;i++)  
    {  
        printf("%d ",a[i]);             //输出测试  
    }  
    printf("\n");  
}   

交换排序

  • 冒泡排序

算法原理

冒泡排序的原理(以递增序为例)是每次从头开始依次比较相邻的两个元素,如果后面一个元素比前一个要大,说明顺序不对,则将它们交换,本次循环完毕之后再次从头开始扫描,直到某次扫描中没有元素交换,说明每个元素都不比它后面的元素大,至此排序完成。

由于冒泡排序简洁的特点,它通常被用来对于计算机程序设计入门的学生介绍算法的概念。

时间复杂度

若文件的初始状态是排好序的的,一趟扫描即可完成排序。所需的关键字比较次数C和记录移动次数 M 均达到最小值(Cmin = n-1、Mmin = 0)

所以,冒泡排序最好的时间复杂度为O(N)。

若初始文件是反序的,需要进行N趟排序。每趟排序要进行 C = N-1次关键字的比较(1≤i≤N-1)和总共(Mmax = (N*(N-1))/2)次的移动(移动次数由乱序对的个数决定,即多少对元素顺序不对,如 1 3 4 2 5 中共有(3,2)、(4,2)两个乱序对),在这种情况下,比较和移动次数均达到最大值
  (Cmax =N*(N-1) + Mmax=(N*(N-1))/2 = O(N^2)。
  所以,冒泡排序的最坏时间复杂度为O(N^2)

综上,冒泡排序总的平均时间复杂度为O(N^2)。

算法稳定性

冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。如果两个相等的元素相邻,那么根据我们的算法。它们之间没有发生交换;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法。

单向冒泡排序

void bubble_sort(int *a,int n)
{
    int i,j;
    int temp = 0;
    for(i = 0;i<n-1;i++)
    {
        for(j = 0;j<n-i-1;j++)
        {
            if(a[j]>a[j+1])   //每一次运行都把最大的一个数运到尚未排的最后
            {
                temp = a[j+1];
                a[j+1] = a[j];
                a[j] = temp;
            }
        }
    }
}

双向冒泡排序

//双向冒泡排序

void double_bubbleswap(int source[],int n)
{
    int start = 0,end = n-1;
    int i;
    while(start<=end)
    {
        for(i=start;i<end;i++)
        {
            if(source[i]>source[i+1])
            {
                int t;
                t = source[i];
                source[i] = source[i+1];
                source[i+1] = t;
            }
        }
        end--;
        for(int j=end;j>start;j--)
        {
            if(source[j]<source[j-1])
            {
                int k=source[j];
                source[j] = source[j-1];
                source[j-1] = k;

            }
        }
        start++;
    }

}
  • 快速排序
    基本思想是:从一个数组中随机选出一个数N,通过一趟排序将数组分割成三个部分,1、小于N的区域 2、等于N的区域 3、大于N的区域,然后再按照此方法对小于区的和大于区分别递归进行,从而达到整个数据变成有序数组。

快排的时间复杂度O(N*logN),空间复杂度O(logN) 【因为每次都是随机事件,坏的情况和差的情况,是等概率的,根据数学期望值可以算出时间复杂度和空间复杂度】,不稳定性排序
图解部分源自此网站https://blog.csdn.net/u010452388/article/details/81218540?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.channel_param&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.channel_param

//快速排序
void quicksort(int a[],int left,int right)
{
    int i = left;
    int j = right;
    int temp = a[i];
    if(left>right)
        return;
    while(i!=j)
    {
        /*先从右往左,当遇到第一个小于temp的值得时候
		跳出循环。a[i]赋值为a[j],这里的a[i]=temp=a[left];*/ 
        while(i<j&&a[j]>temp)
            j--;
        if(i<j)
            a[i] = a[j];
        /*在从左往右,当遇到第一个大于temp的时候,跳出循环
		a[j]赋值为a[i]*/
        while(i<j&&a[i]<=temp)
            i++;
        if(i<j)
            a[j]=a[i];
        

    }
    a[i] = temp;
    quicksort(a,left,i-1);
    quicksort(a,i+1,right);
}

插入排序

  • 直接插入排序
    直接插入排序基本思想是每一步将一个待排序的记录,插入到前面已经排好序的有序序列中去,直到插完所有元素为止。
    直接插入排序算法的运作如下:
    假设有一组无序序列 R0, R1, … , RN-1。
    (1) 我们先将这个序列中下标为 0 的元素视为元素个数为 1 的有序序列。
    (2) 然后,依次把 R1, R2, … , RN-1 插入到这个有序序列中。所以,我们需要一个外部循环,从下标 1 扫描到 N-1 。
    (3) 接下来描述插入过程。假设这是要将 Ri 插入到前面有序的序列中。由前面所述,我们可知,插入Ri时,前 i-1 个数肯定已经是有序了。
    所以我们需要将Ri 和R0 ~ Ri-1 进行比较,确定要插入的合适位置。这就需要一个内部循环,我们一般是从后往前比较,即从下标 i-1 开始向 0 进行扫描。

在这里插入图片描述
当数据正序时,执行效率最好,每次插入都不用移动前面的元素,时间复杂度为O(N)。
当数据反序时,执行效率最差,每次插入都要前面的元素后移,时间复杂度为O(N*2)。

//直接插入排序
void DirectInsertionsort(int A[],int n)
{
    int i,j;
    int temp;
    for(i=1;i<n;i++)  //直接插入排序一般认为A[0]是那个最初排好的第一个序列
    {
        j=i;
        temp = A[i];
        while(j>0&&temp<A[j-1])//当未达到数组的第一个元素或者待插入元素小于当前元素
        {
            A[j] = A[j-1]; //就将该元素后移
            j--;    //下标减一,继续比较
        }
        A[j]=temp;
    }
}

折半插入排序
1、将待排序序列的第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。

2、从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置,在查找元素的适当位置时,采用了折半查找方法。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)

在这里插入图片描述

//折半插入排序
void BinaryInsertionsort(Forsort A[],int n)
{
    int i,k,r;
    Forsort temp;
    for(i=1;i<n;i++)
    {
        temp = A[i];
        k = 0;
        r = i - 1;
        while(k<=r)
        {
            int m;
            m = (k+r)/2;
            if(temp.key<A[m].key)
                r = m-1;
            else
            {
                 k = m+1;
            }
            
        }
        //找到了插入位置为k
        for(r = i;r>k;r--)
        {
            A[r] = A[r-1];
        }
        A[k] = temp;
    }
}

希尔排序
(可以参考一下这篇博客: 点击蓝色文字)

1.首先根据初始序列的长度,选定一个递减增量序列
t1,t2,t3…tk,其中ti>tj,tk = 1。
根据选定的增量序列元素个数k,对初始序列进行k趟排序。
根据增量序列的值ti,每趟排序会把初始序列划分成若干个元素的子序列,然后对这些子序列使用插入排序,因为这是递减增量序列,所以第一趟的排序,增量值最大,那么划分的子序列数量也就最多,每个子序列的元素也就越少,可以看做是一个“几乎”已经排好序的序列,当增量值越来越小的时候,子序列数量也就越少,每个子序列的元素也就越多,但是,基于前几次的排序,这些子序列虽然元素多,但是已经是一个“几乎”排好序的序列了,当最后一次排序的时候,即增量序列的值为1时,就是对整个无序序列进行排序,这时整个序列已经是一个“几乎”排好序的序列了。
以图为例进行说明,假设给定的初始无序序列如下:

4 5 8 2 3 9 7 1
首先,我们选择一个增量序列,这个增量序列如何选择呢?首先我们得保证第一次的所有子序列元素数量应该至少保证为2个或以上,这样是用插入排序才有意义,如果元素数量为1,也就是增量序列的第一个值为初始序列的长度或者更大,那么这次遍历将“无功而返”,所以至少应该保证子序列元素数量为2或以上,当子序列数量退化到初始序列长度时,希尔排序也退化成了插入排序。事实上,希尔排序的效率依赖于增量序列的选择,好的增量序列可以大大的提高希尔排序的效率,但是增量序列的选择是和初始序列有关系的。

一个好的递减增量序列选取的标准是:第一、递减增量序列最后一个值应该为1;第二、递减增量序列中的值,尤其是相邻的值最好不要互为倍数的关系,如果是互为倍数的关系,那么根据这两个序列值的分组将会有重复的情况,可能会做“无用功”。

该示例中,我们选取的递减增量序列位[3,2,1]。

递减增量序列已经有了,下面开始进行3趟(增量序列长度为3)排序,先取增量序列第一个值3,然后将初始序列分组,如下:
在这里插入图片描述
对三组子序列[4,2,7],[5,3,1],[8,9]分别使用插入排序,结果如下:
在这里插入图片描述
然后,对该序列再次进行增量序列的分组,这次增量序列的值为2,分组情况如下:
在这里插入图片描述
对两组子序列分别使用插入排序,结果如下:
在这里插入图片描述
最后,对整个序列做插入排序(增量序列值为1)即可。

从整个过程可以看出,每次的排序,都会将较小的值转移到序列的前边,整个序列的有序性不断的变强,可以使插入排序达到更高的效率。

希尔排序的效率依赖于递减增量序列的选择,时间复杂度最坏的情况是O(nlog2n)。


typedef int Elementtype;

struct forsort
{
    Elementtype key;
};
typedef struct forsort Forsort;

void InitForsort(Forsort *FS,int a)
{
    FS->key = a;
}
//shell排序的思想是先预选一个数s然后把记录分为S个组,
//所有距离为S的数据分在一个组里面,然后取s2=s>>1;再重复上述过程直到最后si = 1
void Shellsort(Forsort A[],int n,int s)
{
    int i,j,k;
    Forsort temp;
    //分组排序,初始的增量为S,每循环一次增量减半,直到增量为0时结束
    for(k = s;k>0;k>>1) // k>>1的意思就是k/=2; 只是这个k>>1的写法会加快运行速度
    {
        for(i=k;i<n;i++)
        {
            temp = A[i];
            j = i - k;
            while(j>=0&&temp.key<A[j].key)
            {
                A[j+k]=A[j];
                j-=k;
            }
            A[j+k] = temp;

        }
    }

}

基数排序

基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或binsort,顾名思义,它是透过键值的部份资讯,将要排序的元素分配至某些“桶”中,藉以达到排序的作用,基数排序法是属于稳定性的排序

假设有欲排数据序列如下所示:73 22 93 43 55 14 28 65 39 81首先根据个位数的数值,在遍历数据时将它们各自分配到编号0至9的桶(个位数值与桶号一一对应)中。分配结果(逻辑想象)如下图所示:
在这里插入图片描述
分配结束后。接下来将所有桶中所盛数据按照桶号由小到大(桶中由顶至底)依次重新收集串起来,得到如下仍然无序的数据序列:
81 22 73 93 43 14 55 65 28 39
接着,再进行一次分配,这次根据十位数值来分配(原理同上),分配结果(逻辑想象)如下图所示:
在这里插入图片描述

//基数排序
#define Max_ 10      //数组个数
#define RADIX_10 10    //整形排序
#define KEYNUM_31 31     //关键字个数,这里为整形位数

//找到num从低到高的第pos位数据

int getnuminpos(int num,int pos)
{
    int temp = 1;
    for(int i;i<pos-1;i++)
        temp *=10;
    return (num/temp)%10;
}
//基数排序  pDataArray 无序数组;iDataNum为无序数据个数
void radixsort(int *pDataArray,int iDataNum)
{
    int *radixArrays[RADIX_10];    //分别为0~9的序列空间,是个二维数组
    for(int i = 0;i<10;i++)
    {
        radixArrays[i] = (int *)malloc(sizeof(int) * (iDataNum + 1));
        radixArrays[i][0] = 0;    //index为0处记录这组数据的个数
    }
    for (int pos = 1; pos <= KEYNUM_31; pos++)    //从个位开始到31位
    {
        for (int i = 0; i < iDataNum; i++)    //分配过程
        {
            int num = GetNumInPos(pDataArray[i], pos);
            int index = ++radixArrays[num][0];
            radixArrays[num][index] = pDataArray[i];
        }
 
        for (int i = 0, j =0; i < RADIX_10; i++)    //收集
        {
            for (int k = 1; k <= radixArrays[i][0]; k++)
                pDataArray[j++] = radixArrays[i][k];
                radixArrays[i][0] = 0;    //复位
        }
    }
}

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

新城里的旧少年^_^

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值