代码随想录算法训练营第一天 | 704. 二分查找,27. 移除元素 [数组篇]

文章介绍了LeetCode中的两道题目,704.二分查找与27.移除元素,详细解析了二分查找的原理、边界处理以及双指针法在移除元素问题中的应用,强调了理解区间定义和指针操作的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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;
}

总结

  1. 一定要搞明白区间的定义,是左闭右闭,还是左闭右开,这决定后面代码的写法。
  2. 两数相加时注意越界、截位、位反转等情况,可以看看这篇文章《地址范围的边界值判断
  3. 在调整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. 时刻要明白每个指针的含义。
  2. 只有在快指针指向的元素不是目标元素时,慢指针才会加1。
  3. 此题也可以在一个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;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值