代码随想录27期|Python|Day1|数组基础|二分查找[两种边界](704、35、34、69)|移除元素[双指针](27、26、383、844、977)

数组基础

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

数组是存放在连续内存空间上的相同类型数据的集合。“连续”体现在以下2个方面:

(1)内存空间地址的连续性。这里需要注意不同的编程语言对于返回地址的表示方式不同。文章中以二维数组为例,C++可以返回相差4位的连续地址;但是对于Java则是虚拟机处理之后的结果,看起来是“离散的”,所以Java可能的存储方式是使用连续的一维数组(行),而行索引则是链表的形式(图源:代码随想录)。

代码随想录27期|Python|Day1|数组基础|二分查找[两种边界](704、35、34、69)|移除元素[双指针](27、26、383、844、977)_第1张图片

(2)对于数组的增删引起的整体平移,元素不能删除,只能被覆盖。以删除为例,数组在删除元素的同时,会把后面所有的元素往前平移,造成一定程度上的内存开销。

题目

704. 二分查找 - 力扣(LeetCode)

视频链接:手把手带你撕出正确的二分法 | 二分查找法 | 二分搜索法 | LeetCode:704. 二分查找_哔哩哔哩_bilibili

思路

简单的二分查找,需要注意两种不同写法的边界值处理

左闭右开[left, right)左闭右闭[left, right]的写法不同:对于闭的位置处,即right是否取。

1、如果right可以取到,即[left, right]:首先在声明的时候就 right = len(nums)-1 保证是合法的数组索引;然后在循环处传入合法的区间,即while left <= right;在更新新的边界时,注意在nums[middle] > target时,已经排除了middle位置,故要保证接下来的循环中传入while判断的区间依然合法,故更新right = middle - 1

class Solution(object):
    def search(self, nums, target):
        """
        :type nums: List[int]
        :type target: int
        :rtype: int
        """
        # 双边闭区间写法
        left, right = 0, len(nums) - 1  # 注意right可以取到
        
        while left <= right:  # 传入闭区间
            middle = left + (right - left) // 2  #索引从0开始的表示方式
            if nums[middle] > target:
                right = middle - 1  # 保证下次传入while的还是合法区间(right取到)
            
            elif nums[middle] < target:  # 注意!if之后的其他互斥条件用elif,不然会跳过到else
                left = middle + 1
            
            else:
                return middle  # 其他的情况是middle == target
        
        return -1  # 没找到,返回异常值

2、如果right取不到,即[left, right):首先声明时需要 right = len(nums),此时right的索引是nums最后一个的后一位,是开区间边界;所以循环判断需要改为 while lef < right,保证是合法区间;更新right时需要注意right = middle。

class Solution(object):
    def search(self, nums, target):
        """
        :type nums: List[int]
        :type target: int
        :rtype: int
        """
        # 左闭右开区间写法
        left, right = 0, len(nums)  # 注意right不能取到

        while left < right:  # 传入开区间
            middle = left + (right - left) // 2

            if nums[middle] > target:
                right = middle  # 此处已经判断middle不是target,所以不用-1

            elif nums[middle] < target:
                left = middle + 1
            
            else:
                return middle

        return -1



35. 搜索插入位置 - 力扣(LeetCode)

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

 思路

本题思路和上文一致,找到第一个比自己小的就返回他的索引,用二分查找即可。代码和上一题一致(也是两个版本),只需要把最后的return 换成right + 1比每一个都大的情况下返回最大索引+1

34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)

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

思路 

情况 和左边界 和右边界 是否存在 输出
1 小于 大于 [-1, -1]
2 大于等于 小于等于 [-1, -1]
3 大于等于 小于等于 [ : ]

三种情况如上,排除异常情况下,所以只需要获取左右边界值对应的索引输出即可,我们分两步来实现。每一步都是在704二分法的基础上进行修改或合并(本题采取左右闭区间方法)

右边界:

        # 二分法找右边界(最后一个)
        def getRightBorder(nums, target):
            left, right = 0, len(nums) - 1
            rightborder = -2
            while left <= right:
                middle = left + (right - left) // 2
                if target < nums[middle]:
                    right = middle -1
                else:  # 相当于把 target == num[middle] 和 target > nums[middle]合并成一个情况,都需要更新left,来找到最后一个target出现的索引
                    left = middle + 1
                    rightborder = left 
            return rightborder

左边界:

        #二分法找左边界(第一个)
        def getLeftBorder(nums, target):
            left, right = 0, len(nums) - 1
            leftborder = -2
            while left <= right:
                middle = left + (right - left) // 2
                if nums[middle] < target:
                    left = middle + 1
                else:  # 同理可以做出合并
                    right = middle -1
                    leftborder = right
            return leftborder

