排序算法总结

算法是从经验中提炼出来的细化步骤.

一.概述

排序是一项基础操作,大量计算任务和作业因为进行了合理的排序预处理而变得简单,所以掌握排序算法是一项必须技能.

本文介绍了10种常见的排序算法,并从各个方面做了总结.

二.相关概念

2.1 排序稳定

相同数相对位置不变,对排序结果而言.

如果两个数相同,对他们进行的排序结果为他们的相对顺序不变。例如A={1,2,1,2,1}这里排序之后是A = {1,1,1,2,2} 稳定就是排序后第一个1就是排序前的第一个1,第二个1就是排序前第二个1,第三个1就是排序前的第三个1。同理2也是一样。不稳定就是他们的顺序与开始顺序不一致。

下列所列举的排序算法都是将元素从小到大排序.

2.2 原地排序

指不申请多余的空间进行的排序,就是在原来的排序数据中比较和交换的排序。例如快速排序,堆排序等都是原地排序,合并排序,计数排序等不是原地排序。

总体上说,排序算法有两种设计思路,一种是基于比较,另一种不是基于比较。

三.基于比较的排序算法

基于比较的排序算法有三种设计思路,分别为插入,交换和选择。对于插入排序,主要有直接插入排序,希尔排序;对于交换排序,主要有冒泡排序,快速排序;对于选择排序,主要有简单选择排序,堆排序;其它排序:归并排序。

3.1 冒泡排序

