每日算法一练:剑指offer——贪心算法与找规律
1. 买卖芯片的最佳时机
数组 prices
记录了某芯片近期的交易价格,其中 prices[i]
表示的 i
天该芯片的价格。你只能选择 某一天 买入芯片,并选择在 未来的某一个不同的日子 卖出该芯片。请设计一个算法计算并返回你从这笔交易中能获取的最大利润。
如果你不能获取任何利润,返回 0。
示例 1:
输入:prices = [3, 6, 2, 9, 8, 5]
输出:7
解释:在第 3 天(芯片价格 = 2)买入,在第 4 天(芯片价格 = 9)卖出,最大利润 = 9 - 2 = 7。
示例 2:
输入:prices = [8, 12, 15, 7, 3, 10]
输出:7
解释:在第 5 天(芯片价格 = 3)买入,在第 6 天(芯片价格 = 10)卖出,最大利润 = 10 - 3 = 7。
提示:
0 <= prices.length <= 10^5
0 <= prices[i] <= 10^4
解题思路
我们的问题是找到一次买入和一次卖出股票的最佳时机,让赚的钱最多,前提是买入必须在卖出之前。
暴力解法
最简单的想法是试试所有可能的买入和卖出时间:
- 先假设某天买入。
- 再假设之后的某天卖出,计算这次交易的利润。
- 比较所有利润,找到最大值。
缺点:如果股票有很多天,比较的次数会很多,时间耗不起。
优化解法:动态规划
我们换个思路,快速找到答案。
-
目标: 我们只要知道每一天卖出股票时,最多能赚多少钱,这样一路算下来就能找到最佳答案。
-
关键问题: 要卖出股票,得先买入!所以每天的利润是当天价格减去之前几天的最低价格。
-
步骤:
- 用一个变量
cost
记录之前几天的最低价格。 - 用一个变量
profit
记录目前为止的最大利润。 - 每天更新这两个变量:
- 更新最低价格:
cost = min(cost, price)
- 更新最大利润:
profit = max(profit, price - cost)
- 更新最低价格:
- 用一个变量
代码实现
class Solution {
public int bestTiming(int[] prices) {
int cost = Integer.MAX_VALUE; // 记录最低价格
int profit = 0; // 记录最大利润
for (int price : prices) {
cost = Math.min(cost, price); // 更新最低价格
profit = Math.max(profit, price - cost); // 计算当前利润
}
return profit; // 返回最大利润
}
}
简单理解
-
想象成逛商场:
cost
就是“见过的最低价格”。- 每天都在看,“今天的价格减去最低价,能赚多少钱?”
- 把这个“最多赚的钱”记录下来。
-
不断更新两个变量:
- 最低价
cost
。 - 最大利润
profit
。
- 最低价
最后答案就是全程最多能赚的钱。
效率分析
- 速度快:只需要扫一遍数组,时间是 O(N)。
- 占空间少:只用了两个变量,空间是 O(1)。
2. 数字1的个数
给定一个整数 num
,计算所有小于等于 num
的非负整数中数字 1
出现的个数。
示例 1:
输入:num = 0 输出:0
示例 2:
输入:num = 13 输出:6
提示:
0 <= num < 109
计算数字 1
出现的次数是一个经典问题,解决思路可以分为暴力解法和数学优化解法。
暴力解法
暴力解法比较直接:我们遍历从 0
到 num
的所有数字,统计每个数字中 1
出现的次数。
步骤
- 遍历从
0
到num
的所有数字。 - 将每个数字转为字符串,检查其中
1
出现的次数。 - 累加这些次数,最终返回结果。
代码
class Solution {
public int countDigitOne(int num) {
int count = 0;
for (int i = 0; i <= num; i++) {
count += countOnesInNumber(i);
}
return count;
}
private int countOnesInNumber(int n) {
int count = 0;
while (n > 0) {
if (n % 10 == 1) {
count++;
}
n /= 10;
}
return count;
}
}
复杂度分析
- 时间复杂度:O(n⋅log10(n))O(n \cdot \log_{10}(n)),其中 nn 是
num
,因为每个数字的位数需要单独统计。 - 空间复杂度:O(1)O(1),没有使用额外的空间。
数学优化解法
暴力法效率较低,我们可以通过数学优化来直接计算每个位置上 1
的贡献。
核心思想
- 我们逐位统计每个数字位上
1
出现的次数。 - 对于十进制表示中的某一位,分为 当前位、低位和高位:
- 高位:表示当前位左边的所有数字。
- 当前位:表示当前正在统计的位。
- 低位:表示当前位右边的所有数字。
计算公式
计算公式
假设当前位是 dd,位权为 10i10^i,根据高位和低位的不同情况讨论每位上 1
的贡献:
-
如果当前位为
贡献=高位×10i0
当前位不会贡献任何1
,所以计算公式为: -
如果当前位为
贡献=高位×10i+低位+11
当前位的1
的贡献取决于低位的数值,计算公式为: -
如果当前位大于
贡献=(高位+1)×10i1
当前位上会完整贡献 10i10^i 个1
,计算公式为:
代码实现
class Solution {
public int countDigitOne(int num) {
int count = 0;
long place = 1; // 当前位权,1 表示个位,10 表示十位,依次类推
while (place <= num) {
long high = num / (place * 10); // 高位
long current = (num / place) % 10; // 当前位
long low = num % place; // 低位
if (current == 0) {
count += high * place;
} else if (current == 1) {
count += high * place + low + 1;
} else {
count += (high + 1) * place;
}
place *= 10; // 进入下一位
}
return count;
}
}
复杂度分析
- 时间复杂度:O(log10(n)),遍历每一位,效率远高于暴力法。
- 空间复杂度:O(1),仅使用了少量变量。
3. 找到第k位数字
某班级学号记录系统发生错乱,原整数学号序列 [1,2,3,4,...] 分隔符丢失后变为 1234... 的字符序列。请实现一个函数返回该字符序列中的第 k 位数字。
示例 1:
输入:k = 5
输出:5
示例 2:
输入:k = 12
输出:1
解释:第 12 位数字在序列 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, ... 里是 1 ,它是 11 的一部分。
提示:
0 <= k < 231
解题思路:
我们要找的是数字序列(如 123456789101112...
)中的第 k
位。可以分三步来确定:
1. 确定第 k
位属于几位数
- 首先,一位数有
9
个,总共占9×1=9
位; - 两位数有
90
个,总共占90×2=180
位; - 三位数有
900
个,总共占900×3=2700
位; - 以此类推,直到找到
k
所属的范围。
具体做法是:
- 从
digit=1
开始(表示 1 位数),依次减去每一段的位数数量count=9×start×digit
; - 当
k ≤ count
时,跳出循环,k
就落在当前的digit
位数中。
2. 确定 k
属于哪个数字
- 在找到的位数中,起始数字为
start
。例如:- 1 位数从
1
开始,start=1
; - 2 位数从
10
开始,start=10
; - 3 位数从
100
开始,start=100
。
- 1 位数从
- 第
k
位在从start
开始的第[(k-1)/digit]
个数字中(这里减1
是因为第一个数字是第 0 个)。
3. 确定数字的哪一位
- 找到具体数字后,将其转成字符串,
(k-1) % digit
就是k
在这个数字中的具体位置。
代码
Java 代码
class Solution {
public int findKthNumber(int k) {
int digit = 1; // 当前是几位数
long start = 1; // 当前位数的起始数字
long count = 9; // 当前位数的总数位数量
// 1. 确定 k 属于几位数
while (k > count) {
k -= count;
digit += 1;
start *= 10;
count = digit * start * 9; // 例如:两位数有 90×2=180 位
}
// 2. 确定 k 属于哪个数字
long num = start + (k - 1) / digit;
// 3. 确定 k 是数字的第几位
String s = Long.toString(num); // 转为字符串
return s.charAt((k - 1) % digit) - '0'; // 找到具体数位并返回
}
}
复杂度分析
-
时间复杂度:
- 第一部分:找到
digit
所需最多执行O(logk)
次循环; - 第三部分:将数字转为字符串,耗时为
O(logk)
; - 所以总时间复杂度为
O(logk)
。
- 第一部分:找到
-
空间复杂度:
- 转换为字符串的过程需要
O(logk)
的额外空间。
- 转换为字符串的过程需要