数据结构和算法基础(8)——排序算法

一、排序介绍

1.分类

1.1 内部排序和外部排序。

  • 内部排序值将需要处理的所有数据都加载到内存中进行排序.
  • 外部排序借助外部存储进行排序。

常见的内部排序有:
在这里插入图片描述

1.2 非线性时间比较类排序和线性时间非比较类排序

  • 非线性时间比较类排序:通过比较来决定元素间的相对次序,由于时间复杂度不能突破O(nlogn),因此成为非线性时间比较类排序。
  • 线性时间非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此被称为线性时间非比较类排序。
    在这里插入图片描述

2.排序算法一张图分析

在这里插入图片描述


二、十大排序算法

1.冒泡排序

1.1 原理

通过对待排序序列从前往后(从下标较小的元素开始),依次比较相邻元素的值,若发现逆序则交换。每一次循环都会将大的值放到后面。
在这里插入图片描述

1.2 代码

	private static void bubbleSort(int[] array) {
        for (int i = 0;i < array.length-1;i++) {
            boolean isSwap = false;
            for (int j = 0;j < array.length -i-1;j++) {
                if (array[j] > array[j+1]) {
                    int temp = array[j];
                    array[j] = array[j+1];
                    array[j+1] = temp;
                    isSwap =true;
                }
            }
            if (!isSwap){
                break;
            }
        }
    }

1.3 分析

(1)最优的情况也就是开始就已经排序好序了,那么就可以不用交换元素了,则时间花销为:[ n(n-1) ] / 2;所以最优的情况时间复杂度为:O( n^2 )。
(2)最坏的情况是逆序,那么比较次数是[ n(n-1) ] / 2,交换次数是[ n(n-1) ] / 2,总执行次数[ n^2 - n ]
(3)平均的时间复杂度为:O( n^2 )。
(4)上述代码是优化过的冒泡排序,最优时间复杂度O( n)。
(5)是稳定排序,相同值的元素不会交换位置。
(6)只需要额外一个临时变量,空间复杂度为O( 1)。


2.选择排序

2.1 原理

(1)每一次遍历的过程中,都假定第一个索引为当前索引,和其他索引处的值一次进行比较。如果其他索引小于当前索引,则当前索引值变为其他索引下标,最终当前索引为最小值所在的下标。
(2)交换第一个索引处和当前索引处的值。
在这里插入图片描述

2.2 代码

	private static void selectSort(int[] array) {
        for (int i = 0;i < array.length-1;i++) {
            int tempIndex = i;
            int temp = array[i];
            for (int j = i + 1;j < array.length;j++) {
                if (temp > array[j]) {
                    temp = array[j];
                    tempIndex = j;
                }
            }
            if (tempIndex != i) {
                array[tempIndex] = array[i];
                array[i] = temp;
            }
        }
    }

2.3 分析

(1)最好、最差、平均时间复杂度都是O(n^2),应为都要比较[ n(n-1) ] / 2次,保留最高阶项,去掉常数因子,为 O(n^2)。
(2)选择排序是不稳定排序。
(3)选择排序是原地排序,空间复杂度O(1)。


3.插入排序

3.1 原理

(1)把所有元素分为两组,已经排序和未排序。
(2)找到未排序的组中的第一个元素,向已经排序的组中进行插入。
(3)倒叙遍历已经排序的元素,依次和待插入的元素进行比较,知道找到一个元素小于待插入元素,那么就把待插入元素放到这个位置,其他元素往后移动一位。
在这里插入图片描述

3.2 代码

	public static void StraightInsertionSort(int[] a) {
	   for (int i = 1; i < a.length  ;i ++) {
	       for (int j = i; j > 0;j --) {
                if (a[j] < a[j-1]) {
                    int temp = a[j];
                    a[j] = a[j-1];
                    a[j-1] = temp;
                } else {
                    break;
                }
           }
       }
    }

八十万的数据量测试时间为120000ms

3.3 分析

(1)最好情况是原数组有序时间复杂度为n,最坏情况为原数组逆序时间复杂度为O(n^2),平均时间复杂度为 O(n^2)。
(2)空间复杂度为O(1),是原地排序。
(3)插入排序是稳定排序。


4.希尔排序

4.1 原理

