代码随想录算法训练营第一天
LeetCode 704. 二分查找
题目链接:704. 二分查找
文章讲解:代码随想录#704. 二分查找
视频讲解:手把手带你撕出正确的二分法
题目描述
给定一个 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
思路
二分查找法的两个前提条件:有序数组,元素无重复
使用二分法之前,必须得搞明白边界问题,也就是区间问题。
平时遇到最多的两种区间:左闭右闭即[left, right],或者左闭右开即[left, right),※其他组合也可以解决问题
本题使用左闭右闭即[left, right],需要注意以下几点
- 因为是闭区间,区间范围为[0, numsSize - 1],所以right的初值为numsSize - 1
- 循环条件中left与right存在两者相等的情况,所以while中要使用<=
- 当中间元素middle的值大于目标值target时,需要更改right变量,由于middle已经比较过了,所以right应该等于middle-1,left同理
左闭右开可以参考代码随想录#704. 二分查找,强烈推荐!!!
参考代码
int search(int* nums, int numsSize, int target) {
// 左闭右闭的区间[left, right]
int middle;
int left = 0;
int right = numsSize - 1; // 因为是闭区间,所以整个区间为[0, numsSize-1]
while (left <= right) { // 因为是左开右开区间,所以存在left=right的情况
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;
}
总结
- 一定要搞明白区间的定义,是左闭右闭,还是左闭右开,这决定后面代码的写法。
- 两数相加时注意越界、截位、位反转等情况,可以看看这篇文章《地址范围的边界值判断》
- 在调整left/right时,一定要想明白到底是middle还是middle±1
扩展
上面的while中的判断有三个条件,即mid>target,mid<target,mid==target,这种情况默认当前数组中包含目标值。
如果数组中不包含目标值时,需要求出目标值数值中合适的位置时,可以将while中的判断缩减成两个,即 mid>=target,和 mid<target,可以求出大于等于目标值的下标。
类型1
int findIdx(int *nums, int size, int target)
{
int left = 0;
int right = size - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] >= target) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return left;
}
结果
输入: {-2,-1,-1,1,2,3}
int p1 = findIdx(nums, numsSize, 0);
输出: p1 = 3
int p2 = findIdx(nums, numsSize, 1);
输出: p2 = 3
输入: {-3,-2,-1,0,0,1,2}
int p1 = findIdx(nums, numsSize, 0);
输出: p1 = 3
int p2 = findIdx(nums, numsSize, 1);
输出: p2 = 5
类型2
int findIdx(int *nums, int size, int target)
{
int left = 0;
int right = size - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] > target) { // 注意:此处没有等于号
right = mid - 1;
} else {
left = mid + 1;
}
}
return left;
}
结果
输入: {-2,-1,-1,1,2,3}
int p1 = findIdx(nums, numsSize, 0);
输出: p1 = 3
int p2 = findIdx(nums, numsSize, 1);
输出: p2 = 4
输入: {-3,-2,-1,0,0,1,2}
int p1 = findIdx(nums, numsSize, 0);
输出: p1 = 5
int p2 = findIdx(nums, numsSize, 1);
输出: p2 = 6
相关题目
相关1:2529. 正整数和负整数的最大计数
题目链接:2529. 正整数和负整数的最大计数
题目描述
给你一个按非递减顺序排列的数组 nums ,返回正整数数目和负整数数目中的最大值。
- 换句话讲,如果 nums 中正整数的数目是 pos ,而负整数的数目是 neg ,返回 pos 和 neg二者中的最大值。
- 注意:0 既不是正整数也不是负整数。
示例
输入:nums = [-3,-2,-1,0,0,1,2]
输出:3
解释:共有 2 个正整数和 3 个负整数。计数得到的最大值是 3
思路
在一个非递减顺序的数组中,已知0既不是正整数也不是负整数,分别求出正数和负数个数,然后再判断出这两个数的最大值。
①需要计算出数组中大于等于0的元素索引,即负整数的个数。
②需要计算出数组中大于等于1的元素索引,也就是说,从该索引之后皆为正整数。
③用元素个数减去大于等于1的元素索引,即正整数的个数。
代码
int findIdx(int *nums, int size, int target)
{
int left = 0;
int right = size - 1;
while (left <= right) { // 闭区间[left, right]
int mid = left + (right - left) / 2;
// 此处必须是大于等于,这样就可以计算出大于等于目标值的索引,因为最终返回的是left
if (nums[mid] >= target) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return left;
}
int maximumCount(int* nums, int numsSize) {
int p1 = findIdx(nums, numsSize, 0);
int p2 = numsSize - findIdx(nums, numsSize, 1);
return p1 > p2 ? p1 : p2;
}
相关2:35.搜索插入位置
题目链接:35.搜索插入位置
文章讲解:代码随想录#35.搜索插入位置
题目描述
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。
如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n) 的算法。
示例1
输入: nums = [1,3,5,6], target = 5
输出: 2
示例 2
输入: nums = [1,3,5,6], target = 2
输出: 1
思路
与上面的题是一样的,在while循环中需要用大于等于target来比较
代码
int searchInsert(int* nums, int numsSize, int target) {
int left = 0;
int right = numsSize - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] >= target) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return left;
}
LeetCode 27. 移除元素
题目链接:27. 移除元素
文章讲解:代码随想录#27. 移除元素
视频讲解:数组中移除元素并不容易!
题目描述
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
示例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],也会被视作正确答案。
示例2
输入: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。注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素。
思路
本题的解法比较多,可以使用暴力解法,需要用两层for循环,外层for循环用来遍历整个数组,里面的for循环用来调整数组(因为在数组中不能直接删除数组中的元素),这样的话时间复杂比较大。
本题也可以使用双指针法,这样只需要一层for循环,通过一个快指针和慢指针在一个for循环下完成移除元素,时间复杂度比较低。
想要使用快指针和慢指针,需要搞清楚它俩的含义。
- 快指针遍历的是不包含目标元素的数组,遇到目标元素后直接跳过向后遍历
- 慢指针用来构造新数组,新数组中不会包含目标元素
参考代码
int removeElement(int* nums, int numsSize, int val) {
int fast, slow;
slow = 0;
for (fast = 0; fast < numsSize; fast++) {
if (nums[fast] != val) { // 遍历目标元素以外的元素,遇到目标元素后直接跳过
nums[slow++] = nums[fast]; // 慢指针用来构建新的不包含目标的数组
}
}
return slow;
}
总结
- 时刻要明白每个指针的含义。
- 只有在快指针指向的元素不是目标元素时,慢指针才会加1。
- 此题也可以在一个for循环下将目标元素标记成负数的办法,同时将元素个数减1,这样不使用双指针也可以实现,不过不推荐。
相关题目
相关1:26. 删除有序数组中的重复项
题目链接:26. 删除有序数组中的重复项
题目描述
给你一个 非严格递增排列 的数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums 中唯一元素的个数。
考虑 nums 的唯一元素的数量为 k ,你需要做以下事情确保你的题解可以被通过:
- 更改数组 nums ,使 nums 的前 k 个元素包含唯一元素,并按照它们最初在 nums 中出现的顺序排列。nums 的其余元素与 nums 的大小不重要。
- 返回 k 。
示例
输入:nums = [1,1,2]
输出:2, nums = [1,2,_]
解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。
思路
采用双指针,并且用一个target记录上一个元素的值,如果下一个元素的值与target相同,则快指针向后移动一次,如果下一个元素的值与target不相同,则更新target的值,并且将值更新到数组中,同时慢指针加1。
代码
int removeDuplicates(int* nums, int numsSize) {
int target = nums[0];
int slow = 1;
int fast = 1;
for (; fast < numsSize; fast++) {
if (nums[fast] != target) {
target = nums[fast];
nums[slow++] = target;
}
}
return slow;
}
相关2:283. 移动零
题目链接:283. 移动零
题目描述
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
示例
输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]
思路
采用双指针,在数组中移动非0的元素,并记录0的个数,然后在数组的末尾补0就可以了
代码
void moveZeroes(int* nums, int numsSize) {
int cnt = 0;
int slow = 0;
int fast = 0;
for (; fast < numsSize; fast++) {
if (nums[fast] != 0) {
nums[slow++] = nums[fast];
} else {
cnt++;
}
}
while (cnt--) {
nums[slow++] = 0;
}
}