每日刷2道leetcode算法题,在博客分享一些题目的题解和总结,愿与君共进步!
1.两数之和
1.初步暴力解:
class Solution:
def twoSum(self, nums, target):
n=len(nums)
for i in range(n):
for j in range(i+1,n):
if nums[i]+nums[j]==target:
return [i,j]
执行用时:3372 ms,内存消耗:15.5 MB
2.换个思路,第二次循环的时候直接在列表里搜索
t
a
r
g
e
t
−
n
u
m
[
i
]
target-num[i]
target−num[i],这样可以减少一次循环:
class Solution:
def twoSum(self, nums, target):
n=len(nums)
for i in range(n):
if (target-nums[i]) in nums:
j = nums.index(target-nums[i])
if j != i:
return [i,j]
执行用时:736 ms,内存消耗:15.3 MB
3.进一步优化上述解法,
n
u
m
2
num2
num2的查找并不需要每次从
n
u
m
s
nums
nums查找一遍,只需要从
n
u
m
1
num1
num1位置之前或之后查找即可。但为了方便
i
n
d
e
x
index
index这里选择从
n
u
m
1
num1
num1位置之前查找
class Solution:
def twoSum(self, nums, target):
n=len(nums)
for i in range(n):
if (target-nums[i]) in nums[:i]:
j = nums.index(target-nums[i])
if j != i:
return [i,j]
执行用时:500 ms,内存消耗:15.6 MB
4.采用哈希表查询,用字典的形式存储与查询
class Solution:
def twoSum(self, nums, target):
hashmap={}
for i,n in enumerate(nums):
hashmap[n] = i
for i,n in enumerate(nums):
j = hashmap.get(target - n)
if j is not None and i!=j:
return [i,j]
执行用时:36 ms,内存消耗:16 MB
2.两数相加
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def addTwoNumbers(self, l1: ListNode, l2: ListNode) -> ListNode:
# 初始化个位节点,先不做进位
newPoint = ListNode(l1.val + l2.val)
# rt用来作为最后return的节点,tp用来遍历节点
rt, tp = newPoint, newPoint
# l1,l2只要后面还有节点,就继续往后遍历;或者新链表还需要继续往后进位
while (l1 and (l1.next != None)) or (l2 and (l2.next != None)) or (tp.val > 9):
l1, l2 = l1.next if l1 else l1, l2.next if l2 else l2
tmpsum = (l1.val if l1 else 0) + (l2.val if l2 else 0)
# 计算新链表下个节点的值(当前节点的进位+当前l1 l2的值之和),先不做进位
tp.next = ListNode(tp.val//10 + tmpsum)
# 新链表当前节点的值取个位
tp.val %= 10
# 新链表往后遍历一个节点
tp = tp.next
return rt
执行用时:68 ms,内存消耗:14.9 MB
换个写法
class Solution:
def addTwoNumbers(self, l1: ListNode, l2: ListNode) -> ListNode:
dummy = p = ListNode(None)
s = 0
while l1 or l2 or s != 0:
s += (l1.val if l1 else 0) + (l2.val if l2 else 0)
p.next = ListNode(s % 10)
p = p.next
if l1: l1 = l1.next
if l2: l2 = l2.next
s = s // 10
return dummy.next
3.无重复字符的最长子串
哈希表+双指针
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
dic, res, i = {}, 0, -1 # i是左指针
for j in range(len(s)): # j是右指针
if s[j] in dic: # 若当前元素在之前出现过,更新左指针
# 当之前出现的元素在左右指针中间,左指针更新为之前元素下标,若不在中间,左指针不变
i = max(i, dic[s[j]])
dic[s[j]] = j # 将当前元素加入哈希表中
res = max(res, j - i)
return res
执行用时:64 ms,内存消耗:15.1 MB
5. 最长回文子串
给你一个字符串 s s s,找到 s s s中最长的回文子串。
动态规划(填dp表、当前ij状态、过去ij状态、如何联合得到输出、边界条件)
1.定义状态:题目让我们求什么,就把什么设置为状态
题目求s中最长的回文子串,那就判断所有子串是否为回文子串,选出最长的
因此:
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]表示
s
[
i
:
j
+
1
]
s[i:j+1]
s[i:j+1]是否为回文子串(这里+1是为了构造闭区间)
2.状态转移方程:对空间进行分类讨论(当前ij状态、过去ij状态 如何联合得到输出)
当前ij状态:头尾必须相等(
s
[
i
]
=
=
s
[
j
]
s[i]==s[j]
s[i]==s[j])
过去ij状态:去掉头尾之后还是一个回文(
d
p
[
i
+
1
]
[
j
−
1
]
i
s
T
r
u
e
dp[i+1][j-1] is True
dp[i+1][j−1]isTrue)
边界条件:只要是找过去ij状态的时候,就会涉及边界条件(即超出边界情况处理)
当i==j时一定是回文
j-1-(i+1)<=0,即j-i<=2时,只要当
s
[
i
]
=
=
s
[
j
]
s[i]==s[j]
s[i]==s[j]时就是回文,不用判断
d
p
[
i
+
1
]
[
j
−
1
]
dp[i+1][j-1]
dp[i+1][j−1],
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]为截取的子串
3.初始状态:这里已经直接判断 j − i < = 2 j-i<=2 j−i<=2的情况了,因此用不到初始状态,可以不设
4.输出内容:每次发现新回文都比较一下长度,记录i与长度
5.优化空间提速
class Solution:
def longestPalindrome(self, s: str) -> str:
size = len(s)
# 特殊处理
if size == 1:
return s
# 创建动态规划dp表
dp = [[False for _ in range(size)] for _ in range(size)]
# 初始长度为1,这样万一不存在回文,就返回第一个值(初始条件设置的时候一定要考虑输出)
max_len = 1
start = 0
for j in range(1,size):
for i in range(j):
# 边界条件:
# 只要头尾相等(s[i]==s[j])就能返回True
if j-i<=2:
if s[i]==s[j]:
dp[i][j] = True
cur_len = j-i+1
# 状态转移方程
# 当前dp[i][j]状态:头尾相等(s[i]==s[j])
# 过去dp[i][j]状态:去掉头尾之后还是一个回文(dp[i+1][j-1] is True)
else:
if s[i]==s[j] and dp[i+1][j-1]:
dp[i][j] = True
cur_len = j-i+1
# 出现回文更新输出
if dp[i][j]:
if cur_len > max_len:
max_len = cur_len
start = i
return s[start:start+max_len]
647. 回文子串
法一:暴力法。暴力法主要浪费在判断回文串上,不能有效利用同中心的回文串的状态,简单来说就是此时我们假设前面的子串 s [ j , i ] s[j,i] s[j,i]是回文串,那么,子串 s [ j − 1 , i + 1 ] s[j-1,i+1] s[j−1,i+1]也有可能是回文串,不难想出当且仅当子串 s [ j , i ] s[j,i] s[j,i]是回文串且 s [ j − 1 ] = s [ i + 1 ] s[j-1]=s[i+1] s[j−1]=s[i+1]时,子串 s [ j − 1 , i + 1 ] s[j-1,i+1] s[j−1,i+1]也是回文串,于是我们可以通过数组保存子串是否是回文串,然后通过递推上一次的状态,得到下一次的状态,属于动态规划的解法,令 d p [ j ] [ i ] dp[j][i] dp[j][i]表示子串 s [ j , i ] s[j,i] s[j,i]是否是回文串,状态转移如下:
1当
i
=
j
i=j
i=j时,单个字符肯定是回文串,可以看成奇数回文串的起点
2当
s
[
i
]
=
s
[
j
]
s[i]=s[j]
s[i]=s[j]且
i
−
j
=
1
i-j=1
i−j=1,则
d
p
[
j
]
[
i
]
dp[j][i]
dp[j][i]是回文串,可以看成偶数回文串的起点
3当
s
[
i
]
=
s
[
j
]
s[i]=s[j]
s[i]=s[j]且
d
p
[
j
+
1
]
[
i
−
1
]
dp[j+1][i-1]
dp[j+1][i−1]是回文串,则
d
p
[
j
]
[
i
]
dp[j][i]
dp[j][i]也是回文串
4其他情形都不是回文串
其中条件1、2、3是可以合并的:
当
s
[
i
]
=
s
[
j
]
s[i]=s[j]
s[i]=s[j]时,
d
p
[
j
+
1
]
[
i
−
1
]
dp[j+1][i-1]
dp[j+1][i−1]是回文串或
i
−
j
<
2
i-j<2
i−j<2,则
d
p
[
j
]
[
i
]
dp[j][i]
dp[j][i]也是回文串
class Solution:
def countSubstrings(self, s: str) -> int:
res = 0
n = len(s)
dp = [[0] * n for _ in range(n)]
for i in range(n):
for j in range(i + 1):
if s[i] == s[j] and (i - j < 2 or dp[j + 1][i - 1]):
dp[j][i] = 1
res += 1
return res
复杂度分析
时间复杂度:
O
(
n
2
)
O(n^2)
O(n2)
空间复杂度:
O
(
n
2
)
O(n^2)
O(n2)
法二:动态规划的思想优点是比较好想,缺点也很明显,空间复杂度比较高,那么回顾一下,我们只是需要得到同中心的回文子串数目,显然只要我们找到子串的中心,然后使用两个指针不断向两端延伸即可,也就是中心扩展法,这样空间复杂度就降到 O ( 1 ) O(1) O(1)了。找中心很简单,字符串中的元素逐一遍历即可,但是要注意是奇数还是偶数回文串,奇数回文串中心只有一个,而偶数回文串中心有两个,我们事先不知道奇偶,解决办法可以参考官方题解的,也可以都计算一遍,这样比较好想。
class Solution:
def countSubstrings(self, s: str) -> int:
n = len(s)
self.res = 0
def helper(i,j):
while i >= 0 and j < n and s[i] == s[j]:
i -= 1
j += 1
self.res += 1
for i in range(n):
helper(i,i)
helper(i,i+1)
return self.res
复杂度分析
时间复杂度:
O
(
n
2
)
O(n^2)
O(n2)
空间复杂度:
O
(
1
)
O(1)
O(1)