Oracle:一个Schema是一个user,一个数据库只有一个,可以高sass玩法,数据库层面就做物理隔离了

需求

并发场景可能存在的需求-每个执行结果的回调

CompleteableFuture大家都用过,里面有supply、then、combine、allOf等等方法,都可以用来接收一个任务,最终将多个任务汇总成一个结果。

但有一个问题,你supply一个任务后,这个任务就黑盒了。如果你编排了很多个任务,每一个任务的执行情况,执行到哪一步了,每一步的执行结果情况,我们是不知道的。只能等它最终执行完毕后,最后汇总结果。

一个并行框架,它最好是对每一步的执行都能监控。每一步的执行结果,无论成功与失败,它应该有个回调,才算完整。拥有回调的任务,可以监控任务的执行状况,如果执行失败、超时,可以记录异常信息或者处理个性化的默认值。

CompleteableFuture中也有一些回调方法,例如:thenAccept(),whenComplete(),handle(),exceptionally()等,这些方法也能支持任务的回调,但是前提是任务执行了,才能完成回调。在某些场景中,有些任务单元是可能被SKIP跳过不执行的,不执行的任务也应该有回调。

并发场景可能存在的需求-依赖上游的执行结果作为入参

future1.thenCompose(f2,()->xxx)

下游依赖于上游的结果

并发场景可能存在的需求-全组任务的超时

一组任务,虽然内部的各个执行单元的时间不可控,但是可以控制全组的执行时间不超过某个值。通过设置timeOut,来控制全组的执行阈值。

1
CompletableFuture.allOf(futures).get(timeout, TimeUnit.MILLISECONDS);

并发场景可能存在的需求-高性能、低线程数

复用线程

该框架全程无锁,没有一个加锁的地方。

创建线程量少。如上图④中场景。A会运行在B、C执行更慢的那个单元的线程上,而不会额外创建线程。

总结

一个并发框架可能需要具备哪些能力?

1、提供任何形式的串行、并行执行单元的组合

2、为每个执行单元提供执行成功、失败、超时、异常的回调

3、支持为单个执行单元设置异常、失败后的默认值

4、支持为整个group(多个任意组合的执行单元)设置超时时间。单个执行单元失败,不影响其他单元的回调和最终结果获取。如果自己依赖的任务失败,则自己也失败,并返回默认值。

5、整个group执行完毕或超时后,同步阻塞返回所有执行单元结果集,按添加的顺序返回list。也支持整个group的异步回调不阻塞主线程

6、支持每个group独享线程池,或所有group共享线程池(默认)

异步回调如何实现

上面我们总结了多线程的编排场景及实现,以及并发场景的一些潜在需求及实现。

该框架的难点和重点,主要有两点,分别是任务的顺序编排任务结果的回调

回调是个很有用的模式,譬如我的主线程执行过程中,要执行一个非常耗时的逻辑。为了不阻塞主线程,自然我们会想到用异步的形式去执行这个耗时逻辑,新建个线程,让这个耗时的逻辑在线程中执行,不阻塞主线程。但问题来了,异步执行没毛病,执行成功、失败后出结果了,该怎么通知主线程?

4.1、CompletableFuture的回调

CompletableFuture提供了许多回调的方法,例如:thenAccept(),whenComplete(),handle(),exceptionally()等。下面列举一些比较常用的回调方法,如下:

1
2
3
4
5
6
7
8
//1.计算结果完成,或者异常时执行给定action(当前线程执行)
CompletableFuture<T> whenComplete(BiConsumer<? super T, ? super Throwable> action);
//2.计算结果完成,或者异常时执行给定action(另起线程执行)
CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T, ? super Throwable> action);
//3.执行完成时对结果进行处理,还可以处理异常
<U> CompletableFuture<U> handle(BiFunction<? super T, Throwable, ? extends U> fn)
//4.异常时,返回指定结果
CompletableFuture<T> exceptionally(Function<Throwable, ? extends T> fn)

CompleteableFuture提供的回调方法,这些方法也能支持任务的回调,但是前提是任务执行了,才能完成回调。在某些场景中,有些任务单元是可能被SKIP跳过不执行的,不执行的任务也应该有回调。

