栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

常用排序算法比较与分析

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

常用排序算法比较与分析

前言

一、各种排序算法比较 

二、具体排序算法介绍

2.1 冒泡排序 

 2.2 选择排序

2.3 插入排序

2.4 快速排序

2.5 希尔排序

2.6 堆排序 

2.7 归并排序

2.8 桶排序

2.9 计数排序 

2.10 基数排序

三. 总结:


前言

        我们在平时学习和工作中经常用到各种排序算法,不仅要考虑算法的时间空间复杂度也要注意稳定性问题。为更好的理解排序算法,在此整理出常见的排序算法,包括:选择排序、冒泡排序、插入排序、快速排序、归并排序、堆排序、希尔排序、桶排序、计数排序、基数排序。


一、各种排序算法比较 

复杂度和稳定性的比较(来自维基百科) 

这里给大家介绍马老师的口诀记忆法:

        《忆排序》

选泡插,快归堆希桶计基

恩方恩老恩一三

对恩加K恩乘K

不稳稳稳不稳稳

不稳不稳稳稳稳



二、具体排序算法介绍



2.1 冒泡排序 

主要思想:冒泡排序的名字很形象,像水中的气泡一样,气泡大的会向上浮出水面,依次对两个数比较大小,大的数冒出来,小的数压下去。

优点:稳定

缺点:效率慢,每次只能移动相邻的俩个数据

冒泡排序的时间复杂度为O(n*n),空间复杂度为O(1),在数据有序的时候时间复杂度可以达到O(n)。适用的情景为数据量量不大,对稳定性有要求,且数据基本有序的情况下。

代码如下(示例): 

void bubble_sort (int a[], int n) {
    int i, j, lastSwap, tmp;
    for (j=n-1; j>0; j=lastSwap) {       
        lastSwap=0;     
        for (i=0; i a[i+1]) {
                tmp=a[i];
                a[i]=a[i+1];
                a[i+1]=tmp;
           //最后一次交换位置的坐标
                lastSwap = i;
            }
        }
    }
}

 2.2 选择排序

主要思想:

(1)从待排序序列中,找到关键字最小的元素,如果最小元素不是待排序序列的第一个元素,将其和第一个元素互换。

(2)从余下的 N - 1 个元素中,找出关键字最小的元素。

  重复(1)、(2)步,直到排序结束。 

特点:比冒泡更快一些,但代价是跳跃性交换,排序不稳定。

选择排序的时间复杂度为O(n*n),空间复杂度为O(1),由于每次选出待排序数据中的最小值(增序)或最大值(降序)插入到当前的有序队列中,相对于冒泡排序减少了交换的次数。当数据量不大,且对稳定性没有要求的时候,适用于选择排序 

代码如下(示例):

void selection_sort (int a[], int n) {
    int i,j,pos,tmp;
    for (i=0; ia[j])
                pos=j;
        if (pos != i) {
            tmp=a[i];
            a[i]=a[pos];
            a[pos]=tmp;
        }
    }
}

2.3 插入排序

主要思想:过程跟拿牌一样,依次拿N张牌,每次拿到到牌后,从后往前看,遇到合适位置就插进去。最终手上的牌从小到大。把N个待排的数据看作一个无序表和一个有序表,开始的时候有序表中只有一个元素,无序表中有N-1个元素,之后依次从无序表中取出元素插入到有序表中的适当位置,使之形成新的有序表。

特点:当数据规模较小或者数据基本有序时,效率较高。

插入排序的时间复杂度为O(n*n),空间复杂度为O(1),最好的情况下即当数据有序时可以达到O(n)的时间复杂度。适用于数据量不大,对算法的稳定性有要求,且数据局部或者整体有序的情况。

代码如下(示例):

