LeetCode详解之如何一步步优化到最佳解法:1.两数之和
1.两数之和
题目链接:1.两数之和
解法1:暴力解法
代码:
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
for i in range(len(nums)-1):
for j in range(i+1, len(nums)):
if nums[i] + nums[j] == target:
return [i, j]
解法性能:
解法分析:
该解法使用了两层嵌套循环,对于每一个合理的组合都进行了判断,包括1次加法(nums[i] + nums[j]),1次比较(”nums[i] + nums[j] == target”中的”==”)。
因为对于嵌套内的每一个组合都进行1次加法,1次比较,所以总共执行了
O ( N 2 ) O(N^2) O(N2)次加法,和 O ( N 2 ) O(N^2) O(N2)次比较
上面这个加法和比较的复杂度,我们取复杂度最高的一个作为时间复杂度,所以该算法的时间复杂度为:
O ( N 2 ) O(N^2) O(N2)
同时,我们使用的变量只有i和j两个整型类型的变量,所以该算法的空间复杂度为:
O ( 1 ) O(1) O(1)
那么,我们进一步分析,对于最外层的嵌套i,是否每一个i,都要进行N次的加法呢?
答案为,不是的。
我们需要寻找nums[i] + nums[j] == target,而对于当前的嵌套i,nums[i]是不变的,我们需要寻找target - nums[i]是否和某一个j对应的nums[j]相等。那么,对于一个i,我们只需要算一次target - nums[i],然后判断这个值是否和某一个j对应的nums[j]相等即可。对应的代码如下:
解法2:改进版1
代码:
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
for i in range(len(nums)-1):
target_j_value = target - nums[i]
for j in range(i+1, len(nums)):
if nums[j] == target_j_value:
return [i, j]
解法性能:
解法分析:
通过比较该解法性能和解法1的性能,我们可以看到所需要的时间大幅减少。这是因为对于最外层的嵌套i,每一个i,我们只需要执行1次加法”target_j_value = target - nums[i]”(减法可以理解为一种系数为-1的加法)。这样,整体需要的加法次数为:
O ( N ) O(N) O(N)
当然,需要的比较次数没有什么变化,还是 O ( N 2 ) O(N^2) O(N2)次比较。因此,比较的次数决定了整体的时间复杂度为:
O ( N 2 ) O(N^2) O(N2)
对于空间复杂度,虽然多了一个整型变量target_j_value,但是不影响整体的空间复杂度:
O ( 1 ) O(1) O(1)
接下来,我们研究如何减少比较的次数,从而降低时间复杂度。
根据解法2的代码,我们可以发现,对于每一个i,都进行了 O ( N ) O(N) O(N)次的比较,查询是否有对应的数能满足条件。那么,对于每一个i,我们能否做到只需要 O ( 1 ) O(1) O(1)次的比较就能做出同样的判断呢?
答案是可以的,只需要先遍历一遍数组,获得一个哈希表即可。对应的代码如下:
解法3:改进版2
代码:
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
store = {}
length = len(nums)
for i in range(length):
if nums[i] not in store.keys():
store[nums[i]] = [i]
else:
store[nums[i]].append(i)
for i in range(length-1):
target_value = target - nums[i]
if target_value in store.keys():
for index in store[target_value]:
if index > i:
return [i, index]
解法性能:
解法分析:
当前这个解法,因为我们将整个数组每一个元素的数值和对应的索引值存储到了哈希表中,所以空间复杂度和数组的长度有关,为:
O ( N ) O(N) O(N)
整体来说,第一个for循环是 O ( N ) O(N) O(N)的时间复杂度,因为只是遍历了数组并将数据存储进哈希表中。
第二个for循环中,对于每一个i,进行了1次加法,和1次比较(”if target_value in store.keys()”这是比较),所以第二个for循环的时间复杂度也是 O ( N ) O(N) O(N)。
整体来说,时间复杂度为 O ( N ) O(N) O(N),可以对比解法2和解法3所需要的时间看出性能上的优化。
但是,整体遍历了2次。那么,我们能不能只遍历1次,就能达到目标呢?
我们来分析一下:
我们在寻找一个i = m, j = n,其中m < n, i < j,使得
nums[m] + nums[n] == target
即:nums[m] == target - nums[n]
我们在一遍遍历的时候,可以先判断当前的i,(target - nums[i])是否在之前有对应的数。
如果没有的话,把nums[i]和对应的索引i存进哈希表,留给后面判断;如果有的话,则(target - nums[i])对应的索引,和当前的索引i,即是我们需要的答案。
当然,对于i = 0, 对于第一个值,没有比它更靠前的了,所以直接将它存储到哈希表中即可。对应的代码如下:
解法4:最终版
代码:
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
store = {}
for i in range(0, len(nums)):
if nums[i] in store.keys():
return [store[nums[i]], i]
else:
current_diff = target - nums[i]
if current_diff not in store.keys():
store[current_diff] = i
解法性能:
解法分析:
对比解法3和解法4,在时间复杂度和空间复杂度上并没有优化(因为复杂度不考虑前面的系数),但是所需要的运行时间优化了不少。
这就是第1题,从最开始的暴力解法一步步优化到最优解法的全部优化分析过程。
如有疑问,欢迎大家在评论区讨论。