4.2、Netty future中的回调

Netty中的回调是非常多的。netty中的future,可以添加Listener,当异步任务执行完毕后,主动回调一下自己就可以了。

整个netty里面大量充斥着类似的回调,但是如果我们要用,仅仅是针对一个或多个异步任务,希望能有个类似的回调,netty就帮不上忙了。

Netty回调的伪代码:其中doSomething是在异步线程里,而回调是在主线程里的。(回调代码是在主线程,回调任务实际是在异步线程里执行的)

1
2
3
4
5
6
7
8
9
10
//主线程
main {
//doSomething是在异步线程里,回调是在主线程
doSomething().async().addListener(new Listener(){
@Override
public void complete() throws Exception {
//do your job
}
});
}

4.3、如何自己实现一个简单回调的异步任务

首先我们有这么一个需求,有N个耗时任务,可能是IO任务,我们希望他执行时不会阻塞主线程,而期望它执行完毕(成功\失败)后,来发起一次回调,最好还有超时、异常的回调控制。

我们可以有这样的角色:

  1. Bootstrap主线程
  2. worker 任务
  3. listener 监听器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package com.soyo.simple;

/**
* @author: lkl
* @date: 2025/9/12 22:24
*/
public class Bootstrap {

public static void main(String[] args) {
Bootstrap bootstrap = new Bootstrap();
Worker worker = bootstrap.newWorker();

Wrapper wrapper = new Wrapper();
wrapper.setWorker(worker);
wrapper.setParam("hello");

//2.回调方法,输出worker中的内容
bootstrap.doWorker(wrapper).addListener(new Listener() {
@Override
public void callback(Object result) {
System.out.println(Thread.currentThread().getName());
System.out.println(result);
}
});

//1.主线程不阻塞,打印当前线程
System.out.println(Thread.currentThread().getName());
}

private Wrapper doWorker(Wrapper wrapper) {
new Thread(() -> {
Worker worker = wrapper.getWorker();
String result = worker.action(wrapper.getParam());
wrapper.getListener().callback(result);
}).start();

return wrapper;
}

private Worker newWorker() {
return new Worker() {
@Override
public String action(String obj) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return obj + " callback!";
}
};
}

}


思考

核心线程数为0的线程池

作者在issue上推荐一个tomcat只使用一个不定长线程池。

AsyncTool关于多线程并发执行的问题。如果不指定线程池,默认使用不定长线程池。

1
2
3
4
5
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}

注意,newCachedThreadPool() 它的核心线程数是0,最大线程数是Integer.MAX_VALUE。可以说它的线程数是可以无限扩大的线程池。

他使用的队列是没有存储空间的,只要请求过来了,如果没有空闲线程,就会创建一个新的线程。

这种不定长线程池,适合处理执行时间比较短的任务。在高并发下,如果在耗时长的RPC\IO操作中使用该线程池,势必会造成系统 线程爆炸。

AsyncTool作者在京东使用的场景多数为低耗时(10ms)高并发,瞬间冲击的场景。这种高并发,且任务耗时较少的,适合使用不定长线程池。

但是这种低耗时的场景也不多,对于耗时较长的场景,推荐使用自定义线程池,可以避免那些耗时长的任务长时间占用线程,造成线程 ”爆炸 “,容错率更高。

总结:

  • **newCachedThreadPool**** 特性**:核心线程数 0,最大线程数无限,队列无缓存 → 任务一来没线程就新建线程。
  • 适合短耗时任务:任务执行快,线程能很快释放和复用,高并发时能快速扩容,冲击过去后线程又能被回收,不会长期暴涨。
  • 不适合长耗时任务:线程长时间被占用,新任务只能不断新建线程,最终线程数“爆炸”,导致内存和 CPU 切换压力过大,系统崩溃。
  • 结论:短平快的高并发任务用不定长线程池;耗时任务必须用自定义线程池(限制核心数、最大数和队列),避免线程无限增长。

