九种常用排序算法

目录

 

排序分类:

排序算法优劣指标:

一,直接插入排序:

二,折半插入排序(二分插入排序)

三,希尔排序

四,冒泡排序

五,快速排序

六,直接选择排序

七,堆排序

八,归并排序

九,基数排序

排序分类:

《九种常用排序算法》

1、插入排序:直接插入排序(InsertSort),二分插入排序,希尔排序(希尔排序)

2,选择排序:简单选择排序,堆排序

3,交换排序:冒泡排序,快速排序

4,归并排序

5,基数排序

 

排序算法优劣指标:

(1)稳定性:当序列中存在两个或两个以上的关键字相等的时候,如果排序前序列中R1领先于R2,那么排序后R1如果仍旧领先R2的话,则是稳定的(相等的元素排序后相对位置不变)

(2)不稳定性:当序列中存在两个或两个以上的关键字相等的时候,如果排序前序列中R1领先于R2,那么排序后R1如果落后R2的话,则是不稳定的(相等的元素排序后相对位置发生改变)

(3)时间复杂度:算法的时间开销是衡量其好坏的最重要的标志高效率的算法应该具有更少的比较次数和记录移动次数。

(4)空间复杂度:即执行算法所需要的辅助存储的空间。

插入排序

一,直接插入排序:

基本思想:

数列前面部分看为有序,依次将后面的无序数列元素插入到前面的有序数列中,初始状态有序数列仅有一个元素,即首元素。遍历序列的关键字,将待排序的关键字字依次与其前一个关键字起逐个向前扫描比较,若是待排关键字小于扫描到的关键字,则将扫描到的关键字的向后移动一个位置,直到找到没有比待排关键字大的地方插入(待排序关键字前面的都是有序数列)

具体算法:

//以从小到大排序为例

void InsertSort(int a[],int len)
{
    int i;
    int tmp;
    for(i=1;i<len;i++)
    {
        tmp=a[i];
        j=i-1; //从右向左在有序区找到插入位置
        while(j>=0&&tmp<a[j])
        {
            a[j+1]=a[j]; //元素后移
            j--;
        }
        a[j+1]=tmp; //在j+1处插入a[i];
    }
}

 算法分析:

直接插入排序最好的情况就是不需要移动,直接就是一个从大到小排好的顺序,那么就只需要比较一遍就可以了(不需要移动<交换>),时间复杂度为O(n)的的的,最差的情况下需要全部移动一遍,就是按照从小到大的顺序排的,那么,时间复杂度为O(N²),空间复杂度为O(1),因为只需要一个数据存储要插入的数据即可,是一个稳定的排序算法,链式存储也可以用,比较适合基本有序的数据。

 

二,折半插入排序(二分插入排序)

二分插入排序算法实质上是对直接插入排序的一种优化,在排序数量大的情况下,速度远高于直接插入排序。

基本思想:以待排关键字所在位置将序列分为有序数列和无序数列两部分,然后对有序数列进行折半查找,找出一个点,左边的序列都是小于待排序关键字,该点与其右边至待排关键字的序列都是大于待排关键字的,将右边序列右移然后插入空处。

具体算法:

void InsertSort_OP(int* a, int n)//插入排序—利用二分法优化
{
    int i,j,low,high,mid;
    int tmp;
    for(i=1;i<n;i++)
    {
        tmp=a[i];
        low=0;high=i-1;
        while(low<=high)			//在a[low .. high ]中折半查找 有序插入位置
        {
            mid=(low+high)/2;		//取中间位置
            if(tmp<a[mid])
            high=mid-1;				//插入点在左边
            else
            low=mid+1;  			//插入点在右边
        }
        for(j=i-1;j>=high+1;j--)	//元素后移
        a[j+1]=a[j];
        a[high+1]=tmp;              //插入
    }
}

算法分析:

