0 直接插入排序

背景

直接插入排序(Straight Insertion Sort)是一种最简单的排序方法,其基本操作是将一条记录插入到已排好的有序表中,从而得到一个新的、记录数量增1的有序表。

算法思想

在正式讲解算法思想之前,我先给大家讲解一个生活中的例子。不清楚大家是否玩过扑克牌(没玩过也没关系),在打扑克牌的时候我们需要经历洗牌、发牌、整理牌以及出牌四个阶段。而在整理牌这个阶段,大家一般都会按照一定的规则把扑克牌整理好方便自己查看,而最常见的就是按照大小顺序把扑克牌整理好。其实在按大小顺序插牌的过程中我们已经默认使用了直接插入排序这种思想:逐步把一张新抽到的牌插入到已经有序的牌中。
在这里插入图片描述

下面了解一下题设:假设我们存在一组n个数序列 ⟨ a 1 , a 2 , ⋯   , a n ⟩ \langle a_{1},a_{2},\cdots,a_{n}\rangle a1,a2,,an希望输出整个序列的排序 ⟨ a 1 ′ , a 2 ′ , ⋯   , a n ′ ⟩ \langle a_{1}^{'},a_{2}^{'},\cdots,a_{n}^{'}\rangle a1,a2,,an,其中 a 1 ′ ≤ a 2 ′ ≤ ⋯ ≤ a n ′ a_{1}^{'} \le a_{2}^{'} \le \cdots \le a_{n}^{'} a1a2an。如果你会打扑克牌并且看懂了上述文字的话,那么你一定会脱口而出:“使用直接插入排序”。
了解了基本的假设和背景之后,下面我们用伪代码的方式来表述一下直接插入排序的整个过程。在正式表述之前,我们先了解一下伪代码的书写规则。

  • 缩进表示块结构
  • while,forrepeat-until等循环结构以及if-else与目前流行的各大语言中的那些结构具有类似的解释
  • 符号//表示注释
  • 变量若无显式说明都是局部的
  • 数组元素通过数组名[下标]这样的形式来访问
  • 通过对象.属性的形式来获取对象的特性
for j = 2: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

可能伪代码看起来很抽象,但是基本上所有算法基本上都是由伪代码描述的,希望大家适应。下面贴一个例子来帮助大家理解上述过程。首先我们把第一个元素5当作元素个数为1的有序序列,然后依次把第二个、第三个…直到第n个插入到这个有序序列中。
在这里插入图片描述

代码

废话少说,show me code。下面我们直接开整!

#include <stdio.h>
#include <stdlib.h>

/** InsertSort(int arr[], int length) -> None
 * 插入排序
 * @params
 * @param arr[]    需要进行排序的数组
 * @param length   需要排序的数组长度
 * @return         none
 */
void InsertSort(int arr[], int length) {
    if (length < 1)    printf("please enter the correct size!\n");
    for (int j = 1; j < length; j++) {
        int tmp = arr[j];
        int i = j - 1;
        while (i >= 0 && arr[i] > tmp) {
            arr[i+1] = arr[i];
            i -= 1;
        }
        arr[i+1] = tmp;
    }
} 