短耗时 + 高并发场景 → 秒杀下单接口

  • 用户在 11.11 秒杀活动一开始,会有 瞬间几十万请求冲进来。
  • 每个请求要做的事情很简单:
    • 校验库存(内存中或 Redis 中)
    • 扣减库存标记
    • 返回“抢购成功/失败”结果
  • 整个处理逻辑可能只需要 5~10ms

在这种场景下:

  • 请求量短时间非常大 → 需要线程池快速扩容,防止请求被阻塞。
  • 每个任务很快执行完 → 线程会迅速释放,可以被复用,不会造成线程无限积压。
  • 高峰一过,线程池里的线程就会在 60s 内回收 → 节省资源。

👉 所以 newCachedThreadPool 就特别适合这种“短平快、高并发、瞬时冲击”的场景。

而且双11这种平台肯定是希望用户请求在机器不会崩的情况下流量越多越好,因为下单的单数越多,那么平台赚得越多

源码解析

两套方案

cluster,sentinel

sentinel redis

当发现Redis主节点宕机后,多个sentinel之间会选一个裁判出来,通过投票的方式,多数大于小数,由于这个东西,所以机器数都是基数的,不然就脑裂了

然后再进行从新选主,且由裁判执行

不同系统之间的选举

leader elects master

sentinel leader负责选举新的leader,这个过程中还可以挑一个最好的

选择规则:

  1. 优先选数据最完整的slave,因为主从同步是有时间窗口的
  2. slave slave-prioirty 值最低的优先
  3. 多个slave满足条件后,选runid最小的

redis cluster

就是分片,就是分库分表那一套

有个slot的概念,内置了16384个槽,且不能改变。一个slot只能在一台机器上,且在主节点上

crc16(key) % 16384(固定值,不能调节了,所以就不用了 )分配在一个slot上。

resharding

Redis cluster的数据量极限,16384个slot,一个slot一个Redis master节点

1000w * 16384 = 16384000w条数据

分布式数据库为什么不采用slot

PS:俄罗斯人造的东西都有个特点就是快,Nginx也是,走一个极致路线

对比Redis,Redis其实不是纯粹的缓存,自称NOSQL DB,因为可以落盘么

而memcached就是纯粹的缓存

特点

  1. 协议简单
  2. 使用libevent进行事件处理
  3. 内置内存
  4. 之间不进行通信的分布式

简单协议

过于简单了,都没自己的命令行工具,直接上去就是开敲

telnet 本地主机 端口

用人来告诉计算机代码更快

之间不进行通信的分布式方式

存储原理

slab allocator,也是池化的思想,不用了还回去,用了再回来

PS:文件真删了,还是能找回来的

原理:

内存结构类似于 快递的蜂巢

画格子

其实就是创建一堆类似不同的数组,内存连续,且内存不会释放,且命令执行多线程,所以才快,

内部碎片

虽然解决了外部碎片,但还是会有内部碎片问题

解决方案:就是重启,凌晨重启

题外话

Redis往里加数据,得架构师评省一下,比如加一个set,评故下n,因为那东西是单线程,如果你加的集合后面慢慢变过大的话,是会影响其他的

slab内存分配和slub内存分配

命令

都是基于set 字符串 get 字符串进行的一个扩展

hash

取模

一致性hash

目的是为了解决传统hash服务上下限导致的hash全都得全部映射好,只需要部分改变,比如k1推到s1,但s1没了,只会影响k1,而不会影响到其他

以及可以让同一类请求每次都映射到同一节点上,适用于 本地缓存之类的场景,提高缓存命中率

虚拟节点解决的问题:实例节点分布不均

虚拟节点越多,hash上的对象分配更均匀

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package com.grace.gateway.core.algorithm;

import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;

