189.轮转数组https://leetcode.cn/problems/rotate-array/?envType=study-plan-v2&envId=top-interview-150
189.轮转数组
给定一个整数数组 nums
,将数组中的元素向右轮转 k
个位置,其中 k
是非负数。
示例 1:
输入: nums = [1,2,3,4,5,6,7], k = 3 输出:[5,6,7,1,2,3,4]
解释: 向右轮转 1 步:[7,1,2,3,4,5,6]
向右轮转 2 步:[6,7,1,2,3,4,5]
向右轮转 3 步:[5,6,7,1,2,3,4]
示例 2:
输入:nums = [-1,-100,3,99], k = 2 输出:[3,99,-1,-100] 解释: 向右轮转 1 步: [99,-1,-100,3] 向右轮转 2 步: [3,99,-1,-100]
提示:
1 <= nums.length <= 105
-231 <= nums[i] <= 231 - 1
0 <= k <= 105
进阶:
- 尽可能想出更多的解决方案,至少有 三种 不同的方法可以解决这个问题。
- 你可以使用空间复杂度为
O(1)
的 原地 算法解决这个问题吗?
原题解析
题目要求给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。例如,给定数组 [1, 2, 3, 4, 5, 6, 7] 和 k = 3,将数组的元素向右移动 3 个位置,输出结果为 [5, 6, 7, 1, 2, 3, 4]。
关键在于:
数组元素右移:最后 k 个元素移到数组前面,前面的元素依次右移。
考虑数组的循环性质:当 k 超过数组长度时,移动会出现循环,因此需要计算 k 对数组长度 n 取模,即 k = k % n。
代码
class Solution {
public:
void rotate(vector<int>& nums, int k) {
int n=nums.size();
k=k%n;
if(k==0)return;
reverse(nums,0,n-1);
reverse(nums,0,k-1);
reverse(nums,k,n-1);
}
private:
void reverse(vector<int>&nums,int start,int end){
while(start<end){
swap(nums[start],nums[end]);
start++;
end--;
}
}
};
算法逻辑
-
问题的转化:为了有效地实现数组的右移,算法通过“反转”技巧来简化操作。我们可以将问题分解为三次反转操作:
-
反转整个数组。
-
反转前 k 个元素。
-
反转剩余的元素。
-
-
反转步骤的逻辑:
-
第一步:通过反转整个数组,将需要右移到前面的元素移到前面,但它们的顺序是反的。
-
第二步:对前 k 个元素进行反转,恢复它们的顺序。
-
第三步:对剩余的元素进行反转,恢复它们的顺序。
这种反转方法能够保证在不使用额外空间的情况下完成轮转。
-
数学原理
设数组 nums 的长度为 n,我们将问题分为三次反转操作:
-
整体反转:首先,将数组的所有元素反转,即对于数组 nums[0, 1, ..., n-1],其顺序变为 nums[n-1, ..., 0]。
-
前 k 个元素反转:将反转后的数组的前 k 个元素再次反转。反转 nums[0, k-1],恢复这部分的正确顺序。
-
后 ( n - k ) 个元素反转:将反转后的数组的剩余部分,即 nums[k, n-1],反转回正确顺序。
数学公式表达:
设初始数组为 ,经过反转操作后:
-
第一次反转:
-
第二次反转:对 A' 的前 k 个元素 反转,得到
-
第三次反转:对剩余的后 (n - k) 个元素 反转,得到
最后的结果即为,与向右轮转 k 次的结果一致。
性能分析
-
时间复杂度:每次反转操作的时间复杂度为 O(n),三次反转操作的总时间复杂度为 O(n) ,其中 n 是数组的长度。
-
空间复杂度:算法没有使用额外的存储空间,只使用了常数级的辅助变量,因此空间复杂度为 O(1) 。
相较于使用辅助数组的方法(空间复杂度 O(n)),该算法通过原地反转操作实现了最优的空间利用。
示例解释
示例 1
输入:nums = [1, 2, 3, 4, 5, 6, 7], k = 3。
-
第一步:反转整个数组 [1, 2, 3, 4, 5, 6, 7],得到 [7, 6, 5, 4, 3, 2, 1]。
-
第二步:反转前 k = 3 个元素 [7, 6, 5],得到 [5, 6, 7, 4, 3, 2, 1]。
-
第三步:反转剩余的元素 [4, 3, 2, 1],得到 [5, 6, 7, 1, 2, 3, 4]。
最终结果为 [5, 6, 7, 1, 2, 3, 4]。
示例 2
输入:nums = [-1, -100, 3, 99], k = 2。
-
第一步:反转整个数组 [-1, -100, 3, 99],得到 [99, 3, -100, -1]。
-
第二步:反转前 k = 2 个元素 [99, 3],得到 [3, 99, -100, -1]。
-
第三步:反转剩余的元素 [-100, -1],得到 [3, 99, -1, -100]。
最终结果为 [3, 99, -1, -100]。
121.买股票最佳时机
给定一个数组 prices
,它的第 i
个元素 prices[i]
表示一支给定股票第 i
天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0
。
示例 1:
输入:[7,1,5,3,6,4] 输出:5 解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。 注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
示例 2:
输入:prices = [7,6,4,3,1] 输出:0 解释:在这种情况下, 没有交易完成, 所以最大利润为 0。
提示:
1 <= prices.length <= 105
0 <= prices[i] <= 104
原题解析
题目要求给定一个股票价格数组 prices,其中 prices[i] 表示第 i 天的股票价格。你需要选择一天买入股票,并在之后的某一天卖出,以获得最大的利润。如果没有任何盈利的机会,则返回 0。
需要注意的是,买入的时间必须在卖出时间之前,因此不能在过去的时间卖出股票。
代码
class Solution {
public:
int maxProfit(vector<int>& prices) {
if(prices.empty())return 0;
int min_price=prices[0];
int max_profit=0;
for(int i=1;i<prices.size();i++){
if(prices[i]<min_price){
min_price=prices[i];
}
int profit=prices[i]-min_price;
if(profit>max_profit){
max_profit=profit;
}
}
return max_profit;
}
};
算法逻辑
该算法通过遍历 prices 数组,实时追踪到当前为止的最低买入价格 min_price,并计算在当前价格下卖出的潜在利润。具体步骤如下:
-
初始化:
-
min_price:记录遍历过程中的最低买入价格,初始化为 prices[0]。
-
max_profit:记录当前最大的利润,初始为 0。
-
-
遍历数组:
-
每次遍历时,将当前价格 prices[i] 与 min_price 进行比较,更新 min_price 为较小的值。
-
计算在当前价格 prices[i] 卖出时的利润 profit = prices[i] - min_price,并将其与当前最大利润 max_profit 比较,更新 max_profit。
-
-
返回结果:
-
遍历结束后,返回最大利润 max_profit。若没有盈利机会,则 max_profit 保持为 0。
-
数学原理
本问题可以归结为寻找数组中两个元素的差值最大的问题,且要求卖出的那一天对应的价格要晚于买入那一天的价格。因此,可以通过维护一个最小买入价格来计算每一天卖出时的潜在利润。
设 prices[i] 为第 i 天的股票价格,min_price 为当前为止最低的价格,profit 为在第 i 天卖出的利润,则:
在遍历过程中,逐步更新 min_price 和 max_profit,通过动态调整来找到最大利润。
性能分析
-
时间复杂度:该算法仅需遍历一次 prices 数组,每次计算涉及常数操作,因此时间复杂度为 O(n) ,其中 n 是数组 prices 的长度。
-
空间复杂度:只使用了常数空间来存储 min_price 和 max_profit,因此空间复杂度为 O(1) 。
这是一种贪心算法,在遍历过程中实时更新最优解,并且只需一次遍历就能得出最大利润,因此是时间和空间效率都较优的算法。
示例解释
122.买股票最佳时机 II
原题解析
给你一个整数数组 prices
,其中 prices[i]
表示某支股票第 i
天的价格。
在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
返回 你能获得的 最大 利润 。
示例 1:
输入:prices = [7,1,5,3,6,4] 输出:7 解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4。 随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3。 最大总利润为 4 + 3 = 7 。
示例 2:
输入:prices = [1,2,3,4,5] 输出:4 解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4。 最大总利润为 4 。
示例 3:
输入:prices = [7,6,4,3,1] 输出:0 解释:在这种情况下, 交易无法获得正利润,所以不参与交易可以获得最大利润,最大利润为 0。
提示:
1 <= prices.length <= 3 * 104
0 <= prices[i] <= 104
给定一个整数数组 prices,其中 prices[i] 表示某支股票在第 i 天的价格。你可以在任何一天购买和出售股票,但在任意时刻你最多只能持有一股股票,且可以在同一天买入并卖出。要求计算在所有可能的交易组合中,你能获得的最大利润。
本问题的核心在于找到所有可以产生利润的价格差。由于你可以在同一天买入和卖出,因此问题可以简化为寻找所有连续两天之间的价格上升部分,并将它们累加以得到最大利润。
代码
class Solution {
public:
int maxProfit(vector<int>& prices) {
int totalProfit=0;
for(int i=0;i<prices.size()-1;i++){
if(prices[i+1]>prices[i]){
totalProfit+=prices[i+1]-prices[i];
}
}
return totalProfit;
}
};
算法逻辑
-
初始化一个变量 totalProfit 来记录总利润,初始值为 0。
-
遍历 prices 数组的每一对相邻元素 prices[i] 和 prices[i+1]:
-
如果 prices[i+1] > prices[i],则将这两天的差价计入利润,即 totalProfit += prices[i+1] - prices[i]。
-
-
最终返回 totalProfit,即为最大利润。
数学原理
假设股票的价格变化是离散的时间序列 prices,我们的目标是从中找到最大收益。 对于每一天的价格,如果第二天的价格比今天高,那么可以选择在今天买入、明天卖出。 利润可以表示为: 这里 n 是天数。对每一对相邻天数,我们只选择那些有正向价格差异的部分来累加利润。
性能分析
-
时间复杂度:O(n),其中 n 是 prices 数组的长度。只需要遍历一次数组即可完成计算。
-
空间复杂度:O(1),只使用了常数个额外空间。
示例解释
示例 1: 输入:prices = [7, 1, 5, 3, 6, 4]
-
第 2 天买入(价格 1),第 3 天卖出(价格 5),利润为 5 - 1 = 4;
-
第 4 天买入(价格 3),第 5 天卖出(价格 6),利润为 6 - 3 = 3;
-
总利润为 4 + 3 = 7。 输出:7
示例 2: 输入:prices = [1, 2, 3, 4, 5]
-
第 1 天买入(价格 1),第 5 天卖出(价格 5),利润为 5 - 1 = 4;
-
总利润为 4。 输出:4
示例 3: 输入:prices = [7, 6, 4, 3, 1]
-
所有天数价格均递减,没有任何可以产生正利润的交易,因此不参与交易,最大利润为 0。 输出:0
55.跳跃游戏https://leetcode.cn/problems/jump-game/?envType=study-plan-v2&envId=top-interview-150
55.跳跃游戏
给你一个非负整数数组 nums
,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标,如果可以,返回 true
;否则,返回 false
。
示例 1:
输入:nums = [2,3,1,1,4] 输出:true 解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。
示例 2:
输入:nums = [3,2,1,0,4] 输出:false 解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。
提示:
1 <= nums.length <= 104
0 <= nums[i] <= 105
原题解析
给定一个非负整数数组 nums,你最初位于数组的第一个下标。数组中的每个元素表示你在该位置可以跳跃的最大长度。你需要判断是否能够从数组的第一个下标跳跃到最后一个下标。
本问题可以通过贪心算法来解决。每个位置 i 能跳到的最远位置是 i + nums[i],我们维护一个变量 maxReach 表示能到达的最远位置,遍历数组,若某个位置的索引 i 超过了当前 maxReach,说明无法到达这个位置,因此直接返回 false。如果能够遍历完所有可达位置且 maxReach 覆盖了最后一个下标,则返回 true。
代码
class Solution {
public:
bool canJump(vector<int>& nums) {
int maxReach=nums[0];
for(int i=1;i<nums.size();i++){
if(i>maxReach){
return false;
}
maxReach=max(maxReach,i+nums[i]);
}
return maxReach>=nums.size()-1;
}
};
算法逻辑
-
初始化 maxReach 为 nums[0],表示起始位置能跳到的最远距离。
-
遍历数组,索引 i 从 1 开始:
-
如果当前索引 i 大于 maxReach,说明当前位置无法到达,返回 false。
-
否则更新 maxReach 为 max(maxReach, i + nums[i]),即更新到达的最远位置。
-
-
遍历结束后,检查 maxReach 是否至少能覆盖数组的最后一个下标,若可以则返回 true。
数学原理
-
假设当前在索引 i,那么在这个位置可以跳跃的最远距离是 i + nums[i]。通过不断更新当前能够到达的最远距离 maxReach,我们可以判断是否能到达目标。
-
如果遍历过程中,某个位置的索引超过了 maxReach,则说明我们无法到达该位置,这时可以直接判断无法到达最后一个下标。
性能分析
-
时间复杂度:O(n),其中 n 是 nums 数组的长度。我们只需遍历一次数组即可完成判断。
-
空间复杂度:O(1),只使用了常数个额外空间。
示例解释
示例 1: 输入:nums = [2, 3, 1, 1, 4]
- 起始位置 0 可以跳跃 2 步,最远可到达位置是 0 + 2 = 2。
- 到达位置 1 后,可以跳跃 3 步,最远可以到达 1 + 3 = 4,此时已经能够到达最后一个下标。
输出:true
示例 2: 输入:nums = [3, 2, 1, 0, 4]
- 起始位置 0 可以跳跃 3 步,最远可到达位置是 0 + 3 = 3。
- 但在到达位置 3 后,跳跃的最大长度为 0,无法继续前进到下标 4。
输出:false
56.跳跃游戏 IIhttps://leetcode.cn/problems/jump-game-ii/?envType=study-plan-v2&envId=top-interview-150
56.跳跃游戏 II
给定一个长度为 n
的 0 索引整数数组 nums
。初始位置为 nums[0]
。
每个元素 nums[i]
表示从索引 i
向前跳转的最大长度。换句话说,如果你在 nums[i]
处,你可以跳转到任意 nums[i + j]
处:
0 <= j <= nums[i]
i + j < n
返回到达 nums[n - 1]
的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]
。
示例 1:
输入: nums = [2,3,1,1,4] 输出: 2 解释: 跳到最后一个位置的最小跳跃数是2
。 从下标为 0 跳到下标为 1 的位置,跳1
步,然后跳3
步到达数组的最后一个位置。
示例 2:
输入: nums = [2,3,0,1,4] 输出: 2
提示:
1 <= nums.length <= 104
0 <= nums[i] <= 1000
- 题目保证可以到达
nums[n-1]
原题解析
给定一个非负整数数组 nums,数组中的每个元素表示你可以从当前索引跳跃的最大长度。初始位置在数组的第一个元素 nums[0],目标是到达数组的最后一个位置 nums[n-1],你需要求出最少的跳跃次数才能到达目标。
这个问题可以通过贪心算法解决。核心思想是在遍历数组的过程中,动态维护当前能够到达的最远位置 currentReach 和下一步能够到达的最远位置 nextReach。每当你到达当前能够跳跃的最远位置时,必须进行一次跳跃,同时更新 currentReach 为 nextReach。这种策略可以保证你使用最少的跳跃次数到达目标。
代码
class Solution {
public:
int jump(vector<int>& nums) {
int jumps=0,currentReach=0,nextReach=0;
for(int i=0;i<nums.size()-1;i++){
nextReach=max(nextReach,i+nums[i]);
if(i==currentReach){
jumps++;
currentReach=nextReach;
}
}
return jumps;
}
};
算法逻辑
-
初始化跳跃次数 jumps = 0,当前能够到达的最远位置 currentReach = 0,下一步能够到达的最远位置 nextReach = 0。
-
遍历数组的每一个索引 i:
-
更新 nextReach 为 max(nextReach, i + nums[i]),表示从当前位置可以跳到的最远位置。
-
如果当前索引 i 等于 currentReach,说明需要进行一次跳跃,更新 currentReach = nextReach 并增加跳跃次数 jumps++。
-
-
当遍历完成时,返回 jumps,即为最小跳跃次数。
数学原理
假设当前在索引 i 处,nums[i] 表示你可以跳跃的最大距离。那么你可以跳跃的下一个位置是 i + j,其中 0 <= j <= nums[i],且跳跃不能超过数组的边界。 通过不断更新下一步可以跳跃的最远位置 nextReach,并在必要时增加跳跃次数,可以保证最终到达目标所需的最少跳跃次数。
跳跃过程可以总结为:
-
在遍历每个元素时,更新可以到达的最远位置 nextReach。
-
如果遍历到达了当前跳跃的最远位置 currentReach,则增加跳跃次数并更新当前的最远位置为 nextReach。
-
当 currentReach 能够覆盖数组的最后一个元素时,返回跳跃次数。
性能分析
-
时间复杂度:O(n),其中 n 是数组 nums 的长度。我们只需要遍历一次数组即可计算出最小跳跃次数。
-
空间复杂度:O(1),只使用了常数个额外空间来存储跳跃次数和最远可达位置。
示例解释
示例 1: 输入:nums = [2, 3, 1, 1, 4]
-
从 0 跳到 1(跳 1 步),然后从 1 跳到 4(跳 3 步),总共 2 步。 输出:2
示例 2: 输入:nums = [2, 3, 0, 1, 4]
-
从 0 跳到 1(跳 1 步),然后从 1 跳到 4(跳 3 步),总共 2 步。 输出:2