(优先整理热门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]
的结果。
-
初始化
dp[0] = 0
:金额为0时不需要任何硬币。- 其他金额初始化为
amount + 1
,因为最多需要amount
个硬币(全用面额1的硬币),初始值设为更大的数表示暂时不可达。
-
状态转移
- 对于每个金额
i
,遍历所有硬币面额coin
。 - 若
coin ≤ i
,则可以通过i - coin
的金额加上当前硬币组成i
,即dp[i] = min(dp[i], dp[i - coin] + 1)
。
- 对于每个金额
-
结果判断
- 如果最终
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到amountfor (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时的层数即为最少硬币数。
-
初始化
- 队列存放待处理的金额,初始为
amount
。 - 已访问集合
visited
防止重复处理相同金额。 steps
记录当前层数(即已用硬币数)。
- 队列存放待处理的金额,初始为
-
层序遍历
- 每层开始前记录队列大小,处理完该层所有节点后步数+1。
- 对每个金额尝试所有硬币:
- 若
current - coin == 0
,直接返回当前步数。 - 若新金额合法且未访问过,加入队列并标记,避免重复处理相同金额。
- 若
-
终止条件
- 队列清空时仍未找到解,返回
-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)
。
- 统计频率:使用哈希表记录每个元素的出现次数。
- 维护最小堆:将元素按频率加入堆,堆大小超过k时移除堆顶(最小频率元素)。
- 提取结果:堆中剩余的元素即为前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());// 维护堆的大小为kfor (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)
,适合处理大数据量。
- 统计频率:记录每个元素出现的次数及最大频率。
- 构建频率桶:将元素按频率存入对应桶中。
- 逆序收集结果:从高频率到低频率遍历桶,收集前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
,例如不会出现像 3a
或 2[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]
方法:栈辅助法
利用栈来处理嵌套的括号结构,保存每层括号外的字符串和重复次数。遍历字符串时解析数字,遇到左括号时将当前状态压栈,遇到右括号时弹出栈顶元素进行字符串拼接。
- 初始化栈和变量:使用两个栈分别保存当前层的字符串和重复次数,当前字符串和数字变量记录正在处理的部分。
- 遍历字符:
- 数字:累加计算多位数。
- 左括号:压栈当前字符串和数字,重置变量。
- 右括号:弹出栈顶的字符串和数字,将当前字符串重复后拼接。
- 字母:直接追加到当前字符串。
- 返回结果:最终拼接完成的字符串即为答案。
代码实现(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
(持续更新,未完待续)