16. 买卖股票的最佳时机
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择某一天买入这只股票,并选择在未来的某一个不同的日子卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
示例:
输入:[7,1,5,3,6,4] 输出:5 解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
常用的算法分别是:一次遍历法、快慢指针法、暴力法 和 分治法。
一次遍历法:
使用一次遍历求最大股票利润的代码的思路:
- 初始化最小价格 min_price 为正无穷大,最大利润 max_profit 为 0
- 遍历价格数组 prices,对每个价格 price:
- 更新 min_price,使其始终记录到当前为止遇到的最小价格
- 计算当前价格减去最小价格的利润 profit
- 更新 max_profit,使其维护最大利润
- 遍历完成后,max_profit 即为最大利润
# 一次遍历法 时间复杂度:O(n) 空间复杂度:O(1)
def maxProfit1(prices):
min_price = float('inf')
max_profit = 0
for price in prices:
min_price = min(min_price, price)
profit = price - min_price
max_profit = max(max_profit, profit)
return max_profit
时间复杂度:O(n),只需要遍历一次价格数组,进行最小价格和最大利润的更新计算,所以时间复杂度为线性复杂度O(n)。
空间复杂度:O(1),只需要两个变量min_price和max_profit来保存中间结果,空间复杂度为常数阶O(1)。
快慢指针法:
两种快慢指针解法思路都是正确的:
- 快慢指针法1:
- 使用while循环遍历,两个指针i, j
- 根据价格比较情况,更新左指针
- 计算利润,更新最大利润
- 快慢指针法2:
- 使用for循环,一个指针i,,一个索引j
- 比较价格更新左指针
- 计算利润,更新最大利润
两种实现思路非常相似,主要不同在于:
- 第一种使用双指针i,j和while循环
- 第二种简化为单指针i和for循环
# 快慢指针法1 时间复杂度:O(n) 空间复杂度:O(1)
def maxProfit2(prices):
profit = 0
i, j = 0, 1
if len(prices) <= j:
return 0
left, right = prices[i], prices[j]
while True:
if right < left:
left = right
i = j
else:
profit = max(profit, right - left)
left = prices[i]
j += 1
if j >= len(prices):
break
else:
right = prices[j]
return profit
# 快慢指针法2 时间复杂度:O(n) 空间复杂度:O(1)
def maxProfit2_1(prices):
profit = 0
i = 0
for j in range(1, len(prices)): # 用for循环遍历数组
if prices[j] < prices[i]: # 如果右边的元素小于左边的元素
i = j # 更新左边的指针
else: # 否则
profit = max(profit, prices[j] - prices[i]) # 更新利润
return profit
时间复杂度:O(n),快慢指针需要线性遍历数组一次,所以时间复杂度为 O(n)。
空间复杂度:O(1),只需要几个变量存放指针和中间结果,所以空间复杂度为 O(1) 的常数复杂度。
暴力法:
使用暴力法求解最大股票利润的代码思路是:
- 使用双层for循环枚举所有买入和卖出的组合
- 计算每种组合的利润profit
- 更新最大利润max_profit
# 暴力法 时间复杂度:O(n^2) 空间复杂度:O(1)
def maxProfit3(prices):
max_profit = 0
for i in range(len(prices)):
for j in range(i+1, len(prices)):
profit = prices[j] - prices[i]
max_profit = max(max_profit, profit)
return max_profit
时间复杂度:O(N^2),外层循环为N次,内层循环也为N次,所以总体复杂度是N^2。
空间复杂度:O(1),只使用了两个变量max_profit和profit,占用常数大小的额外空间。
差分法:
使用差分法求最大股票利润的思路:
- 计算价格数组的差分(对应两天的价格差值)
- 将差分累加可以得到截止到当前天的总利润
- 如果累加利润小于0,则重置为0(处理价格下跌情况)
- 比较当前累加利润和之前最大利润,实时维护最大利润
- 遍历完成后,最大利润即所求
# 差分法 时间复杂度:O(n) 空间复杂度:O(1)
def maxProfit4(prices):
# 初始化最大利润、初始化当前利润
max_profit = 0
cur_profit = 0
for i in range(1, len(prices)):
# 计算差分
diff = prices[i] - prices[i-1]
# 更新当前利润,如果小于0,则设置为0
cur_profit = max(cur_profit+diff, 0)
# 更新最大利润
max_profit = max(max_profit, cur_profit)
return max_profit
时间复杂度: O(n),需要线性遍历数组一次
空间复杂度: O(1):,只使用了两个变量
分治法:
分治法求最大股票利润算法的思路:
- 定义递归函数helper,以区间[left, right]为参数
- 如果区间只有1个或0个元素,最大利润为0
- 找到区间的中点mid,递归求解左右两区间
- 计算跨越mid的最大利润:找到左区间最大值、右区间最小值
- 返回三者最大值作为该区间的最大利润
# 分治法 时间复杂度:O(nlogn) 空间复杂度:O(log(n))
def maxProfit5(prices): # 定义函数maxProfit5,接受一个数组prices作为参数
# 定义辅助函数helper,求解区间[left, right]内的最大利润
def helper(left, right):
# 如果区间长度小于等于1,则返回0
if left >= right:
return 0
# 计算中点位置,左/右半部分的最大利润
mid = (left + right) // 2 # 使用整除运算符//来计算中点位置
left_max = helper(left, mid) # 递归调用helper函数,求解左半部分[left, mid]的最大利润
right_max = helper(mid+1, right) # 递归调用helper函数,求解右半部分[mid+1, right]的最大利润
# 求解跨越中点的最大利润,即找到左半部分的最低价格和右半部分的最高价格
min_price = prices[left] # 初始化最低价格为区间左端点的价格
for i in range(left+1, mid+1): # 遍历左半部分[left+1, mid]的元素
min_price = min(min_price, prices[i]) # 更新最低价格为当前价格和之前最低价格中的较小值
max_price = prices[mid + 1] # 初始化最高价格为区间右端点的价格
for i in range(mid+2, right+1): # 遍历右半部分[mid+2, right]的元素
max_price = max(max_price, prices[i]) # 更新最高价格为当前价格和之前最高价格中的较大值
# 计算跨越中点的最大利润
cross_max = max_price - min_price # 使用最高价格减去最低价格得到跨越中点的最大利润
# 返回三者中的最大值
return max(left_max, right_max, cross_max) # 使用max函数返回左半部分、右半部分和跨越中点三者中的最大值作为区间[left, right]内的最大利润
# 调用辅助函数helper,求解整个数组的最大利润
return helper(0, len(prices)-1) # 传入参数0和len(prices)-1,表示整个数组的区间[0, len(prices)-1]
时间复杂度:每次将区间分成两个子区间,然后递归求解它们,直到区间长度小于等于1为止。这样,每一层递归都需要遍历整个数组,所以每一层递归的时间复杂度是O(n),其中n是数组的长度。而递归的深度是log(n),因为每次区间长度减半。所以总的时间复杂度是O(n)乘以O(log(n)),即O(nlog(n))。
空间复杂度:只需要常数个额外变量来存储最低价格、最高价格、中点位置等信息,所以额外空间复杂度是O(1)。而递归调用会消耗栈空间,栈空间与递归深度成正比,所以栈空间复杂度是O(log(n))。所以总的空间复杂度是O(1)加上O(log(n)),即O(log(n))。