Day 1 - 数组

本文详细介绍了二分查找算法的原理、C++和Python代码实现,以及左闭右闭和左闭右开两种情况的区别。同时讨论了移除元素问题的双指针解决方案,包括基础版、优化版和暴力法,强调了不同方法的时间和空间复杂度。
摘要由CSDN通过智能技术生成

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

704. 二分查找

文档讲解:代码随想录 | 数组 | 二分查找
视频讲解:手把手带你撕出正确的二分法 | 二分查找法 | 二分搜索法 | LeetCode:704. 二分查找
状态:第一遍一写就废,看了讲解后明白了!

题目链接:704. 二分查找
解题思路:二分查找

  • 前提:数组严格升序/降序,必须无重复元素
  • 两种思路:左闭右闭 [left, right] + 左闭右开 [left, right)
  • 注意要点:应严格遵循循环/区间不变量原则
  • 时间复杂度: O ( l o g n ) O(log n) O(logn)
  • 空间复杂度: O ( 1 ) O(1) O(1)

二分法:左闭右闭

思路
  1. 定义查找空间: 左闭右闭写法,我们定义 target 在一个左闭右闭的区间里 [ l e f t , r i g h t ] [left, right] [left,right] 中,这个区间的定义十分重要!
  2. 循环二分,缩窄查找区间:使用向下取整除法,定义 middle = left + ((right - left) / 2)
    因为定义 target [ l e f t , r i g h t ] [left, right] [left,right] 区间,所以有如下几点:
    • 因为 l e f t = r i g h t left = right left=right 有意义,所以 while (left <= right) 要使用 <= ,才能保证查找区间完整
    • nums[middle] > target 时( target左区间 [ l e f t , m i d d l e ] [left, middle] [left,middle] 时),更新查找左区间右边界 right ;由判断条件可知当前 nums[middle] 一定不是 target,则左区间不能包含 m i d d l e middle middle,下一轮循环的查找区间结束下标位置应为 m i d d l e − 1 middle - 1 middle1,即 right 应赋值为 m i d d l e − 1 middle - 1 middle1
    • nums[middle] < target 时( target右区间 [ m i d d l e , r i g h t ] [middle, right] [middle,right] 时),更新查找右区间左边界 left ;由判断条件可知当前 nums[middle] 一定不是 target,则右区间不能包含 m i d d l e middle middle,下一轮循环的查找区间开始下标位置就是 m i d d l e + 1 middle + 1 middle+1,即 left 应赋值为 m i d d l e + 1 middle + 1 middle+1
    • nums[middle] == target ,说明找到 target ,返回索引 middle 即可
  3. 不满足 l e f t < = r i g h t left <= right left<=right 时跳出循环,此时代表无法在数组中找到 target ,因此返回 − 1 -1 1
代码

C++ 代码如下:

// 版本一:左闭右闭
class Solution {
public:
    int search(vector<int>& nums, int target) {
        int left = 0, right = nums.size() - 1; // 定义target在左闭右闭的区间里,[left, right]
        while (left <= right) { // 当left==right,区间[left, right]依然有效,所以用 <=
            int middle = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2
            if (nums[middle] > target) {
                right = middle - 1; // target 在左区间,所以[left, middle - 1]
            } else if (nums[middle] < target) {
                left = middle + 1; // target 在右区间,所以[middle + 1, right]
            } else { // nums[middle] == target
                return middle; // 数组中找到目标值,直接返回下标
            }
        }
        // 未找到目标值
        return -1;
    }
};

Python 代码如下:

class Solution:
    def search(self, nums: List[int], target: int) -> int:
        left, right = 0, len(nums) - 1  # 定义target在左闭右闭的区间里,[left, right]

        while left <= right:
            middle = left + (right - left) // 2
            
            if nums[middle] > target:
                right = middle - 1  # target在左区间,所以[left, middle - 1]
            elif nums[middle] < target:
                left = middle + 1  # target在右区间,所以[middle + 1, right]
            else:
                return middle  # 数组中找到目标值,直接返回下标
        return -1  # 未找到目标值

二分法:左闭右开

思路