public class ConsistentHashing {

private final int virtualNodeNum;

// 哈希环
private final SortedMap<Integer, String> hashCircle = new TreeMap<>();

// 构造函数,初始化一致性哈希环
public ConsistentHashing(List<String> nodes, int virtualNodeNum) {
this.virtualNodeNum = virtualNodeNum;
for (String node : nodes) {
addNode(node);
}
}

public void addNode(String node) {
for (int i = 0; i < virtualNodeNum; i++) {
String virtualNode = node + "&&VN" + i;
hashCircle.put(getHash(virtualNode), node);
}
}

public String getNode(String key) {
if (hashCircle.isEmpty()) {
return null;
}
int hash = getHash(key);
SortedMap<Integer, String> tailMap = hashCircle.tailMap(hash);
Integer nodeHash = tailMap.isEmpty() ? hashCircle.firstKey() : tailMap.firstKey();
return hashCircle.get(nodeHash);
}

private int getHash(String str) {
final int p = 16777619;
int hash = (int) 2166136261L;
for (int i = 0; i < str.length(); i++) {
hash = (hash ^ str.charAt(i)) * p;
}
hash += hash << 13;
hash ^= hash >> 7;
hash += hash << 3;
hash ^= hash >> 17;
hash += hash << 5;
if (hash < 0) {
hash = Math.abs(hash);
}
return hash;
}

}

权重随机

1
2
3
4
5
6
7
8
9
10
11
@Override
public ServiceInstance selectInstance(GatewayContext context, List<ServiceInstance> instances) {
int totalWeight = instances.stream().mapToInt(ServiceInstance::getWeight).sum();
if (totalWeight <= 0) return null;
int randomWeight = ThreadLocalRandom.current().nextInt(totalWeight);
for (ServiceInstance instance : instances) {
randomWeight -= instance.getWeight();
if (randomWeight < 0) return instance;
}
return null;
}

基础

单调栈分为单调递增,单调递减

  • 递增栈
    • 栈内元素从栈底到栈顶递增。
    • 用于寻找 下一个更小元素
  • 递减栈
    • 栈内元素从栈底到栈顶递减。
    • 用于寻找 下一个更大元素

👉 核心模式就是:

1
2
3
4
5
6
7
for (...) {
while (!stack.isEmpty() && 当前元素和栈顶元素满足某条件) {
// 处理逻辑:清算旧账
stack.pop();
}
stack.push(当前元素/下标);
}

每日温度[🔥59]

题目:找出每个元素多少index后会出现比他大的元素

思路:

  1. 维护一个单调递减栈,存的是index
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public int[] dailyTemperatures(int[] temperatures) {
//维护一个单调递增栈,用来存放已经遍历过的元素,然后就可以与当前元素进行一个比较
//该题是一个单调递减栈,栈顶 --> 栈低 是递增的
Stack<Integer> stack = new Stack<>();
int[] res = new int[temperatures.length];
for (int i = 0; i < temperatures.length; i++) {
//队列不为空 然后如果当前元素大于那个已经遍历过的元素,那就去清理旧账
while (!stack.isEmpty() && temperatures[stack.peek()] < temperatures[i]) {
int curIndex = stack.pop();
//求的是间隔
res[curIndex] = i - curIndex;
}
stack.add(i);
}
return res;
}
}

时间复杂度:ON

空间复杂度:ON

84. 柱状图中最大的矩形[28]

思路:

  1. 本质上还是接雨水,理解两个点: 1. 面积等于高度 * 宽度,所以对于任何一个柱子,都要看看往左右两边伸展的最大宽度是多少,使用单调栈 2. 栈有两个语义,栈[-2] 是栈[-1]的左边界,出栈操作时,表示栈[-1] > 当前元素,这样就为栈顶元素确定了两个边界。
  2. 这里解释一下,range 为什么是 range = i - leftIndex - 1; i 即右边第一个比 h(mid) 小的柱子位置,而leftindex 即左边第一个比它小的柱子(这个值很容易,根据单调栈特性,我们就可以直接peek()出来)
  3. 那么当前mid柱子 可扩展的范围 也就是 最右 - 最左 - 1 这里的 - 1就是要把最右最左给排除掉
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Solution {
//以 heights[i]为高的 矩形面积有多大
//heights[i] 为基准
public int largestRectangleArea(int[] heights) {
int n = heights.length;
int[] newHeights = new int[n+2];
newHeights[0] = 0;
newHeights[newHeights.length - 1] = 0;
System.arraycopy(heights, 0, newHeights, 1 , n);
//System.out.println(Arrays.toString(newHeights));

Stack<Integer> s = new Stack();
s.add(0);
int res = 0;
for(int i = 0; i < newHeights.length; i++){
while(!s.isEmpty() && newHeights[i] < newHeights[s.peek()]){
int midIndex = s.pop();
if(s.isEmpty()){
break;
}
int leftIndex = s.peek();
//以h(mid)为高度 左右扩展的范围
// leftIndex midIndex .... i
int range = i - leftIndex - 1;
res = Math.max(res, newHeights[midIndex] * range);
}
s.add(i);
}
return res;
}
}

