Leetcode数组

数组

Leetcode数组刷题记录

二分查找

二分查找的难点在于区间的判断,主要有两个易错点:

  • while left <= right: or while left < right
  • nums[mid] > targetright = mid还是right = mid - 1

防止int数据类型相加越界,可以采用 mid = left + (right - left) // 2

区间的定义和概念才是解决这类问题的根本

  • 对于[left, right] 左闭右闭的区间(初始赋值right = len - 1)
    只要区间合法,就进入循环,所以while left <= right:
    判断过nums[mid] > target,mid一定不在区间内,所以right = mid - 1,把mid排除在外,如果用了right = mid就会出现[1, 2, 3, 4] target = 5 left = right = 3一直死循环
  • 对于[left, right) 左闭右闭的区间(初始赋值right = len)
    只要区间合法,就进入循环,所以while left < right:当left = right的时候,区间里已经不包含任何一个数了
    判断过nums[mid] > target,mid一定不在区间内,所以right = mid,把mid排除在外
def search(self, nums: List[int], target: int) -> int:
       left = 0
       right = len(nums) - 1
       while left <= right:
           mid = (left + right) // 2
           if nums[mid] == target:
               return mid
           elif nums[mid] < target:
               left = mid + 1
           else:
               right = mid - 1
       return -1

循环不变量:每次的区间都“可能”包含了target,区间的定义是不变量

变体应用:

  • 搜索升序 无重复数的数组,若存在返回索引,若不存在返回应该插入位置的索引
    个人觉得难点在于没有找到的时候return left 还是return right 注意根据左闭右闭区间的定义,此时nums[right] < target target 显然不能插在right的位置,right刚好是target前一位 return left or return rignt+1
  • x \sqrt x x ,找不到的时候,如果是取下整,就返回return right,理由同前

数组双指针

题1:我们需要删除数组中某个元素,保证其他不被删除的元素排列在数组的前K位,并返回K+1(有效元素的个数)

不知道双指针解法的我,用了一种类似的思路

def removeElement(self, nums: List[int], val: int) -> int:
	n = len(nums) - 1
      i = 0
      while n >= i:
          if nums[i] == val:
              while nums[n] == val and n > i:
              	n -= 1
              nums[i] = nums[n]
              n -= 1
          i += 1
      return n + 1

其中i从前往后遍历,n从后往前遍历,效率也是O(n),但是由于存在循环嵌套,比双指针的解法还是差点

用双指针的方式可以在一个for循环下完成两个for的工作双指针问题一定要明确两个指针的含义是什么(个人理解:另一种循环不变量,在每次循环中表达的含义都相同)

  • 慢指针:“自成体系”的新建了一个总共有K+1个元素的数组,它指向这个新数组的下标索引
  • 快指针: 指向“老”的数组中的元素,方便我们判断这个元素是否合适被放进“新”数组里面;或者可以理解成,指向新数组的元素(可能被纳入新数组的元素)
def removeElement(self, nums: List[int], val: int) -> int:
        slow = 0
        for fast in range(len(nums)):
            if nums[fast] != val:
                nums[slow] = nums[fast] #填补空缺
                slow += 1
        return slow

最后return就是slow,因为每次slow都加1,所以相当于在下一次循环开始前,slow指向新数组的后一位

题2:要求把0都放到数组最后,保持前面的数排序不变

我一开始的解答

def moveZeroes(self, nums: List[int]) -> None:
    slow = 0
    for fast in range(1, len(nums)):
        if nums[slow] == 0 and nums[fast] != 0:
            nums[slow], nums[fast] = nums[fast], nums[slow]
            slow += 1
        elif nums[slow] != 0:
            slow += 1

但如果明确在这道题目里面的双指针含义,根本不需要用这么复杂的分类讨论

  • 快指针:指向有效的元素开头(还未处理的部分)
  • 慢指针:指向已经处理好的序列尾部
  • 慢指针和快指针中间全部是0

抓住这些循环不变量,程序完全可以套用一般的双指针,只要快指针指向的元素非0就交换并更新慢指针

