主题列表:juejin, github, smartblue, cyanosis, channing-cyan, fancy, hydrogen, condensed-night-purple, greenwillow, v-green, vue-pro, healer-readable, mk-cute, jzman, geek-black
贡献主题:github.com/xitu/juejin…
theme: juejin highlight:
常见的二分查找实现
减治思想实现二分查找
-
循环可以继续的条件写成 while (left < right)
-
写if else 时思考当 nums[mid] 满足什么性质时,nums[mid] 不是目标元素。接着判断 mid 左边有没有可能存在目标元素,mid 右边有没有可能存在目标元素。
-
根据边界收缩行为修改取中间数的方式
-
int mid = left + (right - left) / 2; 避免 (left + right) / 2 造成整型溢出的风险。
-
"/" 整数除法默认向下取整会带来一个问题:
int mid = left + (right - left) / 2; 永远取不到右边界 right,,在面对 left = mid 和 right = mid - 1 这种边界收缩行为时,有可能发生死循环。
-
-
针对目标元素在查询数组中不存在的情况,需要在退出循环后需要对 nums[left] 是否是目标元素做下判断。
LC 35 搜索插入位置
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
你可以假设数组中无重复元素。
示例 1:
输入: [1,3,5,6], 5
输出: 2
示例 2:
输入: [1,3,5,6], 2
输出: 1
示例 3:
输入: [1,3,5,6], 7
输出: 4
示例 4:
输入: [1,3,5,6], 0
输出: 0
当 mid 满足 nums[mid] < target 时,mid 必定不是目标元素。以此作 if 语句,当 nums[mid] < target 时下次数组搜索范围是 [mid + 1, right] 相反的 else 的下次数组搜索范围是 [left, mid]。
代码如下:
public int searchInsert(int[] nums, int target) {
int len = nums.length;
if (len == 0) {
return 0;
}
// 此处还可以简化逻辑
if (nums[len-1] < target) {
return len;
}
int left = 0, right = len - 1;
while (left < right) {
// 当nums[mid]严格小于target,nums[mid]不是目标元素
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
// 下一次搜索区间 [mid+1, right]
left = mid + 1;
} else {
// 相反地,下一次搜索区间 [left, mid]
right = mid;
}
}
return left;
}
- 时间复杂度:O(logn) 其中 n 是数组的长度,二分查找的时间复杂度是 O(logn)
- 空间复杂度:O(1) 我们只用到了变量 left right mid。
以上代码还可以继续优化,将 nums[len-1] < target 即 target 作为新元素插入数组末尾的特殊情况,纳入通用流程。
right 扩展到 len,因为这种情况下会一直执行 left = mid + 1,直到 left = right = len,和原逻辑返回值 len 相同的效果。
public int searchInsert(int[] nums, int target) {
int len = nums.length;
if (len == 0) {
return 0;
}
int left = 0, right = len;
while (left < right) {
// 当nums[mid]严格小于target,nums[mid]不是目标元素
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
// 下一次搜索区间 [mid+1, right]
left = mid + 1;
} else {
// 相反地,下一次搜索区间 [left, mid]
right = mid;
}
}
return left;
}
LC 34 在排序数组中查找元素的第一个和最后一个位置
给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
你的算法时间复杂度必须是 O(log n) 级别。
如果数组中不存在目标值,返回 [-1, -1]。
示例 1:
输入: nums = [5,7,7,8,8,10], target = 8 输出: [3,4] 示例 2:
输入: nums = [5,7,7,8,8,10], target = 6 输出: [-1,-1]
来源:力扣(LeetCode) 链接:leetcode-cn.com/problems/fi…
分治思想进行二分查找的核心是找出 mid 必定不是 target 元素的可能,以此来写 if 语句。为了简化问题,你可以先查找 target 的第一个位置,如果找不到 target 出现的第一个位置,说明数组中不存在该元素,剪枝查找最后位置逻辑,直接返回 {-1, -1}。
-
查找元素出现的第一个位置 findFirstIndex。假设 nums[mid] < target 由于是升序数组所以 target 第一个位置只能在[mid + 1, right]范围内。
相反地 nums[mid] < target 的对立面是 nums[mid] >= target 这说明 mid 有可能是 target 的第一个位置但也有可能是最后一个位置。所以下次搜索范围是[left, mid]不能跳过mid。
-
查找元素的最后一个位置 findLastIndex。加和 nums[mid] > target 同样由于是升序数组,所以 target 最后一个位置必定不在[mid, right] 内,下次搜索范围是 [left, mid-1]。
相反地 nums[mid] > target 的对立面是 nums[mid] <= target 此时 mid 有可能是 target 的最后一个位置,但同样也可能是第一个位置。下次搜索范围是[mid, right] 不能跳过 mid。
-
向下和向上取整中位数 当遇到 right = mid -1 & left = mid 的情况时,默认的向下取整中位数有可能会出现死循环,你需要向上取整。可以形象的记忆口诀:左动取左,右动取右。即
if(...) left = mid + 1时向下(左)取整;if (...) right = mid - 1时向上(右)取整。
class Solution {
public int[] searchRange(int[] nums, int target) {
int len = nums.length;
if (len == 0) {
return new int[]{-1,-1};
}
if (len == 1 && nums[0] != target) {
return new int[]{-1, -1};
}
int firstIndex = this.findFirstIndex(nums, target, len);
if (firstIndex == -1) {
return new int[]{-1, -1};
}
int lastIndex = this.findLastIndex(nums, target, len);
return new int[]{firstIndex, lastIndex};
}
public int findFirstIndex(int[] nums, int target, int len) {
int left = 0, right = len - 1;
while (left < right) {
int mid = left + (right - left) / 2;
// nums[mid] < target 寻找目标元素的的开始位置,收缩边界
if (nums[mid] < target) {
// nums[mid] 不是要找的开始位置,下次查询范围 [mid+1, right]
left = mid + 1;
} else {
// 反面 nums[mid] >= target,nums[mid]有可能是开始位置也可能是结束位置;
// 下次查询范围 [left, mid]
right = mid;
}
}
if (nums[left] != target) {
return -1;
}
return left;
}
public int findLastIndex(int[] nums, int target, int len) {
int left = 0, right = len - 1;
while (left < right) {
// 注意向上取整,否则出现死循环!
int mid = left + (right - left + 1) / 2;
// nums[mid] > target 寻找目标元素的结束位置,收缩边界
if (nums[mid] > target) {
// [mid, right] 一定不存在结束位置,下次搜索范围 [left, mid-1]
right = mid - 1;
} else {
// 反面 nums[mid] <= target,nums[mid]有可能是结束位置,但也有可能是开始位置
// 下次搜索范围 [mid, right]
left = mid;
}
}
return left;
}
}
补充出现死循环的测试用例如下:
nums:[5,7,7,8,8,10] target:8
left -> 0 right -> 5
left -> 2 right -> 5
left -> 3 right -> 5
left -> 4 right -> 5
left -> 4 right -> 5//如果默认向下取整取中位数,此时 mid= 3+(5-3)/2=4&&nums[4]==target走left=mid=4逻辑。left恒等于4。
left -> 4 right -> 5
......