数组
704. 二分查找
二分查找思路不再赘述, 注意二分查找适用于有序, 无重复元素的数组, 要是有重复元素, 算法查找到的未知不唯一. 主要难点在于边界处理:
- while中
left<=right
还是left<right
? - 重新赋值right时是
right=middle
还是right=middle + 1
?
要用数学中区间的观点去处理边界, 常用的是左闭右闭 [left, right]
, 左闭右开 [left, right)
.
整个算法要保证这个不变量不变, 也就是到底用左闭右闭还是左闭右开
二分查找就是通过和中间位置的元素比较大小, 重新划定区间, 再和中间位置元素比较大小, 循环往复直到找到这个位置, 如果区间不合法了, 也就是不存在了.
关于区间的问题:
所以对于查找来说, 这个区间一定要是合法的, 也就是进入while循环的条件一定要是合法的区间 ( while内代码的功能就是查找)
- 对于左闭右闭: left == right 是合法的, 比如[3,3].
那么while中的条件就是while (left<=right)
- 对于左闭右开: left == right 是不合法的, 比如[1,1).
那么while中的条件就是while (left<right)
关于middle取值的问题
用 if (num[middle] > target)举例子, 需要重新赋值right, 已经判断了middle, 下一次循环不需要带上middle, 要从left到middle-1就可以. 比如数组 [0 1 3 4 5 7 9 12], middle=3 对应元素4,
- 对于左闭右闭:
- 若
right=middle
, 下一次循环while (left<=right)
中就会[left, middle]
,
middle这个位置的元素再次被判断, 所以不可以. right=middle-1
就可以实现[left, middle-1]
- 对于左闭右开:
-
若
right=middle-1
, 下一次循环while (left<right)
中就是[left, middle-1),
那middle-1这个位置的元素就是始终没有判断, 所以不可以. -
若
right=middle
就可以实现[left, middle)
在看题解的过程中我发现还需要对target先进性一个判断,要是比最小的小或者比最大的大 直接就返回-1了。if (target < nums[0] || target > nums[nums.length - 1]) { return -1; }
左闭右闭的代码如下:
public int search(int[] nums, int target) {
if (target < nums[0] || target > nums[nums.length - 1]) {
return -1;
}
int left = 0;
int right = nums.length - 1;
int middle = 0;
while (left <= right) {
middle = (left + right)/2;
if (nums[middle] > target) {
right = middle - 1;
} else if (nums[middle] < target) {
left = middle + 1;
} else {
return middle;
}
}
return -1;
}
左闭右开的代码如下:
public int search(int[] nums, int target) {
if (target < nums[0] || target > nums[nums.length - 1]) {
return -1;
}
int left = 0;
int right = nums.length;
int middle = 0;
while (left < right) {
middle = (left + right)/2;
if (nums[middle] > target) {
right = middle;
} else if (nums[middle] < target) {
left = middle + 1;
} else {
return middle;
}
}
return -1;
}
27. 移除元素
数组中删除元素不是直接删除, 要知道数组的元素在内存地址中是连续的,不能单独删除数组中的某个元素,只能覆盖.
- 暴力解法: 两层for循环, 外层遍历整个数组, 若找到val就将它后面的元素向前移一位
- 快慢指针(重要): 快慢指针的时间复杂度是O(n), 而不是O(1). 这也是erase()函数的实现方式. fast指针是找新数组中合法的元素, 慢指针是合法元素应该在的位置.
暴力解法代码:
public int removeElement(int[] nums, int val) {
int size = nums.length;
for (int i=0; i<size; i++) {
if (nums[i]==val) {
size--;
for (int j=i; j<size; j++){
nums[j] = nums[j+1]; //元素前移实现覆盖
}
i--; //这一行很关键, 重新检查当前位置覆盖的新元素
}
}
return size;
}
其中 i--
; 我在第一次写的时候落下了, 就出现了错误. 比如[0, 1, 2, 2, 3, 2]
数组, 删除元素2, 得到的应该是[0, 1, 3]
. 但我得到的却是[0, 1, 2, 3]
原因就是没有 i--
, 当检测到第一个2时 i=2, 后面的元素前移, 第二个2就到了第一个2的位置上变成了 [0, 1, 2, 3, 2],
此时再下一次循环中i=3, 会去看元素3, 错过了前移的第二个2.
所以再覆盖元素之后需要i--
, 再次检测当前位置覆盖之后的元素
双指针解法
public int removeElement(int[] nums, int val) {
int fast=0;
int slow=0;
for (fast=0; fast<nums.length; fast++) {
if (nums[fast] != val) {
nums[slow] = nums[fast];
slow++;
}
}
return slow;
}
快慢指针是一个将二维循环降到一维很常用的方法
比如[0, 1, 2, 2, 3, 2]
数组, 对于0 和1元素, 它们重新赋值了自己
对于第一个2, 慢指针递增的if语句没有进去, 慢指针停下了, 但是快指针继续+1 . slow=3, fast=4
对于第二个2,同理, slow还是3, fast来到了5
遇到了3, slow的位置, 也就是第三个元素赋值为第五个元素5. slow=4, fast=6
遇到最后一个2, 同理slow不动, fast=7 for循环结束, 最后返回数组长度, 也就是slow