def moveZeroes(self, nums: List[int]) -> None:
        slow = 0
        for fast in range(len(nums)):
            if nums[fast] != 0:
                nums[slow], nums[fast] = nums[fast], nums[slow]
                slow += 1

题3:比较两个字符串是否相同,其中#表示回车,会删除之前的字母,对着空字符串按回车,仍然为空字符串

不用双指针,暴力遍历解答,用了一下栈这种数据结构,毕竟回车操作是栈的典型代表

def backspaceCompare(self, s: str, t: str) -> bool:
    ls = [''] * len(s)
    lt = [''] * len(t)
    ptrs = -1
    ptrt = -1
    # 后进先出是栈的数据结构
    for letter in s:
        if letter != '#':
            ptrs += 1
            ls[ptrs] = letter
        elif ptrs != -1:
            ptrs -= 1
    for letter in t:
        if letter != '#':
            ptrt += 1
            lt[ptrt] = letter
        elif ptrt != -1:
            ptrt -= 1
    if ls[:ptrs+1] == lt[:ptrt+1]:
        return True
    else:
        return False

双指针,不过此处的双指针,非彼处的双指针,之前都是用在一个数组里的快慢指针,现在是遍历两个字符串的指针

此题的关键在于看到,如果从前往后比较,我们不知道这个字符会不会被删除,但是如果从后往前比较,我们可以肯定这个字符是否被删除

def get_next_char(self, string: str, ptr: int, skip : int)  -> tuple:
    while ptr >= 0:
        if string[ptr] == "#":
            skip += 1
            ptr -= 1
        elif skip != 0:
            skip -= 1
            ptr -= 1
        else:
            break # 需要比较的字符
    return (ptr, skip)

def backspaceCompare(self, s: str, t: str) -> bool:
    ptrs = len(s) - 1
    ptrt = len(t) - 1
    skips = 0
    skipt = 0
    while ptrs >= 0 or ptrt >= 0:
        ptrs, skips = self.get_next_char(s, ptrs, skips)
        ptrt, skipt = self.get_next_char(t, ptrt, skipt)
        if ptrs >= 0 and ptrt >= 0 and s[ptrs] != t[ptrt]:
            return False
        ptrs -= 1
        ptrt -= 1

    if ptrs == ptrt:
        return True
    else:
        return False

做了很久需要回看

题4:给你一个按非递减顺 排序的整数数组 nums,返回每个数字的平方组成的新数组,要求也按非递减顺序排序

有思维定势,总觉得非降序排序一定要从小排到大,但是如果从两边开始确定,就根本不需要找“0”作为正负临界点啦,对比一下代码,明显第二个更简单

def sortedSquares(self, nums: List[int]) -> List[int]:
    neg = 0
    pos = 0
    lit = []
    while pos < len(nums) and nums[pos] < 0:
        pos += 1
    neg = pos - 1
    while neg >= 0 and pos < len(nums):
        if nums[neg] * nums[neg] < nums[pos] * nums[pos]:
            lit.append(nums[neg] * nums[neg])
            neg -= 1
        else:
            lit.append(nums[pos] * nums[pos])
            pos += 1
    while neg >= 0:
        lit.append(nums[neg] * nums[neg])
        neg -= 1
    while pos < len(nums):
        lit.append(nums[pos] * nums[pos])
        pos += 1
    return lit
def sortedSquares(self, nums: List[int]) -> List[int]:
    neg = 0
    pos = len(nums) - 1
    lit = [0]* len(nums)
    i = len(nums) - 1
    while neg <= pos:
        if nums[neg] * nums[neg] < nums[pos] * nums[pos]:
            lit[i] = nums[pos] * nums[pos]
            pos -= 1
        else:
            lit[i] = nums[neg] * nums[neg]
            neg += 1
        i -= 1
    return lit

滑动窗口

也是一种双指针,只不过此处指针的作用是确定子区间

题1: 给定一个含有 n 个正整数的数组和一个正整数 target,找出该数组中满足其总和大于等于 target 的长度最小的,子数组

