代码随想录算法训练营 DAY1 |二分查找和双指针

今天,我正式开启了「代码随想录」的刷题之旅!第一章就是数据结构的基石——数组。说实话,数组既熟悉又陌生,总觉得它很简单,但深究起来,无论是内存原理还是各种花式操作,都大有文章。

按照计划,今天的核心任务是掌握数组的理论基础,并死磕几道经典的 LeetCode 题目。我的目标很明确:不仅要AC,更要理解通透,尤其是二分查找和双指针这两种神级技巧。

一、热身:数组理论基础

开动之前,我先花时间阅读了 Carl 哥关于数组理论的文章。数组在内存中是连续存储的,这个特性是它能够支持快速随机访问(通过索引)的根本原因,也是二分查找等算法高效执行的前提。

  • 学习感悟: 以前觉得数组简单,但现在意识到,理解它的内存模型对于写出高性能代码至关重要。
  • 理论指南: 代码随想录-数组理论基础

二、主线任务:啃下三道硬骨头

1. LeetCode 704. 二分查找

这是今天的重头戏。二分查找听起来简单,但细节是魔鬼,尤其是区间的开闭问题,很容易让人头晕。今天的目标就是要把 左闭右闭 [left, right]左闭右开 [left, right) 两种写法都彻底掌握,形成肌肉记忆。

我的实现 1:左闭右闭 [left, right]

这种方式最直观,while 循环条件是 left <= right,因为当 left == right 时,区间 [left, left] 依然有效。

class Solution:
    def search(self, nums: List[int], target: int) -> int:
        left, right = 0, len(nums) - 1  # 定义搜索区间 [left, right]
        while left <= right:
            mid = (right - left) // 2 + left
            if nums[mid] < target:
                left = mid + 1
            elif nums[mid] > target:
                right = mid - 1
            else:
                return mid
        return -1
我的实现 2:左闭右开 [left, right)

这种方式下 right 指向的是开区间边界,所以 while 循环条件是 left < right。当 nums[mid] > target 时,right 要更新为 mid,因为新的搜索区间是 [left, mid),不包含 mid

class Solution:
    def search(self, nums: List[int], target: int) -> int:
        left, right = 0, len(nums)  # 定义搜索区间 [left, right)
        while left < right:
            mid = (right - left) // 2 + left
            if nums[mid] < target:
                left = mid + 1
            elif nums[mid] > target:
                right = mid
            else:
                return mid
        return -1
2. LeetCode 27. 移除元素

这道题是双指针的入门经典。题目要求原地移除元素,并返回新数组的长度。

我的实现 1:快慢指针法 (推荐)

我用了快慢指针,slow 指针指向下一个非目标元素要填充的位置,fast 指针负责探索整个数组。

class Solution:
    def removeElement(self, nums: List[int], val: int) -> int:
        # l 是慢指针,i 是快指针
        l = 0
        for i in range(len(nums)):
            if nums[i] != val:
                nums[l] = nums[i]
                l += 1
        return l

(注:上面这种写法可以当成“栈”或“循环不变量”来看,是最高效、最通用的解法)

我的实现 2:暴力解法

while 循环和 del 操作,代码很直接,但在列表/数组中删除元素是个耗时操作。

class Solution:
    def removeElement(self, nums: List[int], val: int) -> int:
        i = 0
        while i < len(nums):
            if nums[i] == val:
                del nums[i]  # 列表删除操作,时间复杂度较高
            else:
                i += 1
        return len(nums)
我的实现 3:双指针优化(头尾指针)

这种方法也很有趣,当 left 指针找到目标值 val 时,就把它和 right 指针指向的元素交换,然后 right 指针前移。这避免了数据的整体移动。

class Solution:
    def removeElement(self, nums: List[int], val: int) -> int:
        left, right = 0, len(nums) - 1
        while left <= right:
            # 从左边找第一个等于 val 的元素
            while left <= right and nums[left] != val:
                left += 1
            
            # 从右边找第一个不等于 val 的元素
            while left <= right and nums[right] == val:
                right -= 1
            
            # 交换并移动指针
            if left < right:
                nums[left] = nums[right]
                left += 1
                right -= 1
        return left
3. LeetCode 977. 有序数组的平方

这道题完美地展示了双指针思想的威力。一个非递减数组,平方后最大的数只可能出现在原数组的两端。

我的实现:双指针(头尾指针)
class Solution:
    def sortedSquares(self, nums: List[int]) -> List[int]:
        n = len(nums)
        result = [0] * n
        left, right = 0, n - 1
        k = n - 1 # 指向 result 数组的末尾
        
        while left <= right:
            lsq = nums[left] ** 2
            rsq = nums[right] ** 2
            if lsq < rsq:
                result[k] = rsq
                right -= 1
            else:
                result[k] = lsq
                left += 1
            k -= 1
        return result

三、支线任务:选做挑战

感觉状态不错,我顺手把计划中选做的两道题也解决了。它们都是二分查找的变种,很好的练习。

1. LeetCode 35. 搜索插入的位置

题目描述: 给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

我的实现

这题的逻辑和二分查找几乎一样,唯一的区别在于如果没找到,left 指针最终停留的位置就是应该插入的位置。

class Solution:
    def searchInsert(self, nums: List[int], target: int) -> int:
        left, right = 0, len(nums) - 1
        while left <= right:
            mid = (right - left) // 2 + left
            if nums[mid] < target:
                left = mid + 1
            elif nums[mid] > target:
                right = mid - 1
            else:
                return mid
        return left
2. LeetCode 34. 在排序数组中查找元素的第一个和最后一个位置

题目描述: 给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。如果数组中不存在目标值 target,返回 [-1, -1]

我的实现

这题的精髓在于构造一个巧妙的二分查找函数,用它来分别寻找 target 的起始位置和 target + 1 的起始位置,从而确定 target 的结束边界。非常巧妙!

class Solution:
    def searchRange(self, nums: List[int], target: int) -> List[int]:
        # 寻找大于等于 target 的第一个数
        def search_first_ge(nums: List[int], target: int):
            left, right = 0, len(nums) - 1
            while left <= right:
                mid = (right - left) // 2 + left
                if nums[mid] < target:
                    left = mid + 1
                else:
                    right = mid - 1
            return left

        start = search_first_ge(nums, target)
        # 如果起始位置越界,或者该位置的值不等于 target,说明不存在
        if start == len(nums) or nums[start] != target:
            return [-1, -1]
        else:
            # 寻找 target + 1 的起始位置,再减 1,就是 target 的结束位置
            end = search_first_ge(nums, target + 1) - 1
            return [start, end]

总结

今天收获满满!对数组的理解更深了一层,对二分查找的两种写法和双指针的多种应用场景都变得更加熟练。果然,踏踏实实地刷题、总结、记录是进步最快的方式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值