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

代码随想录刷题第一天

二分查找法 (LC704)

题目链接

给定一个 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

解题思路:
这道题重要运用的思想是二查找分法。 需要主要的是,二分查找法在使用前需要注意以下内容:

  • 查找的数组必须是升序排列,即 [1,2,3,4,5] 而非 [3,2,1,4,5], 否则无法更新边界。 如果没有升序排序,需要先排序 list.sort()

  • 数组不可以有重复元素,如果有重复元素,则返回的下标不唯一。LC34 找区间 是这道题的变体,其数组内有重复元素,目的是找到重复元素区间。(请看附录

  • 一定要注意循环不变量。在这里是对于区间的定义。左右区间是左闭右开 [0,1) 还是左闭右闭 [0,1]。两者在代码实现上有所区别。

左闭右闭区间

左闭右闭区间,也就是target 在[left, right] 区间,由此定义了代码的书写规范:

  • 循环二分时使用 while (left <= right), 因为left == right 依然在左闭右闭区间中依然是有意义的,需要继续查找。例如当 left=right=1时,仍需查看nums[left]是否为target
  • 在更新左区间的右边界时,即:if (nums[middle] > target), 因为num[middle] != target, 所以根据左闭右闭区间原则,我们不需要将middle放在区间中,所以right = middle-1。
  • 同理,在更新右区间的左边界时,应该使用left = middle+1
  • 右指针初始化时使用 right = len(nums)-1, 因为包含最右边的下标
while (left <= right):
   middle = ...
   if (nums[middle]>target):
   	right = middle-1
   .....

左闭右开区间

左闭右开区间,也就是target在[left, right)区间,由此定义了如下代码规范:

  • 循环二分时使用 while (left < right), 因为left == right 不在左闭右开区间中,没有意义
  • 更新左区间的右边界时,num[middle] != target,由于区间右边不包括最右边的元素,所以更新时使用right = middle
  • 左边界更新不变
  • 右指针初始化时使用 right = len(nums), 因为不包含最后一个数的下标,所以需要加一位。
while (left < right):
   middle = ...
   if (nums[middle]>target):
   	right = middle
   .....

代码实现:

左闭右闭

class Solution(object):
   def search(self, nums, target):
       if len(nums) == 0 or nums is None:
           return -1
       # 初始化坐标        
       l = 0
       r = len(nums)-1   	# 初始化,闭区间包含最右侧,即nums中最后一个下标
       # 循环二分
       while (l<=r):			  # 右闭,l<=r
           mid = l+(r-l)//2    # 计算mid
           if (nums[mid] < target):
               l = mid+1       
           elif (nums[mid]>target):
               r = mid-1       # 右闭,包含最右侧,需要排除middle
           else:
               return mid
       return -1

左闭右开

class Solution(object):
    def search(self, nums, target):
        if len(nums) == 0 or nums is None:
            return -1
        # 初始化坐标        
        l = 0
        r = len(nums)           # 右开,不包含最右侧,使用nums最后一个下标+1
        # 循环二分
        while (l<r):            # 右开,l<r
            mid = l+(r-l)//2    # 计算mid
            if (nums[mid] < target):
                l = mid+1       
            elif (nums[mid]>target):
                r = mid       # 右闭,不包含最右侧,使用middle
            else:
                return mid
        return -1

移除元素 (LC27) 双指针

题目链接
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。不要使用额外的数组空间,你必须仅使用 O(1) 额外空间原地 修改输入数组。元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素

示例:

输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2]
解释:函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。

暴力法

简单的方法就是使用两次循环,第一次循环遍历整个数组,如果当前所指的数字是val,则使用第二层循环,将给下标后面的所有内容往前移动,完成向前覆盖。
覆盖前覆盖后
!!! 这里使用Python的时候与代码随想录中的写法有区别,debug了很久~

  • 注意: Python 在使用 for i in range(xx) 的时候,i 这个变量是固定递增的,无法在循环内部更改,永远是在循环的时候逐渐递增。 例如:
