在了解插入排序之前,先简单认识一下什么是二分搜索。
一、二分搜索
如何确定一个元素在数组中的位置?(假设数组里面全都是整数)
如果是无序数组,从第0个位置开始遍历搜索,平均时间复杂度是O(n)
。
如果是有序数组,可以使用二分搜索(Binary Search),最坏时间复杂度是O(logn)
。
1.1. 思路
假设在[begin, end)
范围内搜索某个元素v
,mid(中间索引) == (begin + end) / 2
,m
是最中间的元素。
- 如果
v < m
,去[begin, mid)
范围内二分搜索 - 如果
v > m
,去[mid + 1, end)
范围内二分搜索 - 如果
v == m
,直接返回mid
1.2. 实例
示例一(搜索10):
- 找到中间位置
(0 + 7) / 2 = 3
,索引3对应的元素是8,由于8 < 10
,所以在索引3的右边继续二分搜索; - 中间位置是
(4 + 7) / 2 = 5
,索引5对应的元素是12,由于12 > 10
,所以在索引5的左边继续二分搜索; - 中间位置是
(4 + 5) / 2 = 4
,索引4对应的元素是10,由于10 == 10
,最终找到索引4的位置就是要搜索的结果。
示例二(搜索3):
搜索方法同上面的示例一,只是到最后一步时发现中间位置(0 + 1) / 2 = 0
,索引0对应的元素是2,由于2 < 3
,所以需要到索引0的右边继续二分搜索。但是索引0右边没有元素了,最终没有找到要搜索的元素(begin是上一次搜索的mid,因为是向右查找,end保持不变,所以begin == end
。如果是往左查找,begin保持不变,end等于mid,也是begin == end
。所以当begin == end
条件成立时,就无法搜索到对应结果,可以停止搜索。)。
1.3. 代码实现
1 | // 查找某个元素在数组中是否存在 |
思考:如果存在多个重复的值,返回的是哪一个?这是不确定的。
二、插入排序
插入排序(Insertion Sort)非常类似于扑克牌的排序。每次从牌池中拿到牌插入到手中,使其变成有序的牌(手上的牌是有序的)。
2.1. 实现思路
- 在执行过程中,插入排序会将序列分为2部分:头部是已经排好序的,尾部是待排序的;
- 从头开始扫描每一个元素,每当扫描到一个元素,就将它插入到头部合适的位置,使得头部数据依然保持有序。
2.2. 代码实现
1 | public void insertionSort(Integer[] array) { |
上面的代码还可以从插入时机进一步优化,优化前我们先了解一下什么是逆序对。
2.3. 逆序对
什么是逆序对(Inversion)?例如数组<2,3,8,6,1>
的逆序对为<2,1> <3,1> <8,1> <8,6> <6,1>
共5个逆序对(一般情况下数组从左到右看,从小到大是正序,而逆序就是从大到小)。
插入排序的时间复杂度与逆序对的数量成正比关系。逆序对的数量越多,插入排序的时间复杂度越高。
如下图,逆序对达到最大值,每次拿到一个元素进行插入排序,都是插入到数组的最前面(即最远距离)。绿色区域就是排序所消耗的时间(时间复杂度 = 三角形面积 = n^2
)。
最坏、平均时间复杂度:O(n^2)
(原数据的逆序对达到最大值),最好时间复杂度:O(n)
(原数据是升序的,只需要和前面的值进行比较),空间复杂度:O(1)
,属于稳定排序。
当逆序对的数量极少时,插入排序的效率特别高。甚至速度比O(nlogn)
级别的快速排序还要快。数据量不是特别大的时候,插入排序的效率也是非常好的。
2.4. 优化一
优化思路:将【交换】转为【挪动】
- 先将待插入的元素备份;
- 头部有序数据中比待插入元素大的,都朝尾部方向挪动1个位置;
- 将待插入元素放到最终的合适位置。
如下图,红色块是待插入元素,从红色块位置开始向左进行比较,只要发现比红色块值大的元素就让这个元素往右边进行挪动(同时记录当前挪动的位置),直到发现小于或等于红色块值的元素时,把元素插入到上次记录的位置。
1 | public void insertionSort(Integer[] array) { |
上面的优化在逆序对比较多的情况下效率越明显。但是需要和已排序好的每个元素进行比较,因为是和有序数据进行比较,所以使用二分搜索就能使比较效率提高不少。
2.5. 优化二
在元素v
的插入过程中,可以先二分搜索出合适的插入位置,然后再将元素v
插入。
如上图,要求二分搜索返回插入位置的特点:第1个大于v
的元素位置(保证了相等元素的数据稳定性)。
- 如果
v
是5,返回2 - 如果
v
是1,返回0 - 如果
v
是15,返回7 - 如果
v
是8,返回5 - ……
优化思路:
假设在[begin, end)
范围内搜索某个元素v
,mid(中间索引) == (begin + end) / 2
,m
是最中间的元素。
- 如果
v < m
,去[begin, mid)
范围内二分搜索 - 如果
v ≥ m
,去[mid + 1, end)
范围内二分搜索
1 | public class InsertionSort { |
注意:需要注意的是,使用了二分搜索后,只是减少了比较次数,但插入排序的平均时间复杂度依然是
O(n^2)
。