插入排序

释放双眼,带上耳机,听听看~!

《目录》

  • 插入排序
  • 循环不变式
  • 优化插入排序

算法导论的第二章,以一个简单的算法(插入排序)入手,讲解算法分析里一些关键的技术。

算法是计算方法的简称,而计算方法是为了解决问题而存在的。

那么插入排序这样的算法是因为是什么问题而存在的呢 ?

:排序问题。

 

插入排序

请用数学语言结合算法的要求来描述排序问题,以保证问题的清晰、简洁、直接、齐整。

输入: 个数的一个序列 ;

输出:序列的排列 ,满足 。

我们称排序的数为 key,算法的核心就是为一系列的 key 排序,对这些 key 排序需要用的某种算法。

算法导论举了一个很有意思的例子,拿扑克牌的抓牌形式来类比某种算法。

插入排序

左手拿的牌,是已经排好顺序的。右手就从牌堆里头取一张牌,取出来后再放在左手中合适的位置。

这种插入算法牌多了,效率就低了,右边的图就表示还有比插入排序更好的方法,不过因为插入排序好理解,我们以插入排序为例。

其次,插入排序运用在小规模问题上,效率还是挺好的。

 

插入排序的算法步骤:

  • 左手为空且桌子上的牌面向下
  • 每次从桌子上拿走一张牌并插在合适的位置上
  • 为了找到某张牌的正确位置,从右到左将 ta 与已在手中的每张牌进行比较,保证左手拿着的牌都是排序好的

 

我们已经确定了这种算法,而且已经知道了算法原理(扑克牌排序)。

首先,确定算法的名字以及此算法的函数声明原型。

  • 名字:Insertion_sort
  • 函数原型:需不需要输入参数(应该传什么参数给函数)、需不需要输出参数(把什么数据作为结果返回)。

分析:

  • 排序算法需要有  个数的一个序列,传一个数组 做为输入参数即可;

  • 输出的是一个序列,就地排序,不需要申请新的空间,因此,没有输出参数,空间复制度为常数 。


1
2
3
4
5
1// 定义通用类型
2#define T int  
3
4void Insertion_sort(T arr[], int len){ ... }
5

 

接下来,就是算法的边界判断了,为了程序的安全,要对输入参数进行严格的审查。

    ……

 

描述插入排序,我们最好先举一例子看看:。

插入排序

以  图为例,有 灰格、黑格、白格:

  • 灰格:左手上已排序好的牌
  • 黑格:右手抓的排,需要找到一个合适的位置
  • 白格:桌上的牌,目前没什么作用

我来模拟过程:

插入排序

 

总结:算法比较简单,每次处理一个新的元素,从后往前(数组)去看,挨个比较找到合适的地方,而后移动即可。

细节:挨个比较需要俩个数组下标或俩个指针,另外,循环开始的时候可以从第 2 个元素开始(因为第 1 个元素位置确定,不需要排序)。

 

算法实现:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
1// 下标从 1 开始,即第 2 个元素
2for(int i=1; i<ARRAY_SIZE(arr); i++)  
3{
4    T key = arr[i];         // arr[i] 这个元素,先抓起来
5    
6    int j=i-1;                // 从后往前看,下标 = i-1,注意:i最小时下标为1。
7
8    while( key < arr[j] ){    // 抓的牌比左手上的牌小时
9        arr[j+1] = arr[j];    // 找到合适的位置(往后挪动一位)
10
11    /*
12     算法结束条件:
13        1. 牌抓完了           即 j = 0
14        2. 抓的牌大于左手上的牌 即 key > arr[j]
15    */  
16        if( (j--) == 0 )      // 从后往前看,即 j--
17            break;            // 这一步可以优化,使用短路表达式优化:  while( (j--) > 0 && key < arr[j] )
18    
19    }
20    arr[j+1] = key;             // 位置确定,插入在后面
21}
22

完整版:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
1#include<stdio.h>
2
3// 定义通用类型
4#define T int  
5
6void Insertion_sort( T arr[], int len){
7for(int i=1; i<len; i++)  
8{
9    T key = arr[i];        
10    
11    int j=i-1;                // 从后往前看,下标 = i-1,注意:i最小时下标为1。
12
13    while( key < arr[j] ){    // 抓的牌比左手上的牌小时
14        arr[j+1] = arr[j];       // 寻找合适的位置(往后挪动一位)
15
16    /*
17     算法结束条件:
18        1. 牌抓完了           即 j = 0
19        2. 抓的牌大于左手上的牌 即 key > arr[j]
20    */  
21        if( (j--) == 0 )      // 从后往前看,即 j--
22            break;
23    
24    }
25    arr[j+1] = key;             // 位置确定,插入到后面
26}
27}
28
29int main( )
30{
31  T a[] = {5, 2, 4, 6, 1, 3};
32  int len = sizeof(a)/sizeof(a[0]);
33  Insertion_sort(a, len);
34  for( int i=0; i<len; i++ )
35      printf("%d  ", a[i]);
36}
37

 

