代码随想录算法训练营第一天 | 704.二分查找、27. 移除元素

466 阅读4分钟

@author LemonCat

@time 2023/7/26

@target 第一章 数组 part01 (qq.com)

今日任务

  • 数组理论基础,704. 二分查找,27. 移除元素

数组理论基础

  • 文章链接:代码随想录 (programmercarl.com)

  • 定义:数组是存放在连续内存空间上的相同类型数据的集合

    • 数组下标都是从 0 开始的。
    • 数组内存空间的地址是连续的
    • 数组中不支持直接删除元素,只能进行覆盖操作
  • 二维数组

    • int [first][second] first 指的是行,second 指的是列
    • 可以想成是行列式

704. 二分查找

题目建议:大家能把 704 掌握就可以,35.搜索插入位置 和 34. 在排序数组中查找元素的第一个和最后一个位置 ,如果有时间就去看一下,没时间可以先不看,二刷的时候在看。

先把 704 写熟练,要熟悉 根据 左闭右开,左闭右闭 两种区间规则 写出来的二分法。

题目链接:leetcode.cn/problems/bi…

文章讲解:programmercarl.com/0704.%E4%BA…

视频讲解:www.bilibili.com/video/BV1fA…

思想

二分法:

  • 思想 - 不断缩小目标范围去寻找目标

  • 前提 - 顺序数组

  • 使用方法

    1. 寻找区间 - 左闭右闭
    2. 寻找区间 - 左闭右开
    3. 也可以有其他方法 - 左开右闭(不常用)

方法一:左闭右闭

  • ​int right = nums.length - 1​​

    • 左闭右闭 因此右边在寻找范围内 即为 nums.length - 1
  • ​left <= right​​ 是因为 left 和 right 相等的时候是合法的

    • 因为区间是 左闭右闭 -> [left, right] => 例如:[1, 1] - 合法的
  • 当 nums[mid]小于目标值的时候 - 目标值在右边 left = mid + 1​​

    • 因为搜索区间是 [left, right],middle 已经不等于目标 -> 这样可以减少重复寻找
  • 当 nums[mid]大于目标值的时候 - 目标值在左边 ​right = mid - 1​​

    • 因为搜索区间是 [left, right],middle 已经不等于目标
  • 若 nums[mid]与目标值相等时,就直接返回中间值

/**
 * 方法一 - 左闭右闭
 */
class Solution {
    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; // 左闭右闭 因此右边在寻找范围内 即为 nums.length - 1

        while (left <= right) { // [left, right]

            int mid = left + ((right - left) >> 1); // 1. 防止溢出 2. 位运算计算速度更快

            if (target < nums[mid]) {
                right = mid - 1;
            } else if (target > nums[mid]) {
                left = mid + 1;
            } else if (target == nums[mid]) {
                return mid;
            }
        }

        return -1;

    }
}
  • 时间复杂度:O(log n)
  • 空间复杂度:O(1)

方法二:左闭右开

  • ​int right = nums.length​​

    • 左闭右开 因此右边不在寻找范围内 即为 nums.length
  • ​left < right​​ left 和 right 相等的时候是不合法的

    • 因为区间是 左闭右开 -> [left, right) => 例如:[1, 1) - 不合法的
  • 当 nums[mid]小于目标值的时候 - 目标值在右边 left = mid​​

    • 因为搜索区间是 [left, right),middle 虽然不等于目标 但由于右边开 本质上搜索区间不包含右侧
  • 当 nums[mid]大于目标值的时候 - 目标值在左边 right = mid - 1​​

    • 因为搜索区间是 [left, right]),middle 已经不等于目标
  • 若 nums[mid]与目标值相等时,就直接返回中间值

class Solution {
    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; // 左闭右开 因此右边不在寻找范围内 即为 nums.length

        while (left < right) { // [left, right)

            int mid = left + ((right - left) >> 1); // 1. 防止溢出 2. 位运算计算速度更快

            if (target < nums[mid]) {
                right = mid;
            } else if (target > nums[mid]) {
                left = mid + 1;
            } else if (target == nums[mid]) {
                return mid;
            }
        }
        return -1;

    }
}
  • 时间复杂度:O(log n)
  • 空间复杂度:O(1)

27. 移除元素

题目建议: 暴力的解法,可以锻炼一下我们的代码实现能力,建议先把暴力写法写一遍。 双指针法 是本题的精髓,今日需要掌握,至于拓展题目可以先不看。

题目链接:leetcode.cn/problems/re…

文章讲解:programmercarl.com/0027.%E7%A7…

视频讲解:www.bilibili.com/video/BV12A…

方法一 - 暴力解法

  • 双层 for 循环

    • 第一个 遍历数组元素
    • 第二个 更新数组 - 将后面的值移动到前面
/**
 * 方法一 - 暴力解法
 */
class Solution {
    public int removeElement(int[] nums, int val) {

        int size = nums.length; // 记录原数组长度(返回的)

        for (int i = 0; i < size; i++) { // 发现需要移除的元素,就将数组集体向前移动一位
            if (nums[i] == val) {
                // 第二个for循环,将后续数组集体前移 - nums[j] = nums[j + 1]
            	// 即当 j 初始为 i - 注意 j < size - 1 否则会数组下标越界 OOE
                for (int j = i; j < size - 1; j++) {
                    nums[j] = nums[j + 1];
                }
                i--; // 因为下标 i 以后的数值都向前移动了一位,所以 i 也向前移动一位 否则则会跳过一个值
                size--; // 此时数组的大小-1 - 因为向前移动了
            }
        }
        return size;
    }
}
  • 时间复杂度:O(n^2)
  • 空间复杂度:O(1)

方法二 - 双指针法

  • 双指针思路:

    • 快指针 遍历数组
    • 慢指针 只有遍历到不是目标值时向前移动 并将我们需要的数组 快指针的值 替换 慢指针的值
  • 双指针法(快慢指针法): 通过一个快指针和慢指针在一个 for 循环下完成两个 for 循环的工作

    定义快慢指针

    • 快指针:寻找新数组的元素 - 新数组就是不含有目标元素的数组
    • 慢指针:指向要更新的元素 - 新数组下标的位置
/**
 * 方法二 - 双指针
 */
class Solution {
    public int removeElement(int[] nums, int val) {

        int fast = 0; // 快指针
        int slow = 0; // 慢指针

        for (int i = 0; i < nums.length; i++) {

            if (nums[i] != val) { // 若果不是要移出的值 - 快指针值 赋给 慢指针值 慢指针++
                nums[slow] = nums[fast];
                slow++;
            }
            fast++; // 无论什么条件 快指针永远向前进

        }
        return slow;
    }
}
  • 我们发现 上面的代码有很多逻辑重复的地方 -> 对上面代码进行优化

    1. 遍历循环的索引 i 本质就是 fast 快指针
    2. 慢指针 ++ 的过程 可以放到数组中 -> 代码更加简洁
/**
 * 方法二 - 双指针 PLUS
 */
class Solution {
    public int removeElement(int[] nums, int val) {

        int slow = 0; // 慢指针

        for (int fast = 0; fast < nums.length; fast++) { // 快指针
            if (nums[fast] != val) { // 若果不是要移出的值 - 快指针值 赋给 慢指针值 慢指针++
                nums[slow++] = nums[fast];
            }
        }
        return slow;
    }
}
  • 结束 - 门前大桥下 游过一群鸭 快来快来数一数 全是柯尔鸭 ~ 明天继续加油!