LeetCode100天Day14-轮转数组与买卖股票最佳时机:数组旋转与暴力搜索
摘要:本文详细解析了LeetCode中两道经典数组题目------"轮转数组"和"买卖股票的最佳时机"。通过数组克隆和索引映射实现轮转,以及使用暴力搜索寻找最大利润,帮助读者掌握数组操作和最优化问题的基本技巧。
目录
-
TOC
1. 轮转数组(Rotate Array)
1.1 题目描述
给定一个整数数组 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.2 解题思路
这道题使用数组克隆和索引映射的方法:
- 克隆原数组作为备份
- 计算每个位置的新索引
- 将原数组的元素按照新索引放入目标位置
解题步骤:
- 克隆nums到nums1
- 遍历数组,计算每个元素的新位置
- 使用公式:(原索引 + k) % 数组长度 得到旋转后的索引
- 或者反向:原索引 = (新索引 - k) % 数组长度
1.3 代码实现
java
class Solution {
public void rotate(int[] nums, int k) {
int[] nums1 = nums.clone();
for(int i = 0;i < nums.length;i++){
nums[i] = nums1[( nums.length + i - k%nums.length) %nums.length ];
}
}
}
1.4 代码逐行解释
第一部分:克隆数组
java
int[] nums1 = nums.clone();
功能:创建原数组的副本
| 操作 | 说明 |
|---|---|
nums.clone() |
浅克隆数组,复制所有元素 |
nums1 |
原数组的备份,保留原始数据 |
为什么要克隆:
如果不克隆,直接修改nums:
nums = [1, 2, 3, 4, 5]
k = 2
i=0: nums[0] = nums[?]
但nums[?]可能已经被修改了!
克隆后:
nums1 = [1, 2, 3, 4, 5] (备份,不变)
nums = [?, ?, ?, ?, ?] (目标,从nums1读取)
第二部分:计算新索引
java
for(int i = 0;i < nums.length;i++){
nums[i] = nums1[( nums.length + i - k%nums.length) %nums.length ];
}
索引映射公式:
java
原索引 → 新索引
原索引j → 新索引i = (j + k) % n
反向:新索引i → 原索引j
j = (i - k) % n
j = (i - k + n) % n // 处理负数
j = (n + i - k%n) % n // 最终公式
公式详解:
| 部分 | 说明 |
|---|---|
k % nums.length |
k可能大于数组长度,需要取模 |
i - k % nums.length |
计算原索引(可能为负) |
nums.length + i - k % nums.length |
加上数组长度,确保非负 |
% nums.length |
最终取模,得到有效索引 |
1.5 执行流程详解
示例1 :nums = [1,2,3,4,5,6,7], k = 3
初始状态:
nums = [1, 2, 3, 4, 5, 6, 7]
nums1 = [1, 2, 3, 4, 5, 6, 7]
n = 7
k % n = 3 % 7 = 3
i=0:
原索引 = (7 + 0 - 3) % 7 = 4 % 7 = 4
nums[0] = nums1[4] = 5
nums = [5, 2, 3, 4, 5, 6, 7]
i=1:
原索引 = (7 + 1 - 3) % 7 = 5 % 7 = 5
nums[1] = nums1[5] = 6
nums = [5, 6, 3, 4, 5, 6, 7]
i=2:
原索引 = (7 + 2 - 3) % 7 = 6 % 7 = 6
nums[2] = nums1[6] = 7
nums = [5, 6, 7, 4, 5, 6, 7]
i=3:
原索引 = (7 + 3 - 3) % 7 = 7 % 7 = 0
nums[3] = nums1[0] = 1
nums = [5, 6, 7, 1, 5, 6, 7]
i=4:
原索引 = (7 + 4 - 3) % 7 = 8 % 7 = 1
nums[4] = nums1[1] = 2
nums = [5, 6, 7, 1, 2, 6, 7]
i=5:
原索引 = (7 + 5 - 3) % 7 = 9 % 7 = 2
nums[5] = nums1[2] = 3
nums = [5, 6, 7, 1, 2, 3, 7]
i=6:
原索引 = (7 + 6 - 3) % 7 = 10 % 7 = 3
nums[6] = nums1[3] = 4
nums = [5, 6, 7, 1, 2, 3, 4]
最终输出: [5, 6, 7, 1, 2, 3, 4]
示例2 :nums = [-1,-100,3,99], k = 2
初始状态:
nums = [-1, -100, 3, 99]
nums1 = [-1, -100, 3, 99]
n = 4
k % n = 2 % 4 = 2
i=0:
原索引 = (4 + 0 - 2) % 4 = 2
nums[0] = nums1[2] = 3
nums = [3, -100, 3, 99]
i=1:
原索引 = (4 + 1 - 2) % 4 = 3
nums[1] = nums1[3] = 99
nums = [3, 99, 3, 99]
i=2:
原索引 = (4 + 2 - 2) % 4 = 4 % 4 = 0
nums[2] = nums1[0] = -1
nums = [3, 99, -1, 99]
i=3:
原索引 = (4 + 3 - 2) % 4 = 5 % 4 = 1
nums[3] = nums1[1] = -100
nums = [3, 99, -1, -100]
最终输出: [3, 99, -1, -100]
1.6 算法图解
原始数组: [1, 2, 3, 4, 5, 6, 7]
索引: 0 1 2 3 4 5 6
k = 3
向右旋转3步:
步骤1: 旋转1步
[1, 2, 3, 4, 5, 6, 7]
↓
[7, 1, 2, 3, 4, 5, 6]
步骤2: 旋转2步
[7, 1, 2, 3, 4, 5, 6]
↓
[6, 7, 1, 2, 3, 4, 5]
步骤3: 旋转3步
[6, 7, 1, 2, 3, 4, 5]
↓
[5, 6, 7, 1, 2, 3, 4]
索引映射关系:
新索引0 ← 原索引4 (nums[4]=5 → nums[0])
新索引1 ← 原索引5 (nums[5]=6 → nums[1])
新索引2 ← 原索引6 (nums[6]=7 → nums[2])
新索引3 ← 原索引0 (nums[0]=1 → nums[3])
新索引4 ← 原索引1 (nums[1]=2 → nums[4])
新索引5 ← 原索引2 (nums[2]=3 → nums[5])
新索引6 ← 原索引3 (nums[3]=4 → nums[6])
1.7 复杂度分析
| 分析维度 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(n) | 遍历数组一次 |
| 空间复杂度 | O(n) | 克隆数组 |
优化思路:可以使用三次翻转实现O(1)空间
java
// 优化版本:三次翻转
class Solution {
public void rotate(int[] nums, int k) {
k %= nums.length;
reverse(nums, 0, nums.length - 1); // 翻转整个数组
reverse(nums, 0, k - 1); // 翻转前k个
reverse(nums, k, nums.length - 1); // 翻转剩余部分
}
private void reverse(int[] nums, int start, int end) {
while (start < end) {
int temp = nums[start];
nums[start] = nums[end];
nums[end] = temp;
start++;
end--;
}
}
}
翻转过程:
原始: [1, 2, 3, 4, 5, 6, 7], k = 3
步骤1: 翻转整个数组
[7, 6, 5, 4, 3, 2, 1]
步骤2: 翻转前k个
[5, 6, 7, 4, 3, 2, 1]
步骤3: 翻转剩余部分
[5, 6, 7, 1, 2, 3, 4]
1.8 边界情况
| nums | k | 说明 | 输出 |
|---|---|---|---|
[1,2,3] |
0 |
不旋转 | [1,2,3] |
[1,2,3] |
3 |
旋转一周 | [1,2,3] |
[1] |
5 |
单元素 | [1] |
[1,2] |
1 |
旋转一次 | [2,1] |
2. 买卖股票的最佳时机(Best Time to Buy and Sell Stock)
2.1 题目描述
给定一个数组 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。
2.2 解题思路
这道题使用暴力枚举的方法:
- 使用两层循环遍历所有买入和卖出的组合
- 外层循环选择买入日期
- 内层循环选择卖出日期(必须在买入日期之后)
- 计算利润并更新最大值
解题步骤:
- 初始化maxprofit = 0
- 外层循环遍历买入日期i
- 内层循环遍历卖出日期j(j > i)
- 计算profit = prices[j] - prices[i]
- 更新maxprofit
2.3 代码实现
java
public class Solution {
public int maxProfit(int[] prices) {
int maxprofit = 0;
for (int i = 0; i < prices.length - 1; i++) {
for (int j = i + 1; j < prices.length; j++) {
int profit = prices[j] - prices[i];
if (profit > maxprofit) {
maxprofit = profit;
}
}
}
return maxprofit;
}
}
2.4 代码逐行解释
第一部分:初始化
java
int maxprofit = 0;
功能:记录最大利润,初始为0
| 含义 | 说明 |
|---|---|
maxprofit = 0 |
如果找不到利润,返回0 |
第二部分:双层循环
java
for (int i = 0; i < prices.length - 1; i++) {
for (int j = i + 1; j < prices.length; j++) {
int profit = prices[j] - prices[i];
if (profit > maxprofit) {
maxprofit = profit;
}
}
}
循环说明:
| 循环 | 变量 | 起始值 | 结束值 | 作用 |
|---|---|---|---|---|
| 外层 | i | 0 | length-2 | 买入日期 |
| 内层 | j | i+1 | length-1 | 卖出日期 |
为什么i < prices.length - 1:
prices = [7, 1, 5, 3, 6, 4]
索引: 0 1 2 3 4 5
如果i = 5(最后一天):
j从6开始,但数组长度是6,越界!
所以i最大是4(length-2)
利润计算:
java
int profit = prices[j] - prices[i];
| 变量 | 说明 |
|---|---|
prices[i] |
买入价格(第i天) |
prices[j] |
卖出价格(第j天) |
profit |
利润 = 卖出价 - 买入价 |
2.5 执行流程详解
示例1 :prices = [7,1,5,3,6,4]
初始状态:
prices = [7, 1, 5, 3, 6, 4]
maxprofit = 0
i=0, prices[i]=7:
j=1: prices[1]=1, profit=1-7=-6, maxprofit=0
j=2: prices[2]=5, profit=5-7=-2, maxprofit=0
j=3: prices[3]=3, profit=3-7=-4, maxprofit=0
j=4: prices[4]=6, profit=6-7=-1, maxprofit=0
j=5: prices[5]=4, profit=4-7=-3, maxprofit=0
i=1, prices[i]=1:
j=2: prices[2]=5, profit=5-1=4, maxprofit=4
j=3: prices[3]=3, profit=3-1=2, maxprofit=4
j=4: prices[4]=6, profit=6-1=5, maxprofit=5
j=5: prices[5]=4, profit=4-1=3, maxprofit=5
i=2, prices[i]=5:
j=3: prices[3]=3, profit=3-5=-2, maxprofit=5
j=4: prices[4]=6, profit=6-5=1, maxprofit=5
j=5: prices[5]=4, profit=4-5=-1, maxprofit=5
i=3, prices[i]=3:
j=4: prices[4]=6, profit=6-3=3, maxprofit=5
j=5: prices[5]=4, profit=4-3=1, maxprofit=5
i=4, prices[i]=6:
j=5: prices[5]=4, profit=4-6=-2, maxprofit=5
循环结束,返回 maxprofit = 5
输出: 5
示例2 :prices = [7,6,4,3,1]
初始状态:
prices = [7, 6, 4, 3, 1]
maxprofit = 0
i=0, prices[i]=7:
j=1: profit=6-7=-1, maxprofit=0
j=2: profit=4-7=-3, maxprofit=0
j=3: profit=3-7=-4, maxprofit=0
j=4: profit=1-7=-6, maxprofit=0
i=1, prices[i]=6:
j=2: profit=4-6=-2, maxprofit=0
j=3: profit=3-6=-3, maxprofit=0
j=4: profit=1-6=-5, maxprofit=0
i=2, prices[i]=4:
j=3: profit=3-4=-1, maxprofit=0
j=4: profit=1-4=-3, maxprofit=0
i=3, prices[i]=3:
j=4: profit=1-3=-2, maxprofit=0
循环结束,返回 maxprofit = 0
输出: 0
2.6 算法图解
prices = [7, 1, 5, 3, 6, 4]
天数: 0 1 2 3 4 5
所有买入卖出组合:
买入0: 7
卖出1: 1 → 利润: -6
卖出2: 5 → 利润: -2
卖出3: 3 → 利润: -4
卖出4: 6 → 利润: -1
卖出5: 4 → 利润: -3
买入1: 1
卖出2: 5 → 利润: 4
卖出3: 3 → 利润: 2
卖出4: 6 → 利润: 5 ← 最大
卖出5: 4 → 利润: 3
买入2: 5
卖出3: 3 → 利润: -2
卖出4: 6 → 利润: 1
卖出5: 4 → 利润: -1
买入3: 3
卖出4: 6 → 利润: 3
卖出5: 4 → 利润: 1
买入4: 6
卖出5: 4 → 利润: -2
最大利润: 5
2.7 复杂度分析
| 分析维度 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(n²) | 两层循环 |
| 空间复杂度 | O(1) | 只使用常数空间 |
优化思路:可以使用一次遍历优化到O(n)
java
// 优化版本:一次遍历
public class Solution {
public int maxProfit(int[] prices) {
int minprice = Integer.MAX_VALUE;
int maxprofit = 0;
for (int i = 0; i < prices.length; i++) {
if (prices[i] < minprice) {
minprice = prices[i]; // 更新最低价格
} else if (prices[i] - minprice > maxprofit) {
maxprofit = prices[i] - minprice; // 更新最大利润
}
}
return maxprofit;
}
}
一次遍历过程:
prices = [7, 1, 5, 3, 6, 4]
i=0: price=7
7 < MAX_VALUE? 是
minprice = 7
i=1: price=1
1 < 7? 是
minprice = 1
i=2: price=5
5 < 1? 否
5-1=4 > 0? 是
maxprofit = 4
i=3: price=3
3 < 1? 否
3-1=2 > 4? 否
i=4: price=6
6 < 1? 否
6-1=5 > 4? 是
maxprofit = 5
i=5: price=4
4 < 1? 否
4-1=3 > 5? 否
最终: maxprofit = 5
2.8 边界情况
| prices | 说明 | 输出 |
|---|---|---|
[7,6,4,3,1] |
持续下跌 | 0 |
[1,2,3,4,5] |
持续上涨 | 4 |
[1] |
单个价格 | 0 |
[2,4,1] |
先涨后跌 | 2 |
3. 两题对比与总结
3.1 算法对比
| 对比项 | 轮转数组 | 买卖股票最佳时机 |
|---|---|---|
| 核心算法 | 索引映射 | 暴力枚举 |
| 数据结构 | 数组 | 数组 |
| 时间复杂度 | O(n) | O(n²) |
| 空间复杂度 | O(n) | O(1) |
| 应用场景 | 数组旋转 | 最优化问题 |
3.2 数组旋转的技巧
方法一:克隆+索引映射
java
int[] nums1 = nums.clone();
for (int i = 0; i < nums.length; i++) {
nums[i] = nums1[(n + i - k % n) % n];
}
方法二:三次翻转
java
reverse(nums, 0, n - 1);
reverse(nums, 0, k - 1);
reverse(nums, k, n - 1);
方法三:环状替换
java
int count = 0;
for (int start = 0; count < n; start++) {
int current = start;
int prev = nums[start];
do {
int next = (current + k) % n;
int temp = nums[next];
nums[next] = prev;
prev = temp;
current = next;
count++;
} while (start != current);
}
3.3 最大利润的解法
暴力枚举:
java
for (int i = 0; i < n - 1; i++) {
for (int j = i + 1; j < n; j++) {
maxprofit = Math.max(maxprofit, prices[j] - prices[i]);
}
}
一次遍历:
java
int minprice = Integer.MAX_VALUE;
int maxprofit = 0;
for (int price : prices) {
minprice = Math.min(minprice, price);
maxprofit = Math.max(maxprofit, price - minprice);
}
3.4 模运算的应用
计算旋转后的索引:
java
// 向右旋转k步
新索引 = (原索引 + k) % n
// 向左旋转k步
新索引 = (原索引 - k + n) % n
// 或者
新索引 = (n + 原索引 - k % n) % n
处理k大于n的情况:
n = 5, k = 7
旋转7步 = 旋转 (7 % 5) = 旋转2步
所以先用 k % n 减少计算量
4. 总结
今天我们学习了两道数组操作题目:
- 轮转数组:掌握索引映射实现数组旋转,理解模运算在循环移位中的应用
- 买卖股票的最佳时机:掌握暴力枚举解决最优化问题,理解买入卖出的约束
核心收获:
- 数组克隆可以在修改时保留原始数据
- 模运算可以处理循环移位问题
- 暴力枚举是最直接的解决方法,但效率较低
- 一次遍历可以显著优化时间复杂度
- 最优化问题通常需要遍历所有可能的情况
练习建议:
- 尝试用三次翻转实现数组旋转
- 学习用一次遍历解决买卖股票问题
- 思考如何处理多次买卖的情况
参考资源
文章标签
#LeetCode #算法 #Java #数组 #暴力搜索
喜欢这篇文章吗?别忘了点赞、收藏和分享!你的支持是我创作的最大动力!