最大矩形[27]

思路:

  1. 先做84题,是它的父问题。就是把它切 m(martix.length )个 84题问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class Solution {
int res = 0;
public int maximalRectangle(char[][] matrix) {
int[][] t = new int[matrix.length][matrix[0].length + 2];
for(int i = 0; i < t.length; i++){
t[i][0] = 0;
t[i][t[0].length - 1] = 0;
}
// 转换成 84题那种柱状图
for(int i = 0; i < t.length; i++){
for(int j = 1; j < t[0].length - 1; j++){
if(matrix[i][j - 1] == '1'){
t[i][j] = 1;
//当前 位置为 1时,才能有高度
if(i > 0) t[i][j] += t[i - 1][j];
}
else t[i][j] = 0;

}
}
// for(int [] tt: t){
// System.out.println(Arrays.toString(tt));
// }

for(int[] tt: t){
f1(tt);
}
return res;
}
public void f1(int []hegihts){
Stack<Integer> s = new Stack();
s.add(0);
for(int i = 1; i < hegihts.length; i++){
while(!s.isEmpty() && hegihts[s.peek()] > hegihts[i]){
int midIndex = s.pop();
if(s.isEmpty()){
break;
}
int leftIndex = s.peek();
res = Math.max(res, hegihts[midIndex] * (i - leftIndex - 1));
}
s.add(i);
}
}
}

去除重复字母

最长重复子数组[🔥64]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Solution {
public int findLength(int[] nums1, int[] nums2) {
//dp吧
//dp数组表示什么? : 以 i结尾的nums1 以j 结尾的nums2 ,最大的子数组长度
//遍历顺序
//状态方程
//dp数组就表示
// 1 2 3 4
// 1 2 3 5

// 1 2 3 4 6
// 1 2 5 4
int [][]dp = new int[nums1.length+1][nums2.length+1];
int res=0;
for(int i=0;i<nums1.length;i++){
for(int j=0;j<nums2.length;j++){
if(nums1[i]==nums2[j]){
dp[i+1][j+1]=dp[i][j]+1;
res=Math.max(res,dp[i+1][j+1]);
}
}
}
return res;
}
}

买卖股票的最佳时机 II [82]

题目:

给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。

在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。

返回 你能获得的 最大 利润

思路:

  1. 贪心:低价买入,如果第二天高价就卖出,局部最优解
1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public int maxProfit(int[] prices) {
int res =0;
//上帝视角看
//我们要实现最大利润,那么从股票走势图来看就是 递增的每一段我都要赚,只要第二天涨价我就卖,每一段上升期我都吃
for(int i=1;i<prices.length;i++){
if(prices[i]-prices[i-1]>0){
res+=prices[i]-prices[i-1];
}
}
return res;
}
}
  1. 动态规划:状态:有股票or没股票,dp数组含义:第i天持有or没持有股票的最大利润,状态推导
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public int maxProfit(int[] prices) {
//dp : i 这一天可获得的最大利润
// 股票状态:有或者没有
int [][] dp = new int[prices.length][2];
dp[0][0] = -prices[0];
dp[0][1] = 0;
for(int i=1;i<prices.length;i++){
//0:有:昨天有今天继续持有 or 昨天没有但今天买入
dp[i][0]=Math.max(dp[i-1][0],dp[i-1][1]-prices[i]);
//1:没有:昨天没有今天继续没有 or 昨天有但今天卖出
dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]+prices[i]);
}
//因为最后一天手里 不能有股票 才是最大利润(卖掉股票才是真实收益)。
return dp[prices.length-1][1];
}
}


不同路径[72]

