十大排序算法(入门篇)

一. 引言

Hello, 小伙伴们, 本篇是算法系列的第一篇文章, 会介绍一些简单的算法并通过这些算法详细说明一个算法是否优劣的评价标准。

算法核心思想都是一样的, 不分编程语言, 但是在本系列文章中主要会用 python、Java 或者 JavaScript 三种语言来实现相关代码, 特此说明一下。

想查看更多的文章请关注公众号:IT巡游屋
十大排序算法(入门篇)_第1张图片

二. 算法概述

2.1 使用场景

那我们先看一下算法的使用场景吧, 算法在程序员的世界里无处不在, 也无时无刻不在改变着我们现在的生活, 比如:

  • 我们的出行

    滴滴打车不拥有出租车,而是使用算法来连接司机和乘客。

  • 我们的订餐

    大众点评和美团战略合作后的新美大自己并不生产食物,而是使用算法连接了商家, 客户和物流,将食物直接“投递”到手中。

  • 我们的购物

    全球第二大零售商阿里巴巴没有库存,而是使用算法来帮助他人销售和购买产品。

2.2 算法概述

算法(Algorithm)是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令,算法代表着用系统的方法描述解决问题的策略机制。也就是说,能够对一定规范的输入,在有限时间内获得所要求的输出。

不同的算法可能用不同的时间、空间或效率来完成同样的任务。一个算法的优劣可以用 空间复杂度时间复杂度 来衡量。

一句话总结:算法就是描述解决问题的方法。

2.3 算法时间复杂度分析 —— 大O表示法

算法的时间复杂度通过用大O符号来进行表述,定义为: T(n)= O(f(n));

  • 如果一个问题的规模是n,解决这一问题的某一算法所需要的时间为T(n),T(n)称为这一算法的”时间复杂度“。

  • f(n)是问题规模n的某个函数,随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐进时间复杂度。

  • 一般情况下,随着n的增大,T(n)增长最慢的算法为最优算法

2.3.1 常数阶

无论代码执行了多少行,只要是没有循环等复杂结构,这个代码的复杂度都是 O(1), 如:

var sum = 0;
var n = 100;
sum = (1+n)*n/2;
console.log("sum =", sum);

​ 上述代码,无论n为多少,都只执行三次,这种算法的执行次数与n的多少无关,即为时间恒定的算法,我们称之为具有 O(1) 的时间时间复杂度,又叫常数阶。(无论这类代码有多长,即使有几万行,都可以用O(1)来表示它的 时间复杂度)

2.3.2 线性阶

​ 下面这段代码,它的循环时间复杂度为O(n),因为循环体中的代码必须要执行n次,因此该算法消耗的时间是随着n的变化而变化,这类代码都可以用O(n)来表示它的时间复杂度。

var sum = 0
var n = 100;
for (var i = 1; i <= n; i++) {
     
    sum = sum + i;
}

2.3.3 对数阶

​ 先来看下面的这段代码:

var n = 100;
var i = 1;
while(i < n){
     
	i = i * 2;
}

​ 上述代码主要计算循环体中 i = i * 2; 这行代码执行的次数,该行代码每执行一次,i都会乘以2更接近n,而 i 的初始值=1,第一次执行后 i=2,第二次执行 i = 2 * 2,第三次执行 i= 2 * 2 * 2 也就是 2³,假如 代码执行了 a 次后 i 的值 = n, 那么可以推导出公式:2ª= n;得到 a = log_2(n) ;所以这个循环的时间复杂度记为 O(logn)

2.3.4 线性对数阶

​ 在理解了上述对数阶后,再理解线性对数阶就更好理解了,线性对数阶的时间复杂度表示为:O(nlogn) ; 那么其实就是在上述代码的外面还有一层循环N次的循环体,如下代码:

var n = 100
for(var x = 1; x <= n ;x++){
     
	var i = 1;
	while(i < n){
     
		i = i * 2;
	}
}

2.3.5 平方阶

​ 下面这段代码是我们刚才看到的第三种算法:

var x = 0;
var sum = 0;
var n = 100;
for (var i = 1; i <= n; i++) {
     
    for (var j = 1; j <= n; j++) {
     
        x++;
        sum = sum + x;
    }
}

​ 第一层for循环里面的代码会执行n次,而第二层for循环里面的代码也会执行n次,因此整体的时间复杂度为 n*n = n²,用大O表示法为: O(n²)

​ 在理解了平方阶的基础上,立方阶 O ( n 3 ) {O(n^3)} O(n3) 也可以理解了。