没有接触双指针之前,我写了这样一种错误答案

def minSubArrayLen(self, target: int, nums: List[int]) -> int:
    left = 0
    right = 1
    # 左闭右开区间
    while right <= len(nums):
        if sum(nums[left:right]) < target:
            right += 1
        elif sum(nums[left+1:right]) >= target:
            left += 1
        elif right < len(nums) and nums[left] <= nums[right]:
            left += 1
            right += 1
        else:
            break
    print(left, right)
    if left == 0 and right > len(nums):
        return 0
    else:
        return right - left 

这个在跑测试nums = [10,5,13,4,8,4,5,11,14,9,16,10,20,8] 过不了,原因在于更新指针的方法没有想清楚,规定当nums[left] <= nums[right]时更新,但是这样有问题,因为有可能 right往后移动遇到了更大的数字,虽然目前的right指向的数字是小于left的,但是移动后还是能找到更短的区间

构建双指针的思路,应该从暴力解法开始想(本质上,滑动窗口就是对暴力遍历解的一种优化)在暴力遍历解法中,一个循环确定区间初始位置,一个循环确定区间终止位置,那么哪一个循环可以优化呢?终止位置显然不能优化(必须把每个数都遍历一遍才能找到最短区间),但是起始位置却不是每次都要更新的,如果当前区间都没达到target的要求,那起始位置不需要更新

def minSubArrayLen(self, target: int, nums: List[int]) -> int:
    left = 0
    count = 0
    length = len(nums)
    # 左闭右闭区间
    for right in range(len(nums)):
        count += nums[right]
        while count >= target:
            if right - left + 1 < length:
                length = right - left + 1
            count -= nums[left]
            left += 1
    if length == len(nums) and left == 0 and count < target:
        return 0
    else:
        return length

个人思考比较久的几个点

  • 计数器(length)的使用 一开始的思路没有想着记录最短长度,认为直接通过滑动left 和 right,使得最后找到的就是最短长度。不使用计数器length来记录当前的最短长度,会导致更新条件非常难设计 — 回到我的解法错误的原因
  • while循环取不取等while count >= target or while count > target 这个等于号如果不加上的话,会导致找出来的区间长一个单位。要明确每次for 循环的时候的不变量,每次for 循环的时候当前子序列的sum一定是target的,不然就不需要加上nums[right]啦(如果都满足target要求,再加上一个不就多出来了吗?区间也更长了)
  • while循环的作用:其实这种思路在我看来就是确定子区间的尾(通过外循环for),找出以这个数结尾的最短子区间,并和已有记录的最短子区间(length)比较,如果小于目前全局的最短子区间,就更新记录,而while循环就是在找当前以right结尾区间中,符合要求的最短子区间
  • 什么时候return 0 一开始写了if length == len(nums) and count < target:sum(nums) == target时有问题,因为while经历了left + 1和count - nums[left]的过程,所以加上了left == 0这个判断

题2: 每个树长一种果子,篮子里只能装两种不同类型的果子,但是对于果子的数量没有限制,问最多能装多少果子

def totalFruit(self, fruits: List[int]) -> int:
    result = 0
    basket = [0] * len(fruits)
    fruittype = 0
    left = 0
    for right in range(len(fruits)):
        basket[fruits[right]] += 1
        if basket[fruits[right]] == 1:
            fruittype += 1
        while fruittype > 2:
            basket[fruits[left]] -= 1
            if basket[fruits[left]] == 0:
                fruittype -= 1
            left += 1
        a = right - left + 1
        if a > result:
            result = a
    return result

关键:

  • 滑动窗口:滑动窗口之所以能把 O ( n 2 ) O(n^2) O(n2) 的时间复杂度降低到 O ( n ) O(n) O(n) 关键在于左指针的移动,右指针还是要遍历每个数的,但是左指针每次都是移动到使得区间符合要求(有效的区间)左指针的移动次数有上界 ≤ n \leq n n
  • 计数容器basket:如何记录目前篮子里的水果种类,用数组可以,也可以用字典

题3:最小覆盖子串