思路:DP,四部曲 ,数组含义,初始化,状态,推导

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public int uniquePaths(int m, int n) {
//dp:表示达到该位置i j最大有多少条路径
int [][] dp = new int[m][n];
//初始化
dp[0][0]=1;
for(int i=1;i<m;i++){
dp[i][0]=1;
}
for(int j=1;j<n;j++){
dp[0][j]=1;
}
for(int i=1;i<m;i++){
for(int j=1;j<n;j++){
//状态推导,当前位置 = 左 + 上
dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
}
return dp[m-1][n-1];
}
}

编辑距离[183]

思路:

  1. 背诵 哈哈哈
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Solution {
public int minDistance(String word1, String word2) {
int [][]dp=new int[word1.length()+1][word2.length()+1];
//dp[i][j] 表示 word 中前 i个字符,变换到word2中的 前j个字符,需要的最小次数
// dp[0][0] 表示 空字符串

//
for(int i=0;i<=word1.length();i++){
dp[i][0]=i;
}
for(int j=0;j<=word2.length();j++){
dp[0][j]=j;
}
for(int i=1;i<=word1.length();i++){
for(int j=1;j<=word2.length();j++){

//eg:
// ros i=2
// rs i=1
//特殊情况 word1[i-1]==word[j-1]
if(word1.charAt(i-1)==word2.charAt(j-1)){
dp[i][j]=dp[i-1][j-1];
}
//当前状态 是增,删,改 哪个操作数小?
else dp[i][j]=Math.min(dp[i-1][j-1],Math.min(dp[i-1][j],dp[i][j-1]))+1;
}
}
return dp[word1.length()][word2.length()];
}
}

爬楼梯

思路:

  1. 动态规划,思考dp数组含义,当前状态是怎么由前一个状态推导过来的
  2. //两种情况:比如我在三阶梯,我可以从一阶梯一下子两步上来,以及从二阶梯一步上来
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public int climbStairs(int n) {
if(n < 2) return n;
int []dp = new int[n+1];
dp[0] = 1;
dp[1] = 1;
dp[2] = 2;
//4
//1:1
//2:2
//3: 2
for(int i = 3;i <= n;i++){
//两种情况:比如我在三阶梯,我可以从一阶梯一下子两步上来,以及从二阶梯一步上来
dp[i] = dp[i-1] + dp[i-2];
}
return dp[n];
}
}

零钱兑换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public int coinChange(int[] coins, int amount) {
//dp数组含义: 构成当前金额的最小硬币数
//dp[i] = dp[ i - cions ] + 1
if(amount <= 0){
return 0;
}
int []dp = new int[amount+1];
Arrays.fill(dp,Integer.MAX_VALUE);
dp[0] = 0;
for(int i = 0;i < dp.length;i++){
for(int j = 0;j<coins.length;j++){
if( i - coins[j] >= 0 && dp[i-coins[j]] != Integer.MAX_VALUE){
dp[i] = Math.min(dp[i-coins[j]] + 1, dp[i]);
}
}
}
System.out.println(Arrays.toString(dp));
return dp[amount] == Integer.MAX_VALUE? -1: dp[amount];
}
}

最大正方形[81]

思路:动态规划

就嗯记,dp思路 相邻三个矩形边长的最小值+1

dp数组含义即 以matrix[i][j] 为右下角为右下角的最大正方形边长

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public int maximalSquare(char[][] matrix) {
int n = matrix.length;
int m = matrix[0].length;
//dp数组含义 dp[i][j] 表示:以 matrix[i][j] 为右下角的最大正方形边长。
int[][] dp = new int[n][m];
int res = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if(matrix[i][j] == '1') {
if(i == 0|| j == 0){
dp[i][j] = 1;
}
//min (相邻三个正方形) + 1
else dp[i][j] = Math.min(dp[i - 1][j - 1], Math.min(dp[i][j - 1], dp[i - 1][j])) + 1;
res = Math.max(res, dp[i][j]);
}
}
}
return res * res;
}
}

打家劫舍[70]

