总结一下大学生必须知道的的排序算法

说明

选择排序算法准则
每种排序算法都各有优缺点
影响排序的因素有很多,平均时间复杂度低的算法并不一定就是最优的。相反,有时平均时间复杂度高的算法可能更适合某些特殊情况。同时,选择算法时还得考虑它的可读性,以利于软件的维护。一般而言,需要考虑的因素有以下四点:

1.待排序的记录数目n的大小
2.记录本身数据量的大小,也就是记录中除关键字外的其他信息量的大小
3.关键字的结构及其分布情况
4.对排序稳定性的要求。

1.冒泡排序(Bubble Sort)

1.1基本思想

在要排序的一组数中,对当前还未排好序的范围内的全部数,自上而下对相邻的两个数依次进行比较和调整,让较大的数往下沉,较小的往上冒。即:每当两相邻的数比较后发现它们的排序与排序要求相反时,就将它们互换。
这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

1.2算法描述

  1. 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
  3. 针对所有的元素重复以上的步骤,除了最后一个;
  4. 重复步骤1~3,直到排序完成。
    在这里插入图片描述

1.3复杂程度

时间复杂度:O(n^2)
空间复杂度O(1)

没有借助辅助空间,原址操作

1.4代码实现

void BubbleSort(int *arr, int size)  
{ 
	assert(arr);   //判断arr数组是否传入错误,防止程序崩溃
    int i, j, tmp;  
    for (i = 0; i < size - 1; i++) {  
        for (j = 0; j < size - i - 1; j++) {  
            if (arr[j] > arr[j+1]) {  
                tmp = arr[j];  
                arr[j] = arr[j+1];  
                arr[j+1] = tmp;  
            }  
        }  
    }  
}  

assert宏的原型定义在<assert.h>中,其作用是如果它的条件返回错误,则终止程序执行。
原型定义:
#include <assert.h>
void assert( int expression );
assert的作用是现计算表达式 expression ,如果其值为假(即为0),那么它先向stderr打印一条出错信息,然后通过调用 abort 来终止程序运行。

1.5冒泡排序的优化

冒泡排序是含有n个数的数组进行n此循环比较,但是如果一个数组中只有一组数需要交换,根据原代码还是会进行剩下的循环比较,这样降低了代码的效率,举个例子

int arr = {3,2,1,4,5,6,7,8,9};

我们一眼就看出只需要交换’3’和‘1’,这个数组就是有序的了,当然冒泡排序在交换完之后依旧会傻乎乎的继续循环,我们可以添加一个标记,优化冒泡代码

void BubbleSort2(int *arr, int length)
{
//这个同样是检测arr数组是否传入错误
//很明显没有assert函数好用......
    if (arr == nullptr || length < 0 || length == 0)
        return;          
    bool flag = true;
    for (int i = 0; i < length && flag; i++)
    {
        flag = false;
        for (int j = 0; j < length - 1 - i; j++)
        {
            if (arr[j] > arr[j + 1])
            {
                Swap(arr, j, j + 1);
                flag = true;
            }
        }
    }
}

冒泡排序在处理大型数组时的效率不够理想,因为经常需要重复的数据交换来将单个项目放置到正确的位置。

2.选择排序(Selection Sort)

2.1基本思想

第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。
选择排序和冒泡排序一样,每趟只放置一个项目到正确的位置。但是,通常情况下它执行的交换会比较少,因为它会立即将项目移动到数组中正确的位置。

2.2算法描述

  1. 选出待排序序列中最大或最小的数与最后的数交换值
  2. 针对待排序元素重复以上步骤,每次循环可确定一个数的位置

由于选择排序一次能确定一个元素的位置,所以选择排序需要循环size-1次。
没有借助辅助空间,原址操作

在这里插入图片描述

2.3复杂程度和稳定性

时间复杂度:O(n^2)
空间复杂度:O(1)
稳定性:不稳定

没有借助辅助空间,原址操作

2.4代码实现

