重新思考二分法/二分插入排序
起因是leetcode315. 计算右侧小于当前元素的个数
给定一个整数数组nums
,按要求返回一个新数组counts
数组counts
有该性质:counts[i]
的值是nums[i]
右侧小于nums[i]
的元素的数量
示例:
输入:[5,2,6,1]
输出:[2,1,1,0]
解释:
5 的右侧有 2 个更小的元素 (2 和 1)
2 的右侧仅有 1 个更小的元素 (1)
6 的右侧有 1 个更小的元素 (1)
1 的右侧有 0 个更小的元素
该题最简单的解法是Brute force,两层循环判断第二层循环的元素是否小于第一层循环的元素,简单明了,但作为一道Hard题目,显然这种方法会超时
一般解法中,有从后往前使用插入排序的,也就是说,假定后半部为有序,从倒数第二位起,将元素在有序子集中找到合适的位置进行插入,并记录移动了多少位,得出结果数组
插入排序时间复杂度也是$O(n^2)$,同样会超时,需要进行优化,而直接插入排序的一般优化方法就是使用二分法来找到合适的插入位置,可以将查找的时间复杂度降为$O(log_{2}n)$
然而在写二分插入排序的过程中,对上下边界处理屡屡出错,不得正解,于是重新实验,归纳总结,形成模板,死记硬背
查找
对于一个从小到大的有序数组nums
,比较目标值target
和集合中间元素nums[mid]
的大小
- 如果
nums[mid] < target
,说明target
应该在后半部分
- 否则说明
target
应该在前半部分
对于数组或其前半/后半子集的起点和终点,假设用left
和right
表示,初始则有
数组下标范围是[0, nums.length - 1]
闭区间
但也可以使用[0, nums.length)
左闭右开区间
使用不同的区间,后续流程的写法也不同
- 循环终止条件
随着子集划分越来越小,left
和right
也在不断靠近,直到重合,循环的条件应该写成while(left < right)
还是while(left <= right)
?
- 子集的取值范围
缩小查找范围后,去左边区间时,left
不变,right = mid - 1
还是right = mid
?
去右边区间时,right
不变,left = mid + 1
还是right = mid
?
先从取值范围来看,如果一开始的取值使用了闭区间,子集的取值也应该是闭区间
如果一开始使用了左闭右开区间,子集的取值也应该是左闭右开区间
target
应该存在于[left, right)
区间中,设target
为集合的最后一个元素,区间即为[nums.length - 1, nums.length)
,left
再增加会越界,循环应写成while(left < right)
同理在闭区间中,[nums.length - 1, nums.length - 1]
是可以存在的,循环应写成while(left <= right)
交换
对于排序来说,找到合适的位置后,就要将元素插入到该位置上,而合适的位置是查找阶段得出的left
或right
,它们是相等的,可以随便使用一个
通常对于一个集合插入排序来说,假定前半部分是有序的,即假设第一位是有序的,从第二位开始逐渐扩大有序集合,将当前元素与前面的元素做交换,直到插入到有序的前半部分中合适的位置
那么就是从当前位置到left
,从后往前做交换了
使用golang将正序、逆序、从后往前、从前往后、假定前部有序、假定后部有序的的二分插入排序都实现了一遍,都使用了闭区间
需要注意的是,反向遍历得出的位置left,使用的时候需要减一