最坏情况:整个序列是逆序的时候,则内层循环的条件左<=右始终成立,此时对于每一次外层循环,第一个内循环达到最大值(log21~LOG2第(N-1 )),第二个内层循环次数每次达到最大值(即内层循环位我次),外层循环我取值为1~I-1,所以总的执行次数为N(N-1) / 2 + nlog2n,时间复杂度为O(n ^ 2)。

最好情况:整个序列为正序的时候内层循环条件始终不成立,所以内层循环始终不执行,始终执行语句如果(NUMS [I] <NUMS [I-1]),所以时间复杂度为上)。

空间复杂度:算法所需的辅助存储空间不随待排序列的规模变化而变化,是个常量,所以为O(1)。

 

三,希尔排序

该方法实质上是一种分组插入方法。先取一个小于Ñ的整数D1作为第一个增量,把文件的全部记录分组。所有距离为D1的倍数的记录放在同一个组中。先在各内组进行直接插入排序 ;然后,取第二个增量D2 <D1重复上述的分组和排序,直至所取的增量

《九种常用排序算法》即所有记录放在同一组中进行直接插入排序为止。

具体算法:

//根据当前增量进行插入排序
void shellInsert(int a[],int n)
{
    int i,j,gap;
    int tmp;
    gap=n/2;
    while(gap>0)
    {
        for(i=gap;i<n;i++)
         {
            tmp=a[i];
            j=i-gap;
            while(j>0&&tmp<a[j])
            { 
                a[j+gap]=a[j];
                j=j-gap;
            }
            a[j+gap]=tmp;
        }
        gap=gap/2;
    }
}

算法分析:

插入排序的改进版为了减少数据的移动次数,在初始序列较大时取较大的步长,通常取序列长度的一半,此时只有两个元素比较,交换一次。之后步长依次减半直至步长为1,即为插入排序,由于此时序列已接近有序,故插入元素时数据移动的次数会相对较少,效率得到了提高。

时间复杂度:通常认为是O(N3 / 2),未验证稳定性:不稳定

 

交换排序

 

四,冒泡排序

它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。

冒泡排序算法的原理如下:

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

(2)对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。

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

(4)持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

具体算法:

void BubbleSort(int* a, int n)//冒泡排序
{
    int i ,j;
    int tmp;
    for (i = 0; i < n- 1; i++)
    {
        for (j = n-1; j >i; j--)
        {
            if (a[j] <a [j -1])
            {
                tmp = a[j];
                a[j] = a[j -1];
                a[j -1] = tmp;
            }
        }
    }
}

算法分析:

最坏情况:序列逆序,此时内层循环如果语句的条件始终成立,基本操作执行的次数为ni.i取值为1〜N-1,所以总的执行次数为第(n-1 + 1) )第(n-1)/ 2 = N(N-1)/ 2,所以时间复杂度为O(N ^ 2)最好情况:。序列正序此时内层循环的条件比较语句始终不成立,不发生交换,内层循环执行N-1次,所以时间复杂度为O(N)。空间复杂度为O(1)。

 

五,快速排序

快速排序是一种分治的排序算法它将一个数组分成两个子数组,将两部分独立地排序快速排序和归并排序是互补的:归并排序将数组分成两个子数组分别排序,并将有序的子数组归并以将整个数组排序;而快速排序将数组排序的方式则是当两个子数组都有序时整个数组也就自然有序了在第一种情况中,递归调用发生在处理整个数组之前;在第二种情况中,递归调用发生在处理整个数组之后在归并排序中,一个数组被等分为两半;在快速排序中,切分的位置取决于数组的内容如图

《九种常用排序算法》