说到算法,肯定要提冒泡排序,冒泡法可以说是我第一个学到的算法.也是我本科使用最多的算法 (:sad.

3.1.1 思想

bubble sort

如图,冒泡法分为若干趟进行,每一趟比较相邻两个数的大小,若前面大于后面,则交换,第一趟下来,最大的数就跑到最后面.下一趟只要考虑前n-1个数,需要比较的也是前n-1个数.一直下去,直到没有交换操作.

其核心在于比较+交换.

什么时候我们会用冒泡排序呢?比如,体育课上从矮到高排队时,站队完毕后总会有人出来,比较挨着的两个人的身高,指挥到:你们俩调换一下,你们俩换一下.

3.1.2 关键代码
void bubbleSort(int[] array) {
    int n = array.length;
    int i,j,temp;
    boolean swapped;
    for (i=0;i<n-1;i++) {
        swapped = false;
        for (j=0;j<n-1-i;j++) {
            if (array[j]>array[j+1]) {
                temp = array[j];
                array[j] = array[j+1];
                array[j+1] = temp;
                swapped = true;
            }
        }
        if (!swapped) {
            return;
        }
    }
}
3.1.3 特点

稳定排序,原地排序,时间复杂度O(n*n)

3.1.4 适用场景

数据基本有序.


3.2 选择排序

3.2.1 思想

selectionSort

如图,将排序序列分成排序和未排序的.首先在未排序的序列中找到最小值和第一个元素交换,然后在剩下的元素中继续寻找最小,和第一个元素交换.直到所有元素都排序完毕.

生活中的例子:假设班级选美,你肯定会选出最好看放在第一位,然后从剩下的选择最好看,以此类推,直到所有女生都排好序.

其核心思想在于查找+交换.
查找里包含比较.

3.2.2 关键代码
void selectionSort(int[] array) {
    int len = array.length();
    int i,j,min,temp;
    for (i=0;i<len;++i) {
        min = i;   //记录下标
        for (j=i+1;j<len;++j) {
            if (array[min]>array[j]){
                min = j;
            }
        }
        temp = array[i];
        array[i] = array[min];
        array[min] = temp;
    }
}
3.2.3 特点

不稳定排序(比如对3 3 2三个数进行排序,第一个3会与2交换),原地排序,时间复杂度O(N*N)

3.2.4 适用场景

交换少

43.3 堆排序

3.3.1 思想

HeapSort

利用堆这种数据结构,将待排序序列分成有序序列和无序序列.对无序序列构造最大堆,获得堆顶(最大值)放入有序序列,然后从剩下的元素继续前面的步骤,直到排序结束.
在堆中定义以下几种操作:
- 最大堆调整(Max-Heapify):将堆的末端子节点作调整,使得子节点永远小于父节点
- 创建最大堆(Build-Max-Heap):将堆所有数据重新排序,使其成为最大堆
- 堆排序(Heap-Sort):移除位在第一个数据的根节点,并做最大堆调整的递归运算

堆用到了选择排序的思想,不同之处在于用到了堆数据结构.其核心思想在于构造最大堆+交换.

下面以书上的一个例子作为演示:

堆排序

3.3.2 关键代码
private static void soutCore(int[] array) {
        int len = array.length;
        //构造大顶堆
        buildHeap(array);
        //堆顶排到尾部,分成有序块和无序块,对无序块重新构造大顶推
        for (int i=len-1;i>0;--i){
            swap(array,0,i);
            //重新把最大值抬升到顶部
            heapify(array,0,i);
        }
    }

    private static void swap(int[] array, int i, int j) {
        int temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }

    private static void buildHeap(int[] array) {
        int len = array.length;
        //从中间元素开始
        for (int i=len/2-1;i>=0;--i){
            heapify(array, i, len);
        }
    }

    //最大数抬升到堆顶
    private static void heapify(int[] array, int idx, int max) {
        int left = idx*2+1;
        int right = idx*2+2;
        int largestIndex;
        if (left < max && array[left]>array[idx]) {
            largestIndex = left;
        }else {
            largestIndex = idx;
        }

        if (right < max && array[right]>array[largestIndex]) {
            largestIndex = right;
        }

        if (largestIndex!=idx) {
            swap(array, idx, largestIndex);
            heapify(array, largestIndex, max);
        }
    }
3.3.3 特点

非稳定排序,原地排序,时间复杂度O(N*lg N)

3.3.4 适用场景

时间复杂度稳定,保持O(N*lg N),但不如快排广泛.


3.4 快速排序

3.4.1 思想

Quicksort

如图,选取最后一个数作为基准,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面,在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作.该基准值所在位置就属排序的位置.然后递归地(recursively)把小于基准值元素的子数列和大于基准值元素的子数列排序。
递归到最底部时,数列的大小是零或一,也就是已经排序好了。这个算法一定会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。

其核心在于递归+分而治之.

3.4.2 关键代码
private static void solve(int[] array, int start, int end) {
        if (start<end) {
            int p = solveCore(array, start,end);
            solve(array,start, p-1);
            solve(array, p+1,end);

        }
    }

private static int solveCore(int[] array,int start,int end) {
    int x = array[end];
    int p = -1;
    for (int j=0;j<end;++j){
        if (array[j]<=x) {   //一定是小于等于
            p++;
            swap(array,p,j);
        }
    }
    swap(array, p+1, end);
    return p+1;
}

private static void swap(int[] array, int i, int j) {
    if (array != null) {
        int temp = array[i];
        array[i] =array[j];
        array[j] = temp;
    }
}
3.4.3 特点

不稳定排序,原地排序,时间复杂度O(N*lg N)

3.4.4 适用场景

应用很广泛,差不多各种语言均提供了快排API

3.5 归并排序

3.5.1 思想

Merge sort

如图,(假设序列共有n个元素):
将序列每相邻两个数字进行归并操作,形成 floor(n/2)个序列,排序后每个序列包含两个元素
将上述序列再次归并,形成 floor(n/4)个序列,每个序列包含四个元素
重复步骤2,直到所有元素排序完毕

其核心在于递归+分而治之.

3.5.2 关键代码
static void merge_sort_recursive(int[] arr, int[] result, int start, int end) {
    if (start >= end)
        return;
    int len = end - start, mid = (len >> 1) + start;
    int start1 = start, end1 = mid;
    int start2 = mid + 1, end2 = end;
    merge_sort_recursive(arr, result, start1, end1);
    merge_sort_recursive(arr, result, start2, end2);
    int k = start;
    while (start1 <= end1 && start2 <= end2)
        result[k++] = arr[start1] < arr[start2] ? arr[start1++] : arr[start2++];
    while (start1 <= end1)
        result[k++] = arr[start1++];
    while (start2 <= end2)
        result[k++] = arr[start2++];
    for (k = start; k <= end; k++)
        arr[k] = result[k];
}
public static void merge_sort(int[] arr) {
    int len = arr.length;
    int[] result = new int[len];
    merge_sort_recursive(arr, result, 0, len - 1);
}
3.5.3 特点

稳定排序,非原地排序,时间复杂度O(N*logN)

3.5.4 适用场景

外部排序和大批量数据的排序.


3.6 插入排序

3.6.1 思想

Insertion Sort

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

其核心在于比较+交换.

3.6.2 关键代码
public static void insertion_sort(int[] arr) {
    for (int i = 1; i < arr.length; i++ ) {
        int temp = arr[i];
        int j = i - 1;  
 //如果将赋值放到下一行的for循环内, 会导致在第10行出现j未声明的错误
        for (; j >= 0 && arr[j] > temp; j-- ) {
            arr[j + 1] = arr[j];
        }
        arr[j + 1] = temp;
    }
}
3.6.3 特点

稳定排序,原地排序,时间复杂度O(N*N)

3.6.4 适用场景

当数据已经基本有序时,采用插入排序可以明显减少数据交换和数据移动次数,进而提升排序效率。

3.7 shell排序

3.7.1 思想

Shell Sort

Shell 排序是对插入排序的一种改进.将数组列在一个表中并对列排序(用插入排序)。重复这过程,不过每次用更长的列来进行。最后整个表就只有一列了。将数组转换至表是为了更好地理解这算法,算法本身仅仅对原数组进行排序(通过增加索引的步长,例如是用i += step_size而不是i++)。

其核心在于选择步长+插入排序.

3.7.2 关键代码
public static void shell_sort(int[] arr) {
    int gap = 1, i, j, len = arr.length;
    int temp;
    while (gap < len / 3)
        gap = gap * 3 + 1; // <O(n^(3/2)) by Knuth,1973>: 1, 4, 13, 40, 121, ...
    for (; gap > 0; gap /= 3)
        for (i = gap; i < len; i++) {
            temp = arr[i];
            for (j = i - gap; j >= 0 && arr[j] > temp; j -= gap)
                arr[j + gap] = arr[j];
            arr[j + gap] = temp;
        }
}
3.7.3 特点

非稳定排序,原地排序,时间复杂度O(n^lamda)(1 < lamda < 2), lamda和每次步长选择有关。

3.7.4 适用场景

因为增量初始值不容易选择,所以该算法不常用。

四.非基于比较的排序算法

4.1 计数排序

4.1.1 思想

计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数。然后根据数组C来将A中的元素排到正确的位置。

通俗地理解,例如有10个年龄不同的人,统计出有8个人的年龄比A小,那A的年龄就排在第9位,用这个方法可以得到其他每个人的位置,也就排好了序。当然,年龄有重复时需要特殊处理(保证稳定性),这就是为什么最后要反向填充目标数组,以及将每个数字的统计减去1的原因。

其核心在于统计+数范围限制.

4.1.2 关键代码
 private static void sortCore(int[] array) {
        int len = array.length;
        //数组范围0-n,创建这样的K个容器
        int[] b = new int[len];
        for (int i=0;i<len;++i) {
            b[array[i]]++;   //计数
        }

        int index = 0;
        for (int j=0;j<len;++j){
            while (b[j]>0){
                array[index++] = j;
                b[j]--;
            }
        }
    }
4.1.3 特点

稳定排序,非原地排序,时间复杂度O(N)

4.1.4 适用场景

比基数排序和桶排序广泛得多。

4.2 桶排序

4.1.1 思想

分桶

将元素装入对应的桶.

输出

对桶内元素排序(插入排序),然后输出.

4.1.2 关键代码
/**
     * @param a 待排序数组元素
     * @param step 步长(桶的宽度/区间),具体长度可根据情况设定
     * @return 桶的位置/索引
     */
    private int indexFor(int a,int step){
        return a/step;
    }
    public void bucketSort(int []arr){

        int max=arr[0],min=arr[0];
        for (int a:arr) {
            if (max<a)
                max=a;
            if (min>a)
                min=a;
        }
        //该值也可根据实际情况选择
        int bucketNum=max/10-min/10+1;
        List buckList=new ArrayList<List<Integer>>();
        //create bucket
        for (int i=1;i<=bucketNum;i++){
            buckList.add(new ArrayList<Integer>());
        }
        //push into the bucket
        for (int i=0;i<arr.length;i++){
            int index=indexFor(arr[i],10);
            ((ArrayList<Integer>)buckList.get(index)).add(arr[i]);
        }
        ArrayList<Integer> bucket=null;
        int index=0;
        for (int i=0;i<bucketNum;i++){
            bucket=(ArrayList<Integer>)buckList.get(i);
            insertSort(bucket);
            for (int k : bucket) {
                arr[index++]=k;
            }
        }

    }
    //把桶内元素插入排序
    private void insertSort(List<Integer> bucket){
        for (int i=1;i<bucket.size();i++){
            int temp=bucket.get(i);
            int j=i-1;
            for (; j>=0 && bucket.get(j)>temp;j--){
                bucket.set(j+1,bucket.get(j));
            }
            bucket.set(j+1,temp);
        }
    }
4.1.3 特点

稳定排序,非原地排序,时间复杂度O(N)

4.1.4 适用场景

4.3 基数排序

此处不做说明.

4.1.1 思想

将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。

它是这样实现的:将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。

五. 选择排序算法的标准

排序算法那么多,是不是有绝对的最优呢?
是不是时间复杂度越低,算法就最优呢?
像排序算法,归并算法,堆排序这三种,时间复杂度都是O(n^2), 又该如何选择呢?
时间最优还是空间最优?

考虑各个算法的短板.通过短板和应用场景来选择正确的排序算法.

标准排序算法
元素很少插入排序
几乎有序插入排序
关注最差情堆排序(牢记:堆排序的最差时间复杂度依然是O(nlogn))
平均较好快速排序
元素从密集范围取出桶排序
代码量少插入排序

六. 小结

各算法时间复杂度和空间复杂度列表

各算法时间复杂度和空间复杂度列表

上表有些不严谨,待更.

参考文献

  1. 算法之排序算法的算法思想和使用场景总结
  2. 从零开始学算法:十种排序算法介绍(中)
  3. <算法技术手册>
  4. Bubble sort
  5. quicksort
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值