思路:

  1. 动规,详情见代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public int rob(int[] nums) {
if(nums.length == 1) return nums[0];
int []dp = new int[nums.length];
//dp数组含义 小偷走到当前i index房子时 能偷的最高金额
//初始化
dp[0] = nums[0];
dp[1] = Math.max(nums[0], nums[1]);
int res = dp[1];
for(int i = 2;i < nums.length; i++){
//两种情况 偷 or 不偷,且不能相邻偷
dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1]);
res = Math.max(res, dp[i]);
}
return res;
}
}

输出路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Solution {
public int rob(int[] nums) {
if(nums.length == 1) return nums[0];
int []dp = new int[nums.length];
//dp数组含义 小偷走到当前i index房子时 能偷的最高金额
//初始化
dp[0] = nums[0];
dp[1] = Math.max(nums[0], nums[1]);
int res = dp[1];

//路径
List<Integer> resList = new ArrayList<>();
//resList.add();
for(int i = 2;i < nums.length; i++){
//偷 or 不偷
if(dp[i - 2] + nums[i] > dp[i - 1]){
if(i == 2) resList.add(dp[0]);
resList.add(nums[i]);
}
//两种情况 偷 or 不偷,且不能相邻偷
dp[i] = Math.max(dp[i-2] + nums[i], dp[i - 1]);
res = Math.max(res, dp[i]);
}
System.out.println(resList);
return res;
}
}

单词拆分[64]

思路:

  1. DP做法:看成背包问题,即s是否可以被物品wordDict装满
  2. 那么这时候dp数组的含义:dp[i] 表示0….i 范围的s是否可以被worddict里的单词分隔
  3. 这种做法时间复杂度 O()?不太行哦
  4. 时间复杂度:O(n³)
  • n = 字符串长度
  • 因为三层操作:外层 i、内层 j、substring 复制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
//dp数组含义 以i为结尾的字符串,是否可以被转换
HashSet<String> set = new HashSet(wordDict);
boolean[] dp = new boolean[s.length()];
for (int i = 0; i < s.length(); i++) {
for (int j = 0; j <= i; j++) {
//j...i
String sub = s.substring(j, i + 1);
// 0...j...i
//这里的条件成立为两部分:[0..j-1] [j...i] 可以被分隔
//两者共同使 以i为结尾的字符串,是否可以被转换
if ((j == 0 || dp[j - 1]) && set.contains(sub)) {
dp[i] = true;
break;
}
}
}
return dp[s.length() - 1];
}
}

https://chatgpt.com/s/t_68ea8e7831c48191ba35865a1392cdee

分割等和子集[28]

思路

  1. 可能性问题可以优先想到 DP
  2. 必会题,01背包
  3. dp[i][j] 表示[0..i]里若干个元素是否可以构成和为j

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Solution {
public boolean canPartition(int[] nums) {
int sum=0;
for(int i=0;i<nums.length;i++) sum+=nums[i];
int avg =sum/2;
if(sum % 2 == 1) return false;
//dp数组含义:nums[0.。i] 是否存在若干个元素的和 等于 j
boolean [][]dp = new boolean[nums.length][avg + 1];
//初始化
if(nums[0] <= avg){
dp[0][nums[0]] = true;
}
for(int i = 1; i < nums.length; i++){
for(int j = 0; j <= avg; j++){
//j为 目标和
if(nums[i] <= j){
//不选 则 [0....i-1]若干个元素和为j
//选,则[0....i-1]若干个元素和为j-nums[i]
dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i]];
}
//nums[i] 本身超过了 j,那肯定是不选的
else dp[i][j] = dp[i - 1][j];
}
if(dp[i][avg]) return true;
}
return dp[nums.length - 1][avg];
}
}

压缩:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public boolean canPartition(int[] nums) {
int sum=0;
for(int i=0;i<nums.length;i++) sum+=nums[i];
int avg =sum/2;
if(sum % 2 == 1) return false;
//dp数组含义:是否存在若干个元素的和 等于 i
boolean []dp = new boolean[avg + 1];
if(nums[0] <= avg) dp[nums[0]] = true;
//初始化
for(int i = 1; i < nums.length; i++){
for(int j = avg; j >= 0; j--){
if(nums[i] <= j) dp[j] = dp[j] || dp[j - nums[i]];
}
if(dp[avg]) return true;
}
return dp[avg];
}
}