然后分别对三种情况进行计算,完整代码如下:

class Solution(object):
    def searchRange(self, nums, target):
        """
        :type nums: List[int]
        :type target: int
        :rtype: List[int]
        """
        # 二分法找右边界(最后一个)
        def getRightBorder(nums, target):
            left, right = 0, len(nums) - 1
            rightborder = -2
            while left <= right:
                middle = left + (right - left) // 2
                if target < nums[middle]:
                    right = middle -1
                else:  # 相当于把 target == num[middle] 和 target > nums[middle]合并成一个情况,都需要更新left,来找到最后一个target出现的索引
                    left = middle + 1
                    rightborder = left 
            return rightborder
        #二分法找左边界(第一个)
        def getLeftBorder(nums, target):
            left, right = 0, len(nums) - 1
            leftborder = -2
            while left <= right:
                middle = left + (right - left) // 2
                if nums[middle] < target:
                    left = middle + 1
                else:  # 同理可以做出合并
                    right = middle -1
                    leftborder = right
            return leftborder

        rightborder = getRightBorder(nums, target)
        leftborder = getLeftBorder(nums, target)
    # 接下来分三种情况完成输出
    # 情况一
        if leftborder == -2 or rightborder == -2:  # 说明没找到
            return [-1,-1]
    # 情况三
        if rightborder - leftborder > 1:
            return [leftborder + 1, rightborder - 1] # 注意!
    # 情况二
        return [-1,-1]
    

69. x 的平方根 - 力扣(LeetCode)

这题放在这里是有一种解法可以用二分法迭代找平方根

 思路

由于x的完全平方根肯定在[0,x]内,所以只需要在这个闭区间内使用二分查找,对704的二分查找做修改,由于现在是求出整数部分,所以相当于是找一个最小值的上界,那么对于以上代码进行修改:

class Solution(object):
    def mySqrt(self, x):
        """
        :type x: int
        :rtype: int
        """
        left, right = 0, x  # 平方根闭区间设定为[0, x]
        ans = -1  # 初始化
        while left <= right:
            middle = left + (right - left) // 2
            if middle * middle <= x:
                left = middle + 1
                ans = middle  # 相当于是找一个上界,可以和上面的getrightBorder对比
            else:
                right = middle - 1
        return ans

27. 移除元素 - 力扣(LeetCode)

这题输入输出上有一个细节:输出是修改后数组的长度n,根据官方的解释,后台会检查数组前n个是否正确

所以[2, 2, 0, 0]和[2, 2, 3, 3]在n=2输出上没有差别(雾)

思路

1、暴力求解,嵌套循环。只要找到了一个需要删除的就把后面的全部往前移动一格。时间复杂度O(n^2)这里需要注意,第一次写的时候用了range函数,无法AC的原因是i -= 1这一步根本不会被执行!

class Solution(object):
    def removeElement(self, nums, val):
        """
        :type nums: List[int]
        :type val: int
        :rtype: int
        """
        n = len(nums)
        i = 0
        while i < n:
        # 注意!!这里不能用库函数“for i in range(n)”
        # 因为range是封装好的,每次i++,不会执行i -= 1的操作!!!
            if nums[i] == val:
                for j in range(i+1, n):
                    nums[j-1] = nums[j]
                # 当前这个位置已经是新的i-1了,所以数组的长度和当前的索引位置都需要更改
                i -= 1
                n -= 1
            i += 1
        return n

2、双指针(这几道题都有涉及到“原地”操作,今后看到原地操作数组可以考虑双指针)。本质上是一个fast用来遍历数组的value,负责“查找”和“判断”,另外一个slow用来“跟进”和“保存”position。其关键是在fast识别到需要删除的元素之后,保持slow不变,也就是索引位置不变,这样fast进入到下一个值的时候就可以正常实现更新。由于过程实现在原始数组上,是一个“原地”in_place操作,所以需要时间复杂度O(n)​​​​​​​

class Solution(object):
    def removeElement(self, nums, val):
        """
        :type nums: List[int]
        :type val: int
        :rtype: int
        """

        # 双指针

        n = len(nums)
        fast, slow = 0, 0
        for fast in range(n):
            if nums[fast] != val:
                nums[slow] = nums[fast]
                slow += 1
                n -= 1
        return slow  # 最后slow指向的就是数组的最后一个索引+1,也就是数组长度

我们可以总结出双指针的基本框架

        n = len(nums)
        fast, slow = 0, 0  # 初始化位置可以修改(一端同向、双端相向)
        for fast in range(n):
            if nums[fast] != val:  # 不满足删除条件的保留在原数组
                nums[slow] = nums[fast]
                slow += 1
                n -= 1

 

