目录
五、数组相关的技术
参考文献:https://leetcode-cn.com/explore/learn/card/array-and-string/202/conclusion/790/
5.1 杨辉三角
5.1.1 问题描述
5.1.2 求解过程
个人实现
法一:暴力迭代 + 动态规划。正常地生成各行以及行中各元素。复杂度 O(n^2)。
2020/06/18 - 9.06%
class Solution:
def generate(self, numRows: int) -> List[List[int]]:
if not numRows:
return []
if numRows == 1:
return [[1]]
if numRows == 2:
return [[1],[1,1]]
res = [[1],[1,1]] # 初始化结果列表
for line in range(2, numRows): # 当前行行号
temp = [1] # 初始化当前行头
for j in range(1, line): # 当前行需计算的中间元素数
num = res[line-1][j-1] + res[line-1][j] # 求和生成中间元素
temp.append(num) # 中间元素加入临时列表
temp.append(1) # 补充当前行尾
res.append(temp) # 当前行加入结果列表
return res
法一改:暴力迭代 + 动态规划 改。稍作简化,思想不变。复杂度 O(n^2)。
2020/06/18 - 65.52%
class Solution:
def generate(self, numRows: int) -> List[List[int]]:
if not numRows:
return []
if numRows == 1:
return [[1]]
res = [[1]] # 初始化结果列表 res 第0行
for line in range(numRows-1): # 上一行 line 行号
cur = [1] # 当前行 cur 行头取1
for index in range(line): # 当前行 cur 需计算的中间元素数
cur.append(res[line][index] + res[line][index+1]) # 求和生成中间元素加入当前行 cur
cur.append(1) # 当前行 cur 行尾取1
res.append(cur) # 当前行 cur 加入结果列表 res
return res
法二:迭代 + 动态规划 再改。由于每行都是回文串,所以可以 通过对称性使运算量减半。复杂度 O(n^2)。
2020/06/18 - 65.52%
class Solution:
def generate(self, numRows: int) -> List[List[int]]:
if not numRows:
return []
if numRows == 1:
return [[1]]
res = [[1]]
for line in range(1, numRows): # 当前行 temp 的 index
temp = [0 for _ in range(line+1)] # 当前行 temp 0值初始化
temp[0] = temp[-1] = 1 # 当前行 temp 首尾均为1
for j in range(line // 2): # 对称运算与赋值
temp[j+1] = temp[-j-2] = res[line-1][j] + res[line-1][j+1]
res.append(temp) # 结果列表 res 加入当前行 temp
return res
官方实现与说明
## 比个人实现-法一改还要简洁泛化
class Solution:
def generate(self, num_rows):
triangle = []
for row_num in range(num_rows):
# The first and last row elements are always 1.
row = [None for _ in range(row_num+1)]
row[0], row[-1] = 1, 1
# Each triangle element is equal to the sum of the elements
# above-and-to-the-left and above-and-to-the-right.
for j in range(1, len(row)-1):
row[j] = triangle[row_num-1][j-1] + triangle[row_num-1][j]
triangle.append(row)
return triangle
2020/06/18 - 65.52%
其他实现
法一:递归法,不论是效率还是简洁度都显著提升,值得认真学习。
2020/06/18 - 99.50% - 最佳
class Solution:
def generate(self, numRows: int) -> List[List[int]]:
if numRows == 1:
return [[1]]
if not numRows:
return []
## 递归
pre = self.generate(numRows-1) # 取得上一行, 从首行[[1]]开始
pre.append([None for _ in range(numRows)]) # 初始化新行 pre[numRows-1]
pre[numRows-1][0], pre[numRows-1][numRows-1] = 1, 1 # 当前行首、尾元素总为1
## 若直接用 pre.append([1 for _ in range(numRows)]) 可能更简洁些
for index in range(1, numRows-1): # 当前行中间元素求和与加入
pre[numRows-1][index] = pre[numRows-2][index-1] + pre[numRows-2][index]
return pre
参考文献
https://leetcode-cn.com/explore/learn/card/array-and-string/202/conclusion/1421/
https://leetcode-cn.com/problems/pascals-triangle/solution/yang-hui-san-jiao-by-leetcode/
5.2 杨辉三角 II
5.2.1 问题描述
5.2.2 求解过程
法一:对《杨辉三角 I》的解答稍作修改,朴素地计算每一行并记录在 res 中,然后返回最后一行 res[-1]。
2020/06/18 - 98.50% - 还有优化空间
class Solution:
def getRow(self, rowIndex: int) -> List[int]:
if rowIndex == 0:
return [1]
res = [[1]]
for line in range(1, rowIndex+1): # 当前行 temp 的 index
temp = [1 for _ in range(line+1)] # 当前行 temp 1 值初始化
for j in range(line // 2): # 对称运算与赋值
temp[j+1] = temp[-j-2] = res[line-1][j] + res[line-1][j+1]
res.append(temp) # 结果列表 res 加入当前行 temp
return res[-1]
法二:递归法,每次递归维护和计算两个列表,即前一行 pre 和当前行 cur。
2020/06/18 - 99.75% - 几乎效率最佳且写法最简洁
class Solution:
def getRow(self, rowIndex: int) -> List[int]:
if rowIndex == 0:
return [1]
## 递归
pre = self.getRow(rowIndex-1) # 前一行
cur = [1 for _ in range(rowIndex+1)] # 本行, 1 代替 0 值初始化
for i in range(rowIndex//2):
# 左右相同位置元素对称
cur[i+1] = cur[-i-2] = pre[i] + pre[i+1]
return cur
参考文献
https://leetcode-cn.com/explore/learn/card/array-and-string/202/conclusion/1422/
5.3 反转字符串中的单词 III
5.3.1 问题描述
5.3.2 求解过程
法一:使用 str.split() 以 " " 为间隔分割字符串并存入列表 lst 中,然后用列表推导式生成每个元素的反转并存入列表 temp 中,最后用 " ".join() 以 " " 为间隔将列表 temp 分割并返回新的字符串。有多种等价表示。复杂度 O(n)。
2020/06/18 - 99.50% - 大量使用 Python 内置函数而且没有复杂的要求当然快...
class Solution:
def reverseWords(self, s: str) -> str:
# s = "Let's take LeetCode contest"
lst = s.split()
# lst = ["Let's", 'take', 'LeetCode', 'contest']
temp = [lst[i][::-1] for i in range(len(lst))]
# temp = ["s'teL", 'ekat', 'edoCteeL', 'tsetnoc']
return " ".join(temp)
# "s'teL ekat edoCteeL tsetnoc"
# ----------------------------------------------------------------------
'''
lst = s.split()
res = []
for i in range(len(lst)):
res.append(lst[i][::-1])
return ' '.join(res)
'''
# ----------------------------------------------------------------------
'''
return ' '.join(s.split(' ')[::-1])[::-1]
'''
如果不允许使用内置函数或根本没有相关内置函数,则手动遍历和处理也可以。
5.4 寻找旋转排序数组中的最小值
5.4.1 问题描述
5.4.2 求解过程
事实上,遍历整个数组依次比较和查找,或直接使用内置函数 min() 就能解答,但其复杂度为 O(n),属于次优解法,此处不再赘述。出题者可能希望进一步优化时间复杂度,故最先想到的就是二分法。
法一:二分法查找,复杂度 O(logn)。
2020/06/19 - 44.75% - 说明存在特殊的规律及其优化方法
class Solution:
def findMin(self, nums: List[int]) -> int:
## 二分法
start = 0
end = len(nums)-1
while start < end:
mid = (start+end) // 2
if nums[mid] > nums[end]:
start = mid + 1 # 搜索区间右移
if nums[mid] < nums[end]:
end = mid # 搜索区间左移
return nums[start]
法二:根据规律,对二分法在开头特殊条件部分稍作优化,复杂度 O(logn)。
2020/06/19 - 69.49% - 这其实基本是最高效的方法之一了。
class Solution:
def findMin(self, nums: List[int]) -> int:
## 二分法-特定优化
if (len(nums) == 1) or (nums[0] < nums[-1]):
return nums[0] # 单元素 或 全顺序
start = 0
end = len(nums)-1
while start < end:
mid = (start+end) // 2
if nums[mid] > nums[end]:
start = mid + 1 # 搜索区间右移
if nums[mid] < nums[end]:
end = mid # 搜索区间左移
return nums[start]
5.5 删除排序数组中的重复项
5.5.1 问题描述
5.5.2 求解过程
法一:双指针,遍历所有元素,遇见相同元素直接 del 删除,最后只保留一个长度为 k 的、元素各不重复的列表。复杂度 O(n^2)。
2020/06/19 - 31.72% - del 操作不断调整列表 nums 的长度及整体元素的位移,因此效率极低!
class Solution:
def removeDuplicates(self, nums: List[int]) -> int:
if len(nums) <= 1:
return len(nums)
p = 1 # 令指针初始 index = 1
while p < len(nums):
if nums[p-1] == nums[p]: # 相当于双指针/滑动窗口
del nums[p-1] # del 是 O(j-k+1) 复杂度的操作, 并不高效
else:
p += 1
return len(nums)
法二:快慢指针,充分利用题中条件,只在列表 nums 的前 k 个位置保留不重复元素,而无需关心后 n - k 个元素。复杂度 O(n)。
2020/06/19 - 99.95% - 最简洁和高效的方式
class Solution:
def removeDuplicates(self, nums: List[int]) -> int:
# 充分利用题给条件 + 只有O(1)的 index 操作
# 不需要考虑数组中超出新长度后面的元素!
slow = 0 # slow 是不重复元素慢指针,
for fast in range(slow+1, len(nums)): # fast 是顺序搜索快指针
if nums[fast] != nums[slow]: # 一旦找到不同元素, 直接替换到下一位
slow += 1 # 移到待修改位置的 index
nums[slow] = nums[fast] # 替换
return slow+1
# 此外, 以下方法不可行,因为使用了额外空间!导致结果异常
#nums = list(set(nums))
#return len(nums)
5.6 移动零
5.6.1 问题描述
5.6.2 求解过程
法一:快慢指针,慢指针 slow 指向 0 元素,快指针 fast 指向非 0 元素,满足此条件则交换元素,否则分别后移搜素,直至满足条件或超出数组长度。复杂度 O(n)。
2020/06/19 - 85.30% - 朴素的方式
class Solution:
def moveZeroes(self, nums: List[int]) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
if len(nums) <= 1:
return nums
slow = 0 # 0元素慢指针
while nums[slow] != 0:
slow += 1 # 找到第一个0元素索引
if slow >= len(nums):
return nums # 没有0元素则无需后续操作了
fast = slow + 1 # 非0元素快指针
while slow < fast < len(nums):
if nums[slow] == 0 and nums[fast] != 0:
nums[slow], nums[fast] = nums[fast], nums[slow] # 交换
if nums[slow]: # 查找0元素
slow += 1
if not nums[fast]: # 查找非0元素
fast += 1
return nums
''' 测试用例
[0,1,0,3,12]
[0]
[0,0]
[1,0]
[1,0,1]
[4,2,4,0,0,3,0,5,1,0]
'''
法二:对法一的简化与优化,避免不够抽象导致的过于冗长,使纯属简单问题复杂化。复杂度 O(n)。
2020/06/19 - 99.95% - 多维护一个作用域更大的变量开销还是较大的。
class Solution:
def moveZeroes(self, nums: List[int]) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
slow = 0
# 既然 fast 总要遍历整个数组, 那么直接用 for loop 内的局部变量迭代即可
for fast in range(len(nums)):
if nums[fast] != 0:
nums[slow], nums[fast] = nums[fast], nums[slow]
slow += 1
return nums