本文基于清华大学邓俊辉老师的数据结构算法课程讲解的二分查找书写。
递归实现:
public static int binary_search_recursive(int[] nums, int target, int left, int right) {
if (nums == null || nums.length == 0) {//必须有此
return -1;
}
int mid = left + ((right - left) >>2);
if (target == nums[mid]) {
return mid;
}
if (left >= right) {
return -1;
}
if (target < nums[mid]) {
return binary_search_recursive(nums, target, left, mid - 1);
} else if (nums[mid] < target) {
// 直接返回
return binary_search_recursive(nums, target, mid + 1, right);
}
return -1;
}
非递归实现:
在有序区间[low,high)内查找元素e
「二分」的名字,就是把「待搜索区间」分为「有目标元素的区间」和「不包含目标元素的区间」,排除掉「不包含目标元素的区间」的区间,剩下就是「有目标元素的区间」。
版本一:给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回-1。
public static int binSearch(int[]A, int e, int low, int high) {
while (low < high) {
int mid = left + ((right - left) >>2);//以中点为轴点,经比较后确定深入
if (e< A[mid]) {//深入前半段[low,mid)进行查找
high = mid;
} else if (A[mid] < e) {//深入后半段(mid, high)
low = mid + 1;
} else {//命中
return mid;
}
}
return -1;//查找失败
}
版本二:
二分查找中左右分支转向代价不平衡的解决:每次迭代(或每个递归实例)仅做1次关键码比较,如此,所有分支只有两个方向(一次比较),而不再是三个(两次比较)
同样地,轴点mid取做中点,则查找每深入一层,问题规模也缩减一半
1)e<x:则e若存在必属于左侧子区间S[low,mid),故可递归深入
2)x<=e:则e若存在必属于右侧子区间S[mid,high),亦可递归深入
只有当元素数目high-low=1时,才判断该元素是否命中
public static int binSearch2(int[]A, int e, int low, int high) {
while (1 < high - low) {//有效查找区间的宽度缩短至1时,算法才算终止
int mid = left + ((right - left) >>2);//以中点为轴点,经比较后确定深入
if (e < A[mid]) {//[low,mid)
high = mid;
} else {//[mid, high)
low = mid;
}
}//出口时high=low+1,查找区间仅含一个元素A[low]
return e == A[low] ? low : -1;//返回名字的元素的位置或者-1
}//相对于版本一,最好(坏)情况下更坏(好);各种情况下的SL更加接近,整体性能更趋稳定
版本三:返回不大于e的最后一个元素
语义约定:以上二分查找未严格兑现search()接口的语义约定:返回不大于e的最后一个元素
只有兑现这一约定,才可以有效支持相关算法,比如:V.insert(1 + V.search(e), e)
1)当有多个命中元素时,必须返回最靠后者
2)失败时,应返回小于e的最大者(含哨兵[low-1],high)
public static int binSearch3(int[]A, int e, int low, int high) {
while (low < high) {//不变性:A[0,low) <= e < A[high, n)
int mid = left + ((right - left) >>2);//以中点为轴点,经比较后确定深入
if (e < A[mid]) {//[low,mid)
high = mid;
} else {//([mid, high)
low = mid + 1;
}
}//出口时,A[low = high]为大于e的最小元素的位置
return --low;//故low-1即不大于e的元素的最大位置
}
版本三与版本二的差异:
1)待查找区间的宽度缩减至0而非1时,算法才结束
2)转入右侧子区间时,左边界取作mid+1而非mid --A[mid]会被遗漏?
3)无论成功与否,返回的位置严格符合接口的语义约定。。。
版本三的正确性
不变性:A[0,low) <= e < A[high, n)
初始时,low=0且high=n,A[0,low) = A[high, n)=Ø。自然成立
数学归纳:假设不变性一直保持至(a),以下无非两种情况。。。
单调性:显而易见
附加题1:
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。当有多个重复元素时,返回最靠后者。
解法1:
public class Search {
//leetcode 35题 搜索插入位置
public static int searchInsert(int[] nums, int target) {
int low = 0;
int high = nums.length;
while (low < high) {
int mid = left + ((right - left) >>2);
if (target < nums[mid]) {
high = mid;
} else {
low = mid + 1;
}
}
if (low > 0 && nums[low -1] == target) {//**注意此处的判断条件**
return low -1;
} else {
return low;
}
}
public static void main(String[] args) {
int[] arr = {1,3,5,6};
System.out.println(searchInsert(arr, 0));
System.out.println(searchInsert(arr, 8));
}
}
返回0和4
执行结果:通过显示详情执行用时 :0 ms在所有 Java 提交中击败了100.00%的用户
内存消耗 :39.4 MB, 在所有 Java 提交中击败了5.01%的用户
解法2:
public static int searchInsert1(int[] nums, int target) {
int low = 0;
int high = nums.length;
while (low < high) {
int mid = left + ((right - left) >>2);
if (target == nums[mid]) {
return mid;
} else if (target < nums[mid]) {
high = mid;
} else {
low = mid + 1;
}
}
return low;
}
执行用时 :0 ms, 在所有 Java 提交中击败了100.00%的用户
内存消耗 :39 MB,在所有 Java 提交中击败了47.95%的用户
附加题2:
//leetcode 34题 在排序数组中查找元素的第一个和最后一个位置
public int[] searchRange(int[] nums, int target) {
int low= 0;
int high = nums.length;
int[] result ={-1, -1};
while (low < high) {
int mid = left + ((right - left) >>2);
if (target < nums[mid]) {
high = mid;
} else {
low = mid + 1;
}
}
//找到一个 target,然后向左或向右线性搜索
if (low > 0 && nums[low -1] == target) {
result[1] = low -1;
result[0] = low -1;
for (int i = low -2;i>=0;i--) {
if (nums[i] == target) {
result[0] = i;
}
}
}
return result;
}
找到一个 target,然后向左或向右线性搜索不行吗?可以,但是不好,因为这样难以保证二分查找对数级的复杂度了。下一篇博客说明不好处。