具体算法:

 void quickSort(int arr[],int _left,int _right){
        int left = _left;
        int right = _right;
        int temp = 0;
        if(left <= right){   //待排序的元素至少有两个的情况
            temp = arr[left];  //待排序的第一个元素作为基准元素
            while(left != right){   //从左右两边交替扫描,直到left = right

                while(right > left && arr[right] >= temp)  
                     right --;        //从右往左扫描,找到第一个比基准元素小的元素
                  arr[left] = arr[right];  //找到这种元素arr[right]后与arr[left]交换

                while(left < right && arr[left] <= temp)
                     left ++;         //从左往右扫描,找到第一个比基准元素大的元素
                  arr[right] = arr[left];  //找到这种元素arr[left]后,与arr[right]交换

            }
            arr[right] = temp;    //基准元素归位
            quickSort(arr,_left,left-1);  //对基准元素左边的元素进行递归排序
            quickSort(arr, right+1,_right);  //对基准元素右边的进行递归排序
        }        
    }

算法分析:

1.当分区选取的基准元素为待排序元素中的最大或最小值时,为最坏的情况,时间复杂度和直接插入排序的一样,移动次数达到最大值

                  Cmax = 1 + 2 + … +(n-1)= n *(n-1)/ 2 = O(n2)此时最好时间复杂为O(n2) 

2.当分区选取的基准元素为待排序元素中的“中值”,为最好的情况,时间复杂度为O(nlog2n)。

3.快速排序的空间复杂度为O(log2n)。 

4.当待排序元素类似[6,1,3,7,3]且基准元素为6时,经过分区,形成[1,3,3,6,7],两个3的相对位置发生了改变,所是快速排序是一种不稳定排序。

选择排序

 

六,直接选择排序