左闭右开思路与左闭右闭仅有三个地方有差异:

  1. target 定义在一个左闭右开的区间里 [ l e f t , r i g h t ) [left, right) [left,right) 中,因此为了遍历整个数组,右边界 right 初始值从 nums.size() - 1 变为 nums.size()
  2. target [ l e f t , r i g h t ] [left, right] [left,right] 区间,因此 l e f t = r i g h t left = right left=right 无意义,所以 while (left < right) 要使用 <
  3. nums[middle] > target 时(即 target左区间时),在 [ l e f t , m i d d l e ) [left, middle) [left,middle) 中,因为查找区间始终为 [ l e f t , r i g h t ) [left, right) [left,right) ,因此 right 应赋值为 m i d d l e middle middle
代码

C++ 代码如下:

// 版本二:左闭右开
class Solution {
public:
    int search(vector<int>& nums, int target) {
        int left = 0, right = nums.size(); // 定义target在左闭右开的区间里,即:[left, right)
        while (left < right) { // 因为left == right的时候,在[left, right)是无效的空间,所以使用 <
            int middle = left + ((right - left) >> 1);
            if (nums[middle] > target) {
                right = middle; // target 在左区间,在[left, middle)中
            } else if (nums[middle] < target) {
                left = middle + 1; // target 在右区间,在[middle + 1, right)中
            } else { // nums[middle] == target
                return middle; // 数组中找到目标值,直接返回下标
            }
        }
        // 未找到目标值
        return -1;
    }
};

Python 代码如下:

class Solution:
    def search(self, nums: List[int], target: int) -> int:
        left, right = 0, len(nums)  # 定义target在左闭右开的区间里,即:[left, right)

        while left < right:  # 因为left == right的时候,在[left, right)是无效的空间,所以使用 <
            middle = left + (right - left) // 2

            if nums[middle] > target:
                right = middle  # target 在左区间,在[left, middle)中
            elif nums[middle] < target:
                left = middle + 1  # target 在右区间,在[middle + 1, right)中
            else:
                return middle  # 数组中找到目标值,直接返回下标
        return -1  # 未找到目标值

注意要点

  1. middle 的写法问题:使用 middle = left + ((right - left) / 2) 可等效替代 (left + right)/2 ,且可防止数值溢出;此外对于整型的正数之间,/ 也可由右移运算符 << 等效替换,不过用位运算的好处是比直接相除的操作更快
  2. 对区间的定义不同,影响的有三个地方
    • 指针的初始化
    • 循环结束条件
    • 指针更新方式
  3. 二分的最大优势在于其时间复杂度为 O ( l o g n ) O(log n) O(logn)当看到有序数组都要第一时间反问自己是否可以使用二分!
  4. 二分的应用场景很多且有着许多变体,比如说查找第一个大于 target 的元素或者第一个满足条件的元素,本质原理都是一样的,根据是否满足题目的条件来缩小答案所在的区间,这个就是二分的本质
  5. 思维应当灵活变通,要知道二分的输入不一定是数组,也可以是数组中某一区间的起始位置和终止位置

相关题目推荐

待补充

27. 移除元素

文档讲解:代码随想录 | 数组 | 移除元素
视频讲解:数组中移除元素并不容易! | LeetCode:27. 移除元素
状态:第一遍写个暴力搜索困难重重,纯菜比www根本没想到双指针法

题目链接:27. 移除元素
题目分析: 此题要求仅使用 O ( 1 ) O(1) O(1) 额外空间完成,因此只能在原数组上进行操作,最后返回一个长度,不过元素顺序是可以改变的
解题思路-1:双指针法

  • 优势:不会改变元素顺序
  • 时间复杂度: O ( n ) O(n) O(n) ,其中 n n n 为序列长度,且我们只需要遍历该序列至多两次
  • 空间复杂度: O ( 1 ) O(1) O(1)

解题思路-2:双指针法优化

  • 优势:时间复杂度优于思路1,两个指针在最坏的情况下合起来只遍历了数组一次
  • 劣势:改变了元素的顺序(不过本题允许改变元素顺序)
  • 时间复杂度: O ( n ) O(n) O(n) ,其中 n n n 为序列长度,且我们只需要遍历该序列至多一次
  • 空间复杂度: O ( 1 ) O(1) O(1)

