题目及解题思路来源:LeetCode
1. 两数之和
问题: 给定一个整数数组 n u m s nums nums和一个目标值 t a r g e t target target,请你在该数组中找出和为目标值的那两个整数,并返回他们的数组下标。你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。
示例:
给定 nums = [2, 7, 11, 15], target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9,所以返回 [0, 1]
解决方法
1. 暴力法
遍历每个元素
x
x
x,并查找是否存在一个值与
t
a
r
g
e
t
−
x
target-x
target−x相等的目标元素
class Solution:
def twoSum(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: List[int]
"""
for i in range(len(nums)):
for j in range(i+1, len(nums)):
if nums[j]==target-nums[i]:
return [i, j]
return None
复杂度分析:
- 时间复杂度: O ( n 2 ) O(n^{2}) O(n2),对于每个元素,我们试图通过遍历数组的其余部分来寻找它所对应的目标元素,这将耗费 O ( n ) O(n) O(n)的时间。因此时间复杂度为 O ( n 2 ) O(n^{2}) O(n2)。
- 空间复杂度: O ( 1 ) O(1) O(1)
2. 两遍哈希表
为了对时间复杂度进行优化,我们需要一种更有效的方法检查数组中是否存在目标元素,我们需要找出它的索引。保持数组中的每个元素与其索引相互对应的最好方法是什么?哈希表。
通过以空间换取速度的方式,我们可以将查找时间从
O
(
n
)
O(n)
O(n)降到
O
(
1
)
O(1)
O(1)。哈希表正是为此目的而构建的,它支持以近似恒定的时间进行快速查找。“近似”原因是一旦出现冲突,查找用时可能会退化到
O
(
n
)
O(n)
O(n)。但是只要仔细地挑选哈希函数,在哈希表中进行查找的用时应当被摊销为
O
(
1
)
O(1)
O(1)。
两次迭代。第一次迭代,将每个元素的值和它的索引添加到表中。然后,在第二次迭代中,检查每个元素对应的目标元素
(
t
a
r
g
e
t
−
n
u
m
s
[
i
]
)
(target-nums[i])
(target−nums[i])是否存在表中。注意,该元素不能是
n
u
m
s
[
i
]
nums[i]
nums[i]本身!
# 利用字典实现哈希表
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
"""
:type nums: List[int]
:type target: int
:rtype: List[int]
"""
hashmap = {}
for index, num in enumerate(nums):
hashmap[num] = index
for index, num in enumerate(nums):
j = hashmap.get(target - num)
if j is not None and index != j:
return [index, j]
return None
复杂度分析:
- 时间复杂度: O ( n ) O(n) O(n),我们把包含有 n n n个元素的列表遍历两次。由于哈希表将查找时间缩短到 O ( 1 ) O(1) O(1),所以时间复杂度为 O ( 1 ) O(1) O(1)。
- 空间复杂度: O ( n ) O(n) O(n),所需的额外空间取决于哈希表中存储的元素数量,该表中存储了 n n n个元素。
3. 一遍哈希表
事实证明,我们可以一次完成。再进行迭代并将元素插入到表中的同时,我们还会回头检查表中是否已经存在当前元素所对应的目标元素。如果它存在,那我们已经找到了对应解,并立即将其返回。
# 利用字典实现哈希表
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
"""
:type nums: List[int]
:type target: int
:rtype: List[int]
"""
hashmap = {}
for index, num in enumerate(nums):
another_num = target - num
if another_num in hashmap:
return [hashmap[another_num], index]
hashmap[num] = index
return None
复杂度分析:
- 时间复杂度: O ( n ) O(n) O(n),我们只遍历了包含有 n n n个元素的列表一次。在表中进行的每次查找只花费 O ( 1 ) O(1) O(1)的时间。
- 空间复杂度: O ( n ) O(n) O(n),所需的额外空间取决于哈希表中存储的元素数量,该表中存储了 n n n个元素。
2. 两数相加
问题: 给出两个非空的链表用来表示两个非负的整数。其中,它们各自的位数是按照逆序的方式存储的,并且它们的每个节点只能存储 一位 数字。如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。您可以假设除了数字 0 之外,这两个数都不会以 0 开头。
示例:
输入:(2 -> 4 -> 3) + (5 -> 6 -> 4)
输出: 7 -> 0 -> 8
原因: 342 + 465 = 807
解决方法:
测试用例 | 说明 |
---|---|
l 1 = [ 0 , 1 ] , l 2 = [ 0 , 1 , 2 ] l1=[0,1], l2=[0,1,2] l1=[0,1],l2=[0,1,2] | 当一个列表比另一个列表长时 |
l 1 = [ ] , l 2 = [ 0 , 1 ] l1=[], l2=[0,1] l1=[],l2=[0,1] | 当一个列表为空时,即出现空列表 |
l 1 = [ 9 , 9 ] , l 2 = [ 1 ] l1=[9,9], l2=[1] l1=[9,9],l2=[1] | 求和运算最后可能出现额外的进位,这一点很容易忘记 |
class Solution:
def addTwoNumbers(self, l1: ListNode, l2: ListNode) -> ListNode:
l_sum = ListNode(0)
p = l1
q = l2
curr = l_sum
carry = 0
while(p or q):
x = p.val if p else 0
y = q.val if q else 0
node_sum = x + y + carry
carry = node_sum // 10
curr.next = ListNode(node_sum % 10)
curr = curr.next
p = p.next if p else None
q = q.next if q else None
if carry>0:
curr.next = ListNode(carry)
return l_sum.next
复杂度分析:
- 时间复杂度: O ( m a x ( m , n ) ) O(max(m,n)) O(max(m,n)),假设 m m m和 n n n分别表示 l 1 l1 l1和 l 2 l2 l2的长度,上面的算法最多重复 m a x ( m , n ) max(m,n) max(m,n)次。
- 空间复杂度: O ( m a x ( m , n ) ) O(max(m,n)) O(max(m,n)),新列表的长度最多为 m a x ( m , n ) + 1 max(m,n)+1 max(m,n)+1。
3. 无重复字符的最长字串
问题: 给定一个字符串,请你找出其中不含有重复字符的最长子串的长度。
示例:1
输入: “abcabcbb”
输出: 3
解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。
示例:2
输入: “bbbbb”
输出: 1
解释: 因为无重复字符的最长子串是 “b”,所以其长度为 1。
输入: “pwwkew”
输出: 3
解释: 因为无重复字符的最长子串是 “wke”,所以其长度为 3。
请注意,你的答案必须是子串的长度,“pwke” 是一个子序列,不是子串。
解决方法
1. 暴力法
逐个检查所有的子字符串,看它是否不含有重复的字符。
假设我们有一个函数 boolean allUnique(String substring)
,如果字符串中的字符都是唯一的,它会返回true,否则会返回false。我们可以遍历给定字符串s
的所有可能的字符串并调用函数allUnique
。如果事实证明返回值为true,那么我们将会更新无重复字符串的最大长度的答案。
缺少的部分:
1. 为了给定字符串的所有子字符串,我们需要枚举它们开始和结束的索引。假设开始和结束的索引分别为
i
i
i和
j
j
j。那么我们有
0
≤
i
<
j
≤
n
0\leq i<j\leq n
0≤i<j≤n(这里的结束索引
j
j
j是按惯例排除的)。因此,使用
i
i
i从0到
n
−
1
n-1
n−1以及
j
j
j从
i
+
1
i+1
i+1到
n
n
n的这两个嵌套的循环,我们可以枚举出s
的所有子字符串。
2. 要检查一个字符串是否有重复字符,我们可以使用集合。我们遍历字符串中的所有字符串,并将它们逐个放入set
中。在放置一个字符前,我们检查该集合是否已经包含它。如果包含,我们会返回false
。循环结束后,我们返回true
。
#用到python中的set
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
len_Substring = 0
for i in range(len(s)):
for j in range(i+1, len(s)+1):
if (len(set(s[i:j])) == len(s[i:j])):
len_Substring = max(len_Substring, j-i)
return len_Substring
复杂度分析:
- 时间复杂度(个人理解):
O
(
n
3
)
O(n^{3})
O(n3)
要验证索引范围在 [ i , j ) [i,j) [i,j)内的字符是否都是唯一的,我们需要检查范围中的所有字符。python切片花费 O ( j − i ) O(j-i) O(j−i)的时间。
对于给定的i
,对于所有 j ∈ [ i + 1 , n ] j\in[i+1, n] j∈[i+1,n]所耗费的时间总和为: ∑ i + 1 n O ( j − 1 ) \sum_{i+1}^nO(j-1) ∑i+1nO(j−1)
因此,执行所有步骤耗去的时间总和为: O ( ∑ i = 0 n − 1 ( ∑ j = i + 1 n ( j − i ) ) ) = O ( ∑ i = 0 n − 1 ( 1 + n − i ) ( n − i ) 2 ) = O ( n 3 ) O\Big(\sum_{i=0}^{n-1}\Big(\sum_{j=i+1}^n(j-i)\Big)\Big)=O\Big(\sum_{i=0}^{n-1}\frac{(1+n-i)(n-i)}{2}\Big)=O(n^3) O(∑i=0n−1(∑j=i+1n(j−i)))=O(∑i=0n−12(1+n−i)(n−i))=O(n3) - 空间复杂度:
O
(
m
i
n
(
n
,
m
)
)
O(min(n,m))
O(min(n,m)),我们需要
O
(
k
)
O(k)
O(k)的空间来检查子字符串中是否有重复字符,其中
k
k
k表示
Set
的大小。而Set
的大小取决于字符串n
的大小以及字符集/字母m
的大小。
2. 滑窗法
什么是滑动窗口?
其实就是一个队列,比如例题中的 abcabcbb,进入这个队列(窗口)为 abc 满足题目要求,当再进入 a,队列变成了 abca,这时候不满足要求。所以,我们要移动这个队列!
如何移动?
我们只要把队列的左边的元素移出就行了,直到满足题目要求!一直维持这样的队列,找出队列出现最长的长度时候,求出解!
#利用python中的set进行判断
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
left = 0
lookup = set()
max_len = 0
cur_len = 0
for i in range(len(s)):
cur_len += 1
while s[i] in lookup:
lookup.remove(s[left])
left += 1
cur_len -= 1
if cur_len > max_len:
max_len = cur_len
lookup.add(s[i])
return max_len
复杂度分析:
- 时间复杂度: O ( n ) O(n) O(n),只进行一次的遍历。
- 空间复杂度: O ( m , n ) O(m,n) O(m,n)。