先谈谈二分查找,二分查找是一种非常高效的查找算法,例题如下:
给定一个整形有序数组,如何找出某一整数是否在这个数组中,以及返回该整数所对应的下标?
此题最简单的方法就是遍历一遍原数组来判断是否存在,这也是顺序查找,时间复杂度为O(n)。还可以建立一个哈希表,但是这种方法的空间复杂度为O(n)。所以,此时最佳的算法便是二分查找,其时间复杂度为O(logn),空间复杂度为O(1)。
下面给出二分查找算法递归与非递归的两种形式:
private int binarySearch(int[] nums, int left, int right, int target) {
if (left > right) {
return -1;
}
int mid = left + (right - left) / 2;//防止数值溢出
if (nums[mid] > target) {
return binarySearch(nums, left, mid - 1, target);
} else if (nums[mid] < target) {
return binarySearch(nums,mid + 1,right,target);
}
return mid;
}
private int binarySearch(int[] nums, int left, int right, int target) {
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
return mid;
}
}
return -1;
}
也许,这并不足以证明二分查找的威力,下面扩展一下原题目。
假设按照升序排序的数组在预先未知的某个点上进行了旋转
( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
搜索一个给定的目标值,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。你可以假设数组中不存在重复的元素。
这个题我给出两种解题思路,而这两种方法都用到了二分查找。
第一种方法:二分查找加分治算法,第一次取得原数组的中间元素进行比较(中间元素恰好是目标值的情况单独拿出来即可),不难发现总有一侧的元素是有序,另外一侧的元素是无序的,有序的一侧直接复用二分查找即可,而无序的一侧仿照刚才思路再来一次,就这样通过二分来实现递归分治来判断目标值是否存在。
具体代码如下:
public int search1(int[] nums, int target) {
return find(nums, 0, nums.length - 1, target);
}
private int find(int[] nums, int left, int right, int target) {
//递归的终止条件
if (left > right) {
return -1;
}
//判断边界条件
int mid = left + (right - left) / 2;
if (target == nums[left]) {
return left;
}
if (target == nums[right]) {
return right;
}
if (target == nums[mid]) {
return mid;
}
if (target > nums[right] && target < nums[left]) {
return -1;
}
//判断的核心代码
if (nums[mid] < nums[right]) {
if (target > nums[mid] && target < nums[right]) {
return binarySearch(nums, mid + 1, right - 1, target);
} else {
return find(nums, left, mid - 1, target);
}
} else {
if (target > nums[left] && target < nums[mid]) {
return binarySearch(nums, left + 1, mid - 1, target);
} else {
return find(nums, mid + 1, right, target);
}
}
}
private int binarySearch(int[] nums, int left, int right, int target) {
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
return mid;
}
}
return -1;
}
第二种方法:二分查找加二分查找,先通过整体的二分查找得到数组的旋转点,从而不难得出以旋转点为中心的左右两侧都是有序数组,进行再分别对其两侧进行二分查找来判断目标值是否存在。
具体代码如下:
public int search2(int[] nums, int target) {
if (nums == null || nums.length == 0) {
return -1;
}
int partition = findPartition(nums);
if (partition != -1) {
if (target == nums[partition]) {
return partition;
} else if (target > nums[nums.length - 1]) {
return binarySearch(nums, 0, partition - 1, target);
} else {
return binarySearch(nums, partition + 1, nums.length - 1, target);
}
}
return binarySearch(nums, 0, nums.length - 1, target);
}
private int findPartition(int[] nums) {
int left = 0;
int right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
//此处是通过旋转点特殊的性质来得到旋转点
if (mid != right && nums[mid] > nums[mid + 1]) {
return mid;
}
if (mid != left && nums[mid] < nums[mid - 1]) {
return mid - 1;
}
if (nums[mid] < nums[left]) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return -1;
}
通过上面的题目就足以见得二分查找的威力不小。
其实就上面的题目还能进一步拓展,就是把上述数组中不存在重复的元素的条件去掉。
此时题目难度又增加了一些,因为有重复的元素必然会影响到二分查找的性能和查找的正确性,从而一个很直接的想法就是去重,即在上述代码的基础上加上一段可以去掉数组重复元素的代码即可。
具体代码如下:
public boolean search1(int[] nums, int target) {
return find(nums, 0, nums.length - 1, target);
}
private boolean find(int[] nums, int left, int right, int target) {
if (left > right) {
return false;
}
while (left < right && nums[left] == nums[left + 1]) {
left++;
}
while (left < right && nums[right] == nums[right - 1]) {
right--;
}
int mid = left + (right - left) / 2;
if (target == nums[left]) {
return true;
}
if (target == nums[right]) {
return true;
}
if (target == nums[mid]) {
return true;
}
if (target > nums[right] && target < nums[left]) {
return false;
}
if (nums[mid] < nums[right]) {
if (target > nums[mid] && target < nums[right]) {
return binarySearch(nums, mid + 1, right - 1, target);
} else {
return find(nums, left, mid - 1, target);
}
} else {
if (target > nums[left] && target < nums[mid]) {
return binarySearch(nums, left + 1, mid - 1, target);
} else {
return find(nums, mid + 1, right, target);
}
}
}
private boolean binarySearch(int[] nums, int left, int right, int target) {
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
return true;
}
}
return false;
}
以上就是我对这几个题目以及二分查找的理解,如若读者有更好的想法或者方法愿意分享,感谢留言。