2.3.6 总结

​ 常用的时间复杂度所耗费的时间从小到大依次为:
O ( 1 ) < O ( l o g n ) < O ( n ) < O ( n l o g n ) < O ( n 3 ) < O ( 2 n ) < O ( n ! ) < O ( n n ) O(1)O(1)<O(logn)<O(n)<O(nlogn)<O(n3)<O(2n)<O(n!)<O(nn)
​ 我们前面已经接触过了 常数阶 O ( 1 ) {O(1)} O(1)、对数阶 O ( l o g n ) {O(logn)} O(logn)、线性阶 O ( n ) {O(n)} O(n)、线性对数阶 O ( n l o g n ) {O(nlogn)} O(nlogn)、平方阶 O ( n 2 ) {O(n^2)} O(n2);而平方阶后续的值,除非n是很小的值,否则哪怕是100都是噩梦般的运行时间,所以对应一些不切实际的算法时间复杂度,我们一般不做过多讨论。

三. 常见的排序算法

我们平时知道的最多的排序算法就是 冒泡排序, 插入排序, 这两种排序方法也是最简单最容易理解的排序算法.

3.1 冒泡排序(Bubble Sort)

3.2.1 算法描述

现在对序列 A 进行冒泡排序, 步骤如下:

1. 对比第 1 个元素(记为 A[1]) 和 第 2 个元素(记为 A[2]), 如果A[1] > A[2], 就交换这两个元素;

2. 再次对比 A[2] 和 A[3], 如果 A[2] > A[3], 就交换这两个元素;

3. 重复上述步骤, 直到对比 A[n-1] 和 A[n], 如果 A[n-1] > A[n], 就交换这两个元素, 此时 A[n] 一定是最大的那个元素;

4. 再次对 A[1...n-1] 循环执行第 1~3 的步骤, 直到 对比 A[n-2] 和 A[n-1], 如果 A[n-2] > A[n-1], 就交换这两个元素;

5. 重复执行步骤 4, 直到排序完成.

3.2.2 算法伪代码

// BubbleSort(A), 参数 A 表示排序的数组或者列表
for j = 1 to A.length - 1
    for i = 1 to A.length - j
        if A[i] > A[i+1]
			A[i], A[i + 1] = A[i + 1], A[i]

3.2.3 算法演绎

为了便于对上述算法更简单的理解, 我们用一个具体的序列 A = [ 8 , 4 , 5 , 3 , 1 ] {A = [8, 4, 5, 3, 1]} A=[8,4,5,3,1] 演绎一下冒泡排序的执行步骤.

第 1 次循环 —— j = 1 {j=1} j=1, i {i} i 循环 4 次

A = [8, 4, 5, 3, 1]

1. 首先比较 A[1]=8 与 A[2]=4, 此时 8 > 4, 将 A[1] 和 A[2] 交换位置, 此时 A = [4, 8, 5, 3, 1];

2. 然后比较 A[2]=8 与 A[3]=5, 此时 8 > 5, 将 A[2] 和 A[3] 交换位置, 此时 A = [4, 5, 8, 3, 1];

3. 然后比较 A[3]=8 与 A[4]=3, 此时 8 > 3, 将 A[3] 和 A[4] 交换位置, 此时 A = [4, 5, 3, 8, 1];

4. 然后比较 A[4]=8 与 A[5]=1, 此时 8 > 1, 将 A[4] 和 A[5] 交换位置, 此时 A = [4, 5, 3, 1, 8];

此时可以看到 A[5] = 8 就是最大值;

第 2 次循环 —— j = 2 {j=2} j=2, i {i} i 循环 3 次

此时 A = [4, 5, 3, 1, 8]

1. 首先比较 A[1]=4 与 A[2]=5, 此时 4 < 5, 进入下一轮循环;

2. 然后比较 A[2]=5 与 A[3]=3, 此时 5 > 3, 将 A[2] 和 A[3] 交换位置, 此时 A = [4, 3, 5, 1, 8];

3. 然后比较 A[3]=5 与 A[4]=1, 此时 5 > 1, 将 A[3] 和 A[4] 交换位置, 此时 A = [4, 3, 1, 5, 8];

第 3 次循环 —— j = 3 {j=3} j=3, i {i} i 循环 2 次

此时 A = [4, 3, 1, 5, 8]

1. 首先比较 A[1]=4 与 A[2]=3, 此时 4 > 3, 将 A[1] 和 A[2] 交换位置, 此时 A = [3, 4, 1, 5, 8];

