LeetCode704.二分查找
题目链接
https://leetcode.cn/problems/binary-search/
视频讲解
https://www.bilibili.com/video/BV1fA4y1o715
题目分析
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
该题目给定的数组为有序数组,需要大家根据给定目标值target在数组中搜索该值并返回下标值,如没有则返回-1。该种题型属于典型的二分查找题型,对于二分查找大家需要熟练掌握。
二分查找代码分析
二分查找的主流写法区间有两种:
左闭右闭[left , right]
左闭右开[left , right)
对于两种不同的区间写法,其关键的区分点有以下三点:
在初始设定右边界时,设定为right = nums.size() - 1还是right = nums.size()
while循环的判定条件应该应取left < right还是left <= right
在更新右边界时,应设定right = middle - 1还是right = middle
关键点一
对于左闭右闭区间[left , right],
right右边界可取,说明right初始设定的最大范围应该恰好在数组的最大下边边界处,此时应设定为right = nums.size() - 1。
对于左闭右开区间[left , right),
right右边界不取,说明right初始设定的最大范围应该恰好比数组的最大下标边界大1,此时应设定为right = nums.size()。
关键点二
判断while循环的判定条件时,只需要将待取的条件left < right、left <= right代入设定的范围中看是否合理即可。
对于左闭右闭区间[left , right],
当left <= right时,区间仍然成立,此时判定条件即为left <= right。
对于左闭右开区间[left , right),
当left < right时,区间仍然成立,当left <= right时,区间不再成立,此时判定条件即为left < right。
对于左闭区间来说,更新左边界时均设定为left = middle + 1。
关键点三
更新右边界时,
对于左闭右闭区间[left , right],
已经将nums[middle]与target进行比较得出了相应结论,此时若更新右边界right = middle,则将再进行重复比较,不合理,故此时应该设置right = middle - 1。
对于左闭右开区间[left , right),
已经将nums[middle]与target进行比较得出了相应结论,此时若更新右边界right = middle,则恰好将右开的范围恰好将已比较的middle项屏蔽掉,故此时应该设置right = middle。
二分查找代码示例
左闭右闭
class Solution {
public:
int search(vector<int>& nums, int target) {
//左闭右闭写法
int left = 0;
int right = nums.size() - 1;
while(left <= right) {
int middle = left + (right - left) / 2;
if(nums[middle] > target) {
right = middle - 1;
}
else if(nums[middle] < target) {
left = middle + 1;
}
else return middle;
}
return -1;
}
};
左闭右开
class Solution {
public:
int search(vector<int>& nums, int target) {
//左闭右开写法
int left = 0;
int right = nums.size();
while(left < right) {
int middle = left + (right - left) / 2;
if(nums[middle] > target) {
right = middle;
}
else if(nums[middle] < target) {
left = middle + 1;
}
else return middle;
}
return -1;
}
};
额外补充
在求取middle值的时候,编者采取的操作是
int middle = left + (right - left) / 2
而没有写成
int middle = (left + right) / 2
其实这两种写法都是正确的,但是第二种写法以为涉及到左右边界相加的问题,容易导致溢出,故在此处采用第一种写法更加的方便和安全。
除此之外,第一种写法还有改进的空间,即——
int middle = left + (right - left) >> 1
这里是采用了位运算符的操作,具体内容可查看相关文章。
十进制数进行 >> 1的位运算就相当于一个除以2的十进制运算,>>2的位运算就相当于一个除以4的十进制运算......以此推类即可。
这种写法避免了浮点数溢出的问题,同时也比直接使用/计算更快一些。
LeetCode27.移除元素
题目链接
https://leetcode.cn/problems/remove-element/
视频讲解
https://www.bilibili.com/video/BV12A4y1Z7LP
题目分析
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
该题目涉及到元素的删除和移动,考察重点在于如何将保留的元素移动到被删除的元素的原处位置上,如下分享两种解法。
解法一——暴力解法
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
//暴力解法
int size = nums.size();
for(int i = 0; i < size; i++) {
if(nums[i] == val) {
for(int j = i; j < size - 1; j++) {
nums[j] = nums[j + 1];
}
size--;
i--;
}
}
return size;
}
};
该代码主要利用了元素的移动,时间复杂度达到O(n2),易错点在于将后部分元素移动到删除元素位置时数组的长度size和指针i都需要进行减一操作来重新匹配长度和位置。
解法二——双指针法
仅在nums数组上借助快慢指针进行操作
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int size = nums.size();
int slow = 0;
for(int fast = 0; fast < size; fast++) {
if(nums[fast] != val) {
nums[slow] = nums[fast];
slow++;
}
}
return slow;
}
};
借助temp数组与nums数组匹配进行快慢指针的操作
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
vector<int> temp(nums.size());
for(int m = 0; m < nums.size(); m++) {
temp[m] = nums[m];
}
int i, j = 0;
while(i < nums.size()) {
if(temp[i] == val) {
i++;
}
else {
nums[j] = temp[i];
j++;
i++;
}
}
return j;
}
};
两种操作没有本质的不同,但在使用借助temp数组的操作时要注意拷贝操作。
总结
今日算法题目较为简单,之前已有练习,但考虑到这两个题目仅仅只是两种算法的最基本操作,后续还需要进行进行进一步的拓展练习进行巩固。
还有几道拓展题型没有尝试,在此标记一下,待之后继续攻克。
二分查找类型——
35.搜索插入位置(opens new window)
34.在排序数组中查找元素的第一个和最后一个位置(opens new window)
69.x 的平方根
367.有效的完全平方数
移除元素类型——
26.删除排序数组中的重复项
283.移动零
844.比较含退格的字符串
977.有序数组的平方