26.删除数组中的重复项

思路

本题和上一题一致的解法,只需要注意把判断条件更换为fast当前值和前一个值的比较。

class Solution(object):
    def removeDuplicates(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """
        n = len(nums)
        fast, slow = 1, 1  # 初始化trick:第一个值一定是unique的,所以 0 不用考虑
        while fast < n:
            if nums[fast - 1] != nums[fast]:  # 此处修改为判断前一个和现在这一个fast索引的值是否相等即可
                nums[slow] = nums[fast]
                slow += 1
            fast += 1
        return slow

283.移动零

思路

1、本人偷懒的思路:两个for循环,一个从头开始,执行删除,一个从slow+1处开始,赋值0,居然过了(喜)。

class Solution(object):
    def moveZeroes(self, nums):
        """
        :type nums: List[int]
        :rtype: None Do not return anything, modify nums in-place instead.
        """
        n = len(nums)
        fast, slow = 0, 0
        # Step 1: 完成0的删除
        for fast in range(n):
            if nums[fast] != 0:
                nums[slow] = nums[fast]
                slow += 1
        # Step 2: 完成末尾0的复制
        for i in range(slow, n):
            nums[i] = 0
        return n

2、双指针+python元素交换:常规做法,slow标记当前非0位置的末尾,fast查找下一个非0的位置,查询到之后,两个数字交换位置即可。

class Solution:
    def moveZeroes(self, nums: List[int]) -> None:
        n = len(nums)
        slow = fast = 0
        while fast < n:
            if nums[fast] != 0:
                nums[slow], nums[fast] = nums[fast], nums[slow]  # python特有的交换规则
                slow += 1
            fast += 1

844.比较含退格的字符串

思路

由于“#”在后,往前删除,所以从后往前遍历,依次比较没有被删掉的值,出现一个不一样的直接退出返回False。需要一个指针来查询,一个计数器保存当前“#”需要删除的个数。

class Solution(object):
    def backspaceCompare(self, s, t):
        """
        :type s: str
        :type t: str
        :rtype: bool
        """
        s_len, t_len = len(s) - 1, len(t) - 1
        skip_s, skip_t = 0, 0
        # 在二者还存在一个没有遍历完成的时候
        while s_len >= 0 or t_len >= 0:
            # Step 1:查询现在需要删除的个数,返回没有被删除的索引
            while s_len >= 0:
                if s[s_len] == '#':
                    skip_s += 1  # 查询到“#”计数器 + 1
                    s_len -= 1。# 索引往前移动 1
                elif skip_s > 0:
                    skip_s -= 1  # 使用一次“#”计数器 - 1
                    s_len -= 1  # 索引往前移动 1 
                else:
                    break

            while t_len >= 0:
                if t[t_len] == '#':
                    skip_t += 1
                    t_len -= 1
                elif skip_t > 0:
                    skip_t -= 1
                    t_len -= 1
                else:
                    break
            
            # Step 2:比较索引位置数值
            if s_len >= 0 and t_len >= 0:
                if s[s_len] != t[t_len]:
                    return False
            elif s_len >= 0 or t_len >= 0:
                return False
                
            # Step 3:比较正确,更新索引位置
            s_len -= 1
            t_len -= 1
        return True

977.有序数组的平方(非降序排序)

思路

双指针+相向遍历:由于数组是非降序排列的,所以我们可以假设大的值一定是出现在两端(或其中一端)。所以构建两个指针,一个指向开头,一个指向结尾,每次比较后移动较大(或等于)的那一个,从后往前保存到ans。

class Solution(object):
    def sortedSquares(self, nums):
        """
        :type nums: List[int]
        :rtype: List[int]
        """
        n = len(nums)
        ans = [0] * n  # 初始化保存答案的数组

        i, j, ans_pos = 0, n-1, n-1  # pos索引是ans保存的位置
        while i <= j:  # 相向而行的结束判据
            if nums[i] * nums[i] > nums[j] * nums[j]:
                ans[ans_pos] = nums[i] * nums[i]
                i += 1  # 只要移动较大的指针即可
            else:
                ans[ans_pos] = nums[j] * nums[j]
                j -= 1
            ans_pos -= 1  # 每次比较完pos 向前移动 1
        return ans

 总结

1、二分查找可以用于搜索数组数值类,需要注意选取的区间对于判断条件取等号、是否需要-1等的影响;

2、双指针解决数组修改类,需要注意单向和双向两种思路;

3、注意一些循环和判据的细节(for range和while的区别)。

第一天结束

你可能感兴趣的:(python,开发语言,算法)