思路同,还是滑动窗口,主要增加了判断一个字符串是否为子串的逻辑

新学一下python default dict的用法

from collections import defaultdict

# 创建一个默认字典,默认值为 0
need = defaultdict(int)
# 访问不存在的键
print(need['a'])  # 输出: 0
# 修改键的值
need['a'] += 1
print(need['a'])  # 输出: 1
# 访问另一个不存在的键
print(need['b'])  # 输出: 0

defualtdict当访问不存在的键的时候返回0,可以简单理解为一个所有键值对都初始化为0的字典

class Solution:
    def check (self, alpha1: dict, alpha2: dict) -> bool:
        for key in alpha1:
            if key in alpha2:
                if alpha1[key] > alpha2[key]:
                    return False
            else:
                return False
        return True
        ## 判断覆盖所有字符
    def minWindow(self, s: str, t: str) -> str:
        alphat = {}
        alphas = {}
        result = float('inf')
        left_a = 0
        right_a = 0
        for char in t:
            if char in alphat:
                alphat[char] += 1
            else:
                alphat[char] = 1
        left = 0
        for right in range(len(s)):
            if s[right] in alphas:
                alphas[s[right]] += 1
            else:
                alphas[s[right]] = 1
            while self.check(alphat, alphas):
                length = right - left + 1
                if length < result:
                    result = length
                    left_a = left
                    right_a = right
                if alphas[s[left]] == 1:
                    del alphas[s[left]]
                else:
                    alphas[s[left]] -= 1
                left += 1
        if result == float('inf'):
            return ""
        else:
            return s[left_a:right_a+1]

螺旋矩阵

感觉这类题目没什么特殊的算法思想,也不是很难,主要考察对边界条件的判断,个人做题习惯是通过例子去想边界条件应该怎样设计,配上适当的调试

题1:给你一个正整数 n ,生成一个包含 1 到 n2 所有元素,且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix

def generateMatrix(self, n: int) -> List[List[int]]:
    lit = [[0 for _ in range(n)] for _ in range(n)]
    col = 0
    row = 0
    k = 0
    i = 1
    while i <= n*n:
        for col in range(k,n-k):
            lit[row][col] = i
            i += 1
        k += 1
        for row in range(k, n + 1 - k):
            lit[row][col] = i
            i += 1
        for col in range(n - k - 1, k - 2,-1):
            lit[row][col] = i
            i += 1
        for row in range(n - k - 1,k - 1,-1):
            lit[row][col] = i
            i += 1
    return lit

但是对比解答,我的这个代码还是有点不够清晰,我处理的时候遵循,第一条边包含头尾,第二条边包含尾部,第三条边包含尾部,第四条边去头去尾,在每次循环的时候处理方式是统一的

其实可以做到对每条边处理方式是统一的,然后 k k k明确的含义是 n − k n-k nk子矩阵(还未被处理的矩阵)的大小

另外:lit = [[0] * n] * n这样创建的列表是浅拷贝,牵一发而动全身,改一个值会影响到其他值,要用lit = [[0 for _ in range(n)] for _ in range(n)]这样的方式创建列表

代码优化

matrix

