704.二分查找
题目描述:
给定一个 n
个元素有序的(升序)整型数组 nums
和一个目标值 target
,写一个函数搜索 nums
中的 target
,如果目标值存在返回下标,否则返回 -1
。
示例一:
输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4
示例二:
输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1
提示:
1、你可以假设 nums 中的所有元素是不重复的。
2、n 将在 [1, 10000]之间。
3、nums 的每个元素都将在 [-9999, 9999]之间。
解题思路:
- 关键词提取:整型数组、数组有序、目标值、返回下标、假定元素不重复
- 暴力解法:一次遍历,判断条件为==target,时间复杂度是o(n)
- 二分法:一次遍历,双指针,不断将数组区间对折,时间复杂度是o(nlogn)
- 左右指针的取值,左闭右闭,左指针取下标0,右指针取下标size-1
- 中值的取值,左右指针之和再除2,换一种写法避免数据类型溢出
- 循环的判断条件,左指针小于或者等于右指针,原因是,左闭右闭的取法,假如最终剩下2个元素,再次对折,就是1个元素,刚好左右指针指向同一个元素
- 中值与目标值的判断之后,左指针和右指针的取值难点,+1和-1的原因。第一次取中值:[nLeft, nMid]和[nMid, nRight]。挖掉中间的nMid值以后,两个区间未检索的值如下:在左区间[nLeft, nMid - 1] 或者 在右区间[nMid + 1, nRight],因此,左指针为nMid + 1, 右指针为nMid - 1
二分法代码如下:
class Solution {
public:
int search(vector<int>& nums, int target) {
int nLeft = 0;
int nRight = nums.size() - 1;
int nMid = 0;
// 在数组中采用二分法找对应的元素
// 二分法的区间:左闭右闭,[nLeft, nRight]区间内的元素都在选择中
while(nLeft <= nRight)
{
// 每次循环,更新中值的元素下标
// 这种写法可以避免nMid出现整型溢出
nMid = nLeft + (nRight - nLeft) / 2;
// 中值偏小,需要在右区间再次检索
if(nums[nMid] < target)
{
nLeft = nMid + 1;
}
// 中值偏大,需要在左区间再次检索
else if(nums[nMid] > target)
{
nRight = nMid - 1;
}
// 找到对应的值,返回元素下标
// nums[nMid] == target
else
{
return nMid;
}
}
// 找不到对应的元素
return -1;
}
};
总结:
- 第一次做的时候,仅仅会暴力解法,而且不知道什么是二分法。
1)看到题解写着二分法,标出左右指针以及中值的计算,仅仅是记住代码模板,往里生搬硬套。
2)后续做题目的时候,仅仅知道检索一个元素特定值,可以采用这种方式,并不知道优势在哪,也不知道有哪些应用条件限制以及如何去应用。 - 跟着代码随想录刷题的时候,才发现二分查找法是有限制的,而且是有着易错点的。
1)是我忽略了区间选择中,左闭右闭和左闭右开的情况,默认都选择左闭右闭。
2)是我忽略了区间缩小时,左右指针重新赋值的依据是左闭右闭的区间选择。
3)假如未排序,那么和暴力解法是一样的时间复杂度。
4)假如有重复元素,会出现多个解。
27.移除元素
题目描述:
给你一个数组 nums
和一个值 val
,你需要 原地 移除所有数值等于 val
的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1)
额外空间并 原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
示例一:
输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2]
解释:函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。
示例二:
输入:nums = [0,1,2,2,3,0,4,2], val = 2
输出:5, nums = [0,1,3,0,4]
解释:函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素。
提示:
0 <= nums.length <= 100
0 <= nums[i] <= 50
0 <= val <= 100
解题思路:
- 关键词提取:数组、目标值、原地移除、新长度
- 暴力法:两次遍历,第一次遍历负责检索目标值,第二次遍历负责删除目标元素,时间复杂度o(n^2)
- 双指针法:一次遍历,双指针,一个指针负责遍历数组,一个指针负责新数组的元素赋值
- 将一个数组复制为两个数组,一个指针在遍历上方数组[3,3,2,2]
- 一个指针在遍历下方数组[3,3,2,2],从下标0开始,检索到3时,不赋值,不移动,检索到2时,先将下标0元素赋值为2,新数组为[2,3,2,2],再移动到下标1,再次检索到2,将下标1元素赋值为2,新数组为[2,2,2,2],再移动到下标2
- 跟随着遍历结束,指针停留在下标2位置,新数组为[2,2],长度跟指针指向的下标位置2一致,直接返回
双指针法代码如下:
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
// 修改原数组,不使用额外数组空间
// 定义一个新的指针,指向数组第一个元素
// 该指针指向的位置,用于保存需要记录的元素
int nNewIndex = 0;
// 开始遍历数组,从第一个元素开始
for(int i = 0; i < nums.size(); i++)
{
// 假如该元素不是目标元素,就按照顺序从第一个元素开始,存放起来
if(nums[i] != val)
{
// 在当前位置,保存需要记录的值(可能是当前位置后的第三个或者第五个),会覆盖当前位置的值
nums[nNewIndex] = nums[i];
// 指针右移一位,等待下一个需要记录的值
nNewIndex++;
}
}
// 返回需要记录的元素的长度
return nNewIndex;
}
};
总结:
- 第一次做的时候,仅仅会暴力解法,而且不知道什么是双指针法。
1)看到题解的时候,才发现,原来可以采用这种双指针的方式,一个循环做两个循环的事情。
2)掌握的思路是,只要新增一个变量,两重循环转换为一重循环,做复杂度降维,那么,是否还有三指针或者四指针法,一个循环将三个循环的事情都做了,后续的三数之和刚好可以对得上这个思考。 - 跟着代码随想录刷题的时候,才发现一道题是有很多的解题思路的,而且有很多的细节要处理。
1)假如只是为了做出题目,那么暴力解法已经完成这个目标,但是要提升自己的话,还是需要再看看别人的思路,多角度去思考,举一反三,才算是掌握。
2)在不改变元素的相对位置的情况下,暴力解法和双指针法,都可以解出该题,但是,假如改变相对位置,会怎样呢。我的想法是,可以先做一遍排序,再去找到目标值的左右区间,然后一次性将后续的元素填充好。二分查找法是否可以优化至o(nlogn)。
3)在不改变元素的相对位置的情况下,优化暴力解法,是否可以一次遍历,找到目标值的指定元素,切割出左右区间,再去精准的移动后续元素,这样子可以少一部份的元素移动次数。