解题思路-3:暴力法

  • 时间复杂度: O ( n 2 ) O(n^2) O(n2) ,其中 n n n 为序列长度
  • 空间复杂度: O ( 1 ) O(1) O(1)

双指针法

双指针法(快慢指针法): 通过一个快指针和慢指针在一个for循环下完成两个for循环的工作

思路
  1. 定义快慢指针
    • 快指针:寻找新数组的元素 ,新数组就是不含有目标元素(需删除值)的数组
    • 慢指针:指向更新后新数组下标的位置
  2. 快指针先移动,如果不是目标元素,快指针指向值赋值给慢指针指向位置,慢指针移动;如果是目标元素,则慢指针不操作,快指针继续移动
  3. 返回慢指针最后指向的下标,就是新数组的大小
代码

C++ 代码如下:

// 时间复杂度:O(n)
// 空间复杂度:O(1)
class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int slowIndex = 0;
        for (int fastIndex = 0; fastIndex < nums.size(); fastIndex++) {
            if (val != nums[fastIndex]) {
                nums[slowIndex++] = nums[fastIndex];
            }
        }
        return slowIndex;
    }
};

Python 代码如下:

# (版本一)快慢指针法
class Solution:
    def removeElement(self, nums: List[int], val: int) -> int:
        # 快慢指针
        fast = 0  # 快指针
        slow = 0  # 慢指针
        size = len(nums)
        while fast < size:  # 不加等于是因为,a = size 时,nums[a] 会越界
            # slow 用来收集不等于 val 的值,如果 fast 对应值不等于 val,则把它与 slow 替换
            if nums[fast] != val:
                nums[slow] = nums[fast]
                slow += 1
            fast += 1
        return slow

双指针法优化

思路
  1. 依然使用双指针,但两个指针初始时分别位于数组的首尾,向中间移动遍历该序列
  2. 如果左指针 left 指向的元素等于 val,此时将右指针 right 指向的元素复制到左指针 left 的位置,然后右指针 right 左移一位。如果赋值过来的元素恰好也等于 val,可以继续把右指针 right指向的元素的值赋值过来(左指针 left 指向的等于 val 的元素的位置继续被覆盖),直到左指针指向的元素的值不等于 val 为止
代码

C++ 代码如下:

// 时间复杂度优于双指针法,两个指针在最坏的情况下合起来只遍历了数组一次
// 左闭右闭写法
class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int left = 0, right = nums.size() - 1;
        while (left <= right) {
            if (nums[left] == val) {
                nums[left] = nums[right--];
            } else {
                left++;
            }
        }
        return left;
    }
};

暴力法

思路

两层 for 循环,第一层 for 循环遍历数组元素,第二个 for 循环用来更新数组;每当第一层循环找到需要删除的目标值 val,第二层循环将后面元素前移一位

代码

C++ 代码如下:

// 时间复杂度:O(n^2)
// 空间复杂度:O(1)
class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int size = nums.size();
        for (int i = 0; i < size; i++) {
            if (nums[i] == val) { // 发现需要移除的元素,就将数组集体向前移动一位
                for (int j = i + 1; j < size; j++) {
                    nums[j - 1] = nums[j];
                }
                i--; // 因为下标i以后的数值都向前移动了一位,所以i也向前移动一位
                size--; // 此时数组的大小-1
            }
        }
        return size;
    }
};

Python 代码如下:

# (版本三)暴力法
class Solution:
    def removeElement(self, nums: List[int], val: int) -> int:
        i, l = 0, len(nums)
        while i < l:
            if nums[i] == val: # 找到等于目标值的节点
                for j in range(i+1, l): # 移除该元素,并将后面元素向前平移
                    nums[j - 1] = nums[j]
                l -= 1
                i -= 1
            i += 1
        return l

注意要点

  1. 移除元素暴力法易错点:发现需要移除的元素 val 并将数组集体向前移一位后,由于下标 i i i 以后的数值都向前移动了一位,所以 i i i 需要向前移动一位,且 i i i 的循环次数这里是 size 也需要减一
  2. 暴力法中发现需要移除的元素 val 只需要将数组集体向前移一位:即 nums[j - 1] = nums[j];不需要申明临时变量 temp 做多余的交换操作!

相关题目推荐

待补充

  • 11
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值