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

算法刷题记录——LeetCode篇(4) [第301~400题](持续更新)

(优先整理热门100及面试150,不定期持续更新,欢迎关注)


322. 零钱兑换

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1
你可以认为每种硬币的数量是无限的。

示例 1:

输入:coins = [1, 2, 5], amount = 11
输出:3 

解释:11 = 5 + 5 + 1

示例 2:

输入:coins = [2], amount = 3
输出:-1

示例 3:

输入:coins = [1], amount = 0
输出:0

提示:

  • 1 <= coins.length <= 12
  • 1 <= coins[i] <= 2^31 - 1
  • 0 <= amount <= 10^4

方法一:动态规划

使用动态规划数组 dp,其中 dp[i] 表示凑成金额 i 所需的最少硬币个数。通过逐步填充 dp 数组,最终得到 dp[amount] 的结果。

  1. 初始化

    • dp[0] = 0:金额为0时不需要任何硬币。
    • 其他金额初始化为 amount + 1,因为最多需要 amount 个硬币(全用面额1的硬币),初始值设为更大的数表示暂时不可达。
  2. 状态转移

    • 对于每个金额 i,遍历所有硬币面额 coin
    • coin ≤ i,则可以通过 i - coin 的金额加上当前硬币组成 i,即 dp[i] = min(dp[i], dp[i - coin] + 1)
  3. 结果判断

    • 如果最终 dp[amount] 仍大于 amount,说明无法凑出目标金额,返回 -1
    • 否则返回 dp[amount],即最少硬币数。

代码实现(Java)

import java.util.Arrays;

class Solution {
    public int coinChange(int[] coins, int amount) {
        if (amount == 0) {
            return 0;
        }
        int[] dp = new int[amount + 1];
        Arrays.fill(dp, amount + 1); // 初始化为一个较大的值(超过最大可能硬币数)
        dp[0] = 0; // 金额0需要0个硬币
      
        // 遍历所有金额,从1到amount
        for (int i = 1; i <= amount; i++) {
            // 遍历所有硬币面额
            for (int coin : coins) {
                if (coin <= i) { // 硬币面额不超过当前金额时,才可能使用
                    dp[i] = Math.min(dp[i], dp[i - coin] + 1);
                }
            }
        }
        // 判断结果是否有效
        return dp[amount] > amount ? -1 : dp[amount];
    }
}

方法二:BFS解法

将问题转化为图的最短路径问题,其中每个节点表示当前剩余金额,边表示使用一枚硬币。BFS按层遍历,首次到达金额0时的层数即为最少硬币数。

  1. 初始化

    • 队列存放待处理的金额,初始为 amount
    • 已访问集合 visited 防止重复处理相同金额。
    • steps 记录当前层数(即已用硬币数)。
  2. 层序遍历

    • 每层开始前记录队列大小,处理完该层所有节点后步数+1。
    • 对每个金额尝试所有硬币:
      • current - coin == 0,直接返回当前步数。
      • 若新金额合法且未访问过,加入队列并标记,避免重复处理相同金额。
  3. 终止条件

    • 队列清空时仍未找到解,返回 -1

代码实现(Java)

import java.util.*;

class Solution {
    public int coinChange(int[] coins, int amount) {
        if (amount == 0) return 0;
      
        Queue<Integer> queue = new LinkedList<>();
        Set<Integer> visited = new HashSet<>();
        queue.offer(amount);
        visited.add(amount);
        int steps = 0;
      
        while (!queue.isEmpty()) {
            int size = queue.size();
            steps++;
          
            for (int i = 0; i < size; i++) {
                int current = queue.poll();
              
                for (int coin : coins) {
                    int next = current - coin;
                  
                    if (next == 0) {
                        return steps; // 找到解,直接返回
                    }
                  
                    if (next > 0 && !visited.contains(next)) {
                        visited.add(next);
                        queue.offer(next);
                    }
                }
            }
        }
        return -1; // 队列清空仍未找到解
    }
}

复杂度分析

  • 动态规划时间复杂度:外层循环:O(amount),内层循环:O(coins.length),总复杂度:O(amount * coins.length)
  • BFS时间复杂度:最坏情况:O(amount * coins.length),实际效率取决于硬币面额分布,大额硬币可能加速收敛。

