你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 1:
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:
输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。
我的代码(4ms):
class Solution(object):
def rob(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
n = len(nums)
if n == 0:
return 0
if n == 1:
return nums[0]
if n == 2:
return max(nums[0], nums[1])
# dp[i] 表示到达第 i 个房子时能够偷窃的最大金额
dp = [0] * n
dp[0] = nums[0]
dp[1] = max(nums[0], nums[1])
for i in range(2, n):
# 数组dp[i]的本质是存储该房子作出选择(偷、不投)后的最大的值,它一定是当前最大金钱数,
#而偷与不偷,看i的前两个
#当i-2的位置的值加i位置的值大于i-1位置的值,则为偷,dp[i]所代表的最大金钱树为dp[i-2]+nums[i]
#当i-2的位置的值加i位置的值小于i-1位置的值,则为不偷,dp[i]所代表的最大金钱树为dp[i-1]
dp[i] = max(dp[i-1], dp[i-2] + nums[i])
return dp[-1] # 最后一间房子存储的就是最大金额
优秀代码(3ms):
class Solution(object):
def rob(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
n = len(nums)
if n == 0:
return 0
if n == 1:
return nums[0]
if n == 2:
return max(nums[0], nums[1])
# dp[i] 表示到达第 i 个房子时能够偷窃的最大金额
dp = [0] * n
dp[0] = nums[0]
dp[1] = max(nums[0], nums[1])
for i in range(2, n):
# 数组dp[i]的本质是存储该房子作出选择(偷、不投)后的最大的值,它一定是当前最大金钱数,
#而偷与不偷,看i的前两个
#当i-2的位置的值加i位置的值大于i-1位置的值,则为偷,dp[i]所代表的最大金钱树为dp[i-2]+nums[i]
#当i-2的位置的值加i位置的值小于i-1位置的值,则为不偷,dp[i]所代表的最大金钱树为dp[i-1]
dp[i] = max(dp[i-1], dp[i-2] + nums[i])
return dp[-1] # 最后一间房子存储的就是最大金额
二者的差别:
改进版代码使用了滚动数组(两个变量代替了原本需要存储整个数组的dp
数组),从而减少了空间复杂度。这种方法更高效,既节省了内存,又减少了操作的开销。
1. 你的第一种代码(使用dp
数组):
dp = [0] * n
dp[0] = nums[0]
dp[1] = max(nums[0], nums[1])
for i in range(2, n):
dp[i] = max(dp[i-1], dp[i-2] + nums[i])
- 时间复杂度:
O(n)
,因为你遍历了数组中的每一个元素一次。 - 空间复杂度:
O(n)
,因为你创建了一个长度为n
的dp
数组来存储每个房子的最大金额。
2. 改进版代码(使用滚动数组):
dp0 = 0
dp1 = 0
for i in range(len(nums)):
dp0, dp1 = dp1, max(dp0 + nums[i], dp1)
- 时间复杂度: 依然是
O(n)
,遍历一次数组,没有改变。 - 空间复杂度:
O(1)
,因为它只使用了两个变量dp0
和dp1
,代替了原本的dp
数组,不再需要额外的数组来存储每个状态。
为什么改进版代码更高效?
-
内存使用更少: 第一种方法使用了一个长度为
n
的数组dp
,来存储每个房子的最大金额。而改进后的方法只需要两个变量dp0
和dp1
来追踪前两间房子是否被偷,所以空间复杂度从O(n)
优化到了O(1)
。在较大的输入情况下,减少内存占用可以有效提升程序性能。 -
变量替代数组: 在每次循环中,
dp0
和dp1
不断被更新,而不需要存储整个dp
数组的历史值。通过这种方式,避免了访问数组带来的额外开销(如内存管理),从而在实际运行中耗时更短。
为什么耗时更短?
虽然两种方法的时间复杂度都是O(n)
,但改进版减少了内存的使用,也省去了对数组的索引和访问操作。数组的索引访问虽然也很快,但相比直接操作变量要稍微慢一点,因为变量存储在寄存器中,而数组需要通过地址去访问内存中的数据。
因此,改进版代码更快,是因为它避免了不必要的内存操作和开销,直接用两个变量更新结果,使得它在时间和空间上都更加高效。
优秀代码(0ms):
class Solution(object):
def rob(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
if not nums:
return 0
dp = [[0 for i in range(len(nums) + 1)] for j in range(2)]
dp[0][1] = 0
dp[1][1] = nums[0]
for j in range(2, len(nums) + 1):
dp[0][j] = max(dp[0][j - 1], dp[1][j - 1])
dp[1][j] = nums[j - 1] + max(dp[0][j - 2], dp[1][j - 2])
return max(dp[0][len(nums)], dp[1][len(nums)])
区别在哪:
这个新代码的设计使用了一个二维数组dp
,它通过状态存储和状态转移实现问题的解决。下面是对这个代码的分析,以及与之前版本的差异和优劣对比。
1. 代码结构说明:
dp = [[0 for i in range(len(nums) + 1)] for j in range(2)]
这里创建了一个二维数组dp
,它的作用是分别保存两种状态:
dp[0][j]
表示到第j
个房子时,不偷这个房子时的最大金额。dp[1][j]
表示到第j
个房子时,偷这个房子时的最大金额。
在循环中,代码分别更新两种情况的最大金额:
- 如果不偷第
j
个房子,最大金额是之前的状态max(dp[0][j-1], dp[1][j-1])
。 - 如果偷第
j
个房子,最大金额是当前房子的金额加上两间之前的最大值nums[j-1] + max(dp[0][j-2], dp[1][j-2])
。
最后,返回不偷或偷最后一个房子的最大值:max(dp[0][len(nums)], dp[1][len(nums)])
。
2. 优劣分析:
优点:
- 清晰的状态表示:通过二维数组分别表示偷与不偷的两种状态,这种设计有助于清晰表达每个状态的含义,便于调试和理解。特别是在有更多条件或状态时,这种代码可以更直观地表达不同的状态转移。
缺点:
-
空间复杂度高:这个代码的空间复杂度是
O(2n)
,因为创建了一个大小为2 x (n+1)
的二维数组。相较于之前的滚动数组版本,显然更浪费内存。实际上,只需要存储最近两个状态,因此二维数组的使用是过度的。 -
不必要的内存分配:只需存储前两个状态的值,因此可以用两个变量而不是二维数组。二维数组在逻辑上是冗余的。
性能对比:
-
时间复杂度: 这段代码的时间复杂度依旧是
O(n)
,因为每个房子都会被访问一次,无论是这段代码还是之前的滚动数组代码,它们的时间复杂度是相同的。 -
空间复杂度: 滚动数组的方法(两个变量)只需要
O(1)
的空间,而这段代码使用了O(n)
的空间,因此滚动数组更为优秀。
3. 总结:
- 差别在于空间使用:这段代码虽然逻辑清晰,但使用了不必要的二维数组,导致空间复杂度为
O(n)
,而之前的滚动数组版本只需O(1)
的空间,因此滚动数组更优秀。 - 优秀之处在于简洁性:滚动数组的代码不仅减少了空间使用,也减少了内存操作,代码更为简洁,同时具备更高的性能。