分割成和最相近的两个子集

完全平方数[22]

完全背包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Solution {
// 和为 n 的完全平方数的最少数量
//
/**
dp数组含义 和为 i 的完全平方数的最少数量

*/
public int numSquares(int n) {
if (n <= 3)
return n;
int length = n / 2;
int[] nums = new int[length];
int count = 1;
for (int i = 0; i < nums.length; i++, count++) {
nums[i] = count * count;
}
//System.out.println(Arrays.toString(nums));
int dp[] = new int[n + 1];
dp[0] = 0;
for (int i = 1; i < dp.length; i++)
dp[i] = Integer.MAX_VALUE;
for (int i = 0; i < nums.length; i++) {
for (int j = 0; j <= n; j++) {
if (j >= nums[i] && dp[j - nums[i]] != Integer.MAX_VALUE)
dp[j] = Math.min(dp[j], dp[j - nums[i]] + 1);
}
}
//System.out.println(Arrays.toString(dp));
return dp[n];
}
}

目标和

1

打家劫舍 III[]

思路

  1. 树形DP,树的每个节点存储两种状态(抢or不抢),然后从底递推到 根节点
  2. 当前节点抢,则 left不抢 + right不抢 + 1
  3. 若当前节点不抢,则左右节点的抢or不抢中取最大,然后相加。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public int rob(TreeNode root) {
int[] res = dfs(root);
//最后这里,就是当前节点抢不抢的问题
return Math.max(res[0], res[1]);
}
public int[] dfs(TreeNode root){
if(root == null){
return new int[]{0,0}; //怎么选都是没有的
}
int []left = dfs(root.left);
int []right = dfs(root.right);
//当前节点抢,则 左右子节点必须为 不抢的
int rob = left[1] + right[1] + root.val;
//当前节点不抢,则 从左右子节点取 最大的,然后一起带上来当前节点
int noRob = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
return new int[]{rob, noRob};
}
}

买卖股票的最佳时机含冷冻期[4]

思路

  1. 两个状态:持有or不持有
  2. dp数组含义 第i 持有or不持有股票 的最大利润
  3. 状态机DP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Solution {
public int maxProfit(int[] prices) {
//status: 0 没持有 1 持有
// 有冻结期 无非就是 今天卖出 明天不能买入、
//prices 是指同一只股票的价格啊!!!!!
//dp数组含义:第i天 没持有or持有 的利润
int[][] dp = new int[prices.length][2];
dp[0][0] = 0;
dp[0][1] = -prices[0];
for (int i = 1; i < prices.length; i++) {

//没持有,可能是 继续没持有,或者 在昨天持有股票的基础上 在今天以今天股票的价格股票给卖了
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);


//持有:可能是继续持有股票 或者是 前天把股票卖出了,然后以今天以今天股票的价格把股票买入了(为什么只能前天,因为不能昨天卖出,然后第二天买入)
//不能同时拥有多只股票,其实只有一直股票
if (i == 1)
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
else
//不能同时参与多笔交易

dp[i][1] = Math.max(dp[i - 1][1], dp[i - 2][0] - prices[i]);


}
//最后肯定得把股票卖了,即没持有,才能实现利益最大化
return dp[prices.length - 1][0];
}
}

比特位计数[2]

思路

  1. dp含义:第数为i,二进制后1的个数为 dp[i]
  2. 递推公式:当i为偶数,当i为奇数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30


class Solution {
public int[] countBits(int n) {
//0 -> 0
//1 -> 1
//2 -> 10
//3 -> 11 3/2 == 1 & 1
//4 -> 100 4/2 == 2
//5 -> 101 5/2 == 2
//6 -> 110
//7 -> 111
//8 -> 1000 2的3次方 8/2 == 3
int[] res = new int[n + 1];
if(n < 1) return res;
res[0] = 0;
res[1] = 1;
for(int i = 2; i <= n; i++){
int num = i;
if(num % 2 == 0){
res[i] = res[num / 2];
}
else {
res[i] = res[num / 2] + 1;
}

}
return res;
}
}