void insertion_sort (int a[], int n) {
    int i,j,v;
    for (i=1; i=0&&v

2.4 快速排序

主要思想:采用分治思想,通常选取第一个数作为基准元素(pivot),依次将剩余元素中比pivot小的数放置其左侧,大于该基准的数放置其右侧;然后取pivot前半部分和后半部分分别进行同样的处理,以此类推直至各子列元素中仅剩一个元素时,即排序完成。

优点:极快数据移动少;

缺点:不稳定;

注意:此排序算法的效率在序列越乱的时候,效率越高。在数据有序时,会退化成冒泡排序;对于小规模数据(n<100),快排由于用了递归,其效率可能还不如插入排序。因此通常可以定义一个阈值,当递归的数据量很小时停止递归,直接调用插排。

快速排序算发的时间复杂度为O(nlogn),由于快速排序使用递归的方式实现,因此空间复杂度为O(nlogn)到O(n)之间。由于快速排序的原理,常用于查找数组中第k小的数。

将数组中某一个元素m作为划分依据,即m=arr[0]。若m前面的元素个数大于k,则第k小的数一定在m前面的元素中,这时我们只需要继续在m前面的元素中找第k小的数;若m前面的元素小于k,则第k小的数一定在m后面的元素中,这时我们只需要在m后面的元素中找第k小的数;

代码如下(示例): 

int mpartition(int a[], int l, int r) {
    int pivot = a[l];

    while (la[l]) l++;
        if (l 

2.5 希尔排序

主要思想:先根据特定步长将整个待排序元素序列分割成若干子序列(由相隔某个“增量”的元素组成的)分别进行直接插入排序,然后依次缩减步长再进行排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。

特点:比较在希尔排序中是最主要的操作,而不是交换。希尔排序的时间复杂度和增量的选择策略有关,用已知最好的步长序列的希尔排序比直接插入排序要快,甚至在小数组中比快速排序和堆排序还快,但在涉及大量数据时希尔排序还是不如快排;

希尔排序时间复杂度为O(nlogn)到O(n*n)之间,空间复杂度为O(1),其排序的效率受到比较距离大小的影响。和插入排序的过程相似,相对于插入排序而言,比较较远距离的数据,使得数据移动跨过多个元素,进行一次比较可能会消除多个元素的交换。

代码如下(示例):

void shell_sort(int a[], int n)
{
    int d, i, j, temp; //d为增量
    for(d = n/2;d >= 1;d = d/2) //增量递减到1使完成排序
    {
        for(i = d; i < n;i++)   //插入排序的一轮
        {
            temp = a[i];
            for(j = i - d;(j >= 0) && (a[j] > temp);j = j-d)
            {
                a[j + d] = a[j];
            }
        a[j + d] = temp;
        }
    }
}

2.6 堆排序 

主要思想:堆排序的第一步是建堆,然后是取堆顶元素进行调整。建堆的过程是自底向上不断调整的,当调整某个结点时,若其左右结点已满足条件,此时两结点不需要动则整个子树不需要调整,若需要调整,则父结点交换到子结点的位置,再以此结点继续调整。

堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。 

堆排序的时间复杂度为O(nlog),空间复杂度为O(1)。实现原理是根据大顶堆或小顶堆得数据结构原理,对待排序数据进行调整,最终达到整体有序的效果,主要应用于寻找M个数中的前K个最小的数并保持有序。

代码如下(示例):

void heapAdjust(int a[], int i, int nLength)
{
    int nChild;
    int nTemp;
    for (nTemp = a[i]; 2 * i + 1 < nLength; i = nChild)
    {
        // 子结点的位置=2*(父结点位置)+ 1
        nChild = 2 * i + 1;
        // 得到子结点中较大的结点
        if ( nChild < nLength-1 && a[nChild + 1] > a[nChild])
            ++nChild;
        // 如果较大的子结点大于父结点那么把较大的子结点往上移动,替换它的父结点
        if (nTemp < a[nChild])
        {
            a[i] = a[nChild];
            a[nChild]= nTemp;
        }
        else
        // 否则退出循环
            break;
    }
}

// 堆排序算法
void heap_sort(int a[],int length)
{
    int tmp;
    // 调整序列的前半部分元素,调整完之后第一个元素是序列的最大的元素
    //length/2-1是第一个非叶节点,此处"/"为整除
    for (int i = length / 2 - 1; i >= 0; --i)
        heapAdjust(a, i, length);
    // 从最后一个元素开始对序列进行调整,不断的缩小调整的范围直到第一个元素
    for (int i = length - 1; i > 0; --i)
    {
        // 把第一个元素和当前的最后一个元素交换,
        // 保证当前的最后一个位置的元素都是在现在的这个序列之中最大的
      ///  Swap(&a[0], &a[i]);
          tmp = a[i];
          a[i] = a[0];
          a[0] = tmp;
        // 不断缩小调整heap的范围,每一次调整完毕保证第一个元素是当前序列的最大值
        heapAdjust(a, 0, i);
    }
}

2.7 归并排序

主要思想:类似两个有序链表的合并,每次两两合并相邻的两个有序序列,直至整个序列有序。思考如何将两个有序数列合并,只要比较两个数列中最小的值,谁小谁先被取,之后再依次进行比较,若其中一个数列为空,则将另一个数列中的元素依次取出即可。

归并排序的时间复杂度为O(nlog),空间复杂度为O(n),其需要借助额外的存储空间,,当数据量较大时,要注意考虑内存空间的开销。

归并排序是采用分治法的一个非常典型的应用,归并算法需要两两比较不存在跳跃,因此归并算法属于稳定的算法,若n较大,并且要求排序稳定,则可以选择归并排序。

代码如下(示例):

void mergearray(int a[], int first, int mid, int last, int temp[])
{
    int i = first, j = mid + 1;
    int m = mid,   n = last;
    int k = 0;

    while (i <= m && j <= n)
    {
        if (a[i] <= a[j])
            temp[k++] = a[i++];
        else
            temp[k++] = a[j++];
    }

    while (i <= m)
        temp[k++] = a[i++];

    while (j <= n)
        temp[k++] = a[j++];

    for (i = 0; i < k; i++)
        a[first + i] = temp[i];
}
void merge_sort(int a[], int first, int last, int temp[])
{
    if (first < last)
    {
        int mid = (first + last) / 2;
        merge_sort(a, first, mid, temp);    //左边有序
        merge_sort(a, mid + 1, last, temp); //右边有序
        mergearray(a, first, mid, last, temp); //再将二个有序数列合并
    }
}

2.8 桶排序

主要思想:是一种简单快速的排序算法,也有较大的局限性,仅适用于数据的分布相对比较集中的时候,其原理是建立一个包含一定数目桶的表,将待排序的数据通过散列算法散列到桶中,然后再遍历桶的到有序的数据。      

 桶排序的时间复杂度为O(n),利用函数的映射关系,减少了计划所有的比较操作,是一种Hash的思想,可以用在海量数据处理中。

计数排序也可以看作是桶排序的特例,数组关键字范围为N,划分为N个桶。

代码如下(示例)

def bucketSort(arr):
    maximum, minimum = max(arr), min(arr)
    bucketArr = [[] for i in range(maximum // 10 - minimum // 10 + 1)]  # set the map rule and apply for space
    for i in arr:  # map every element in array to the corresponding bucket
        index = i // 10 - minimum // 10
        bucketArr[index].append(i)
    arr.clear()
    for i in bucketArr:
        heapSort(i)   # sort the elements in every bucket
        arr.extend(i)  # move the sorted elements in bucket to array

2.9 计数排序 

主要思想:计数排序的思想是,考虑待排序数组中的某一个元素a,如果数组中比a小的元素有s个,那么a在最终排好序的数组中的位置将会是s+1,如何知道比a小的元素有多少个,肯定不是通过比较去觉得,而是通过数字本身的属性,即累加数组中最小值到a之间的每个数字出现的次数(未出现则为0),而每个数字出现的次数可以通过扫描一遍数组获得。

计数排序步骤:

        1. 找出原数组中元素值最大的,记为max。

        2. 创建一个新数组count,其长度是max加1,其元素默认值都为0。

        3. 遍历原数组中的元素,以原数组中的元素作为count数组的索引,以原数组中的元素出现次数作为count数组的元素值。 

        4. 创建结果数组result,起始索引index。

        5. 遍历count数组,找出其中元素值大于0的元素,将其对应的索引作为元素值填充到result数组中去,每处理一次,count中的该元素值减1,直到该元素值不大于0,依次处理count中剩下的元素。

        6. 返回结果数组result。

代码如下(示例):

public int[] countSort(int[] A) {
    // 找出数组A中的最大值
    int max = Integer.MIN_VALUE;
    for (int num : A) {
        max = Math.max(max, num);
    }
    // 初始化计数数组count
    int[] count = new int[max+1];
    // 对计数数组各元素赋值
    for (int num : A) {
        count[num]++;
    }
    // 创建结果数组
    int[] result = new int[A.length];
    // 创建结果数组的起始索引
    int index = 0;
    // 遍历计数数组,将计数数组的索引填充到结果数组中
    for (int i=0; i0) {
            result[index++] = i;
            count[i]--;
        }
    }
    // 返回结果数组
    return result;
}

2.10 基数排序

主要思想:基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。

基数排序的步骤:以整数为例,将整数按十进制位划分,从低位到高位执行以下过程。

      1. 从个位开始,根据0~9的值将数据分到10个桶桶,例如12会划分到2号桶中。 

      2. 将0~9的10个桶中的数据顺序放回到数组中。

      重复上述过程,一直到最高位。

基数排序也可以看作一种桶排序,不断的使用不同的标准对数据划分到桶中,最终实现有序。基数排序的思想是对数据选择多种基数,对每一种基数依次使用桶排序。

基排序的主要适用场景是待排元素是非负整数,且位数相差不大。此外,整数也可以用字符串等表达,所以也可以用于字符串的排序。

代码如下(示例):

int getNumInPos(int num,int pos) //获得某个数字的第pos位的值
{
    int temp = 1;
    for (int i = 0; i < pos - 1; i++)
        temp *= 10;

    return (num / temp) % 10;
}

#define RADIX_10 10    //十个桶,表示每一位的十个数字
#define KEYNUM 5     //整数位数
void radix_sort(int* pDataArray, int iDataNum)
{
    int *radixArrays[RADIX_10];    //分别为0~9的序列空间
    for (int i = 0; i < RADIX_10; i++)
    {
        radixArrays[i] = new int[iDataNum];
        radixArrays[i][0] = 0;    //index为0处记录这组数据的个数
    }

    for (int pos = 1; pos <= KEYNUM; 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++) //写回到原数组中,复位radixArrays
        {
            for (int k = 1; k <= radixArrays[i][0]; k++)
                pDataArray[j++] = radixArrays[i][k];
            radixArrays[i][0] = 0;
        }
    }
}

三. 总结:

本文总结了常用十种排序算法的主要思想以及适用场景,并进行了算法代码的展示,在此特别感谢以下几篇文章对我的帮助,借鉴了许多其中的行文思路和代码思路,再次表示衷心的感谢。

https://www.cnblogs.com/inchbyinch/p/11630388.html

https://www.cnblogs.com/zhaoshuai1215/p/3448154.html

超详细的八大排序算法的各项比较以及各自的特点_柯南的博客-CSDN博客_排序算法比较

常用的排序算法及其适用场景_半夏。的博客-CSDN博客_排序算法应用场景

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/346049.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号