系列文章目录
目录
一、704. 二分查找及其拓展题
(1)704. 二分查找
前提:①数组为有序数组。②数组中无重复元素。③循环不变量原则。
算法缺陷:无法处理target
值为数组中的重复元素时,无法返回左侧边界或者右侧边界。
左闭右闭
class Solution {
//左闭右闭
public int search(int[] nums, int target) {
// <知识点>避免当 target 小于nums[0] nums[nums.length - 1]时多次循环运算
if (target < nums[0] || target > nums[nums.length - 1]) {
return -1;
}
int left = 0;
int right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;//mid = left + ((right - left) >> 1);
if (nums[mid] == target) {
return mid;
} else if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] < target) {
left = mid + 1;
}
}
return -1;
}
}
最终指针:
如果找到了目标元素,则left
和right
会指向目标元素,即left == right
。
如果没有找到目标元素,则left
会指向大于目标元素的第一个位置,right
会指向小于目标元素的最后一个位置。
左闭右开
class Solution {
//左闭右开
public int search(int[] nums, int target) {
// <知识点>避免当 target 小于nums[0] nums[nums.length - 1]时多次循环运算
if (target < nums[0] || target > nums[nums.length - 1]) {
return -1;
}
int left = 0;
int right = nums.length;
while (left < right) {
int mid = left + (right - left) / 2;//mid = left + ((right - left) >> 1);
if (nums[mid] == target) {
return mid;
} else if (nums[mid] > target) {
right = mid;
} else if (nums[mid] < target) {
left = mid + 1;
}
}
return -1;
}
}
最终指针:
如果找到了目标元素,则left
和right
会指向目标元素,即left == right
。
如果没有找到目标元素,则left
会指向大于目标元素的第一个位置,right
会指向大于目标元素的最后一个位置。
知识点
亮点:避免当 target
小于nums[0]
或大于nums[nums.length - 1]
时多次循环运算。
(2) 35.搜索插入位置
①暴力解法
版本一
class Solution {
public int searchInsert(int[] nums, int target) {
//暴力解法(1)
int index = 0;// 默认为0,目标值在数组所有元素之前时可用
for (int i = 0; i < nums.length; i++) {
// 目标值等于数组中某一个元素
// 目标值插入数组中的位置
//目标值在数组所有元素之后的情况
if (nums[i] == target) {
return i;
} else if (nums[i] < target) {
index++;
}
}
return index;
}
}
版本二
class Solution {
public int searchInsert(int[] nums, int target) {
//暴力解法(2)
for (int i = 0; i < nums.length; i++) {
// 分别处理如下三种情况
// 目标值在数组所有元素之前
// 目标值等于数组中某一个元素
// 目标值插入数组中的位置
if (nums[i] >= target) {// 一旦发现大于或者等于target的num[i],那么i就是我们要的结果
return i;
}
}
// 目标值在数组所有元素之后的情况
return nums.length;
}
}
②二分查找法
版本一(左闭右闭)
class Solution {
public int searchInsert(int[] nums, int target) {
//二分法(左闭右闭)
// 定义target在左闭右闭的区间,[left, right]
int left = 0;
int right = nums.length - 1;
while (left <= right) {// 当left==right,区间[left, right]依然有效
int mid = left + ((right - left) >> 1); // 防止溢出
if (nums[mid] == target) { // 1.目标值等于数组中某一个元素 return mid;
return mid;
} else if (nums[mid] < target) {
left = mid + 1;// target 在右区间,所以[mid + 1, right]
} else if (nums[mid] > target) {
right = mid - 1;// target 在左区间,所以[left, mid - 1]
}
}
return left; // 2.目标值在数组所有元素之前 3.目标值插入数组中 4.目标值在数组所有元素之后 return right + 1;
}
}
版本二(左闭右开)
class Solution {
public int searchInsert(int[] nums, int target) {
//二分法(左闭右开)
// 定义target在左闭右闭的区间,[left, right)
int left = 0;
int right = nums.length;
while (left < right) {//左闭右开 [left, right)
int mid = left + ((right - left) >> 1); // 防止溢出
if (nums[mid] == target) { // 目标值等于数组中某一个元素 return mid;
return mid;
} else if (nums[mid] < target) {
left = mid + 1;// target 在右区间,所以[mid + 1, right)
} else if (nums[mid] > target) {
right = mid;// target 在左区间,所以[left, mid)
}
}
// 目标值在数组所有元素之前 [0,0)
// 目标值插入数组中的位置 [left, right) ,return right 即可
// 目标值在数组所有元素之后的情况 [left, right),因为是右开区间,所以 return right
return left;
}
}
(3)34. 在排序数组中查找元素的第一个和最后一个位置
二分法
版本一
class Solution {
public int[] searchRange(int[] nums, int target) {
//左闭右闭 [left, right]
int left = left_bound(nums, target);
int right = right_bound(nums, target);
return new int[]{left, right};
}
//寻找左边界
int left_bound(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while (left <= right) {
int mid = left + ((right - left) >> 1);
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else {
right = mid - 1;
}
}
//即使target与数组第一个元素相等,right为-1,此时退出循环,但可返回left此时为0得到位置
if (left < 0 || left >= nums.length) {
return -1;
}
//之所以要判断是因为left的取值范围为[0,nums.length],left=0有两种情况:
//①target在数组前,此时right=-1.
//②target与数组第一个元素相等,此时right=-1.
//或者nums=[1,1,3,3],target=2
return nums[left] == target ? left : -1;//left=right+1;
}
//寻找右边界
int right_bound(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while (left <= right) {
int mid = left + ((right - left) >> 1);
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else {
left = mid + 1;
}
}
//即使target与数组最后一个元素相等,left为nums.length,此时退出循环,但可返回right此时为nums.length-1得到位置
if (left - 1 < 0 || left - 1 >= nums.length) {
return -1;
}
//①target在数组后,此时right=nums.length-1,left-1=nums.length.
//②target与数组最后一个元素相等,此时right=nums.length-1,left-1=nums.length..
//或者nums=[1,1,3,3],target=2
return nums[left - 1] == target ? left - 1 : -1;//left-1=right
}
}
返回值的判断
①左边界:
//即使target与数组第一个元素相等,right为-1,此时退出循环,但可返回left此时为0得到位置
if (left < 0 || left >= nums.length) {
return -1;
}
//之所以要判断是因为left的取值范围为[0,nums.length],left=0有两种情况:
//①target在数组前,此时right=-1.
//②target与数组第一个元素相等,此时right=-1.
//或者nums=[1,1,3,3],target=2
return nums[left] == target ? left : -1;//left=right+1;
右边界:
//即使target与数组最后一个元素相等,left为nums.length,此时退出循环,但可返回right此时为nums.length-1得到位置
if (left - 1 < 0 || left - 1 >= nums.length) {
return -1;
}
//①target在数组后,此时right=nums.length-1,left-1=nums.length.
//②target与数组最后一个元素相等,此时right=nums.length-1,left-1=nums.length..
//或者nums=[1,1,3,3],target=2
return nums[left - 1] == target ? left - 1 : -1;//left-1=right
②左边界:
// right = left - 1
// 如果存在答案 right是首选
if (right >= 0 && right < nums.length && nums[right] == target) {
return right;
}
if (left >= 0 && left < nums.length && nums[left] == target) {
return left;
}
return -1;
右边界:
// left = right + 1
// 要找最后一次出现 如果有答案 优先找left
if (left >= 0 && left < nums.length && nums[left] == target) {
return left;
}
if (right >= 0 && right < nums.length && nums[right] == target) {
return right;
}
return -1;
版本二(原始二分查找+滑动指针)
1、首先,在 nums 数组中二分查找 target;
2、如果二分查找失败,则 binarySearch 返回 -1,表明 nums 中没有 target。此时,searchRange 直接返回 {-1, -1};
3、如果二分查找成功,则 binarySearch 返回 nums 中值为 target 的一个下标。然后,通过左右滑动指针,来找到符合题意的区间。
class Solution {
public int[] searchRange(int[] nums, int target) {
int index = binarySearch(nums, target);
if (index == -1) {// 二分查找
return new int[]{-1, -1};
}
// nums 中存在 target,则左右滑动指针,来找到符合题意的区间
int left = index;
int right = index;
// 向左滑动,找左边界
while (left - 1 >= 0 && nums[left - 1] == nums[index]) {// 防止数组越界。逻辑短路,两个条件顺序不能换
left--;
}
// 向右滑动,找右边界
while (right + 1 < nums.length && nums[right + 1] == nums[index]) {
right++;
}
return new int[]{left, right};
}
/*
*二分查找
* @param nums
* @param target
*/
public int binarySearch(int[] nums, int target) {
int left = 0;
int right = nums.length - 1; // 不变量:左闭右闭区间
while (left <= right) {
int mid = left + ((right - left) >> 1);
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else {
return mid;
}
}
return -1;
}
}
二、27. 移除元素及其拓展题
(1)27. 移除元素
①暴力解法
双重for循环:
class Solution {
public int removeElement(int[] nums, int val) {
//暴力解法(双重for循环)
int size = nums.length;
for (int i = 0; i < size; i++) {
if (nums[i] == val) {// 发现需要移除的元素,就将数组集体向前移动一位
for (int j = i + 1; j < size; j++) {
nums[j - 1] = nums[j];
}
i--;// 因为下标i以后的数值都向前移动了一位,所以i也向前移动一位以遍历到移到i位置的元素
size--;//此时数组的大小-1
}
}
return size;
}
}
注: i--;
是因为下标i
以后的数值都向前移动了一位,所以i
也向前移动一位以遍历到移到i
位置的元素。
②双指针法
快慢指针法
class Solution {
public int removeElement(int[] nums, int val) {
// 快慢指针
int slowIndex = 0;
for (int fastIndex = 0; fastIndex < nums.length; fastIndex++) {
if (nums[fastIndex] != val) {
nums[slowIndex] = nums[fastIndex];
slowIndex++;
//也可将8-9简化写成nums[slow++]=nums[fast];
}
}
return slowIndex;
}
}
相向双指针法
(1)相向双指针法(版本一,right
指针指向的值一定不等于val
)
class Solution {
public int removeElement(int[] nums, int val) {
// 相向双指针法(版本一,right指针指向的值一定不等于val)
int left = 0;
int right = nums.length - 1;
while (right >= 0 && nums[right] == val) right--;//将right移到从右数第一个值不为val的位置
while (left <= right) {
if (nums[left] == val) {//left位置的元素需要移除
//将right位置的元素移到left(覆盖),right位置移除
nums[left] = nums[right];
right--;
}
while (right >= 0 && nums[right] == val) right--;
left++;
}
return left;
}
}
(2)相向双指针法(版本二,兼容了right
指针指向的值与val
相等的情况)
class Solution {
public int removeElement(int[] nums, int val) {
//相向双指针法(版本二,兼容了`right`指针指向的值与`val`相等的情况)
int left = 0;
int right = nums.length - 1;
while (left <= right) {
if (nums[left] == val) {
nums[left] = nums[right];
right--;
} else {
// 这里兼容了right指针指向的值与val相等的情况
left++;
}
}
return left;
}
}
(2)283. 移动零
①暴力解法
//暴力解法
int size = nums.length;
for (int i = 0; i < size; i++) {
if (nums[i] == 0) {//删除零元素
for (int j = i + 1; j < size; j++) {
nums[j - 1] = nums[j];
}
nums[size - 1] = 0;//每删除一个元素,就将末尾增加一个0
i--;
size--;
}
}
②双指针法(快慢指针)
//快慢指针
int slow = 0;
for (int fast = 0; fast < nums.length; fast++) {
if (nums[fast] != 0) {
//以下两行代码可写成 nums[slow++] = nums[fast];
nums[slow] = nums[fast];
slow++;
}
}
// 后面的元素全变成 0
for (int i = slow; i < nums.length; i++) {
nums[i] = 0;
}