直接选择排序(Straight Select Sorting)也是一种简单的排序方法,它的基本思想是:第一次从R [0] ~R [n-1]中选取最小值,与R [0]交换,第二次从R [1]〜[R [N-1]中选取最小值,与R [1]交换,…​​,第I次从ř[I-1]〜[R [N-1] ]中选取最小值,与[R [I-1]交换,…​​,第n-1个次从ř[N-2]〜[R [N-1]中选取最小值,与[ R [N-2]交换,总共通过n-1个,得到一个按排序码从小到大排列的有序序列。

具体算法:

void SelectSort(int a[], int n) {
    int i, j, k;
    int 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];
            a[i] = R[k];
            a[k] = tmp;
        }
    }
}

 算法分析:

在直接选择排序中,共需要进行n-1次选择和交换,每次选择需要进行nitimes比较(1 <= i <= n-1),而每次交换最多需要3次移动,因此,总的比较次数C =(n * n – n)/ 2,总的移动次数3(n-1)。由此可知,直接选择排序的时间复杂度为O(n2),所以当记录占用字节数较多时,通常比直接插入排序的执行速度快些。由于在直接选择排序中存在着不相邻元素之间的互换,因此,直接选择排序是一种不稳定的排序方法。

 

七,堆排序

堆是一棵顺序存储的完全二叉树。

其中每个结点的关键字都不大于其孩子结点的关键字,这样的堆称为小根堆。

其中每个结点的关键字都不小于其孩子结点的关键字,这样的堆称为大根堆。

举例来说,对于Ñ个元素的序列{R0,R1,…,Rn中}当且仅当满足下列关系之一时,称之为堆:

(1)Ri <= R2i + 1且Ri <= R2i + 2(小根堆)

(2)Ri> = R2i + 1且Ri> = R2i + 2(大根堆)

其中I = 1,2,…,n / 2的向下取整; 

其基本思想为(大顶堆)

  1. 将初始待排序关键字序列(R1,R2 …… Rn)中构建成大顶堆,此堆为初始的无序区
  2. 将堆顶元素R [1]与最后一个元素 – [R [n]的交换,此时得到新的无序区(R1,R2,…… Rn中-1)和新的有序区(RN)中
  3. 由于交换后新的堆顶R [1]可能违反堆的性质,因此需要对当前无序区(R1,R2,…… Rn中-1)调整为新堆,然后再次将R [ 1]与无序区最后一个元素交换,得到新的无序区(R1,R2 …… RN-2)和新的有序区(RN-1,RN)的。不断重复此过程直到有序区的元素个数为N-1,则整个排序过程完成

具体算法:

void sift(int a[],int low ,int high)
{
    int i=low,j=2*i; 			//a[j] 是a[i]的左孩子
    int tmp=a[i]; 
    while(j<=high)
    {
        if(j<high&&a[j]<a[j+1])       //若右孩子较大,把j指向右孩子
            j++;
        if(tmp<a[j])
        {
            a[i]=a[j];			  //将a[j]调整到双亲节点
            i=j;                         	     //修改值,以便继续向下筛选
            j=2*i;
        }
        else
            break;
     }
    a[i]=tmp;
}

void heapsort(int a[],int n)
{
    int i;
    int tmp;
    for(i=n/2;i>=0;i--)
        sift(a,i,n);     				//循环建立初始堆
    for(i=n-1;i>=1;i--)			     // 进行n-1趟排序,每次排序元素减一
    {	
        tmp=a[0];     				//将最后一个元素同当前区间内的a[1]交换
        a[0]=a[i];
        a[i]=tmp;
        sift(a,0,i-1);				//筛选a[1]节点,得到i-1个节点的堆
    }
}

 

 算法分析:

堆的存储表示的英文顺序的。因为堆所对应的二叉树为完全二叉树,而完全二叉树通常采用顺序存储方式。

当想得到一个序列中第ķ个最小的元素之前的部分排序序列,最好采用堆排序。

因为堆排序的时间复杂度是O(n + klog2n),若k≤n/ log2n,则可得到的时间复杂度为O(n)

堆排序英文的一种不稳定的排序方法。

因为在堆的调整过程中,关键字进行比较和交换所走的是该结点到叶子结点的一条路径,

因此对于相同的关键字就可能出现排在后面的关键字被交换到前面来的情况。 

 

八,归并排序

基本思想

  归并排序(合并排序)是利用归并的思想实现的排序方法,该算法采用经典的分治(分而治之)策略(分治法将问题分(除法)成一些小的问题然后递归求解,而治(征服)的阶段则将分的阶段得到的各答案“修补”在一起,即分而治之)。

《九种常用排序算法》

 

 

 

 

 

  可以看到这种结构很像一棵完全二叉树,本文的归并排序我们采用递归去实现(也可采用迭代的方式去实现)。分阶段可以理解为就是递归拆分子序列的过程,递归深度为log2n 。

《九种常用排序算法》

 

合并相邻有序子序列

  再来看看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2, 3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤。

具体算法:

#include <stdlib.h>
#include <stdio.h>
 
void Merge(int sourceArr[],int tempArr[], int startIndex, int midIndex, int endIndex)
{
    int i = startIndex, j=midIndex+1, k = startIndex;
    while(i!=midIndex+1 && j!=endIndex+1)
    {
        if(sourceArr[i] > sourceArr[j])
            tempArr[k++] = sourceArr[j++];
        else
            tempArr[k++] = sourceArr[i++];
    }
    while(i != midIndex+1)
        tempArr[k++] = sourceArr[i++];
    while(j != endIndex+1)
        tempArr[k++] = sourceArr[j++];
    for(i=startIndex; i<=endIndex; i++)
        sourceArr[i] = tempArr[i];
}
 
//内部使用递归
void MergeSort(int sourceArr[], int tempArr[], int startIndex, int endIndex)
{
    int midIndex;
    if(startIndex < endIndex)
    {
        midIndex = startIndex + (endIndex-startIndex) / 2;//避免溢出int
        MergeSort(sourceArr, tempArr, startIndex, midIndex);
        MergeSort(sourceArr, tempArr, midIndex+1, endIndex);
        Merge(sourceArr, tempArr, startIndex, midIndex, endIndex);
    }
}
 
int main(int argc, char * argv[])
{
    int a[8] = {8, 4, 5, 7, 1, 3, 6, 2};
    int i, b[8];
    MergeSort(a, b, 0, 7);
    for(i=0; i<8; i++)
        printf("%d ", a[i]);
    printf("\n");
    return 0;
}

算法分析:

归并排序中,用到了一个临时数组,故空间复杂度为O(N);由归并排序的递归公式:T(N)= 2T(N / 2)+ O(N)可知时间复杂度为O( NlogN);数组的初始顺序会影响到排序过程中的比较次数,但是总的而言,对复杂度没有影响。平均情况或最坏情况下它的复杂度都是O(NlogN)。此外,归并排序中的比较次数是所有排序中最少的。原因是,它一开始是不断地划分,比较只发生在合并各个有序的子数组时。

 

九,基数排序

基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或bin sort,顾名思义,它是透过键值的部份资讯,将要排序的元素分配至某些“桶”中,借以达到排序的作用,基数排序法是属于稳定性的排序,其时间复杂度为为O(n日志(R)M),其中[R为所采取的基数,而米为堆数,在某些时候,基数排序法的效率高于其它的稳定性排序法。

具体算法:

#include <iostream>
using namespace std; 

void printArray(int array[],int length)
{
  for (int i = 0; i < length; ++i)
  {
    cout << array[i] << " ";
  }
  cout << endl;
}
/*
*求数据的最大位数,决定排序次数
*/
int maxbit(int data[], int n) 
{
    int d = 1; //保存最大的位数
    int p = 10;
    for(int i = 0; i < n; ++i)
    {
        while(data[i] >= p)
        {
            p *= 10;
            ++d;
        }
    }
    return d;
}
void radixsort(int data[], int n) //基数排序
{
    int d = maxbit(data, n);
    int tmp[n];
    int count[10]; //计数器
    int i, j, k;
    int radix = 1;
    for(i = 1; i <= d; i++) //进行d次排序
    {
        for(j = 0; j < 10; j++)
            count[j] = 0; //每次分配前清空计数器
        for(j = 0; j < n; j++)
        {
            k = (data[j] / radix) % 10; //统计每个桶中的记录数
            count[k]++;
        }
        for(j = 1; j < 10; j++)
            count[j] = count[j - 1] + count[j]; //将tmp中的位置依次分配给每个桶
        for(j = n - 1; j >= 0; j--) //将所有桶中记录依次收集到tmp中
        {
            k = (data[j] / radix) % 10;
            tmp[count[k] - 1] = data[j];
            count[k]--;
        }
        for(j = 0; j < n; j++) //将临时数组的内容复制到data中
            data[j] = tmp[j];
        radix = radix * 10;
    }
}
 
int main()
{
  int array[10] = {73,22,93,43,55,14,28,65,39,81};
  radixsort(array,10);
  printArray(array,10);
  return 0;
}

算法分析:

时间效率:设待排序列为n个记录,d个关键码,关键码的取值范围为radix,则进行链式基数排序的时间复杂度为O(d(n + radix)),其中,一趟分配时间复杂度为O(n),一进收集时间复杂度为O(基数),共进行d趟分配和收集。空间效率:需要2 * radix个指向队列的辅助空间,以及用于静态链表的ñ个指针

 

目录

排序分类:

排序算法优劣指标:

一,直接插入排序

二,折半插入排序(二分插入排序)

三,希尔排序

四,冒泡排序

五,快速排序

六,直接选择排序

七,堆排序

八,归并排序

九,基数排序

多种排序算法比较:

 

参考资料:

[1] “数据结构教程”第四版李春葆主编

[2] 排序六堆排序 https://www.cnblogs.com/jingmoxukong/p/4303826.html

[3] 图解排序算法(四)归并之排序  https://www.cnblogs.com/chengxiao/p/6194356.html

[4] 系统掌握——九大常用排序算法 https://blog.csdn.net/f1033774377/article/details/80570135

    原文作者:oceanao
    原文地址: https://blog.csdn.net/aoocean/article/details/84678130
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