题目与题解
参考资料:数组理论基础
Tips:
1. 数组一般思维上不难,写起来比较考验代码功底
2. 数组是存放在连续内存空间上的相同类型数据的集合
- 数组下标都是从0开始的。
- 数组内存空间的地址是连续的
3. 数组的元素是不能删的,只能覆盖
4. C++中二维数组在地址空间上是连续的, Java中不一定
704. 二分查找
题目链接:704. 二分查找
代码随想录题解:704. 二分查找
解题思路:
这题已经是老朋友了,只要注意开闭区间的写法,就不会出错。这里采用左闭右闭的写法,比较好理解,也不容易出错
class Solution {
public int search(int[] nums, int target) {
int start = 0, end = nums.length-1;
while (start <= end) {
int mid = (end - start) / 2 + start;
if (nums[mid] == target) return mid;
else if (nums[mid] < target) start = mid + 1;
else end = mid - 1;
}
return -1;
}
}
注意点
如果用左闭右开区间的写法,要修改的地方为:
- end赋初始值的时候用nums.length
- while循环里的条件为start < end
- nums[mid] > target时,end=mid
35.搜索插入位置
题目链接:35.搜索插入位置
代码随想录题解:35.搜索插入位置
解题思路
同样是要求在已经排序的数组里面进行搜索,复杂度为O(logn),显然是用二分算法。
不过这题有可能搜索不到target值,这时候就需要找到插入的位置,在二分算法中会不断搜索mid并判断nums[mid]是不是符合要求的target,所以最后得到的mid其实就是插入位置,返回即可。
class Solution {
public int searchInsert(int[] nums, int target) {
int start = 0, end = nums.length - 1;
int mid = (end - start)/2 + start;
while (start <= end) {
if (nums[mid] < target) start = mid + 1;
else if (nums[mid] > target) end = mid - 1;
else return mid;
mid = (end - start)/2 + start;
}
return mid;
}
}
随想录给出的方法跟704是一模一样的,不同点在于返回值用了right+1,这里分四种情况讨论
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int n = nums.size();
int left = 0;
int right = n - 1; // 定义target在左闭右闭的区间里,[left, right]
while (left <= right) { // 当left==right,区间[left, right]依然有效
int middle = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2
if (nums[middle] > target) {
right = middle - 1; // target 在左区间,所以[left, middle - 1]
} else if (nums[middle] < target) {
left = middle + 1; // target 在右区间,所以[middle + 1, right]
} else { // nums[middle] == target
return middle;
}
}
// 分别处理如下四种情况
// 目标值在数组所有元素之前 [0, -1]
// 目标值等于数组中某一个元素 return middle;
// 目标值插入数组中的位置 [left, right],return right + 1
// 目标值在数组所有元素之后的情况 [left, right], 因为是右闭区间,所以 return right + 1
return right + 1;
}
};
如果循环退出的原因不是找到了nums[mid] = target,那么right必然等于left-1,此时找到的位置已经收束到了left右边,所以此时返回left和right+1都是可以的。
注意点
如果用我自己的写法,其实有一点擦边的风险,如果left=0,right=-1时,由于java代码的特性,-1/2+0=0,所以得到的插入值是正确的,但是如果-1/2=-1的话,就会得到错误结果。所以最好还是用普通二分法最后返回left或者right+1。
34.在排序数组中查找元素的第一个和最后一个位置
题目链接:34.在排序数组中查找元素的第一个和最后一个位置
代码随想录题解:34.在排序数组中查找元素的第一个和最后一个位置
解题思路
这题就是在上一题的基础上,多了一点条件:数组中存在重复元素,要求找到target的始末位置。由于数组已经排序,仍然可以用二分法实现,只不过需要进行两次二分查找。
如果是查找左边界,那么当nums[mid] >= target时,都要继续往左边搜索,end= mid-1。
查找结束后,此时end= start - 1,如果数字存在,与上一题一样,返回end+1即可,不过要注意可能存在找不到的情况,如果end+1溢出边界或nums[end+1]不等于target,直接返回-1数组。
如果是查找右边界,那么当nums[mid] <= target时,都要继续往右边搜索,start= mid+1。
查找结束后,同理,end=start-1,此时需要返回的是右边界,start-1,如果存在找不到的情况,也是start-1溢出边界或者nums[start-1]不等于target。
class Solution {
public int[] searchRange(int[] nums, int target) {
int[] result = new int[]{-1,-1};
int start = 0, end = nums.length-1;
while (start <= end) {
int mid = (end - start) / 2 + start;
if (nums[mid] < target) start = mid + 1;
else end = mid - 1;
}
if (end + 1 < nums.length && nums[end + 1] == target) {
result[0] = end + 1;
} else {
return result;
}
start = 0;
end = nums.length-1;
while (start <= end) {
int mid = (end - start) / 2 + start;
if (nums[mid] <= target) start = mid + 1;
else end = mid - 1;
}
if (start - 1 >= 0 && nums[start - 1] == target) {
result[1] = start - 1;
}
return result;
}
}
注意点
这里写的复杂了一点,思路是一致的,如果是找左边界,最后就取end值,如果找右边界,最后取start值,像随想录一样分开用额外的函数代替会更好看一点,也能避免当数组里不存在结果的时候重复判断的情况。
class Solution {
int[] searchRange(int[] nums, int target) {
int leftBorder = getLeftBorder(nums, target);
int rightBorder = getRightBorder(nums, target);
// 情况一
if (leftBorder == -2 || rightBorder == -2) return new int[]{-1, -1};
// 情况三
if (rightBorder - leftBorder > 1) return new int[]{leftBorder + 1, rightBorder - 1};
// 情况二
return new int[]{-1, -1};
}
int getRightBorder(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
int rightBorder = -2; // 记录一下rightBorder没有被赋值的情况
while (left <= right) {
int middle = left + ((right - left) / 2);
if (nums[middle] > target) {
right = middle - 1;
} else { // 寻找右边界,nums[middle] == target的时候更新left
left = middle + 1;
rightBorder = left;
}
}
return rightBorder;
}
int getLeftBorder(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
int leftBorder = -2; // 记录一下leftBorder没有被赋值的情况
while (left <= right) {
int middle = left + ((right - left) / 2);
if (nums[middle] >= target) { // 寻找左边界,nums[middle] == target的时候更新right
right = middle - 1;
leftBorder = right;
} else {
left = middle + 1;
}
}
return leftBorder;
}
}
69.x 的平方根
题目链接:69.x 的平方根
leetcode题解:69.x 的平方根
解题思路
同样用二分法做,开头是1,结尾为输入值x,然后判断mid*mid与x的大小关系,更新左右边界。思路跟35类似。
但是mid*mid有溢出的风险,无法AC,需要另辟蹊径来做,要么是用long变量存储mid*mid,要么就用替代方式,判断x/mid和mid的关系,也能间接比较大小。
class Solution {
public int mySqrt(int x) {
int left = 1, right = x, ans = 0;
while (left <= right) {
int mid = (right - left)/2 + left;
if (mid > x/mid) {
right = mid - 1;
} else {
ans = mid;
left = mid + 1;
}
}
return ans;
}
}
注意点
如果用long,(long) mid*mid与x比较
如果用x/mid,要注意mid等于0的情况,一开始定义左右边界就把它排除掉。
367.有效的完全平方数
题目链接:367.有效的完全平方数
leetcode题解:367.有效的完全平方数
解题思路
这题其实在前一题的基础上,加了限制,要求是完全平方数,意味着它的平方根是整数,所以可能是找不到结果的,上一题是一定会有一个结果的,需要注意。
如果找到了,mid*mid就等于target,直接返回true即可。否则直到退出循环都找不到,就返回false。
class Solution {
public boolean isPerfectSquare(int num) {
int left = 0, right = num;
while (left <= right) {
int mid = left + (right - left)/2;
long res = (long) mid * mid;
if (res < num) {
left = mid + 1;
} else if (res > num) {
right = mid - 1;
} else {
return true;
}
}
return false;
}
}
注意点
我一开始想沿着前一题的思路,用num/mid和mid的关系做比较,但是发现一个问题,如果要求的数是5,第一次得到的mid是2,5/2=2,直接取整了,所以出错,如果要用除法防止溢出的话,需要用double类型记录num/mid。
class Solution {
public boolean isPerfectSquare(int num) {
int left = 0, right = num;
while (left <= right) {
int mid = left + (right - left)/2;
double res = (double) num/mid;
if (mid < res) {
left = mid + 1;
} else if (mid > res) {
right = mid - 1;
} else {
return true;
}
}
return false;
}
}
今日收获
补了一下一刷没有做的二分法拓展题,有点多,移动元素等下次做吧。
二分法相关题目注意的点有几个:
- 二分法使用条件:已经排序好的数组,或对0-num之间的数字搜索,复杂度一般为O(logn)
- 求边界问题时,一般跳出循环时,end=start-1,根据需要取start或者end返回,最好找几个例子手动试一下,避免出错
- 求乘法时,要注意溢出的问题,需要用long类型或者除法进行替代。