插入排序的运行时间在渐进意义上是较菜的,我们主要的目的是通过插入排序,讲解算法分析里一些关键的技术,比如:循环不变式。

 

循环不变式

循环不变式可以理解为循环中不可不遵循的一个规则。

举个例子,只能一对一的线性表就是个数据结构不变式(为了保证性能,结构上的特性必须满足)

在插入排序中,从 插入排序 的元素已排序,这就是一个循环不变式。

没有排序的部分,从  的元素,是一个向未处理的一个序列。

整个插入排序就是把没有排序的部分排序,这个过程和数学归纳法类似。

  • 有一个基础情形,排序刚开始只有一个元素,没有顺序;
  • 假设推进,从 插入排序 的元素已排序,此时再加一个新的元素进去,仍然排完序;
  • 所以,到最后一个元素也是排好顺序的。

处理循环不变式的过程:

  • 初始化:循环的第一次迭代之前,ta 为真: 有序。
  • 维护不变式:如果循环的某次迭代之前 ta 为真,那下次迭代之前 ta 仍为真: 插入排序 有序, 也有序。
  • 终结: 有序,排序完成。

具体的证明,建议参考《算法导论》。

 

优化插入排序

上面的插入排序,是从后往前查找,这种查找就是顺序查找。

如果要做优化,我们完全可以用二分查找代替顺序查找。

思想是这样,其实也不用我们自己写:C++ 的模版库里有 lower_bound() 可用,TA 的时间复杂度为 。

我封装在下面了:数组、查找方法都用模版库的,很酷很酷的。


1
2
3
4
5
6
7
8
9
10
11
12
13
1template <typename T>
2void insertion_sort(std::vector<T>& V)
3{
4  for (size_t i = 1; i < V.size(); ++i)
5  {
6    auto key = V[i];
7    auto position = std::lower_bound(V.begin(), V.begin() + i, key);
8    for (auto iter = V.begin() + i; iter > position; --iter)
9      *iter = *(iter - 1);
10    *position = key;
11  }
12}
13

C++ 版测试性能 90万个数,可以开到上亿试一试,单位是 min。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
1#include <iostream>
2#include <vector>
3#include <algorithm>
4#include <random>
5#include <ctime>
6
7template <typename T>
8void insertion_sort(std::vector<T>& V)
9{
10  for (size_t i = 1; i < V.size(); ++i)
11  {
12    auto key = V[i];
13    auto position = std::lower_bound(V.begin(), V.begin() + i, key);
14    for (auto iter = V.begin() + i; iter > position; --iter)
15      *iter = *(iter - 1);
16    *position = key;
17  }
18}
19
20int main()
21{
22  double start_t, end_t;  // 计时器.
23
24  // 内存分配计时开始.
25  start_t = clock();
26  std::vector<double> V(900000);  // 90万个数, 需要6.9MB内存.
27  // 内存分配计时结束并输出时间.
28  end_t = clock();
29  std::cout << (end_t - start_t) / (CLOCKS_PER_SEC * 60)
30            << " minutes" << std::endl;
31
32  // 数据赋值计时开始.
33  start_t = clock();
34  // 利用随机数生成器生成0.0到1.0之间的实数.
35  std::default_random_engine generator(time(NULL));
36  std::uniform_real_distribution<double> distribution(0.0, 1.0);
37  for (size_t i = 0; i < V.size(); ++i)
38    V[i] = distribution(generator);
39  // 数据赋值计时结束并输出时间.
40  end_t = clock();
41  std::cout << (end_t - start_t) / (CLOCKS_PER_SEC * 60)
42            << " minutes" << std::endl;
43
44  // 排序计时开始.
45  start_t = clock();
46  // 插入排序相当慢.
47  insertion_sort(V);
48  // 排序计时结束并输出时间.
49  end_t = clock();
50  std::cout << (end_t - start_t) / (CLOCKS_PER_SEC * 60)
51            << " minutes" << std::endl;
52
53  return 0;
54}
55

 

 

给TA打赏
共{{data.count}}人
人已打赏
安全运维

MySQL,Redis,MongoDB 三种数据库优势

2021-12-11 11:36:11

安全运维

Ubuntu上NFS的安装配置

2021-12-19 17:36:11

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索