前言:
算法训练系列是做《代码随想录》一刷,个人的学习笔记和详细的解题思路,总共会有60篇博客来记录,计划用60天的时间刷完。
内容包括了面试常见的10类题目,分别是:数组,链表,哈希表,字符串,栈与队列,二叉树,回溯算法,贪心算法,动态规划,单调栈。
博客记录结构上分为 思路,代码实现,复杂度分析,思考和收获,四个方面。
如果这个系列的博客可以帮助到读者,就是我最大的开心啦,一起LeetCode一起进步呀;)
目录
LeetCode1005.K次取反后最大化的数组和
1. 思路
2. 代码实现
3. 复杂度分析
4. 思考与收获
Leetcode134. 加油站
方法一: 暴力解法
1. 思路
2. 代码实现
3. 复杂度分析
4. 思考与收获
方法二:宏观的贪心算法
1. 思路
2. 代码实现
3. 复杂度分析
4. 思考与收获
方法三:贪心解法
1. 思路
2. 代码实现
3. 复杂度分析
4. 思考与收获
总结
Leetcode135. 分发糖果
1. 思路
2. 代码实现
3. 复杂度分析
4. 思考与收获
链接:1005. K 次取反后最大化的数组和 - 力扣(LeetCode)
本题思路其实比较好想了,如何可以让数组和最大呢?
贪心的思路,局部最优:让绝对值大的负数变为正数,当前数值达到最大,整体最优:整个数组和达到最大。
局部最优可以推出全局最优。
那么如果将负数都转变为正数了,K依然大于0,此时的问题是一个有序正整数序列,如何转变K次正负,让 数组和 达到最大。
那么又是一个贪心:局部最优:只找数值最小的正整数进行反转,当前数值可以达到最大(例如正整数数组{5, 3, 1},反转1 得到-1 比 反转5得到的-5 大多了),全局最优:整个 数组和 达到最大。
虽然这道题目大家做的时候,可能都不会去想什么贪心算法,一鼓作气,就AC了。
我这里其实是为了给大家展现出来 经常被大家忽略的贪心思路,这么一道简单题,就用了两次贪心!
那么本题的解题步骤为:
# time:O(NlogN);space:O(N)
class Solution(object):
def largestSumAfterKNegations(self, nums, k):
"""
:type nums: List[int]
:type k: int
:rtype: int
"""
# 将数组nums按照绝对值大小,从大到小排序
sortedNums = sorted(nums,key=abs,reverse=True)
index = 0
# 从大的开始,把负数变成正数
while k>0 and index0:
sortedNums[-1] *= (-1)**k
return sum(sortedNums)
时间复杂度:O(logN)
其中N为数组nums的长度,首先需要对数组nums进行排序的时间复杂度为O(NlogN);然后需要遍历一遍数组,从大到小把尽可能多的负数变成正数,O(N),还有sum操作,复杂度O(N),总体时间复杂度O(NlogN);
空间复杂度:O(N)
其中N为nums的长度,sorted排序新建了一个数组,O(N);
空间复杂度上还可以继续优化,不用sorted,而用sort,就会在原数组上进行操作,不会新建一个数组,复杂度可以降低为O(1),代码如下:
# time:O(NlogN);space:O(1)
class Solution(object):
def largestSumAfterKNegations(self, nums, k):
"""
:type nums: List[int]
:type k: int
:rtype: int
"""
# 将数组nums按照绝对值大小,从大到小排序
nums.sort(key=abs,reverse=True)
index = 0
# 从大的开始,把负数变成正数
while k>0 and index0:
nums[-1] *= (-1)**k
return sum(nums)
贪心的题目如果简单起来,会让人简单到开始怀疑:本来不就应该这么做么?这也算是算法?我认为这不是贪心?本题其实很简单,不会贪心算法的同学都可以做出来,但是我还是全程用贪心的思路来讲解。因为贪心的思考方式一定要有!如果没有贪心的思考方式(局部最优,全局最优),很容易陷入贪心简单题凭感觉做,贪心难题直接不会做,其实这样就锻炼不了贪心的思考方式了。所以明知道是贪心简单题,也要靠贪心的思考方式来解题,这样对培养解题感觉很有帮助!
Reference:代码随想录 (programmercarl.com)
本题学习时间:30分钟。
链接:134. 加油站 - 力扣(LeetCode)
遍历每一个加油站为起点的情况,模拟一圈;
如果跑了一圈,中途没有断油,而且最后油量大于等于0,说明这个起点是ok的
# 解法一: 暴力解法
# Python 会超时
# time:O(N^2);space:O(1)
class Solution(object):
def canCompleteCircuit(self, gas, cost):
"""
:type gas: List[int]
:type cost: List[int]
:rtype: int
"""
# 每个起点都尝试一遍
for i in range(len(cost)):
# 先走到i的下一步
# 记录剩余的油量
rest = gas[i] - cost[i]
index = (i+1)%len(cost)
# 模拟以i为起点跑下剩余的一圈
while rest>0 and index!=i:
rest += gas[index]-cost[index]
index = (index+1)%len(cost)
# 如果以i为起点跑一圈,剩余油量>=0,返回该起始位置
if rest>=0 and index==i: return i
return -1
时间复杂度:O(N^2)
其中N为加油站的个数,也是gas数组和cost数组的长度,需要以每个加油站为起点,模拟跑圈跑一遍,所以时间复杂度为O(N^2);
空间复杂度:O(1)
只有常数个变量来记录;
直接从全局进行贪心选择,情况如下:
# 解法二:宏观贪心
# time:O(N);space:O(1)
class Solution(object):
def canCompleteCircuit(self, gas, cost):
"""
:type gas: List[int]
:type cost: List[int]
:rtype: int
"""
totalSum = 0
totalMin = float("inf")
n = len(gas)
for i in range(n):
totalSum += gas[i]-cost[i]
totalMin = min(totalMin,totalSum)
if totalSum<0: return -1
if totalMin>=0: return 0
for j in range(n-1,-1,-1):
totalMin += gas[j]-cost[j]
if totalMin >=0: return j
时间复杂度:O(N)
从头到尾遍历数组不超过两遍,所以O(N);
空间复杂度:O(1);
只有常数个变量需要保存;
可以换一个思路,首先如果总油量减去总消耗大于等于零那么一定可以跑完一圈,说明 各个站点的加油站 剩油量rest[i]相加一定是大于等于零的。
每个加油站的剩余量rest[i]为gas[i] - cost[i]。
i从0开始累加rest[i],和记为curSum,一旦curSum小于零,说明[0, i]区间都不能作为起始位置,起始位置从i+1算起,再从0计算curSum。
如图:
那么为什么一旦[i,j] 区间和为负数,起始位置就可以是j+1呢,j+1后面就不会出现更大的负数?
如果出现更大的负数,就是更新j,那么起始位置又变成新的j+1了。
而且j之前出现了多少负数,j后面就会出现多少正数,因为耗油总和是大于零的(前提我们已经确定了一定可以跑完全程)。
那么局部最优:当前累加rest[j]的和curSum一旦小于0,起始位置至少要是j+1,因为从j开始一定不行。全局最优:找到可以跑一圈的起始位置。
局部最优可以推出全局最优,找不出反例,试试贪心!
# 方法三: 贪心算法
# time:O(N);space:(1)
class Solution(object):
def canCompleteCircuit(self, gas, cost):
"""
:type gas: List[int]
:type cost: List[int]
:rtype: int
"""
start = 0
curSum = 0
totalSum = 0
n = len(gas)
for i in range(n):
curSum += gas[i] - cost[i]
totalSum += gas[i] -cost[i]
# 当前累加rest[i]和 curSum一旦小于0
if curSum<0:
# 起始位置更新为i+1,curSum从0开始
curSum = 0
start = i+1
# 说明怎么走都不可能跑一圈了
if totalSum<0: return -1
return start
时间复杂度:O(N)
从头到尾遍历数组,所以O(N);
空间复杂度:O(1);
只有常数个变量需要保存;
Reference: 代码随想录 (programmercarl.com)
本题学习时间:60分钟。
链接:135. 分发糖果 - 力扣(LeetCode)
这道题目一定是要确定一边之后,再确定另一边,例如比较每一个孩子的左边,然后再比较右边,如果两边一起考虑一定会顾此失彼;
先确定右边评分大于左边的情况(也就是从前向后遍历)
如果ratings[i] > ratings[i - 1] 那么[i]的糖 一定要比[i - 1]的糖多一个,所以贪心:candyVec[i] = candyVec[i - 1] + 1
如图:
再确定左孩子大于右孩子的情况(从后向前遍历)
遍历顺序这里有同学可能会有疑问,为什么不能从前向后遍历呢?
因为如果从前向后遍历,根据 ratings[i + 1] 来确定 ratings[i] 对应的糖果,那么每次都不能利用上前一次的比较结果了;
所以确定左孩子大于右孩子的情况一定要从后向前遍历!
如果 ratings[i] > ratings[i + 1],此时candyVec[i](第i个小孩的糖果数量)就有两个选择了,一个是candyVec[i + 1] + 1(从右边这个加1得到的糖果数量),一个是candyVec[i](之前比较右孩子大于左孩子得到的糖果数量)。
那么又要贪心了,局部最优:取candyVec[i + 1] + 1 和 candyVec[i] 最大的糖果数量,保证第i个小孩的糖果数量即大于左边的也大于右边的。全局最优:相邻的孩子中,评分高的孩子获得更多的糖果。
局部最优可以推出全局最优。
所以就取candyVec[i + 1] + 1 和 candyVec[i] 最大的糖果数量,candyVec[i]只有取最大的才能既保持对左边candyVec[i - 1]的糖果多,也比右边candyVec[i + 1]的糖果多。
# 贪心算法
# time:O(N);space:O(N)
class Solution(object):
def candy(self, ratings):
"""
:type ratings: List[int]
:rtype: int
"""
candy = [1]*len(ratings)
# 从前向后
for i in range(1,len(ratings)):
if ratings[i]>ratings[i-1]:
candy[i] = candy[i-1]+1
# 从后向前
for i in range(len(ratings)-2,-1,-1):
if ratings[i]>ratings[i+1]:
candy[i] = max(candy[i],candy[i+1]+1)
return sum(candy)
时间复杂度:O(N)
其中N为数组ratings的长度,也为孩子的个数;本解法需要从左到右遍历一遍,再从右向左遍历一遍,还需要sum数组candy,总的时间复杂度O(N);
空间复杂度:O(N)
其中N为数组ratings的长度,也为孩子的个数;需要新建一个数组candy来记录每个孩子的糖果数;
这在leetcode上是一道困难的题目,其难点就在于贪心的策略,如果在考虑局部的时候想两边兼顾,就会顾此失彼;
那么本题我采用了两次贪心的策略:
这样从局部最优推出了全局最优,即:相邻的孩子中,评分高的孩子获得更多的糖果。
Reference: 代码随想录 (programmercarl.com)
本题学习时间:60分钟。
本篇学习时间近3小时,总结字数6000+;本篇学习了三道贪心算法的题目,第一题相对简单,甚至写完了都不知道自己用了贪心算法的思路,要刻意训练自己这种意识,第二题的贪心思路不太好想,重点是方法二,第三题是不能同时两头兼顾,必须一边一边处理。(求推荐!)