对比总结

方法优势劣势
BFS无需预计算所有状态,快速收敛空间复杂度高(队列膨胀)
动态规划适合多次查询,空间可控需遍历全部状态

347. 前 K 个高频元素

给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按任意顺序返回答案。

示例 1:

输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]

示例 2:

输入: nums = [1], k = 1
输出: [1]

提示:

  • 1 <= nums.length <= 105
  • k 的取值范围是 [1, 数组中不相同的元素的个数]
  • 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的

进阶: 你所设计算法的时间复杂度必须优于 O(n log n) ,其中 n 是数组大小。

方法一:最小堆(优先队列)

使用最小堆维护当前频率最高的k个元素,遍历时保持堆的大小不超过k,时间复杂度为O(n log k)

  1. 统计频率:使用哈希表记录每个元素的出现次数。
  2. 维护最小堆:将元素按频率加入堆,堆大小超过k时移除堆顶(最小频率元素)。
  3. 提取结果:堆中剩余的元素即为前k高的频率元素。

代码实现(Java)

class Solution {
    public int[] topKFrequent(int[] nums, int k) {
        // 统计频率
        Map<Integer, Integer> freqMap = new HashMap<>();
        for (int num : nums) {
            freqMap.put(num, freqMap.getOrDefault(num, 0) + 1);
        }

        // 创建最小堆,按频率升序排序
        PriorityQueue<Map.Entry<Integer, Integer>> heap = new PriorityQueue<>(
            (a, b) -> a.getValue() - b.getValue()
        );

        // 维护堆的大小为k
        for (Map.Entry<Integer, Integer> entry : freqMap.entrySet()) {
            heap.offer(entry);
            if (heap.size() > k) {
                heap.poll();
            }
        }

        // 提取结果
        int[] res = new int[k];
        int idx = 0;
        while (!heap.isEmpty()) {
            res[idx++] = heap.poll().getKey();
        }

        return res;
    }
}

方法二:桶排序

基于频率的桶排序,时间复杂度为O(n),适合处理大数据量。

  1. 统计频率:记录每个元素出现的次数及最大频率。
  2. 构建频率桶:将元素按频率存入对应桶中。
  3. 逆序收集结果:从高频率到低频率遍历桶,收集前k个元素。

代码实现(Java)

class Solution {
    public int[] topKFrequent(int[] nums, int k) {
        // 统计频率
        Map<Integer, Integer> freqMap = new HashMap<>();
        for (int num : nums) {
            freqMap.put(num, freqMap.getOrDefault(num, 0) + 1);
        }

        // 创建桶数组
        List<Integer>[] bucket = new List[nums.length + 1];
        for (Map.Entry<Integer, Integer> entry : freqMap.entrySet()) {
            int freq = entry.getValue();
            if (bucket[freq] == null) {
                bucket[freq] = new ArrayList<>();
            }
            bucket[freq].add(entry.getKey());
        }

        // 收集结果
        int[] res = new int[k];
        int idx = 0;
        for (int i = bucket.length - 1; i >= 0 && idx < k; i--) {
            if (bucket[i] != null) {
                for (int num : bucket[i]) {
                    res[idx++] = num;
                    if (idx == k) break;
                }
            }
        }

        return res;
    }
}

复杂度分析

1、最小堆

  • 时间复杂度:O(n log k),其中n为数组长度,k为结果数量。
    空间复杂度:O(n),哈希表和堆的空间。
  • 优点:空间占用相对较低,适合k较小的情况。
    缺点:当k接近n时,时间复杂度接近O(n log n)

2、桶排序

  • 时间复杂度:O(n),所有操作均为线性时间。
    空间复杂度:O(n),桶数组和哈希表的空间。
  • 优点:时间复杂度严格为O(n),适合大数据量。
    缺点:需要额外空间存储桶,最大频率较高时空间占用大。

394. 字符串解码

给定一个经过编码的字符串,返回它解码后的字符串。
编码规则为: k[encoded_string],表示其中方括号内部的 encoded_string 正好重复 k 次。注意 k 保证为正整数。
你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。
此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数 k ,例如不会出现像 3a2[4] 的输入。

示例 1:

输入:s = "3[a]2[bc]"
输出:"aaabcbc"

示例 2:

