当前位置: 首页 > article >正文

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

代码实现

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. 累加这些次数,最终返回结果。

代码

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

代码实现

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 代码

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) 的额外空间。

http://www.kler.cn/a/460170.html

相关文章:

  • 整合版canal ha搭建--基于1.1.4版本
  • Linux -- 单例模式
  • log4j2的Strategy、log4j2的DefaultRolloverStrategy、删除过期文件
  • LoRA微调系列笔记
  • Dell服务器升级ubuntu 22.04失败解决
  • Python 列表的高级索引技巧
  • NestJS 认证与授权:JWT、OAuth 和 RBAC 实现
  • 【C++】B2064 斐波那契数列
  • 智能家居体验大变革 博联 AI 方案让智能不再繁琐
  • 两台ubuntu的ECS机器共享一个存储
  • 【C++】:volatile 关键字详解
  • Git Flow 工作流:保障修改不破坏主功能的完整指南20241230
  • BGP路由协议的next-hop属性
  • C++ 【回调函数】详解与代码解读
  • 自学记录鸿蒙API 13:实现人脸比对Core Vision Face Comparator
  • Vscode左大括号不另起一行、注释自动换行
  • 1、Jmeter、jdk下载与安装
  • 磁珠选型规范
  • 自学记录鸿蒙 API 13:骨骼点检测应用Core Vision Skeleton Detection
  • LeetCode - Google 校招100题 第7天 序列(数据结构贪心) (15题)
  • XSS Challenges
  • gz、zip等压缩文件postman成功下载但是前端项目中下载解压失败
  • 斗鱼Android面试题及参考答案
  • Edge SCDN有些什么作用?
  • 04-微服务02
  • FreeRTOS Lwip Socket APi TCP Server 1对多