【声明】
非完全原创,部分内容来自于学习其他人的理论和B站视频。如果有侵权,请联系我,可以立即删除掉。
一、插入排序
1、方法和复杂度
1.1、核心思想
基于某序列已经有序排列的情况下,通过一次插入一个元素的方式按照原有排序方式增加元素。每次将1个待排序的记录按其关键字大小插入到前面已经排好序的子序列中,寻找最适当的位置,直至全部记录插入完毕。
1.2、主要方法
从该有序序列的最末端开始执行,即要插入序列中的元素最先和有序序列中最大的元素比较,若其大于该最大元素,则可直接插入最大元素的后面即可,否则再向前一位比较查找直至找到应该插入的位置为止。
1.3、稳定性
当待插入元素和插入元素相等的位置,则将待插入的元素放在该相等元素的后面,从而保证了排序的稳定性
1.4、时间复杂度
最优情况:当待排序数组是有序时,只需当前数跟前一个数比较一下就可以了,数组无需移动,这时一共需要比较N-1
次,时间复杂度为 O ( N ) O(N) O(N)
最坏情况:当待排序数组是逆序时,N-1
次循环需要移动:1+2+3+..+(N-1) = N*(N-1)/2
,时间复杂度为 O ( N 2 ) O(N^2) O(N2)
综合来看,时间复杂度为 O ( N 2 ) O(N^2) O(N2)
1.5、空间复杂度
在整个算法中,只用到了三个辅助变量:i
控制外层循环、j
控制内层循环、tmp
记录待插入的元素。辅助的空间与问题规模无关,因此空间复杂度是 O ( 1 ) O(1) O(1)
2、排序的过程
- 第一趟排序。数组的有序区间(只有第一个元素)下标为
[0, 0]
,此时需要将第二个元素插入到有序区间中,即,将第二个元素和前面的元素进行比较,小于则插入到有序区间的前面(即第一个元素后移一位,第二个元素替换掉第一个元素);大于或等于,则不进行操作。此时有序区间为[0, 1]
- 第二趟排序。有序区间下标
[0, 1]
,此时待插入元素为:第三个元素。即,待插入元素按照从后往前的顺序,与有序区间的元素进行比较,小于有序区间的元素则继续往前查找。在有序区间中,找到大于或等于待插入元素的索引k
,此时区间[k, 1]
整体往后移动1位,然后索引k
处的元素用待插入元素替换。 - 依此类推
3、改进思路:有序区间查找用二分查找法
由于每一趟排序,都需要在有序区间中查找待插入元素的位置,因此可以使用二分查找法来提高查找的效率。
该思路只能提高查找效率,并不能减少比较的趟数,也不能减少元素右移的次数,因此时间复杂度不会改变。由于二分查找只需要多引入三个变量:low
记录查找区间最小元素索引,high
记录查找区间最大元素索引,mid
记录查找区间中间元素的索引,因此空间复杂度也不会改变。
4、代码实现
package main
import "fmt"
const size = 10
func Insert(arr *[size]int) {
var i, j int //用于两层循环
cnt := 0 //记录循环/元素比较的次数
swap_cnt := 0 //记录发生交换的次数
tmp := 0 //临时变量,记录待插入的元素
fmt.Println("原数组序列: ", *arr)
for i = 1; i < len(arr); i++ {
//arr数组[0, i-1]是有序,寻找arr[i]的位置
tmp = arr[i]
for j = i; j > 0 && arr[j-1] > tmp; j-- {
arr[j] = arr[j-1] //整体往后移动
swap_cnt++
}
arr[j] = tmp
cnt += i - j
fmt.Printf("第[%02d]趟: %v\n", i, *arr)
}
fmt.Printf("【Insert】排序的趟数: %d, 排