for i in range(5):
	print(i)     	# [0,1,2,3,4] 永远按顺序,不会因为在当前循环中改变而在下一次循环时改变
	i-=1
	print(i) 		# [-1,0,1,2,3] 循环中调用i后可以改变数值
  • 所以当循环次数取决于循环内部时,需要使用while循环。在本题中,当所有元素向前覆盖时,需要在当前位置再判断一次(i不变), 所以使用while循环。
  • 同时还需注意的是,循环的结束条件不取决于原数组的长度,而是取决于新数组长度size)。因为i>size 为向前覆盖后留下来的没用内容,继续运行会循环超时。

暴力法代码实现:

class Solution(object):
    def removeElement(self, nums, val):
        i = 0
        size = len(nums)
        while (i<size):  # while 循环结束条件取决于新数组的长度
            if nums[i]==val:  # 判断是否需要向前覆盖
                for j in range(i+1, len(nums)):
                    nums[j-1] = nums[j]
                    j+=1
                size-=1		# 向前覆盖一次,新数组长度减一
                i-=1		# 需要在当前位置再循环一次,保证新覆盖的数字不为val
            i+=1
        return size

双指针法

除了暴力法外,这道题更有效的方法是双指针法,只需要O(n)时间复杂度就可完成。这里我们使用快慢双指针法

  • 在快慢双指针算法中,我们有两个指针,快指针和慢指针。两者初始时都指在0。一个for循环更新快指针,每个循环快指针向前移动一次。

  • 慢指针只在快指针指的不是target的时候更新。当目标不为target是,这个数据是要保留的,则将该数据赋值给慢指针所指的单元格,并将慢指针右移一格。如果快指针所指的是target,则跳过赋值,快指针直接向后移动一格

快慢指针图示
双指针代码实现:

class Solution(object):
    def removeElement(self, nums, val):
        slow = 0
        for fast in range(len(nums)):
        	# 快指针所指的数不为target,新数组需要这个数
            if nums[fast]!=val:
                nums[slow] = nums[fast]
                slow+=1 #更新赋值完后,慢指针加一
            fast+=1
        return slow  # 最后慢指针的下标就是新数组的长

附加题 (LC34) 查找元素区间,二分查找法变体

题目链接
给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。

示例1:

输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]

示例2:

输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]

由于题目要求使用 O(logn) 算法解决此问题,所以传统的暴力解法不适用,只可以使用二分法。

因为题目说数组是非递减数组,则即使有重复值,也是紧邻的重复值。由于需要返回一个target的区间范围,有两个下标,因此思路是使用两次二分法查找target的左右边界

然而,传统的二分法单次只能准确的找到数组中独一无二的数(非重复数)。如果数字重复,则二分法所返回的下标可能会是任意一个位置。传统二分法会在找到target后停止查找并返回index。 这里我们当我们找到target后继续更新左/右边界,继续查找不返回,设置新的边界。

  • right = middle-1 找重复数的左边界,看看左区间内是否还有target, 尽量向左挪动边界
  • left = middle+1 找重复数的右边界,看看右区间内是否还有target, 尽量向右挪动边界

在这里插入图片描述
代码实现:

class Solution(object):
    def searchRange(self, nums, target):
        left = 0
        right = len(nums)-1
        first = -1
        last = -1

        # 搜索重复数组的第一个位置(左边界)
        while (left <= right):
            middle = left + (right-left)//2
            if (nums[middle]>target):
                right = middle-1
            elif (nums[middle]<target):
                left = middle+1
            else:
                first = middle
                right = middle-1 # 继续搜索

        # 记得在完成一次二分法后要重新初始化区间
        left = 0
        right = len(nums)-1    
        
        # 搜索重复数组的最后一个位置(右边界)
        while (left <= right):
            middle = left + (right-left)//2
            if (nums[middle]>target):
                right = middle-1
            elif (nums[middle]<target):
                left = middle+1
            else:
                last = middle
                left = middle+1 # 继续搜索

        return (first, last)
        

总结:

今天主要学习了解决数组问题的两种算法,二分查找法和快慢双指针。编程萌新,在手撕代码的时候所花时间较长,尤其是在debug第二题暴力解法的时候花了点时间。希望能够坚持学习打卡,在一刷完力扣后能够在编程上有所提升。加油!!!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值