01_数组_01_二分查找

https://leetcode.cn/problems/binary-search/

内容

给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target  ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1

示例 1:

输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4

示例 2:

输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1

提示:

  1. 你可以假设 nums 中的所有元素是不重复的。
  2. n 将在 [1, 10000]之间。
  3. nums 的每个元素都将在 [-9999, 9999]之间。

代码(线性探测重复值)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Solution {
public:
int search(vector<int>& nums, int target) {
int pos = -1;
int left = 0;
int right = nums.size() - 1;
while (left <= right)
{
int mid = ((right - left) >> 1) + left;
if (target < nums[mid])
{
right = mid - 1;
}
else if (target > nums[mid])
{
left = mid + 1;
}
else // ==
{
pos = mid;
while (pos > left && nums[pos - 1] == target)
{
--pos;
}
break;
}
}
return pos;
}
};

坑1

我第一次在线性向前探测重复值的时候写错了 while 的判断条件,不应该是pos > 0,而是pos > left

坑2

第一次写的时候,把>> 2右移运算符写成了大于号> 2,而且,不应该是>> 2,应该是>> 1右移1位,也就是对应着除以2。这是致命的错误。

但是测试用例居然全都通过了!这可能是因为我用到了线性探测,让算法实际以遍历的形式找到了正确答案…

心得

在做二分查找或者分治思想类的题目,最难、最重要的把握点是物理下标和逻辑下标的关系。
像这个题目中,leftrightmid都是物理下标。
物理下标应该实时更新。
不应该只出现逻辑下标。
就像线性探测时的 while 条件中判断pos > 0就是错误使用了逻辑下标去充当条件。应当是pos > left + 0,省略+ 0即为pos > left

优化代码(继续二分探测)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Solution {
public:
int search(vector<int>& nums, int target) {
int pos = -1;
int left = 0;
int right = nums.size() - 1;
while (left <= right)
{
int mid = ((right - left) >> 1) + left;
if (target < nums[mid])
{
right = mid - 1;
}
else if (target > nums[mid])
{
left = mid + 1;
}
else // ==
{
pos = mid;
right = mid - 1;
}
}
return pos;
}
};

这能让探测行为继续保持二分的O(logn)O(\log {n})的复杂度进行。
O(logn+k)O(\log {n} + k)(k为重复次数)优化至 O(logn)O(\log {n}),完全消除了线性探测的开销。

总结

01_数组_02_移除重复元素

https://leetcode.cn/problems/remove-element/

内容

给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素。元素的顺序可能发生改变。然后返回 nums 中与 val 不同的元素的数量。

假设 nums 中不等于 val 的元素数量为 k,要通过此题,您需要执行以下操作:

  • 更改 nums 数组,使 nums 的前 k 个元素包含不等于 val 的元素。nums 的其余元素和 nums 的大小并不重要。
  • 返回 k

示例 1:

输入: nums = [3,2,2,3], val = 3
输出: 2, nums = [2,2,_,_]
解释: 你的函数函数应该返回 k = 2, 并且 nums 中的前两个元素均为 2。
你在返回的 k 个元素之外留下了什么并不重要(因此它们并不计入评测)。

示例 2:

输入: nums = [0,1,2,2,3,0,4,2], val = 2
输出: 5, nums = [0,1,4,0,3,_,_,_]
解释: 你的函数应该返回 k = 5,并且 nums 中的前五个元素为 0,0,1,3,4。
注意这五个元素可以任意顺序返回。
你在返回的 k 个元素之外留下了什么并不重要(因此它们并不计入评测)。

提示:

  • 0 <= nums.length <= 100
  • 0 <= nums[i] <= 50
  • 0 <= val <= 100

代码(后边界起遍历)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int r = nums.size(); // removed flag
int t = r - 1; // travel flag
while (t >= 0)
{
if (val == nums[t])
{
--r;
std::swap(nums[t], nums[r]);
}
--t;
}
return r;
}
};

思想

这是快慢指针的一个解法。测试用例全部通过。
但是,对于[0,1,2,2,3,0,4,2],结果是[0,1,0,4,3]
虽然符合了题目中的”这五个元素可以任意顺序返回“。
但是最好还是不要改变元素原先在数组中的顺序。

可以改进的点

实际上不需要swap函数,而是直接nums[t] = nums[r]即可,因为删除之后,删除区的元素是什么不重要了,不需要保留原先的删除的数据。

代码(左边界起遍历)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int processed = -1; // processed flag
int travel = 0; // travel flag
while (travel < nums.size())
{
if (val != nums[travel])
{
++processed;
nums[processed] = nums[travel];
}
++travel;
}
return processed + 1;
}
};

复杂度分析

相比于双层for循环的暴力解法,快慢指针的方法可以让时间复杂度降为O(n)O(n),这也是STL中vector中erase的时间复杂度。
空间复杂度O(1)O(1)