题目
最近少年班的C语言课上老师出了一道题目,题目要求先排序,再给一个元素进行二分查找,最后把该元素key插入到数组中仍然有序。
在上机课前思考了一下,假如说元素是往后挪的,如何找出最合适的位置,使得需要挪动的元素个数最小。
- 对于key元素没有重复的一组数,二分查找出的位置(right指针)就是待插的位置。
- 但是key这个数有重复的一组数,二分查找出的位置不一定能保证挪动次数最少。应该要找到第一个大于key的数的位置,在这个位置插入key,才能保证挪动次数最少。
分析
这个问题,其实是变形二分法的一种。最开始想法是仍就用基本的二分法,最后在加一for循环,一个个元素比对过去找出最合适的值。可是,这样子又把查找插入位置的时间复杂度提高到了O(n),能否通过修改二分查找法,直接找出那个位置的。
答案是可以的。开始之前,先看下基本的二分查找法:
1 | //基本的二分法 |
这个代码很简单,要注意while循环里的=不要忘。
首先来看下查完之后,left、right的位置:
- 查到的话要么l与r在mid等距的两边; 要么left=mid,r=l或r=left+1;
- 查不到的话right<left,刚好夹在key两侧
变形的二分查找种类很多,诸如找出第一个与key相等的元素、找出最后一个与key相等的元素……
改的话要:- 删去
if (arr[mid] == key) return mid;
因为mid不一定是合适的位置 - 修改
if (arr[mid] > key)
的大于符号 因为要对right、 left的位置和基本版不同 - 循环结束后要返回查到的位置或返回无结果;
- 删去
如何改这些,主要是要先分析两步:根据问题要求确定left和right的最终位置(要返回left还是right)、结合题目要求修改比较符号。
以“查找第一个与key相等的元素位置”为例,假如一组数1,2,3,3,4,5,待查的数是3。
第一步: 因为删去了if (arr[mid] == key) return mid;
,所以while循环结束时,肯定是left > right
,而且这两个数肯定要是在边界上,如下图所示。这样我们返回left就是answer了。
第二步: 那怎么修改比较符号呢,原来if (arr[mid] > key) right = mid - 1;
每一轮循环里,只有a[mid]是比key大,right才会变化。现在呢,不仅a[mid]比key大时right要改,当a[mid]等于key时,right也得改,这样它才能挪到元素2那里。因此改为if (arr[mid] >= key) right = mid - 1;
最后针对iii: 因为返回的是left,所以对left做一些限定if (left < n && arr[left] == key) return left;
代码实现
代码把基本的二分查找,以及六种变形情况实现了一下,最重要的地方都有注释
1 | public class ExtendeBinarySearch { |
测试结果
1 | int arr[] = new int[] {1, |