输入:s = "3[a2[c]]"
输出:"accaccacc"

示例 3:

输入:s = "2[abc]3[cd]ef"
输出:"abcabccdcdcdef"

示例 4:

输入:s = "abc3[cd]xyz"
输出:"abccdcdcdxyz"

提示:

  • 1 <= s.length <= 30
  • s 由小写英文字母、数字和方括号 '[]' 组成
  • s 保证是一个有效的输入
  • s 中所有整数的取值范围为 [1, 300]

方法:栈辅助法

利用栈来处理嵌套的括号结构,保存每层括号外的字符串和重复次数。遍历字符串时解析数字,遇到左括号时将当前状态压栈,遇到右括号时弹出栈顶元素进行字符串拼接。

  1. 初始化栈和变量:使用两个栈分别保存当前层的字符串和重复次数,当前字符串和数字变量记录正在处理的部分。
  2. 遍历字符
    • 数字:累加计算多位数。
    • 左括号:压栈当前字符串和数字,重置变量。
    • 右括号:弹出栈顶的字符串和数字,将当前字符串重复后拼接。
    • 字母:直接追加到当前字符串。
  3. 返回结果:最终拼接完成的字符串即为答案。

代码实现(Java)

class Solution {
    public String decodeString(String s) {
        Stack<Integer> numStack = new Stack<>();
        Stack<StringBuilder> strStack = new Stack<>();
        StringBuilder currentStr = new StringBuilder();
        int num = 0;

        for (char c : s.toCharArray()) {
            if (Character.isDigit(c)) {
                num = num * 10 + (c - '0');
            } else if (c == '[') {
                strStack.push(currentStr);
                numStack.push(num);
                currentStr = new StringBuilder();
                num = 0;
            } else if (c == ']') {
                int k = numStack.pop();
                StringBuilder prevStr = strStack.pop();
                String temp = currentStr.toString();
                prevStr.append(temp.repeat(k));
                currentStr = prevStr;
            } else {
                currentStr.append(c);
            }
        }

        return currentStr.toString();
    }
}

复杂度分析

  • 时间复杂度O(n × m),其中n是字符串长度,m是最大重复次数。每个字符处理一次,字符串拼接的时间取决于重复次数。
  • 空间复杂度O(n),栈的深度最坏情况下与字符串长度成线性关系。

博客源文件Gitee仓库:

gitee.com/richardmilos/allen-csdn-notes

(持续更新,未完待续)

相关文章:

  • CI/CD构建与注意事项
  • Vue3-高级特性
  • 【微服务】如何用Azure容器应用Job处理异步HTTP API请求
  • docker安装redis
  • 深入理解 HTML 中的统一资源定位器(URL)
  • 无人机校企合作新方向:人才培养,生产研发一体化技术详解
  • Vue生命周期
  • 【一文读懂】RTSP与RTMP的异同点
  • 蓝桥杯备赛(基础语法3)
  • [蓝桥杯 2023 省 B] 飞机降落
  • Dubbo 深度解析
  • 【FLOYD+并查集】蓝桥杯算法提高 Degrees of Separation
  • CC45.【C++ Cont】STL中的哈希表及练习
  • 【Python 算法 1.线性枚举】
  • CSRF(跨站请求伪造)详解:原理、攻击方式与防御手段
  • Flutter 学习之旅 之 flutter 使用 SQLite(sqflite) 实现简单的数据本地化 保存/获取/移除/判断是否存在 的简单封装
  • 《C#上位机开发从门外到门内》3-4:基于TCP/IP的远程监控系统设计与实现
  • docker-compose install nginx(解决fastgpt跨区域)
  • 海外红人营销助力游戏出海:从单一营销到生态构建的转变
  • CSS块元素、行内元素、行内块元素详解
  • 中国代表:美“对等关税”和歧视性补贴政策严重破坏世贸规则
  • 杭州挂牌临平区两宗住宅用地,起始总价约11.02亿元
  • 铺就长三角南北“交通动脉”,乍嘉苏改高速扩建项目首桩入位
  • 国台办:“台独”是绝路,外人靠不住
  • 招商蛇口:一季度营收约204亿元,净利润约4.45亿元
  • 逛了6个小时的上海车展。有些不太成熟的感受。与你分享。