每日算法一练:剑指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

解题思路

我们的问题是找到一次买入和一次卖出股票的最佳时机,让赚的钱最多,前提是买入必须在卖出之前。

暴力解法

最简单的想法是试试所有可能的买入和卖出时间:

  1. 先假设某天买入。
  2. 再假设之后的某天卖出,计算这次交易的利润。
  3. 比较所有利润,找到最大值。

缺点:如果股票有很多天,比较的次数会很多,时间耗不起。

优化解法:动态规划

我们换个思路,快速找到答案。

  1. 目标: 我们只要知道每一天卖出股票时,最多能赚多少钱,这样一路算下来就能找到最佳答案。

  2. 关键问题: 要卖出股票,得先买入!所以每天的利润是当天价格减去之前几天的最低价格。

  3. 步骤:

    • 用一个变量 cost 记录之前几天的最低价格。
    • 用一个变量 profit 记录目前为止的最大利润。
    • 每天更新这两个变量:
      • 更新最低价格:cost = min(cost, price)
      • 更新最大利润:profit = max(profit, price - cost)

代码实现

java 复制代码
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; // 返回最大利润
    }
}

简单理解

  1. 想象成逛商场:

    • cost 就是"见过的最低价格"。
    • 每天都在看,"今天的价格减去最低价,能赚多少钱?"
    • 把这个"最多赚的钱"记录下来。
  2. 不断更新两个变量:

    • 最低价 cost
    • 最大利润 profit

最后答案就是全程最多能赚的钱。

效率分析

  • 速度快:只需要扫一遍数组,时间是 O(N)。
  • 占空间少:只用了两个变量,空间是 O(1)。

2. 数字1的个数

给定一个整数 num,计算所有小于等于 num 的非负整数中数字 1 出现的个数。

示例 1:

复制代码
输入:num = 0
输出:0

示例 2:

复制代码
输入:num = 13
输出:6

提示:

  • 0 <= num < 109

计算数字 1 出现的次数是一个经典问题,解决思路可以分为暴力解法和数学优化解法。

暴力解法

暴力解法比较直接:我们遍历从 0num 的所有数字,统计每个数字中 1 出现的次数。

步骤

  1. 遍历从 0num 的所有数字。
  2. 将每个数字转为字符串,检查其中 1 出现的次数。
  3. 累加这些次数,最终返回结果。

代码

java 复制代码
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⋅log⁡10(n))O(n \cdot \log_{10}(n)),其中 nn 是 num,因为每个数字的位数需要单独统计。
  • 空间复杂度:O(1)O(1),没有使用额外的空间。

数学优化解法

暴力法效率较低,我们可以通过数学优化来直接计算每个位置上 1 的贡献。

核心思想

  • 我们逐位统计每个数字位上 1 出现的次数。
  • 对于十进制表示中的某一位,分为 当前位、低位和高位
    • 高位:表示当前位左边的所有数字。
    • 当前位:表示当前正在统计的位。
    • 低位:表示当前位右边的所有数字。

计算公式

计算公式

假设当前位是 dd,位权为 10i10^i,根据高位和低位的不同情况讨论每位上 1 的贡献:

  1. 如果当前位为 0

    当前位不会贡献任何 1,所以计算公式为:

    贡献=高位×10i

  2. 如果当前位为 1

    当前位的 1 的贡献取决于低位的数值,计算公式为:

    贡献=高位×10i+低位+1

  3. 如果当前位大于 1

    当前位上会完整贡献 10i10^i 个 1,计算公式为:

    贡献=(高位+1)×10i

代码实现

java 复制代码
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(log⁡10(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
  • k 位在从 start 开始的第 [(k-1)/digit] 个数字中(这里减 1 是因为第一个数字是第 0 个)。

3. 确定数字的哪一位

  • 找到具体数字后,将其转成字符串,(k-1) % digit 就是 k 在这个数字中的具体位置。

代码

Java 代码

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'; // 找到具体数位并返回
    }
}

复杂度分析

  1. 时间复杂度

    • 第一部分:找到 digit 所需最多执行 O(logk) 次循环;
    • 第三部分:将数字转为字符串,耗时为 O(logk)
    • 所以总时间复杂度为 O(logk)
  2. 空间复杂度

    • 转换为字符串的过程需要 O(logk) 的额外空间。
相关推荐
破-风5 小时前
leetcode-------mysql
算法·leetcode·职场和发展
自不量力的A同学9 小时前
如何利用人工智能算法优化知识分类和标签?
人工智能·算法·分类
CodeJourney.10 小时前
PyTorch不同优化器比较
人工智能·pytorch·算法·能源
winner888110 小时前
对比学习损失函数 - InfoNCE
学习·算法·对比学习·infonce
南宫生10 小时前
力扣-数据结构-12【算法学习day.83】
java·数据结构·学习·算法·leetcode
KeyPan10 小时前
【数据结构与算法:五、树和二叉树】
java·开发语言·数据结构·人工智能·算法·机器学习·计算机视觉
WBingJ10 小时前
机器学习基础-贝叶斯分类器
人工智能·算法·机器学习
Lenyiin11 小时前
第431场周赛:最长乘积等价子数组、计算字符串的镜像分数、收集连续 K 个袋子可以获得的最多硬币数量、不重叠区间的最大得分
c++·算法·leetcode·周赛·lenyiin
行知SLAM11 小时前
第0章 机器人及自动驾驶SLAM定位方法全解析及入门进阶学习建议
人工智能·算法·机器人·自动驾驶
起名方面没有灵感13 小时前
力扣23.合并K个升序链表
java·算法