def generateMatrix(self, n: int) -> List[List[int]]:
    lit = [[0 for _ in range(n)] for _ in range(n)]
    startx = 0
    starty = 0
    k = 1
    i = 1
    while i < n*n:
        for col in range(starty, n - k):
            lit[startx][col] = i
            i += 1
        for row in range(startx, n - k):
            lit[row][n - k] = i
            i += 1
        for col in range(n - k, starty,-1):
            lit[n - k][col] = i
            i += 1
        for row in range(n - k,startx,-1):
            lit[row][starty] = i
            i += 1
        startx += 1
        starty += 1
        k += 1
    if n % 2 == 1: #奇数填补中间
        lit[n//2][n//2] = n*n
    return lit

这种方法奇数偶数要单独判断,通过startx和starty来遍历的话,最后一次不会进入for循环

题2:螺旋打印 m × n m \times n m×n的矩阵

def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
    lit = []
    m = len(matrix) #行数
    n = len(matrix[0]) #列数
    startx = 0
    starty = 0
    endx = m - 1 #左闭右闭
    endy = n - 1
    # 通过startx starty endx endy边界控制螺旋矩阵
    while startx <= endx and starty <= endy:
        for i in range(starty, endy + 1): #从左往右
            lit.append(matrix[startx][i])
        startx += 1 #最上排的遍历完了,上界下移
        for i in range(startx, endx + 1):
            lit.append(matrix[i][endy])
        endy -= 1 #最右排的遍历完了,右界左移

        if endx >= startx:
            for i in range(endy, starty - 1, -1):
                lit.append(matrix[endx][i])
            endx -= 1
        if endy >= starty:
            for i in range(endx,startx - 1, -1):
                lit.append(matrix[i][starty])
            starty += 1
    return lit

关键:

  • 四个边界:通过startx starty endx endy 直接的框定边界大大方便了程序的逻辑判断,相当于每次确定一个子矩阵,在子矩阵里重复相同的运算
  • 加入if判断防止重复打印: if endx >= startx这个是一开始写没有加入的条件,但是这个是必须的,因为for i in range(endy, starty - 1, -1) 这个for循环只能判断横向区间是否有效,但是不能判断纵向区间是否有效,不加入会出现重复打印,如下图。实际上通过前两个for,上边界从黄色被移动到绿色和下边界重合,右边界从黄色移动到绿色,但是如果不判断上下边界,右边界和左边界之间还夹了一个6,会重复打印

matrix2

前缀和思想

用于快速计算任意子区间的和,方便查询,把查询的时间复杂度从 O ( n ) O(n) O(n) 降低到 O ( 1 ) O(1) O(1)

题1:给定一个整数数组 Array,计算该数组在每个指定区间内元素的总和

注意点:

  • 区间 假设要计算 [ a , b ] [a,b] [a,b]闭区间的sum(a和b都是下标索引)要用 n u m s [ b ] − n u m s [ a − 1 ] nums[b] - nums[a - 1] nums[b]nums[a1]因为前缀和数组nums里面,每个下标存储的都是从 [ 0 , i ] [0,i] [0,i]的sum,包括 i i i,这样的话就会存在数组越界的问题,
  • 前缀和数组定义 n u m s [ b ] − n u m s [ a − 1 ] nums[b] - nums[a - 1] nums[b]nums[a1] 算法当a = 0的时候要出错,我个人喜欢给前缀和数组前加一个0,原数组若length为n,前缀和数组定义为n+1
n = int(input())
array = [0]
count = 0
for _ in range(n):
    count += int(input())
    array.append(count)

while True:
    try:
        line = input().strip()
        parts = line.split()
    except:
        break
    print(array[int(parts[1]) + 1] - array[int(parts[0])])

题2:有 n × m n \times m n×m块土地,要使得横/竖分割土地的价值差异最小

line = input().strip()
parts = line.split()
m = int(parts[1])  # 列数
n = int(parts[0])  # 行数

pre_sum = []
count = 0
for _ in range(n):
    a = [count]
    line = input().strip()
    parts = line.split()
    numbers = list(map(int, parts))
    for i in numbers:
        count += i
        a.append(count)
    pre_sum.append(a)

result = float('inf')
for i in range(1 , n): #非空
    #尝试所有的行分割(横向)
    sum1 = pre_sum[i-1][m] - pre_sum[0][0]
    sum2 = pre_sum[n-1][m] - pre_sum[i][0]
    if abs(sum2 - sum1) < result:
        result = abs(sum2 - sum1)
for i in range(1,m):
    #尝试所有的列分割(竖向)
    sum1 = 0
    sum2 = 0
    for j in range(n):
        # 计算当前行的左半部分和右半部分
        sum1 += pre_sum[j][i] - pre_sum[j][0]
        sum2 += pre_sum[j][m] - pre_sum[j][i]
    if abs(sum2 - sum1) < result:
        result = abs(sum2 - sum1)

print(result)

总结

思维导图

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值