int main() {
    int arr[] = {5, 2, 4, 6, 1, 3};
    int len = sizeof(arr) / sizeof(arr[0]);
    printf("Original array: ");
    for (int i = 0; i < len; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
    InsertSort(arr, len);
    printf("After sort the array: ");
    for (int i = 0; i < len; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
    return 0;
}

算法分析

时间复杂度

对于一个算法,我们首先要做的就是分析其效率,通常情况下我们用算法执行每条语句的运行时间之和来表示。对于上述的直接插入排序,我们假设每条语句执行的代价为 c i c_i ci,并且已知了每条语句的执行次数,那么整个算法执行的代价就为所有单个语句代价的加权求和。在下面的例子中 t j t_j tj表示执行一次循环需要进行交换的次数。

InsertSort                                    代价			次数
for j = 2 to A.length                         c1             n
  key = A[j]                                  c2            n-1
  // 把A[j]插入到A[1..j-1]之中                 0             n-1
  i = j - 1                                   c4            n-1
  while i > 0 and A[i] > key                  c5            \sum(t_j) j=2 to n
    A[i+1] = A[i]                             c6            \sum(t_j-1) j=2 to n
    i = i - 1                                 c7            \sum(t_j-1) j=2 to n
  A[i+1] = key								  c8            n-1

我们可以发现,当输入数组已按照从小到大的顺序排好时,整个算法语句执行次数是最少的,因为在这种情况下 ∑ j = 2 n ( t j − 1 ) = 0 \sum_{j=2}^{n} (t_j-1)=0 j=2n(tj1)=0,该最佳情况的运行时间为:
T n = c 1 n + c 2 ( n − 1 ) + c 4 ( n − 1 ) + c 5 ( n − 1 ) + c 8 ( n − 1 ) = ( c 1 + c 2 + c 4 + c 5 + c 8 ) n − ( c 2 + c 4 + c 5 + c 8 ) T_{n} = c_1 n + c_2 (n-1) + c_4 (n-1) + c_5 (n-1) + c_8 (n-1) \\ =(c_1+c_2+c_4+c_5+c_8)n-(c_2+c_4+c_5+c_8) Tn=c1n+c2(n1)+c4(n1)+c5(n1)+c8(n1)=(c1+c2+c4+c5+c8)n(c2+c4+c5+c8)
我们可以把该运行时间表示成 a n + b an+b an+b,其中 a a a b b b依赖于代价于 c i c_i ci,可以说它是n的线性函数。
但是并不是每一个数组都是已按照规则排好序的,其顺序可以是任意的。因此判断一个算法性能优劣的时候,我们通常探讨其最坏情况下的执行时间。对于上述算法,当传入的数组为逆序时, ∑ j = 2 n t j = n ( n + 1 ) 2 − 1 \sum_{j=2}^{n} t_j=\frac{n(n+1)}{2}-1 j=2ntj=2n(n+1)1,那么整个算法的运行时间就为:
T n = c 1 n + c 2 ( n − 1 ) + c 4 ( n − 1 ) + c 5 ( n ( n + 1 ) 2 − 1 ) + c 6 ( n ( n + 1 ) 2 ) + c 7 ( n ( n + 1 ) 2 ) + c 8 ( n − 1 ) = ( c 5 2 + c 6 2 + c 7 2 ) n 2 + ( c 1 + c 2 + c 4 + c 5 2 − c 6 2 − c 7 2 + c 8 ) n − ( c 2 + c 4 + c 5 + c 8 ) T_n=c_1n+c_2(n-1)+c_4(n-1)+c_5(\frac{n(n+1)}{2}-1)+c_6(\frac{n(n+1)}{2})+c_7(\frac{n(n+1)}{2})+c_8(n-1) \\ =(\frac{c_5}{2}+\frac{c_6}{2}+\frac{c_7}{2})n^2+(c_1+c_2+c_4+\frac{c_5}{2}-\frac{c_6}{2}-\frac{c_7}{2}+c_8)n-(c_2+c_4+c_5+c_8) Tn=c1n+c2(n1)+c4(n1)+c5(2n(n+1)1)+c6(2n(n+1))+c7(2n(n+1))+c8(n1)=(2c5+2c6+2c7)n2+(c1+c2+c4+2c52c62c7+c8)n(c2+c4+c5+c8)
我们可以把该运行时间表示成 a n 2 + b n + c an^2+bn+c an2+bn+c,其中 a , b a, b a,b c c c依赖于代价于 c i c_i ci,可以说它是n的二次函数。
在实际使用中,我们更在意的其实是运行时间的增长率增长量级,所以我们只用考虑公式中最重要的项(即次数最高的项),因为在n很大的时候,低阶项可以被忽略。那么,对于插入排序,其最差情况下的时间复杂度为 O ( n 2 ) O(n^2) O(n2)
综上,直接插入排序算法的时间复杂度在最好的条件下为 O ( n ) O(n) O(n),在最差的条件下为 O ( n 2 ) O(n^2) O(n2)

空间复杂度

由于我们只使用了一个临时变量 k e y key key来保存需要交换的值,所以直接插入排序的空间复杂度为1。

算法稳定性

可以看到在判断的时候我们并未考虑元素相等的情况,因此也就不会交换数值相同元素的位置,所以直接插入排序是稳定的算法

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值