2. 然后比较 A[2]=4 与 A[3]=1, 此时 4 > 1, 将 A[2] 和 A[3] 交换位置, 此时 A = [3, 1, 4, 5, 8];

第 4 次循环 —— j = 4 {j=4} j=4, i {i} i 循环 1 次

此时 A = [3, 1, 4, 5, 8];

1. 比较 A[1]=3 与 A[2]=1, 此时 3 > 1, 将 A[1] 和 A[2] 交换位置, 此时 A = [1, 3, 4, 5, 8];

排序完成

经历 4 个步骤(循环), 此时序列 A 就通过冒泡排序完成排序了.

3.2.4 算法实现

我们今天用 JavaScript 实现以下冒泡排序的算法

// 在 js 中, 数组的编号是从 0 开始计数的, A[0] 表示的是数组 A 的第一个数字
function BubbleSort(A) {
     
    for (var j = 0; j < A.length - 1; j++) {
     
        for (var i = 0; i < A.length - 1 - j; i++) {
     
            if (A[i] > A[i + 1]) {
     
                A[i], A[i + 1] = A[i + 1], A[i];
            }
        }
    }
    return A
}

// 算法测试, 通过调试可以清楚的看到上面的算法演绎的过程
A = [8, 4, 5, 3, 1]
console.log(BubbleSort(A));

再附一个Java的实现:

    public static void main(String[] args) {
     
        int[] arr = {
     8, 4, 5, 3, 1};
        for (int i = 1; i < arr.length; i++) {
     
            //设计一个标记,若为true,则表示此次循环没有进行交换,也就是待排序列已经有序,排序完成
            //当 flag为true即结束循环,节省资源,提升效率
            boolean flag = true;
            for (int j = 0; j < arr.length - i; j++) {
     
                if(arr[j]>arr[j+1]){
     
                    int tmp = arr[j];
                    arr[j]=arr[j+1];
                    arr[j+1]=tmp;
                    flag= false;
                }
            }
            if(flag){
     
                break;
            }
        }
        System.out.println(Arrays.toString(arr));
    }

3.1.5 算法分析 —— 时间复杂度

为了便于计算时间复杂度, 对于单行代码执行的时间复杂度记为 1.

伪代码如下:

// BubbleSort(A), 参数 A 表示排序的数组或者列表
for j = 1 to A.length - 1
    for i = 1 to A.length - j
        if A[i] > A[i+1]
						A[i], A[i + 1] = A[i + 1], A[i]

经过分析, 第 2 行代码执行的次数是 n 次; 在每次执行第一层 for 循环时, 第 3 行代码执行的次数是 n-j 次, 那循环 n 次共执行
n + ( n − 1 ) + . . . + 1 = n ∗ ( n + 1 ) / 2 n + (n-1) + ... + 1 = n*(n+1)/2 n+(n1)+...+1=n(n+1)/2
次, 第 4 行代码执行的次数取决 if 语句判断的结果, 最少为 0 次, 最多为 n-j 次, 所以此算法的执行次数一共是
最 小 值 T ( n ) m i n = n + 2 ∗ n ∗ ( n + 1 ) / 2 = n 2 + 2 n 最小值 T(n)_{min} = n + 2 * n*(n+1)/2 = n^2 + 2n T(n)min=n+2n(n+1)/2=n2+2n

最 大 值 T ( n ) m a x = n + 3 ∗ n ∗ ( n + 1 ) / 2 = 3 n 2 / 2 + 5 n / 2 最大值T(n)_{max} = n + 3 * n * (n + 1)/2 = 3n^2/2 + 5n/2 T(n)max=n+3n(n+1)/2=3n2/2+5n/2

综上, 冒泡排序的时间复杂度是平方阶 O ( n 2 ) {O(n^2)} O(n2)

3.2 插入排序(Insertion Sort)

3.2.1 算法描述

现在对序列 A 进行插入排序, 步骤如下:

1. 第 1 个元素默认已经排好序, 记为 A[1];

2. 取出第 2 个元素 key 开始, 与第 1 个元素比较, 如果 A[2] < A[1], 那么就将A[1]移动到A[2]的位置, 最后将A[2]插入到A[1]的位置, 此时 A[1, 2] 就是排好序的序列;

