简单二分
数组中不包含重复元素,重点关注每次判边的逻辑。
框架 左闭右闭
class Solution {
public int binarySearch(int[] nums, int target) {
int left = 0;
int right = nums.length; // 注意
while(left <right) {
//这个mid每次都要更新,写在循环内部
int mid = left + ((right - left)>>1);
if(nums[mid] == target)
return mid;
else if (nums[mid] < target)
left = mid + 1; // 注意
else if (nums[mid] > target)
right = mid; // 注意
}
return -1;
}
}
分析二分查找的一个技巧是:不要出现 else,而是把所有情况用 else if 写清楚,这样可以清楚地展现所有细节。
统一使用左闭右闭 right 的赋值是 nums.length - 1 while(left <= right) left = mid + 1, right = mid - 1
左闭右开
class Solution {
public int binarySearch(int[] nums, int target) {
int left = 0;
int right = nums.length - 1; // 注意
while(left <= right) {
//这个mid每次都要更新,写在循环内部
int mid = left + ((right - left)>>1);
if(nums[mid] == target)
return mid;
else if (nums[mid] < target)
left = mid + 1; // 注意
else if (nums[mid] > target)
right = mid - 1; // 注意
}
return -1;
}
}
寻找左侧边界的二分搜索
找到 target 时不要立即返回,而是缩小「搜索区间」的上界 right,在区间 [left, mid) 中继续搜索,即不断向左收缩,达到锁定左侧边界的目的。
int left_bound(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] == target) {
// 别返回,锁定左侧边界
right = mid - 1;
}
}
// 判断 target 是否存在于 nums 中
if (left < 0 || left >= nums.length) {
return -1;
}
// 判断一下 nums[left] 是不是 target
return nums[left] == target ? left : -1;
}
寻找右侧边界的二分查找
int right_bound(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] == target) {
// 这里改成收缩左侧边界即可
left = mid + 1; //mid = left - 1
}
}
// 最后改成返回 left - 1
if (right < 0 || right >= nums.length) {
return -1;
}
return nums[right] == target ? right : -1;
}
34 在排序数组中查找元素的第一个和最后一个位置 (存在重复元素)
给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。
class Solution {
public int[] searchRange(int[] nums, int target) {
return new int[]{left_bound(nums, target), right_bound(nums, target)};
}
int left_bound(int[] nums, int target) {
int left = 0, right = nums.length - 1;
// 搜索区间为 [left, right]
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
// 搜索区间变为 [mid+1, right]
left = mid + 1;
} else if (nums[mid] > target) {
// 搜索区间变为 [left, mid-1]
right = mid - 1;
} else if (nums[mid] == target) {
// 收缩右侧边界
right = mid - 1;
}
}
// 检查出界情况
if (left >= nums.length || nums[left] != target) {
return -1;
}
return left;
}
int right_bound(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] == target) {
// 这里改成收缩左侧边界即可
left = mid + 1;
}
}
// 这里改为检查 right 越界的情况,见下图
if (right < 0 || nums[right] != target) {
return -1;
}
return right;
}
}
35 搜索插入位置
使用左闭右开的方式
class Solution {
public int searchInsert(int[] nums, int target) {
return left_bound(nums, target);
}
int left_bound(int[] nums, int target) {
if (nums.length == 0)
return -1;
int left = 0;
int right = nums.length;
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
//right = mid; //当存在重复元素,找最左侧插入点
return mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid;
}
}
return left;
}
}
74 搜索二维矩阵
给你一个满足下述两条属性的 m x n 整数矩阵:
- 每行中的整数从左到右按非严格递增顺序排列。
- 每行的第一个整数大于前一行的最后一个整数。
给你一个整数 target ,如果 target 在矩阵中,返回 true ;否则,返回 false 。
只要知道二维数组的的行数 m 和列数 n,二维数组的坐标 (i, j) 可以映射成一维的 index = i * n + j;反过来也可以通过一维 index 反解出二维坐标 i = index / n, j = index % n。
m3,n=4 mid=6 i=1;j=2; 对应第七个数 0-6 第七个数
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
int m = matrix.length, n = matrix[0].length;
// 把二维数组映射到一维
int left = 0, right = m * n - 1;
// 前文讲的标准的二分搜索框架
while(left <= right) {
int mid = left + (right - left) / 2;
if(get(matrix, mid) == target)
return true;
else if (get(matrix, mid) < target)
left = mid + 1;
else if (get(matrix, mid) > target)
right = mid - 1;
}
return false;
}
// 通过一维坐标访问二维数组中的元素 需要mid作为整体坐标
int get(int[][] matrix, int index) {
int m = matrix.length, n = matrix[0].length;
// 计算二维中的横纵坐标
int i = index / n, j = index % n; // 4*4 13 是3行一列
return matrix[i][j];
}
}
简单二分 + 循环数组
循环数组:4, 5, 6, 7, 8, 0, 1, 2 ⇒ target = 5, target = 1
特点:一分为二后,一侧是有序数组,另一侧是循环数组。
根据这个特点,先判断有序数组、循环数组分别在哪一侧,再判断 target 在有序数组 or 循环数组,判断方法是让 target 和有序数组的首尾做比较,看是否在有序数组中。
循环数组:旋转前升序排列 4, 5, 6, 7, 8, 0, 1, 2 4, 5, 6, 7, 8, 0, 1, 2 复制两份可以看出循环(末尾数小于开头数)
33 搜索旋转排序数组 (左闭右闭)
整数数组 nums 按升序排列,数组中的值 互不相同 。
在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。
给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。
你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。
由于题目说数字无重复,举个例子:
1 2 3 4 5 6 7 可以大致分为两类,
第一类 2 3 4 5 6 7 1 这种,也就是 nums[start] <= nums[mid]。此例子中就是 2 <= 5。
这种情况下,前半部分有序。因此如果 nums[start] <=target<nums[mid],则在前半部分找,否则去后半部分找。
第二类 6 7 1 2 3 4 5 这种,也就是 nums[start] >= nums[mid]。此例子中就是 6 > 2。
这种情况下,后半部分有序。因此如果 nums[mid] <target<=nums[end],则在后半部分找,否则去前半部分找。
class Solution {
public int search(int[] nums, int target) {
if (nums == null || nums.length == 0) {
return -1;
}
int start = 0;
int end = nums.length - 1;
int mid;
while (start <= end) {
mid = start + (end - start) / 2;
if (nums[mid] == target) {
return mid;
}
//前半部分有序,注意此处用小于等于
if (nums[start] <= nums[mid]) {
if (target >= nums[start] && target < nums[mid]) {
end = mid - 1; //在前半部分找
} else {
start = mid + 1;
}
} else {
//后半部分有序,注意此处用小于等于
if (target <= nums[end] && target > nums[mid]) {
start = mid + 1;
} else {
end = mid - 1;
}
}
}
return -1;
}
}
153 寻找旋转排序数组中的最小值
已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到:
- 若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]
- 若旋转 7 次,则可以得到 [0,1,2,4,5,6,7]
注意,数组 [a[0], a[1], a[2], ..., a[n-1]]旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]] 。
给你一个元素值 互不相同 的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。
这段代码通过二分查找的方式来寻找最小值。关键点在于判断最小值是在左侧还是右侧: mid时数组下标
如果nums[mid] > nums[right],说明最小值在mid的右侧,因为数组的右侧是无序的,而左侧是有序的。
如果nums[mid] <= nums[right],则需要判断mid是否是最小值。如果mid是数组的第一个元素,或者nums[mid]比它前面的元素小,那么mid就是最小值。
否则,最小值在mid的左侧。
通过这种方式,我们可以在对数时间复杂度内找到数组中的最小值。
public class Solution {
public int findMin(int[] nums) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2; // 防止溢出,同时找到中间位置
// 如果中间元素大于最右边的元素,说明最小值在右侧
if (nums[mid] > nums[right]) {
left = mid + 1;
} else {
// 如果中间元素是最小值(比前一个元素小或者是数组的第一个元素)
if (mid == 0 || nums[mid] < nums[mid - 1]) {
return nums[mid];
} else {
// 如果中间元素不是最小值,说明最小值在左侧
right = mid - 1;
}
}
}
return -1; // 如果数组为空,返回-1
}
}
面试题 10.09. 排序矩阵查找
给定M×N矩阵,每一行、每一列都按升序排列,请编写代码找出某元素。
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
// 检查矩阵是否为空
if (matrix.length == 0 || matrix[0].length == 0) {
return false;
}
// 遍历矩阵的每一行 在每一行中应用二分查找
for (int[] row : matrix) {
// 在当前行中搜索目标值
int index = search(row, target);
if (index >= 0) {
// 如果目标值存在于当前行,则返回 true
return true;
}
}
// 目标值不存在于矩阵中
return false;
}
// 二分查找
int search(int[] nums, int target) {
int l = 0; // 左边界
int r = nums.length - 1; // 右边界
while (l <= r) {
int mid = l + (r - l) / 2; // 计算中间位置
int num = nums[mid]; // 获取中间位置的值
if (num == target) {
// 如果中间位置的值等于目标值,返回中间位置
return mid;
} else if (num > target) {
// 如果中间位置的值大于目标值,更新右边界
r = mid - 1;
} else {
// 如果中间位置的值小于目标值,更新左边界
l = mid + 1;
}
}
// 目标值不存在于数组中
return -1;
}
}