希尔排序是插入排序的一种,又称缩小增量排序,是插入排序算法的一种更高效的改进版本。
前面的插入排序,存在一个不友好的问题:如果已排序元素为[2,5,7,9,10],如果下一个待插入元素为1,我们需要拿着1依次和10,9,7,5,2交换位置。如果我们想提高效率,直观的想法是一次交换。
(1)选定一个增长量h,按照增长量h作为数据分组的依据,对数据进行分组。
(2)对分好组的每一组数据完成插入排序。
(3)减少增长量,最小减为1,重复第二步的操作。

4.2代码

    public static void shellSort(int[] array) {
	    int temp = 0;
	    // 第一层循环,逐步减少增量
	   for (int gap = array.length / 2; gap > 0 ;gap/=2) {
	       // 内层两次循环和插入排序一样
           // 第二层循环从每个数组的第2个元素开始,所以从gap开始
	       for (int i = gap;i<array.length;i++) {
	           // 第三层循环用来从未排序数组往排序数组中插入j-gap是下一个要比较的元素,应该要大于等于0.
	           for (int j = i ;j >= gap; j -= gap) {
	               if (array[j] < array[j -gap]) {
	                   temp = array[j];
                       array[j] = array[j-1];
                       array[j-1] = temp;
                   }else {
	                   break;
                   }
               }
           }
       }
    }

八百万的数据量测试时间为3500ms,可以看到突破O(N^2)后,算法耗时大大减少。希尔排序大概比插入排序快893倍。

4.3 分析

(1) 希尔排序的时间的时间复杂度为O(n ^(2/3)),希尔排序时间复杂度的下界是n*log2n。希尔排序没有快速排序算法快 O(n(logn)),因此中等大小规模表现良好,对规模非常大的数据排序不是最优选择。但是比O( )复杂度的算法快得多。
(2)空间复杂度为O(1),是原地排序。
(3)希尔排序是不稳定排序。

4.4 希尔排序突破O(N^2)的原因

为啥希尔能突破O(N^2) 的界,可以用逆序数来理解。
假设我们要从小到大排序,一个数组中取两个元素如果前面比后面大,则为一个逆序,容易看出排序的本质就是消除逆序对。
可以证明对于随机数组,逆序数是N^2 数量级的,如果每次交换只能消除一个逆序对,那么就要执行 O(N^2)次,这就是为啥冒泡、插入等算法只能到平方级别的原因。
反过来,基于交换元素的排序要想突破这个下界,必须执行一些比较,交换相隔比较远的元素,使得一次交换能消除一个以上的逆序,希尔、快排、堆排等等算法都是交换比较远的元素,只不过规则各不同罢了。

但是不相邻的交换可以一次消除多个逆序对,那么会不会造成更多的逆序对呢?
答案是:这个需要人为制造规则保证这件事不发生,举个例子:
假设从小到大排序,简单起见设数组元素两两不等
现在发现了a[i]>a[j],i<j,考虑下标闭区间[i,j]这个范围的j-i+1个元素,对任意i<k<j,考虑a[k]
若a[k]<a[j],交换a[i]和a[j]后,三者的逆序数从2变为1(例如3 1 2变成2 1 3)
若a[k]>a[i],交换a[j]和a[i]后,三者的逆序数从2变为1(例如2 3 1变成1 3 2)
若a[i]>a[k]>a[j],交换a[i]和a[j]后,三者的逆序数从3变为0(例如3 2 1变成1 2 3)
所以,除非你去交换两个本来有序的元素,否则逆序数必然是递减的。


5.快速排序

5.1 原理

快速排序是对冒泡排序的一种改进。,它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另一部分的所有数据都要小。然后再按照此方法对这两部分进行快速排序,整个排序过程递归进行。
(1)首先设定一个分界值,通过该分解值将数组分成左右两个部分。
(2)将大于等于分界值的数据放到数组右边,小于分解值的数据放到数组的左边。
(3)然后左边和右边独立排序。每边重复(1)(2)步。
在这里插入图片描述