3. 取出第j个元素 key, 与已经排序好的元素序列A[1, j-1]中从后向前比较:
	(1) 如果第j个元素 key 大于最右边的元素 A[j-1], 此时循环结束;
	(2) 如果第j个元素 key 小于最右边的元素 A[j-1], 就将 A[j-1] 移动到 A[j] 的位置;
	
	(3) 然后再将第j个元素 key 与 A[j-2] 比较, 重复第 (1) 和 (2) 个步骤, 直到某个元素 i 小于或者等于第j个元素 key 时, 循环停止;
	(4) 此时将第j个元素 key 插入到第 i+1 的位置上, A[i+1] = key;

3.2.2 算法伪代码

// InsertSort(A), 参数 A 表示排序的数组或者列表
for j = 2 to A.length
    key = A[j];
	// 将 A[j] 插入到数组 A[1, j-1] 中
    i = j - 1;
    while i > 0 and A[i] > key
        A[i+1] = A[i];
        i = i - 1;
    A[i + 1] = key

3.2.3 算法演绎

为了便于对上述算法更简单的理解, 我们用一个具体的序列 A = [8, 4, 5, 3, 1] 演绎一下插入排序的执行步骤.

步骤1

第 1 个元素默认已经排好序;

步骤2 – 从第 2 个元素 A[2] = 4 开始

1. 与 A[1] = 8 比较, 此时 4 < 8, 将 A[1] 移动到 A[2] 的位置, 此时 A = [8, 8, 5, 3, 1];

2. 最后再将 A[2] 插入到 A[1] 的位置, 最后 A = [4, 8, 5, 3, 1], 可以看出 A[1,2] = [4, 8] 是排好序的序列.

步骤3 – 取出第三个元素 A[3] = 5

1. 首先与 A[2]=8 比较, 由于 5 < 8, 将第 2 个元素移动到第 3 个位置, 此时 A = [4, 8, 8, 3, 1];

2. 再次与 A[1]=4 比较, 由于 5 > 4, 故退出循环, 此时 A = [4, 8, 8, 3, 1];

3. 最后再将 A[3] 插入到 A[2] 的位置, 最后 A = [4, 5, 8, 3, 1], 可以看出 A[1-3] = [4, 5, 8] 是排好序的序列

步骤4 – 取出第四个元素 A[4] = 3

1. 首先与 A[3]=8 比较, 由于 3 < 8, 将第 3 个元素移动到第 4 个位置, 此时 A = [4, 5, 8, 8, 1];

2. 再次与 A[2]=5 比较, 由于 3 < 5, 将第 2 个元素移动到第 3 个位置, 此时 A = [4, 5, 5, 8, 1];

3. 再次与 A[1]=4 比较, 由于 3 < 4, 将第 1 个元素移动到第 2 个位置, 此时 A = [4, 4, 5, 8, 1];

3. 最后再将 A[4] 插入到 A[1] 的位置, 最后 A = [3, 4, 5, 8, 1], 可以看出 A[1-4] = [3, 4, 5, 8] 是排好序的序列

步骤5 – 取出第五个元素 A[5] = 1

1. 首先与 A[4]=8 比较, 由于 1 < 8, 将第 4 个元素移动到第 5 个位置, 此时 A = [3, 4, 5, 8, 8];

1. 接着再与 A[3]=5 比较, 由于 1 < 5, 将第 3 个元素移动到第 4 个位置, 此时 A = [3, 4, 5, 5, 8];

2. 再次与 A[2]=4 比较, 由于 1 < 4, 将第 2 个元素移动到第 3 个位置, 此时 A = [3, 4, 4, 5, 8];

3. 再次与 A[1]=3 比较, 由于 1 < 3, 将第 1 个元素移动到第 2 个位置, 此时 A = [3, 3, 4, 5, 8];

3. 最后再将 A[5] 插入到 A[1] 的位置, 最后 A = [1, 3, 4, 5, 8], 可以看出 A = [1, 3, 4, 5, 8] 是排好序的序列

经历 5 个步骤(循环), 此时序列 A 就通过插入排序完成排序了.

3.2.4 算法实现

我们今天用 JavaScript 实现以下插入排序的算法

// 在 js 中, 数组的编号是从 0 开始计数的, A[0] 表示的是数组 A 的第一个数字
function InsertSort(A) {
     
    for (var j = 1; j <= A.length - 1; j++) {
     
        var key = A[j];
        // 将 A[j] 插入到数组 A[1, j-1] 中
        var i = j - 1;
        while (i >= 0 && A[i] > key) {
     
            A[i + 1] = A[i];
            i--;
        }
        A[i + 1] = key;
    }

    return A;
}