void selection(int *arr,int size)
{
    assert(arr);
    int i,j,k,max=0;
    for(i=0;i<size-1;i++)
    {
        max=0;
        for(j=0;j<size-i;j++)//循环size-1次
        {
            if(arr[j]>arr[max])
                max = j;
        }
       
       //此处判断是否需要交换,因为待排序列最大值可能刚好到需要确定的位置上
        if(max!=size-i-1)  
        {
            k = arr[size-i-1];
            arr[size-i-1] = arr[max];
            arr[max] = k;
        }
        
    } 
}

2.5代码优化

选择排序是一次确定一个元素的位置,而选择排序的优化则是一次确定两个元素的位置,比如降序:每次将最小值放在起始位置,最大值放在末尾位置。

void selection(int *arr,int size)
{
    assert(arr);
    int i,j,k,max,min;
    int left = 0;//左边界
    int right = size -1;//右边界
    while(left<right)
    {
        max = left;
        min = left;
        for(j=left;j<=right;j++)//循环size-1次
        {
            if(arr[j]>arr[max])
                max = j;
            if(arr[j]<arr[min])
                min = j;    
        }
        if(max!=right)
        {
            k = arr[right];
            arr[right] = arr[max];
            arr[max] = k;
        }
        //这里需要判断一下上边循环搜出来的最小值下标是否在right也就是最大值将换过去的位置
        //因为如果在right的位置上,最小值被换跑了,但是min标记还在,会一直无线循环下去
        if(min==right) 
            min = max;
        if(min!=left)
        {
            k = arr[left];
            arr[left] = arr[min];
            arr[min] = k;
        }
        left++,right--;
    }
        
}

为什么要写下面这块代码:

if(min==right) 
   min = max;

比如序列【10, 2 ,4,6 ,0】ma=0,mi=4;
max!=4,交换‘’0‘’和‘’10‘’【0, 2 ,4,6 ,10】但是ma依旧是0,minpos依旧是4,minpos!=0,又再次交换‘’0‘’和‘’10‘’.
  所以为防止minpos在最大值要插入的位置,要加上上面这块代码。

3.插入排序(Insertion Sort)

3.1基本思想

插入排序是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

3.2算法描述

插入排序一般都在原址操作

  1. 从第一个元素开始,该元素可以认为已经被排序;### 3.3代码实现
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描;
  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置;
  4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
  5. 将新元素插入到该位置后;
  6. 重复步骤2~5
    在这里插入图片描述

3.3复杂程度

时间复杂度O(n^2) 空间复杂度O(1)

一般情况下插入排序都是在原址操作,不需要借助辅助空间

3.4代码实现

void selection(int *arr,int size)
{
    assert(arr);
    int i,j,k;
    for(i=1;i<size;i++)
    {
        if(arr[i]<arr[i-1])  //如果这个数比前一个数小
           k = arr[i]; //用k存储这个数值
           for(j=i-1;j>=0&&arr[j]>k;j--)
           {
               arr[j+1] = arr[j]; //把前面每个比k大的数往后推
           }
           arr[j+1] = k; //最后把k值赋给空出来的位置
    }
        
}

3.5代码优化

前面介绍了直接插入排序算法的理论实现和具体的代码实现,如果你善于思考就会发现该算法在查找插入位置时,采用的是顺序查找的方式。
而在查找表中数据本身有序的前提下,可以使用折半查找来代替顺序查找,这种排序的算法就是二分(折半)插入排序算法

3.5.1二分(折半)插入排序

二分排序在直接插入排序的基础上减少比较次数,从而更快的找到插入位置。
复杂程度
平均时间复杂度:O(n^2)
空间复杂度:O(1)

折半插入排序算法相比较于直接插入排序算法,只是减少了关键字间的比较次数,而记录的移动次数没有进行优化,所以该算法的时间复杂度仍是 O(n2)。

void selection(int *arr,int size)
{
    assert(arr);
    int i,j,k;
    int left,right,mid;
    for(i=1;i<size;i++)
    {
        left = 0;
        right = i - 1;
        k = arr[i];
        while(left<=right)
        {
            mid = (left + right)/2;
            if(k<arr[mid])
               right = mid - 1; 
            else 
                left = mid + 1;
        }
        for(j=i-1;j>=left;j--)
        arr[j+1] = arr[j];

        arr[left] = k;
    }
        
}
3.5.22-路插入排序算法

