LeetCode Day1

648 阅读6分钟

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 提示:

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

经典的二分查找题,通常我们遇到二分查找题的时候会有以下基本思路:

  1. 初始化两个指针:left 和 right,分别指向数组的第一个元素和最后一个元素。
  2. 计算中间位置:mid = (left + right) / 2。
  3. 比较中间元素与目标值:
    • 如果 nums[mid] 等于 target,则返回 mid。
    • 如果 nums[mid] 小于 target,说明目标值在 mid 的右侧,因此将 left 设置为 mid + 1。
    • 如果 nums[mid] 大于 target,说明目标值在 mid 的左侧,因此将 right 设置为 mid - 1。
  4. 重复上述步骤,直到 left 大于 right,这意味着目标值不存在于数组中,返回-1。

但此时需要明确一个问题:区间的开闭如何选择,这决定我们在处理边界的时候要做什么样的操作,一旦确定不做更改。我采用了全闭区间的方式

以下是题解

class Solution {
public:
    int search(vector<int>& nums, int target) {
        // 初始化左右边界
        int left=0,right=nums.size()-1;
        
        // 计算初始中间位置,使用 (right - left) / 2 避免整数溢出
        int mid=left + ((right - left) / 2);
        
        // 当左边界不大于右边界时,继续查找
        while(left<=right){
            // 如果目标值小于中间值,更新右边界并重新计算中间位置
            if(target<nums[mid]){
                right=mid-1;
                mid=left + ((right - left) / 2);
            }
            // 如果目标值大于中间值,更新左边界并重新计算中间位置
            else if(target>nums[mid]){
                left=mid+1;
                mid=left + ((right - left) / 2);
            }
            // 如果目标值等于中间值,返回中间位置
            else
                return mid;
        }
        
        // 如果循环结束还没有找到目标值,返回-1
        return -1;
    }
};

我使用mid=left + ((right - left) / 2)代替了mid=(right + left) / 2,有时候可能不太容易想到这个等价的式子,下面是思路: 为什么这样做?leftright 都是大整数时,直接相加可能会导致整数溢出。例如,在32位整数环境中,如果 leftright 都接近 INT_MAX,那么 left + right 就会溢出。 通过使用 left + (right - left) / 2,我们避免了这种直接相加的情况,从而避免了整数溢出的风险。 让我们从数学上深入理解这个表达式:

  1. 传统的平均值计算

1694007334937.png

  1. 展开这个平均值

1694007316447.png 这就是我们的表达式 left + (right - left) / 2

27.移除元素

给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。 不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并原地修改输入数组。 元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。 示例 1: 给定 nums = [3,2,2,3], val = 3, 函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。 你不需要考虑数组中超出新长度后面的元素。 示例 2: 给定 nums = [0,1,2,2,3,0,4,2], val = 2, 函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。 你不需要考虑数组中超出新长度后面的元素。

这个问题可以通过双指针技术来解决。我们可以使用一个慢指针来跟踪新数组的长度,和一个快指针来遍历原数组。以下是基本思路:

  1. 初始化两个指针:i = 0(慢指针)和 j = 0(快指针),其中 i 用于跟踪新数组的长度,j 用于遍历原数组。
  2. 遍历数组:使用快指针 j 遍历数组,检查每个元素是否等于给定的值 val。
  3. 元素不等于给定值:如果 nums[j] 不等于 val,我们将 nums[j] 复制到 nums[i] 的位置,并将慢指针 i 增加 1。
  4. 元素等于给定值:如果 nums[j] 等于 val,我们什么都不做,只是将快指针 j 增加 1,以检查下一个元素。
  5. 返回新数组的长度:遍历完成后,i 将表示新数组的长度。我们返回 i 作为结果。

题解如下:

class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        // 初始化慢指针 slow,用于跟踪新数组的长度
        int slow = 0;

        // 使用快指针 fast 遍历数组
        for (int fast = 0; fast < nums.size(); fast++) {
            // 如果当前元素不等于给定值
            if (nums[fast] != val) {
                // 将当前元素复制到慢指针的位置
                nums[slow] = nums[fast];
                // 增加慢指针的值,表示新数组的长度增加了
                slow++;
            }
            // 如果当前元素等于给定值,快指针继续前进,不做任何操作
        }

        // 返回新数组的长度
        return slow;
    }
};

同样可以使用快慢指针法的题型:

  1. 判断链表是否有环
  • 使用两个指针,一个快指针每次移动两步,一个慢指针每次移动一步。如果链表中存在环,那么快指针最终会追上慢指针。
  1. 找到链表的中点
  • 同样使用快慢指针,当快指针到达链表的末尾时,慢指针恰好在链表的中点。
  1. 找到链表的倒数第 k 个节点
  • 使用两个指针,先让快指针移动 k 步,然后快慢指针同时移动,当快指针到达链表末尾时,慢指针指向的就是倒数第 k 个节点。
  1. 数组中找到两个数的和为给定值
  • 对于有序数组,使用两个指针从数组的两端开始,根据两指针指向的数字之和与目标值的大小关系来移动指针。
  1. 移除数组中的重复元素
  • 使用两个指针,一个指针用于遍历数组,另一个指针用于指向下一个非重复元素应该插入的位置。
  1. 找到数组中的最大/最小子数组/子序列
  • 使用两个指针来定义子数组/子序列的开始和结束位置。
  1. 滑动窗口问题
  • 例如,找到数组中和为给定值的连续子数组,或找到数组中的最小子数组长度等问题。