// 算法测试, 通过调试可以清楚的看到上面的算法演绎的过程
A = [8, 4, 5, 3, 1]
console.log(InsertSort(A));

Java实现代码如下:

int[] arr = {
     8, 4, 5, 3, 1};
for (int i = 1; i < arr.length; i++) {
     
  //记录要插入的数据
  int tmp = arr[i];
  int j = i;
  while (j>0 && arr[j-1]>tmp){
     
    arr[j]=arr[j-1];
    j--;
  }
  //存在比其小的数,插入
  if(j!= i){
     
    arr[j]=tmp;
  }
}

3.2.5 算法分析 —— 时间复杂度

为了便于计算时间复杂度, 对于单行代码执行的时间复杂度记为 1.

伪代码如下:

// InsertSort(A), 参数 A 表示排序的数组或者列表                  执行次数
for j = 2 to A.length                                         n
    key = A[j];                                               n-1
	// 将 A[j] 插入到数组 A[1, j-1] 中                    
    i = j - 1;                                                n-1
    while i > 0 and A[i] > key                                
        A[i+1] = A[i];
        i = i - 1;
    A[i + 1] = key                                            n-1

1. 最好的情况—— A是已经排好序的数组

如果 A 是排好序的数组, 此时在每次执行 for 循环时 第 6 行代码 只需执行 1 次, 共执行 n - 1 次, 而 第 7-8 行代码不执行;

此时代码的执行总次数(时间复杂度)为
T ( n ) = n + ( n − 1 ) + ( n − 1 ) + ( n − 1 ) + ( n − 1 ) = 5 n − 4 T(n) = n + (n-1) + (n-1) + (n-1) + (n-1) = 5n - 4 T(n)=n+(n1)+(n1)+(n1)+(n1)=5n4
因此时间复杂度是线性阶 O ( n ) {O(n)} O(n)

2. 最坏的情况 —— A是反向排序的数组

如果 A 是反向排好序的数组, 此时在每次执行 for 循环时 第 6 行代码 需执行 j {j} j 次, 共执行
2 + 3 + . . . + n = n ∗ ( n + 1 ) / 2 − 1 2 + 3 + ... + n = n*(n+1)/2 - 1 2+3+...+n=n(n+1)/21
次; 而 第 7-8 行代码也需要执行 j − 1 {j - 1} j1 次, 共执行
1 + 2 + . . . + ( n − 1 ) = n ∗ ( n − 1 ) / 2 1 + 2 + ... + (n-1) = n*(n-1)/2 1+2+...+(n1)=n(n1)/2
次;

此时代码的执行总次数(时间复杂度)为
T ( n ) = n + ( n − 1 ) + ( n − 1 ) + n ( n + 1 ) / 2 − 1 + n ∗ ( n − 1 ) + ( n − 1 ) = 3 n 2 / 2 + 7 n / 2 − 4 T(n) = n + (n-1) + (n-1) + n(n+1)/2 - 1 + n*(n-1) + (n-1) = 3n^2/2 + 7n/2 - 4 T(n)=n+(n1)+(n1)+n(n+1)/21+n(n1)+(n1)=3n2/2+7n/24
因此时间复杂度是平方阶 O ( n 2 ) {O(n^2)} O(n2)

3. 平均情况

假定随机选择 n 个数并应用插入排序, 那需要多长时间确定 A [ j ] {A[j]} A[j] 应该插在子数组 A [ 1... j − 1 ] {A[1...j-1]} A[1...j1] 中呢? 平均来说, A [ 1... j − 1 ] {A[1...j-1]} A[1...j1] 中有一半的元素大于 A [ j ] {A[j]} A[j], 还有一半的元素小于 A [ j ] {A[j]} A[j], 那此时在每次执行 for 循环时 第 6 行代码 需执行 j / 2 {j / 2} j/2 次, 第 7-8 行代码也同样需要执行 j / 2 − 1 {j/2 - 1} j/21 次, 此时就导致平均情况运行时间结果向最坏情况一样, 时间复杂度是平方阶 O ( n 2 ) {O(n^2)} O(n2)

综上所述, 是对两种排序算法的详细讲解, 这两种算法理解起来还是比较简单的, 算法的核心思想和我们平时的排序方法也很类似, 但是可以看到其时间复杂度都是平方阶 O ( n 2 ) {O(n^2)} O(n2), 那在本系列的下一篇文章中我们会继续介绍其它更优的排序算法——快速排序算法以及对冒泡排序的优化算法.

你可能感兴趣的:(算法,排序算法,算法,数据结构)