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

704. 二分查找

文档讲解:704. 二分查找
视频讲解:《代码随想录》算法视频公开课:手把手带你撕出正确的二分法

leetcode 题目链接

本题考查基础的二分查找,和暴力解法不同,根据数组的有序性,折半地缩小查找空间,达到快速查找的目的。

二分查找的思想很简单,但是在处理边界上容易出问题,归根结底是对搜索空间的定义不明确,导致查找时漏掉或者多加了元素。

常用的查找空间有两种,一种是左闭右闭,一种是左闭右开,现在分别对这两种实现方法做详细的实现。

左闭右闭

即区间内的元素全是未确定的元素,确定的元素不会在这里面(如要目标元素和一定不是目标的元素)。

class Solution {
    public int search(int[] nums, int target) {
        // 因为数组是有序的,所以小于最小值或大于最大值的元素一定不在数组中
        if (target < nums[0] || target > nums[nums.length - 1]) return -1;

        // 注意:左闭右闭区间,所以 right 起始为数组的最后一个元素
        int left = 0;
        int right = nums.length - 1;

        // 注意:这里是小于等于,因为 left == right 时,搜索区间只有一个元素,仍然是合法的搜索区间,可能这一个元素就是目标值
        while (left <= right) {
            // 获取中间值,避免大数相加溢出
            int mid = left + (right - left) / 2;

            if (nums[mid] == target) {
                return mid;
            } else if (nums[mid] < target) {
                // 目标值在右边,要舍弃另一半查找空间,因为 nums[mid] 已经确定不是目标值,所以 mid 不应该被包含在区间内,因此 left 直接取 mid 的下一个位置
                left = mid + 1;
            } else if (nums[mid] > target) {
                // 目标值在左边,要舍弃另一半查找空间,因为 nums[mid] 已经确定不是目标值,所以 mid 不应该被包含在区间内,因此 right 直接取 mid 的前一个位置
                right = mid - 1;
            }
        }

        return -1;
    }
}

左闭右开

区间的定义和左闭右闭是一样的,但是因为右边是开区间,所以对 right 指针的处理会有些不同。

class Solution {
    public int search(int[] nums, int target) {
        // 因为数组是有序的,所以小于最小值或大于最大值的元素一定不在数组中
        if (target < nums[0] || target > nums[nums.length - 1]) return -1;

        // 注意:左闭右开区间,right 指向的元素并不在区间内,因此 right 起始为 nums.length
        int left = 0;
        int right = nums.length;

        // 注意:这里是小于,因为是左闭右开区间,当 left == right 时,搜索区间为空,不应该再去循环了
        while (left < right) {
            // 获取中间值,避免大数相加溢出
            int mid = left + (right - left) / 2;

            if (nums[mid] == target) {
                return mid;
            } else if (nums[mid] < target) {
                // 和左闭右闭一样
                left = mid + 1;
            } else if (nums[mid] > target) {
                // 因为是左闭右开区间,right 指向的是不在区间内的元素,此时的 nums[mid] 刚确定完不是目标值,要从区间内排除,因此将 mid 赋值给 right
                // 且 mid - 1 可能是目标值,因此需要在区间内
                right = mid;
            }
        }

        return -1;
    }
}

27. 移除元素

文档讲解:27. 移除元素
视频讲解:《代码随想录》算法视频公开课:数组中移除元素并不容易

leetcode 题目链接

题目要求在原数组上修改,所以是在考察数组的基本操作,因此只能采用覆盖的方法,因为数组的长度是固定的。

暴力解法

双层 for 循环遍历元素,如果是目标值,就将目标值后面的元素挨个往前覆盖,要注意循环结束的条件。

class Solution {
    public int removeElement(int[] nums, int val) {
        int len = nums.length;

        // 随着目标值不断的覆盖,遍历的终点在不断前移,因此要 "提前" 结束循环,不能再遍历到 nums.length
        for (int i = 0; i < len; i++) {
            if (nums[i] == val) {
                // 找到目标值,后面的元素往前覆盖,注意内层循环的结束条件是 len - 1,防止访问 j + 1 时越界
                for (int j = i; j < len - 1; j++) {
                    nums[j] = nums[j + 1];
                }
                // 重点:删掉一个元素,长度 - 1
                len--;
                // 重点:因为当前元素已经被后面的元素覆盖了,此时的 i 指向的是覆盖的那个元素,下一轮循环还要对它做判断,所以 i 回退一次
                i--;
            }
        }

        // 遍历结束,len 就是删除指定元素后剩余元素的数量
        return len;
    }
}

双指针

暴力解法的时间复杂度是 O(n^2),时间花费在了挨个移动元素上,所以优化的重点就在这里,能否一次遍历解决问题。

双指针的思路是定义两个指针,初始都指向数组的第一个元素,left 指针表示该指针之前的元素都是非目标值,right 指针用来遍历数组,如果当前遍历的元素不是目标值,则将nums[right] 赋值给 nums[left],且 left、right 同时后移,如果当前遍历的元素是目标值,则 left 停顿,right 继续后移,重复上述循环。

class Solution {
    public int removeElement(int[] nums, int val) {
        int left = 0, right = 0;

        while (right < nums.length) {
            // 重申一下,left 表示该位置之前的元素都是要留下的元素,不包含要删除的元素
            // 所以这里要处理不等于目标值的元素
            if (nums[right] != val) {
                nums[left] = nums[right];
                left++;
            }
            right++;
        }

        return left;
    }
}

这个双指针的思想很巧妙,一个指针用来控场(存储),一个指针用来遍历,符合条件的元素就扔进存储中,存储不断扩大,left 不断后移。

posted @ 2024-01-24 22:17  狐耳  阅读(12)  评论(0编辑  收藏  举报