2-路插入排序算法是在折半插入排序的基础上对其进行改进,减少其在排序过程中移动记录的次数从而提高效率
具体实现思路为:另外设置一个同存储记录的数组大小相同的数组 d,将无序表中第一个记录添加进 d[0] 的位置上,然后从无序表中第二个记录开始,同 d[0] 作比较:如果该值比 d[0] 大,则添加到其右侧;反之添加到其左侧

复杂程度
时间复杂度:O(n2)

2-路插入排序相比于折半插入排序,只是减少了移动记录的次数,没有根本上避免,所以其时间复杂度仍为O(n2)。

代码自行百度,我也不太理解(懒得看....

4.快速排序(Quick Sort)

4.1基本思想(分治递归)

快速排序是选取一个记录作为枢轴,经过一趟排序,将整段序列分为两个部分,其中一部分的值都小于枢轴,另一部分都大于枢轴。然后继续对这两部分继续进行排序,从而使整个序列达到有序。

快速排序(Quick Sort)是对冒泡排序的一种改进

4.2算法描述

快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)

  1. 从数列中挑出一个元素,称为 “基准”(pivot);
  2. 重分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。;
  3. 递归地(recursive)再对左右区间重复第二步,直到各区间只有一个数。。
    在这里插入图片描述
    在这里插入图片描述
    虽然快速排序称为分治法,但分治法这三个字显然无法很好的概括快速排序的全部步骤。因此我的对快速排序作了进一步的说明:挖坑填数+分治法
    以一个数组作为示例,取区间第一个数为基准数。
    在这里插入图片描述
    初始时,i = 0; j = 9; X = a[i] = 72

由于已经将a[0]中的数保存到X中,可以理解成在数组a[0]上挖了个坑,可以将其它数据填充到这来。

从j开始向前找一个比X小或等于X的数。当j=8,符合条件,将a[8]挖出再填到上一个坑a[0]中。a[0]=a[8]; i++; 这样一个坑a[0]就被搞定了,但又形成了一个新坑a[8],这怎么办了?简单,再找数字来填a[8]这个坑。这次从i开始向后找一个大于X的数,当i=3,符合条件,将a[3]挖出再填到上一个坑中a[8]=a[3]; j–;
数组变为:
在这里插入图片描述
i = 3; j = 7; X=72

再重复上面的步骤,先从后向前找,再从前向后找。

从j开始向前找,当j=5,符合条件,将a[5]挖出填到上一个坑中,a[3] = a[5]; i++;
从i开始向后找,当i=5时,由于i==j退出。
此时,i = j = 5,而a[5]刚好又是上次挖的坑,因此将X填入a[5]。
数组变为:
在这里插入图片描述
可以看出a[5]前面的数字都小于它,a[5]后面的数字都大于它。因此再对a[0…4]和a[6…9]这二个子区间重复上述步骤就可以了。
对挖坑填数进行总结

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

4.3复杂程度

平均时间复杂度O(nlogn)

最优空间复杂度O(logn),每一次都平分数组
最差空间复杂度O(n) ,退化为冒泡排序的情况

4.4代码实现

//快速排序
void quick_sort(int s[], int l, int r)
{
    if (l < r)
    {
		//Swap(s[l], s[(l + r) / 2]); //将中间的这个数和第一个数交换 参见注1
        int i = l, j = r, x = s[l];
        while (i < j)
        {
            while(i < j && s[j] >= x) // 从右向左找第一个小于x的数
				j--;  
            if(i < j) 
				s[i++] = s[j];
			
            while(i < j && s[i] < x) // 从左向右找第一个大于等于x的数
				i++;  
            if(i < j) 
				s[j--] = s[i];
        }
        s[i] = x;
        quick_sort(s, l, i - 1); // 递归调用 
        quick_sort(s, i + 1, r);
    }
}