5.2代码

	private static void quickSort(int[] array,int left,int right) {
        if (left < right) {
            int l = left;
            int r = right;
            int temp = array[left];
            int tempSwap = 0;
            while (l < r) {
                while (array[r] >= temp && r > l) {
                    r--;
                }
                while (array[l] < temp && l < r) {
                    l++;
                }
                if (l<r) {
                    tempSwap = array[r];
                    array[r] = array[l];
                    array[l] = tempSwap;
                }
            }
            tempSwap = array[left];
            array[left] = array[l];
            array[l] = tempSwap;
            quickSort(array,left,l);
            quickSort(array,l+1,right);
        }
    }

八百万的数据量测试时间为1500ms

5.3 分析

5.3.1 时间复杂度

在这里插入图片描述
快排会将一个数列分成两个子数列,如上图。它们的长度分别是i-1和n-1,设两个子数列的时间是T(i - 1) 和 T(n - i)。分割两个数列要比较n次,所以设一次分割所需的时间为cn。如下图:
在这里插入图片描述
所以方程式就是:
T(n) = 左边的时间 + 右边的时间 + 分割所需的时间
= T(i - 1) + T(n - i) + cn
当n为0时,不需要排序,所以T(0) = 0。当n为1时,T(1) = 0。
因此完整快排一般方程式如下:

       |-- 0                         (n = 0, or n = 1)
T(n) = |
       |-- T(i - 1) + T(n - i) + cn  (n > 1)
5.3.2 最坏时间复杂度

最差情况下,每次分割的两个子树列的长度为n-1和0,所以所需时间为:

T(n) = T(n - 1) + T(0)  + cn = T(n) = T(n - 1)  + cn

可进行如下图的分解:

T(n)       =    T(n - 1)      +    C*n                    
T(n - 1)   =    T(n - 2)      +    C*(n - 1)      # 将 n 减 1,得到下个子序列
T(n - 2)   =    T(n - 3)      +    C*(n - 3)
T(2)       =    T(1)          +    C*2
T(1)       =    T(0)          +    C

在这里插入图片描述
将下一级表达式依次代入上一级,可以得到:

T(n) = T(0) + C*n + C*(n-1) + C*(n-2) + C*(n-3).....+C*(2)+C
# T(0)是等于0的
T(n) = C*n + C*(n-1) + C*(n-2) + C*(n-3).....+C*(2)+C
T(n) = C*[n + (n-1) + (n-2) + (n-3).....+2+1]
T(n) = C*(n + 1)*n/2

所以 最差时间复杂度为:O(n*n)。

5.3.3 最优时间复杂度

最好情况下,每次分割都在中间,所以所需时间为:

T(n) = T(n/2) + T(n/2) + cn
       = 2T(n/2) + cn

可进行如下公式分解:

T(n/2) = 2T(n/4) + cn/2
T(n/4) = 2T(n/8) + cn/4
T(n/8) = 2T(n/16) + cn/8

所以可得:

T(n) = 2T(n/2) + cn                                       # T(n/2) = 2T(n/4) + cn/2
        = 2*[2T(n/4) + cn/2] + cn
        = 4T(n/4) + 2cn                                     # T(n/4) = 2T(n/8) + cn/4
        = 4*[2T(n/8) + cn/4] + 2cn
        = 8T(n/8) + 3cn                                     # T(n/8) = 2T(n/16) + cn/8
        = 8*[2T(n/16) + cn/8] + 3cn
        = 16T(n/16) + 4cn
        ......
        = 2^kT(n/2^k) + kcn                                  # 一直递推下去,直到 n/2k = 1
        = n*T(1)  + cn*logn                                  # n/2^k = 1   ==>     2^k = n     ==> k = logn   
        = cn*logn                                              # T(1) = T(0) = 0

所以 最好时间复杂度为:O(n*logn)。
平均时间复杂度为:O(n logn)

5.3.4 其他

(1)快速排序是不稳定排序,例如:【5 3 3 4 3 8 9 10 11】
(2)总的空间复杂度 = 每次压栈所需的空间 * 总的递归栈的深度 = 常数*logn = logn,所以不是原地算法。


6.堆排序

6.1 原理

首先要知道堆的几个特性:

  • 堆是完全二叉树
  • 堆采用顺序存储二叉树,所以第n个元素的左子节点为2n+1,第n个元素的右子节点为2n+2,第n个元素的父节点为(n-1)/2。
  • 大顶堆的特点为arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]

未完待续,会持续更新

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值