此章节部分内容转载自https://blog.csdn.net/MoreWindows/article/details/6684558?utm_source=distribute.pc_relevant.none-task

4.5代码优化

4.5.0二分法优化

首先快排的思想是找一个枢轴,然后以枢轴为中介线,一遍都小于它,另一边都大于它,然后对两段区间继续划分,那么枢轴的选取就很关键。
上面的代码思想都是直接拿序列的最后一个值作为枢轴,如果最后这个值刚好是整段序列最大或者最小的值,那么这次划分就是没意义的。
所以当序列是正序或者逆序时,每次选到的枢轴都是没有起到划分的作用。快排的效率会极速退化
所以可以每次在选枢轴时,在序列的第一,中间,最后三个值里面选一个中间值出来作为枢轴,保证每次划分接近均等。
这就用到了二分法(三数取中法)

nt GetMid(int* array,int left,int right)
{
    assert(array);
    int mid = left + ((right - left)>>1);
    if(array[left] <= array[right])
    {
        if(array[mid] <  array[left])
            return left;
        else if(array[mid] > array[right])
            return right;
        else
            return mid;
    }
    else
    {
        if(array[mid] < array[right])
            return right;
        else if(array[mid] > array[left])
            return left;
        else
            return mid;
    }

}
4.5.1直接插入法优化

由于是递归程序,每一次递归都要开辟栈帧,当递归到序列里的值不是很多时,我们可以采用直接插入排序来完成,从而避免这些栈帧的消耗。

5.归并排序(Merge Sort)

5.1基本思想(分治递归)

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

归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。

5.2算法描述

  1. 将原始序列从中间分为左、右两个子序列,此时序列数为2;
    2.将左序列和右序列再分别从中间分为左、右两个子序列,此时序列数为4

  2. 将重复以上步骤,直到每个子序列都只有一个元素,可认为每一个子序列都是有序的

  3. 最后依次进行归并操作,直到序列数变为1

    归并操作,也叫归并算法,指的是将两个已经排序的序列合并成一个序列的操作。

考虑一个问题,如何将两个有序数列合并成一个有序数列?
很简单,由于两个数列都已经有序,我们只需从两个数列的低位轮番拿出各自最小的数来PK就就行了,输的一方为小值,将这个值放入临时数列,然后输的一方继续拿出一个值来PK,直至有一方没有元素后,将另一方的所有元素依次接在临时数列后面即可。此时,临时数列为两个数列的有序合并。归并排序中的归并就是利用这种思想。

在这里插入图片描述

归并排序和快速排序有那么点异曲同工之妙
快速排序:是先把数组粗略的排序成两个子数组,然后递归再粗略分两个子数组,直到子数组里面只有一个元素,那么就自然排好序了,可以总结为先排序再递归.
归并排序:先什么都不管,把数组分为两个子数组,一直递归把数组划分为两个子数组,直到数组里只有一个元素,这时候才开始排序,让两个数组间排好序,依次按照递归的返回来把两个数组进行排好序,到最后就可以把整个数组排好序。

5.3复杂程度

时间复杂度为O(nlogn)
空间复杂度为O(n)

5.4代码实现

void Merge(int r[],int r1[],int s,int m,int t)
{
    int i=s;
    int j=m+1;
    int k=s;
    while(i<=m&&j<=t)
    {
        if(r[i]<=r[j])
            r1[k++]=r[i++];
        else
            r1[k++]=r[j++];
    }
    while(i<=m)
        r1[k++]=r[i++];
    while(j<=t)
        r1[k++]=r[j++];
    for(int l=0; l<8; l++)
        r[l]=r1[l];
}
 
void MergeSort(int r[],int r1[],int s,int t)
{
    if(s==t)
        return;
    else
    {
        int m=(s+t)/2;
        MergeSort(r,r1,s,m);
        MergeSort(r,r1,m+1,t);
        Merge(r,r1,s,m,t);
    }
}

5.5代码优化

内容过多不做赘述

点这里自行查看

至此,常用排序总结先到这里
以后会单独写几篇博客介绍桶排序、希尔排序、堆排序

  • 6
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值