0%

面试总结


Hash算法解决冲突的四种方法

  1. 开放定址法:当发生冲突时,寻找下一个空的哈希地址。这包括:
    • 线性探测法:如果位置被占用,就顺序查找下一个空位。
    • 平方探测法:如果位置被占用,就在前后位置进行查找。
  2. 再哈希法:构造多个不同的哈希函数,当发生冲突时,使用另一个哈希函数计算地址。
  3. 链地址法:将所有哈希地址相同的记录链接在同一链表中。
  4. 建立公共溢出区:将哈希表分为基本表和溢出表,发生冲突的记录存放在溢出表中。

如何创建线程?

一般来说,创建线程有很多种方式,例如继承Thread类、实现Runnable接口、实现Callable接口、使用线程池、使用CompletableFuture类等等

java中线程同步的几种方法

方法一:使用synchronized关键字

方法二:wait和notify

方法三:使用特殊域变量volatile实现线程同步

方法四:使用重入锁实现线程同步

方法五:使用局部变量来实现线程同步

方法六:使用阻塞队列实现线程同步

方法七:使用原子变量实现线程同步

动态链接和静态链接

静态链接和动态链接两者最大的区别就在于链接的时机不一样,静态链接是在形成可执行程序前,而动态链接的进行则是在程序执行时,下面来详细介绍这两种链接方式。

  1. 静态链接
    • 静态链接发生在编译期间。
    • 所有代码在编译时被加载到内存中,并不需要再次加载。
    • 生成的可执行文件包含所有依赖的代码,因此程序体积较大。
    • 适用于不需要频繁更新代码的应用程序。
  2. 动态链接
    • 动态链接发生在运行时。
    • 代码模块只有在需要时才会被加载到内存中。
    • 程序体积较小,因为不需要将所有代码打包到一个可执行文件中。
    • 适用于需要灵活扩展的应用程序,因为它允许按需加载和更新代码。

抓包软件和抓包代码

Wireshark或Charles等抓包工具 tcpdump

中断和异常的区别

相同点:都是CPU系统发生的某个事情做出的一种反应

区别:**中断*外因引起*,异常CPU本身**原因引起。

  • 中断——外部事件引起正在运行的程序所不期望的
  • 异常——内部执行指令引起

img

为啥先放阻塞队列再建非核心线程?

提高资源利用率:创建和销毁线程需要耗费系统资源,而线程池通过复用线程,可以避免由于频繁地创建和销毁线程所带来的系统开销。

提高响应速度:当任务到达时,无需再去创建线程,而是可以直接由线程池中空闲的线程去执行,这样可以显著提高系统的响应速度。

2 在创建新线程的时候,是要获取全局锁的,这个时候其他的就需要阻塞,影响了整体效率。

就好比一个企业里面有十个(core)正式工的名额,最多招十个正式工(核心线程),要是任务超过正式人数(task>core)的情况下,工厂领导(线程池)不是首先扩招工人,还是这十个人,但是任务可以稍积压一下。即先放到队列中去(代价低)。十个正式工慢慢干,迟早会干完的,如果任务还在持续增加,超过正式工的加班忍耐极限了(队列满了),就招外包(非核心线程)帮忙了,还是正式工加外包还不能完成任务,那么新来的任务就会被领导拒绝(线程池拒绝策略)。

spring的动态代理和JDK的动态代理有什么区别

在Java中,动态代理是一种常用的设计模式,用于在运行时动态创建代理类并代理方法调用。Spring框架和Java Development Kit (JDK)都提供了动态代理的实现,但它们之间存在一些关键的区别:

  1. 实现方式

    • JDK动态代理:仅支持接口的代理。它是通过实现接口来创建动态代理的,使用java.lang.reflect.Proxy类和java.lang.reflect.InvocationHandler接口实现。
    • Spring动态代理:可以是基于JDK的动态代理,也可以是基于CGLIB的动态代理。如果代理的目标对象实现了至少一个接口,则Spring默认使用JDK动态代理。如果目标对象没有实现任何接口,则Spring会使用CGLIB来创建代理。
  2. 代理的内容

    • JDK动态代理:代理的是接口,不直接支持类的代理。
    • Spring动态代理(使用CGLIB):可以代理没有实现接口的类。CGLIB(Code Generation Library)是一个强大的高性能代码生成库,用于在运行时扩展Java类和实现接口。
  3. 性能和使用场景

    • JDK动态代理:在代理接口方面,性能通常比CGLIB稍好,因为它只是简单地反射调用方法。更适合那些已经定义了接口的类。
    • Spring动态代理(使用CGLIB):在创建代理类时需要更多的处理,可能在性能上略逊一筹,但是能够代理那些没有实现接口的类。适用于那些不易修改源码或者难以定义接口的旧有代码库。
  4. 配置和使用

    • JDK动态代理:使用较为直接,只需要定义一个实现了InvocationHandler的类即可。
    • Spring动态代理:通常通过Spring AOP(面向切面编程)来配置,可以更灵活地控制代理的行为,例如,通过切点和通知来定义何时以及如何进行方法拦截。

B树结构

B树,多路平衡搜索树,B树即B-树。其存储特点是每个磁盘块上都会存储具体数据和指向下一磁盘块的指针。详情见下图,一个磁盘块16KB大小。这中存储方式看似满足要求,但是也不支持范围查询,并且还有一个很严重的问题,B树的存储结构不满足数据量过大的情况。这里我们做个简单的计算:每个磁盘块16KB大小,假设指针不占用空间,一条数据1KB,每个磁盘块16条数据,三层B树能存储16 * 16 * 16=4096条数据,一个磁盘块代表一次IO,很明显数据量多的情况下,IO次数也会多,会影响查询性能,于是在B树的基础上衍生出了B+树。

B+树
B+数的存储特点是叶子节点存储的是主键key或者是具体的数据,非叶子节点存储的是主键和指向下一页的指针,叶子节点之间又通过指针相连,形成双向链表结构。此时也做一个简单的数据计算:一个两层的B+树,一个页16KB,假设一条记录1KB,一个叶子节点最多存放16条记录,根节点存放的都是指针和主键,假设主键是8字节,InnoDB指针大小是6字节,一共14字节,根节点可以存放16 * 1024B/14,大概是1170条,存储的数据就是1170 * 16=18720条,那么同理三层B+树能支持18720 * 1170=21902400条数据(千万级别)。相比于B树,B+树能在IO次数相同的情况下存储更多的数据,同时由于叶子节点之间是双向链表形式,能支持范围插叙,因此MySQL选择了B+树作为索引结构;

在这里插入图片描述

Minor GC、Major GC、Full GC的区别

今天主要谈谈JVM GC的类型和策略,特别是大家经常混淆的Minor GC、Major GC、Full GC,年轻代GC、老年代GC,之间有什么区别和联系。

Minor GC

JVM堆内存被分为两部分:年轻代(Young Generation)和老年代(Old Generation)。

img

1.年轻代

年轻代是所有新对象产生的地方,当年轻代内存空间被用完时,就会触发垃圾回收,这个垃圾回收叫做Minor GC。

年轻代被分为3个部分——Enden区和两个Survivor区,年轻代空间的要点:

  1. 大多数新建的对象都位于Eden区。
  2. 当Eden区被对象填满时,就会执行Minor GC。并把所有存活下来的对象转移到其中一个survivor区。
  3. Minor GC同样会检查存活下来的对象,并把它们转移到另一个survivor区。这样在一段时间内,总会有一个空的survivor区。
  4. 经过多次GC周期后,仍然存活下来的对象会被转移到年老代内存空间。通常这是在年轻代有资格提升到年老代前通过设定年龄阈值来完成的。

2.年老代

年老代内存里包含了长期存活的对象和经过多次Minor GC后依然存活下来的对象,通常会在老年代内存被占满时进行垃圾回收。

Major GC

老年代的垃圾收集叫做Major GC,Major GC通常是跟full GC是等价的,收集整个GC堆。

Minor GC和Major GC其实就是年轻代GC和年老年GC的俗称。而在Hotspot VM具体实现的收集器:Serial GC, Parallel GC, CMS, G1 GC中,大致可以对应到某个Young GC和Old GC算法组合。

分代GC

针对HotSpot VM的实现,其实GC的准确分类可以分为:

  1. 分代GC
  2. Full GC

以及后续的G1的分区收集本质其实还是一个分代收集器,但是和之前的各类回收器不同,它同时兼顾年轻代和老年代。

分代GC并不收集整个GC堆的模式,而是只专注分代收集

  1. Young GC:只收集年轻代的GC
  2. Old GC:只收集年老代的GC(只有CMS的concurrent collection是这个模式)
  3. Mixed GC:收集整个young gen以及部分old gen的GC(只有G1有这个模式)

Full GC

Full GC定义是相对明确的,就是针对整个新生代、老生代、元空间(metaspace,java8以上版本取代perm gen)的全局范围的GC。

以上就是Minor GC、Major GC、Full GC的一个介绍,后续我们再重点介绍JVM GC相关的垃圾回收算法以及底层的实现。

高并发用户登录会有缓存穿透问题吗

Spring中BeanFactory和FactoryBean的区别

  • BeanFactory是Spring框架的核心接口之一,用于管理和获取应用程序中的bean实例。它是Factory模式的实现,负责创建、配置和管理Bean对象。BeanFactory是Spring IoC容器的基础,它从配置元数据(例如XML文件)中读取Bean定义并实例化和交付那些需要的时候吃豆子。
  • FactoryBean是一个特殊的bean,它是一个工厂对象,用于创建和管理其他bean的实例;FactoryBean接口定义了一种创建bean的方法,允许开发人员在bean创建过程中进行更多的定制。通过实现FactoryBean接口,开发人员可以创建复杂的bean实例,或者在实例化bean之前执行一些额外的逻辑处理。

区别在于,BeanFactory是Spring框架的核心接口,用于管理和提供bean的实例,而FactoryBean是一种特殊的bean,用于创建和管理其他bean的实例。FactoryBean在创建bean的过程中提供了更多的自定义功能,允许额外的逻辑处理。

MQ实现分布式事务

在分布式事务的实现中,ACK消息和半消息是两种实现分布式事务的两种不同机制,它们的共同点是都使用了两阶段提交的机制,但是它们的实现细节和适用场景有所不同。具有一些区别和特点。下面是ACK消息和半消息在实现分布式事务方面的比较:

ACK消息(Acknowledgement):

  • ACK消息是指在分布式事务中,消息的接收者(消费者)在成功处理消息后发送ACK(确认)给消息的发送者(生产者)。
  • 生产者发送消息后,需要等待消费者发送ACK以确认消息已被正确处理。
  • 如果消息未收到ACK确认,生产者可以选择重新发送消息,以确保消息的可靠性。
  • ACK消息的机制能够确保消息的可靠性和一致性,但需要依赖消费者的可用性和稳定性。

使用 ACK 消息实现分布式事务时,需要将发送消息的操作包含在本地事务中,并在本地事务提交后发送 ACK 消息来确认消息的发送。这种方式可以确保发送消息与本地事务的原子性,从而实现分布式事务的一致性和可靠性。

半消息(Half-Message):

  • 半消息是一种通过消息队列实现分布式事务的机制,通常使用消息队列的事务功能或消息可靠性机制来实现。
  • 在半消息中,事务的操作被拆分为两个步骤:发送半消息和确认半消息。
  • 发送半消息时,消息被发送到消息队列,但不会立即被消费。此时消息处于待确认状态。
  • 在执行本地事务逻辑后,如果成功,发送确认消息,消息队列将正式将半消息标记为可消费的状态,最终被消费者消费。
  • 如果本地事务逻辑失败,可以选择不发送确认消息,使半消息超时或被丢弃,实现事务的回滚操作。
  • 半消息的机制能够确保分布式事务的可靠性和一致性,不需要依赖消费者的可用性。

在比较上述两种机制时,关键点在于可靠性和一致性的保证。ACK消息通过消费者的ACK确认实现可靠性,但依赖于消费者的可用性和稳定性。半消息通过消息队列的事务或可靠性机制实现可靠性和一致性,不依赖于消费者的确认。同时,半消息机制还可以支持分布式事务的回滚操作。

具体选择使用ACK消息还是半消息取决于应用场景的需求和特点。需要综合考虑消息可靠性、性能、消费者的可用性要求以及事务回滚的需求等因素。

image-20240510101347827

JVM调优命令

image-20240904111413806

JVM参数解析及调优

image-20240912215748482

索引

image-20240904152848518

重定向和转发的区别

img

volatile 如何实现变量可见性

在这里插入图片描述

1️⃣在生成最低成汇编指令时,对volatile修饰的共享变量写操作增加Lock前缀指令,Lock 前缀的指令会引起 CPU 缓存写回内存;
2️⃣CPU 的缓存回写到内存会导致其他 CPU 缓存了该内存地址的数据无效;
3️⃣volatile 变量通过缓存一致性协议保证每个线程获得最新值;
4️⃣缓存一致性协议保证每个 CPU 通过嗅探在总线上传播的数据来检查自己缓存的值是不是修改;
5️⃣当 CPU 发现自己缓存行对应的内存地址被修改,会将当前 CPU 的缓存行设置成无效状态,重新从内存中把数据读到 CPU 缓存。

JAVA 内存模型

JMM是一组规范,需要各个JVM的实现来遵守JMM规范,以便于开发者可以利用这些规范,更方便地开发多线程程序。 JMM最重要的3点内容是:原子性,有序性,可见性,然后讲讲这几个的意思每个线程有自己的工作内存,线程之间共享进程的内存。讲一讲volatile,synchronized,原子性,有序性,可见性

慢查询定位总结

1、介绍一下当时产生问题的场景(我们当时的一个接口测试的时候非常的慢,压测的结果大概5秒钟) 2、我们系统中当时采用了运维工具( Skywalking ),可以监测出哪个接口,最终因为是sql的问题 3、在mysql中开启了慢日志查询,我们设置的值就是2秒,一旦sql执行超过2秒就会记录到日志中(调试阶段) 4、上线前的压力测试,观察相关的指标 tps / qps / rt / io / cpu

总结: 慢 SQL 处理思路

发现: 系统监控:基于 arthars 运维工具、promethues 、skywalking mysql 慢日志: 会影响一部分系统性能 慢 SQL 处理 由宏观到微观 1、检查 系统相关性能 top / mpstat / pidstat / vmstat 检查 CPU / IO 占比,由进程到线程 2、检查 MySQL 性能情况,show processlist 查看相关进程 | 基于 MySQL Workbench 进行分析 3、检查 SQL 语句索引执行情况,explain 关注 type key extra rows 等关键字 4、检查是否由于 SQL 编写不当造成的不走索引,例如 使用函数、not in、%like%、or 等 5、其他情况: 深分页、数据字段查询过多、Join 连表不当、业务大事物问题、死锁、索引建立过多 6、对于热点数据进行前置,降低 MySQL 的压力 redis、mongodb、memory 7、更改 MySQL 配置 , 处理线程设置一般是 cpu * 核心数 的 2 倍,日志大小 bin-log 8、升级机器性能 or 加机器 9、分表策略,分库策略 10、数据归档

项目


项目12306:
讲一下你这个系统就是怎么处理高并发
布隆过滤器怎么实现平滑上线(历史数据迁移)
并发抢票库存如何设计的
令牌容器存储的什么数据结构?value直接自减吗?如果减完了用户又取消订单怎么办?减完了数据库宕机了怎么办?

下订单和消息队列修改保证一致性?

回滚回滚的是什么

先操作缓存还是先操作数据库

压力测试

项目流程图概念图

.
├── aggregation-service || – # 聚合服务
├── gateway-service || – # 网关服务
├── order-service || – # 订单服务
├── pay-service || – # 支付服务
├── ticket-service || – # 购票服务
└── user-service || – # 用户服务

会员相关核心数据库表如下:

  • t_user 会员数据表:存储会员账号、密码、证件号、邮箱、手机号等信息
  • t_user_mail 会员邮箱数据表:存储会员邮箱和用户名的关系
  • t_user_phone 会员手机号数据表:存储会员手机号和用户名的关系

用户相关扩展功能表如下:

  • t_user_reuse 用户名可复用表:存储已被注销的可用用户名
  • t_user_deletion:用户证件号注销表:存储被注销过得证件号记录数据

加锁粒度:

令牌桶: key trainid value:map {key :起点终点座位类型 value: 座位余量}

本地锁和分布式锁结构:前缀puchase_ticket_%s%d 后缀 :车次ID+座位类型

  • 什么情况下需要分库分表?

    遇到下面几种场景可以考虑分库分表:

    • 单表的数据达到千万级别以上,数据库读写速度比较缓慢。
    • 数据库中的数据占用的空间越来越大,备份时间越来越长。
    • 应用的并发量太大(应该优先考虑其他性能优化方法,而非分库分表)

    为什么读写缓慢响应时间长

    分布式锁如何保证不会释放到其他线程锁,如何续期

题目


56. 合并区间

1312. 让字符串成为回文串的最少插入次数

215. 数组中的第K个最大元素

124. 二叉树中的最大路径和

25. K 个一组翻转链表

93. 复原 IP 地址

287. 寻找重复数

  1. 小红拿到了一个无向图,其中一些边被染成了红色。
    小红定义一个点是“好点”,当且仅当这个点的所有邻边都是红边。
    现在请你求出这个无向图“好点”的数量。
    注:如果一个节点没有任何邻边,那么它也是好点。

  2. 红拿到了一个字符矩阵,她可以从任意一个地方出发,希望走 6 步后恰好形成"tencent"字符串。小红想知道,共有多少种不同的行走方案?
    注:每一步可以选择上、下、左、右中任意一个方向进行行走。不可行走到矩阵外部。

  3. 小红拿到了一个有 n 个节点的无向图,这个图初始并不是连通图。
    现在小红想知道,添加恰好一条边使得这个图连通,有多少种不同的加边方案

  4. 小红拿到了一个数组,她准备将数组分割成 k 段,使得每段内部做按位异或后,再全部求和。小红希望最终这个和尽可能大,你能帮帮她吗?

    此题dp动态规划,dp[i] [j]代表 长度i 数组砍j 刀时最大值 dp[i] [j]等于遍历从当前i拆分到0时的最大左右两个前缀与的和

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public long search(int n,int k,int a[]){
long[][]f=new long[n+1][k+1];
for (int i = 0; i <=n; i++) {
for (int j = 0; j <=k; j++) {
f[i][j]=(long)-1e8;
}
}
f[0][0]=0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < k; j++) {
int xor=0;
for (int l = i; l >=0 ; l++) {
xor ^= a[l];
f[i + 1][j + 1] = Math.max(f[i + 1][j + 1], xor + f[l][j]);
}
}
}
return f[n][k];
}

179. 最大数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public String largestNumber(int[] nums) {
int n = nums.length;
String[] ss = new String[n];
for (int i = 0; i < n; i++) ss[i] = "" + nums[i];
Arrays.sort(ss, (a, b) -> {
String sa = a + b, sb = b + a ;
return sb.compareTo(sa);
});

StringBuilder sb = new StringBuilder();
for (String s : ss) sb.append(s);
int len = sb.length();
int k = 0;
while (k < len - 1 && sb.charAt(k) == '0') k++;
return sb.substring(k);
}

165. 比较版本号

7. 整数反转

手写单例模式


(一)懒汉式(线程不安全)

实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton {
private static Singleton uniqueInstance;

private Singleton() {

}

public static Singleton getUniqueInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}

说明: 先不创建实例,当第一次被调用时,再创建实例,所以被称为懒汉式。

优点: 延迟了实例化,如果不需要使用该类,就不会被实例化,节约了系统资源。

缺点: 线程不安全,多线程环境下,如果多个线程同时进入了 if (uniqueInstance == null) ,若此时还未实例化,也就是uniqueInstance == null,那么就会有多个线程执行 uniqueInstance = new Singleton(); ,就会实例化多个实例;

(二)饿汉式(线程安全)

实现:

1
2
3
4
5
6
7
8
9
10
11
12
public class Singleton {

private static Singleton uniqueInstance = new Singleton();

private Singleton() {
}

public static Singleton getUniqueInstance() {
return uniqueInstance;
}

}

说明: 先不管需不需要使用这个实例,直接先实例化好实例 (饿死鬼一样,所以称为饿汉式),然后当需要使用的时候,直接调方法就可以使用了。

优点: 提前实例化好了一个实例,避免了线程不安全问题的出现。

缺点: 直接实例化好了实例,不再延迟实例化;若系统没有使用这个实例,或者系统运行很久之后才需要使用这个实例,都会操作系统的资源浪费。

(三)懒汉式(线程安全)

实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton {
private static Singleton uniqueInstance;

private static singleton() {
}

private static synchronized Singleton getUinqueInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}

}

说明: 实现和 线程不安全的懒汉式 几乎一样,唯一不同的点是,在get方法上 加了一把 锁。如此一来,多个线程访问,每次只有拿到锁的的线程能够进入该方法,避免了多线程不安全问题的出现。

优点: 延迟实例化,节约了资源,并且是线程安全的。

缺点: 虽然解决了线程安全问题,但是性能降低了。因为,即使实例已经实例化了,既后续不会再出现线程安全问题了,但是锁还在,每次还是只能拿到锁的线程进入该方法,会使线程阻塞,等待时间过长。

四)双重检查锁实现(线程安全)

实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Singleton {

private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
if (uniqueInstance == null) {
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}

为什么使用 volatile 关键字修饰了 uniqueInstance 实例变量

uniqueInstance = new Singleton(); 这段代码执行时分为三步:

  1. 为 uniqueInstance 分配内存空间
  2. 初始化 uniqueInstance
  3. 将 uniqueInstance 指向分配的内存地址

正常的执行顺序当然是 1>2>3 ,但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1>3>2。 单线程环境时,指令重排并没有什么问题;多线程环境时,会导致有些线程可能会获取到还没初始化的实例。 例如:线程A 只执行了 1 和 3 ,此时线程B来调用 getUniqueInstance(),发现 uniqueInstance 不为空,便获取 uniqueInstance 实例,但是其实此时的 uniqueInstance 还没有初始化。

解决办法就是加一个 volatile 关键字修饰 uniqueInstance ,volatile 会禁止 JVM 的指令重排,就可以保证多线程环境下的安全运行。

优点: 延迟实例化,节约了资源;线程安全;并且相对于 线程安全的懒汉式,性能提高了。

缺点: volatile 关键字,对性能也有一些影响。

(五)静态内部类实现(线程安全)

实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton {

private Singleton() {
}

private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}

public static Singleton getUniqueInstance() {
return SingletonHolder.INSTANCE;
}

}

说明: 首先,当外部类 Singleton 被加载时,静态内部类 SingletonHolder 并没有被加载进内存。当调用 getUniqueInstance() 方法时,会运行 return SingletonHolder.INSTANCE; ,触发了 SingletonHolder.INSTANCE ,此时静态内部类 SingletonHolder 才会被加载进内存,并且初始化 INSTANCE 实例,而且 JVM 会确保 INSTANCE 只被实例化一次。

优点: 延迟实例化,节约了资源;且线程安全;性能也提高了。

(六)枚举类实现(线程安全)

实现:

1
2
3
4
5
6
7
8
9
10
public enum Singleton {

INSTANCE;

//添加自己需要的操作
public void doSomeThing() {

}

}

说明: 默认枚举实例的创建就是线程安全的,且在任何情况下都是单例。

优点: 写法简单,线程安全,天然防止反射和反序列化调用。

  • 防止反序列化****序列化:把java对象转换为字节序列的过程; 反序列化: 通过这些字节序列在内存中新建java对象的过程; 说明: 反序列化 将一个单例实例对象写到磁盘再读回来,从而获得了一个新的实例。 我们要防止反序列化,避免得到多个实例。 枚举类天然防止反序列化。 其他单例模式 可以通过 重写 readResolve() 方法,从而防止反序列化,使实例唯一重写 readResolve() :
1
2
3
private Object readResolve() throws ObjectStreamException{
return singleton;
}

生产者-消费者模式

1. wait() / notify()方法

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
public class Storage {

// 仓库容量
private final int MAX_SIZE = 10;
// 仓库存储的载体
private LinkedList<Object> list = new LinkedList<>();

public void produce() {
synchronized (list) {
while (list.size() + 1 > MAX_SIZE) {
System.out.println("【生产者" + Thread.currentThread().getName()
+ "】仓库已满");
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.add(new Object());
System.out.println("【生产者" + Thread.currentThread().getName()
+ "】生产一个产品,现库存" + list.size());
list.notifyAll();
}
}

public void consume() {
synchronized (list) {
while (list.size() == 0) {
System.out.println("【消费者" + Thread.currentThread().getName()
+ "】仓库为空");
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.remove();
System.out.println("【消费者" + Thread.currentThread().getName()
+ "】消费一个产品,现库存" + list.size());
list.notifyAll();
}
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Producer implements Runnable {
private Storage storage;

public Producer(){}

public Producer(Storage storage){
this.storage = storage;
}

@Override
public void run(){
while(true){
try{
Thread.sleep(1000);
storage.produce();
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Consumer implements Runnable{
private Storage storage;

public Consumer(){}

public Consumer(Storage storage){
this.storage = storage;
}

@Override
public void run(){
while(true){
try{
Thread.sleep(3000);
storage.consume();
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}

2.await() / signal()方法

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
import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Storage {

// 仓库最大存储量
private final int MAX_SIZE = 10;
// 仓库存储的载体
private LinkedList<Object> list = new LinkedList<Object>();
// 锁
private final Lock lock = new ReentrantLock();
// 仓库满的条件变量
private final Condition full = lock.newCondition();
// 仓库空的条件变量
private final Condition empty = lock.newCondition();

public void produce() {
lock.lock();
try {
while (list.size() >= MAX_SIZE) {
System.out.println("【生产者" + Thread.currentThread().getName() + "】仓库已满");
try {
full.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 重置中断状态
}
}
list.add(new Object());
System.out.println("【生产者" + Thread.currentThread().getName() + "】生产一个产品,现库存" + list.size());
empty.signal(); // 只唤醒一个等待的消费者
} finally {
lock.unlock();
}
}

public void consume() {
lock.lock();
try {
while (list.isEmpty()) {
System.out.println("【消费者" + Thread.currentThread().getName() + "】仓库为空");
try {
empty.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 重置中断状态
}
}
list.remove();
System.out.println("【消费者" + Thread.currentThread().getName() + "】消费一个产品,现库存" + list.size());
full.signal(); // 只唤醒一个等待的生产者
} finally {
lock.unlock();
}
}
}

多线程交叉打印数字

个线程分别打印 A,B,C,要求这三个线程一起运行,打印 n 次,输出形如“ABCABCABC….”的字符串。

使用 Lock
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
public class PrintABCUsingLock {

private int times; // 控制打印次数
private int state; // 当前状态值:保证三个线程之间交替打印
private Lock lock = new ReentrantLock();

public PrintABCUsingLock(int times) {
this.times = times;
}

private void printLetter(String name, int targetNum) {
for (int i = 0; i < times; ) {
lock.lock();
if (state % 3 == targetNum) {
state++;
i++;
System.out.print(name);
}
lock.unlock();
}
}

public static void main(String[] args) {
PrintABCUsingLock loopThread = new PrintABCUsingLock(1);

new Thread(() -> {
loopThread.printLetter("B", 1);
}, "B").start();

new Thread(() -> {
loopThread.printLetter("A", 0);
}, "A").start();

new Thread(() -> {
loopThread.printLetter("C", 2);
}, "C").start();
}
}

main 方法启动后,3 个线程会抢锁,但是 state 的初始值为 0,所以第一次执行 if 语句的内容只能是 线程 A,然后还在 for 循环之内,此时state = 1,只有 线程 B 才满足 1% 3 == 1,所以第二个执行的是 B,同理只有 线程 C 才满足 2% 3 == 2,所以第三个执行的是 C,执行完 ABC 之后,才去执行第二次 for 循环,所以要把 i++ 写在 for 循环里边,不能写成 for (int i = 0; i < times;i++) 这样。

使用 wait/notify
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
public class PrintABCUsingWaitNotify {
private int state;
private int times;
private static final Object LOCK = new Object();

public PrintABCUsingWaitNotify(int times) {
this.times = times;
}

public static void main(String[] args) {
PrintABCUsingWaitNotify printABC = new PrintABCUsingWaitNotify(10);
new Thread(() -> {
printABC.printLetter("A", 0);
}, "A").start();
new Thread(() -> {
printABC.printLetter("B", 1);
}, "B").start();
new Thread(() -> {
printABC.printLetter("C", 2);
}, "C").start();
}

private void printLetter(String name, int targetState) {
for (int i = 0; i < times; i++) {
synchronized (LOCK) {
while (state % 3 != targetState) {
try {
LOCK.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
state++;
System.out.print(name);
LOCK.notifyAll();
}
}
}
}
第 2 题:两个线程交替打印奇数和偶数
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
public class OddEvenPrinter {

private Object monitor = new Object();
private final int limit;
private volatile int count;

OddEvenPrinter(int initCount, int times) {
this.count = initCount;
this.limit = times;
}

public static void main(String[] args) {

OddEvenPrinter printer = new OddEvenPrinter(0, 10);
new Thread(printer::print, "odd").start();
new Thread(printer::print, "even").start();
}

private void print() {
synchronized (monitor) {
while (count < limit) {
try {
System.out.println(String.format("线程[%s]打印数字:%d", Thread.currentThread().getName(), ++count));
monitor.notifyAll();
monitor.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//防止有子线程被阻塞未被唤醒,导致主线程不退出
monitor.notifyAll();
}
}
}
用两个线程,一个输出字母,一个输出数字,交替输出 1A2B3C4D…26Z
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
public class ThreadDemo2 {
private static final String[] charArr = new String[]{"A","B","C","D","E","F","G","H","I","J","K","L","M","N",
"O","P","Q","R","S","T","U","V","W","X","Y","Z"};
private static final int[] numArr = new int[]{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};
private static Thread thread1 = null;
private static Thread thread2 = null;


public static void main(String[] args) throws Exception {
Object o = new Object();
thread1 = new Thread(()->{
for (String c : charArr) {
synchronized (o) {
// 只有拿到o这把锁才可以打印,由于先启动线程1,所以线程1先拿到这把锁
System.out.print(c);
try {
// 唤醒任意一个线程,让它去竞争锁
o.notify();
// 释放锁
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});

thread2 = new Thread(()->{
for (int n : numArr) {
synchronized (o) {
// 当第一个线程释放锁后 线程2拿到锁
System.out.print(n);
try {
// 叫醒队列里任意一个线程去竞争锁
o.notify();
// 如果是最后一个元素就不用释放锁去排队了
if (n != numArr.length) {
// 释放锁
o.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});

thread1.start();
thread2.start();
}
}

使用 Semaphore
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
public class LoopPrinter {

private final static int THREAD_COUNT = 3;
static int result = 0;
static int maxNum = 10;

public static void main(String[] args) throws InterruptedException {
final Semaphore[] semaphores = new Semaphore[THREAD_COUNT];
for (int i = 0; i < THREAD_COUNT; i++) {
//非公平信号量,每个信号量初始计数都为1
semaphores[i] = new Semaphore(1);
if (i != THREAD_COUNT - 1) {
System.out.println(i+"==="+semaphores[i].getQueueLength());
//获取一个许可前线程将一直阻塞, for 循环之后只有 syncObjects[2] 没有被阻塞
semaphores[i].acquire();
}
}
for (int i = 0; i < THREAD_COUNT; i++) {
// 初次执行,上一个信号量是 syncObjects[2]
final Semaphore lastSemphore = i == 0 ? semaphores[THREAD_COUNT - 1] : semaphores[i - 1];
final Semaphore currentSemphore = semaphores[i];
final int index = i;
new Thread(() -> {
try {
while (true) {
// 初次执行,让第一个 for 循环没有阻塞的 syncObjects[2] 先获得令牌阻塞了
lastSemphore.acquire();
System.out.println("thread" + index + ": " + result++);
if (result > maxNum) {
System.exit(0);
}
// 释放当前的信号量,syncObjects[0] 信号量此时为 1,下次 for 循环中上一个信号量即为syncObjects[0]
currentSemphore.release();
}
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
}
使用 LockSupport

用两个线程,一个输出字母,一个输出数字,交替输出 1A2B3C4D…26Z

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class NumAndLetterPrinter {

private static Thread numThread, letterThread;

public static void main(String[] args) {
letterThread = new Thread(() -> {
for (int i = 0; i < 26; i++) {
System.out.print((char) ('A' + i));
LockSupport.unpark(numThread);
LockSupport.park();
}
}, "letterThread");

numThread = new Thread(() -> {
for (int i = 1; i <= 26; i++) {
System.out.print(i);
LockSupport.park();
LockSupport.unpark(letterThread);
}
}, "numThread");
numThread.start();
letterThread.start();
}
}

代码实现堆溢出,栈溢出,元空间溢出

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
//堆溢出
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
int i=0;
while(true){
list.add(new byte[5*1024*1024]);
System.out.println("分配次数:"+(++i));
}
}

//栈溢出
public class StackOverflowDemo {
public static void main(String[] args) {
recursiveCall(); // 递归调用
}

public static void recursiveCall() {
recursiveCall(); // 不断递归,直到栈空间耗尽
}
}
//元空间(永久代)溢出
public class MetaspaceOverflowDemo {
static class OOMTest {}

public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMTest.class);
enhancer.setUseCache(false);
enhancer.setCallback((MethodInterceptor) (obj, method, args1, proxy) -> proxy.invokeSuper(obj, args1));
enhancer.create(); // 不断生成新的类
}
}
}
//直接内存溢出
public class DirectMemoryOverflowDemo {
public static void main(String[] args) {
while (true) {
ByteBuffer.allocateDirect(1 * 1024 * 1024); // 每次分配1MB直接内存
}
}
}

死锁案例


复制代码

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
 1 public class DeadLockDemo extends Thread{
2
3 String lockA ;
4 String lockB;
5 public DeadLockDemo(String name,String lockA,String lockB){
6 super(name);
7 this.lockA = lockA;
8 this.lockB = lockB;
9 }
10
11 public void run() {
12 synchronized (lockA){
13 System.out.println(Thread.currentThread().getName() + "拿到了" + lockA + ",等待拿到" + lockB);
14 try {
15 Thread.sleep(1000);
16 synchronized (lockB){
17 System.out.println(Thread.currentThread().getName() + "拿到了" + lockB);
18 }
19 } catch (InterruptedException e) {
20 e.printStackTrace();
21 }
22
23 }
24 }
25
26 public static void main(String[] args){
27 String lockA = "lockA";
28 String lockB = "lockB";
29 DeadLockDemo threadA = new DeadLockDemo("ThreadA", lockA, lockB);
30 DeadLockDemo threadB = new DeadLockDemo("ThreadB", lockB, lockA);
31 threadA.start();
32 threadB.start();
33 try {
34 threadA.join();
35 threadB.join();
36 } catch (InterruptedException e) {
37 e.printStackTrace();
38 }
39 }
40 }

list和int[],Integer[]互转


一、Integer[]与ArrayList的互转


1. Integer[]转ArrayList

(1) 方法一:

利用Arrays工具类中的asList方法

1
2
Integer[] arr = {1,2,3};
ArrayList<Integer> list = new ArrayList<>(Arrays.asList(arr));

(2) 方法二:

利用Collections工具类中的addAll方法

1
2
3
Integer[] arr = {1,2,3};
ArrayList<Integer> list = new ArrayList<>(array.length);
Collections.addAll(list, arr);

(3) 注意:
Java中集合只能存放引用数据类型,在使用asListaddAll方法时,被转换的数组必须是存放引用数据类型的数组,如果是基本数据类型数组请在转换前先把其转换为对应的包装类型数组,下面会介绍。

2. ArrayList转Integer[]
1
2
ArrayList<Integer> list = new ArrayList<>();
Integer[] arr = list.toArray(new Integer[0]);

二、Integer[]与int[]互转


1. Integer[]转int[]
1
2
Integer[] arr1 = {1,2,3};
int[] arr2 = Arrays.stream(arr1).mapToInt(Integer::valueOf).toArray();
2. int[]转Integer[]
1
2
int[] arr1 = {1,2,3};
Integer[] arr2 = Arrays.stream(arr1).boxed().toArray(Integer[]::new);

三、int[]与ArrayList的互转


1. int[]转ArrayList
1
2
int[] arr = {1,2,3};
List<Integer> list = Arrays.stream(arr).boxed().collect(Collectors.toList());
2. ArrayList转int[]
1
2
ArrayList<Integer> list = new ArrayList<>();
int[] arr = list.stream().mapToInt(Integer::valueOf).toArray();

MySQL 语句


正则表达式提供各种功能,以下是一些相关功能:

^:表示一个字符串或行的开头

[a-z]:表示一个字符范围,匹配从 a 到 z 的任何字符。

[0-9]:表示一个字符范围,匹配从 0 到 9 的任何字符。

[a-zA-Z]:这个变量匹配从 a 到 z 或 A 到 Z 的任何字符。请注意,你可以在方括号内指定的字符范围的数量没有限制,您可以添加想要匹配的其他字符或范围。

[^a-z]:这个变量匹配不在 a 到 z 范围内的任何字符。请注意,字符 ^ 用来否定字符范围,它在方括号内的含义与它的方括号外表示开始的含义不同。

[a-z]*:表示一个字符范围,匹配从 a 到 z 的任何字符 0 次或多次。

[a-z]+:表示一个字符范围,匹配从 a 到 z 的任何字符 1 次或多次。

.:匹配任意一个字符。

.:表示句点字符。请注意,反斜杠用于转义句点字符,因为句点字符在正则表达式中具有特殊含义。还要注意,在许多语言中,你需要转义反斜杠本身,因此需要使用\.。

$:表示一个字符串或行的结尾。

LRU

We will used the doubly-linked list to build the LRU Dlink will have the prev pointer and the next pointer

the key :when we put new element into LRU we need to check whether we should remove the less used element, the key is used to use the method map.remove(redn.key) and update the hashmap

the value: use it to store the value;

the map : used to find the Dlinknode to choose update or delete the Dlinknode;

important method: addtohead removenode

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
class DLinkedNode {
int key;
int value;
DLinkedNode prev;
DLinkedNode next;
public DLinkedNode() {}
public DLinkedNode(int _key, int _value) {key = _key; value = _value;}
}
Map<Integer,DLinkedNode> map=new HashMap<>();
private int size;
private int capacity;
private DLinkedNode head, tail;
public LRUCache(int capacity) {
this.capacity=capacity;
this.size=0;
head=new DLinkedNode();
tail=new DLinkedNode();
head.next=tail;
tail.prev=head;
}

public int get(int key) {
DLinkedNode dn = map.get(key);
if (dn == null) {
return -1;
}
moveToHead(dn);
return dn.value;
}

public void put(int key, int value) {
DLinkedNode dn=map.get(key);
if(dn==null){
DLinkedNode node=new DLinkedNode(key,value);
addToHead(node);
map.put(key, node);
size++;
if(size>capacity) {
DLinkedNode redn = removeTail();
map.remove(redn.key);
size--;
}
}
else {
dn.value=value;
moveToHead(dn);
}

}

private void addToHead(DLinkedNode node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}

private void removeNode(DLinkedNode node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}

private void moveToHead(DLinkedNode node) {
removeNode(node);
addToHead(node);
}

private DLinkedNode removeTail() {
DLinkedNode res = tail.prev;
removeNode(res);
return res;
}

With the time expiring requirement

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import java.util.HashMap;
import java.util.Map;

// 定义双向链表节点
class Node<K, V> {
K key;
V value;
long expireTime;
Node<K, V> prev;
Node<K, V> next;

public Node(K key, V value, long expireTime) {
this.key = key;
this.value = value;
this.expireTime = expireTime;
}
}

// 实现带过期时间的LRU缓存
public class LRUCacheWithExpiration<K, V> {
private final int capacity;
private final Map<K, Node<K, V>> cache;
private final Node<K, V> head; // 最近访问的节点(头部)
private final Node<K, V> tail; // 最久未访问的节点(尾部)

public LRUCacheWithExpiration(int capacity) {
this.capacity = capacity;
this.cache = new HashMap<>();
this.head = new Node<>(null, null, 0);
this.tail = new Node<>(null, null, 0);
this.head.next = this.tail;
this.tail.prev = this.head;
}

public V get(K key) {
Node<K, V> node = cache.get(key);
if (node != null) {
// 检查是否过期
if (node.expireTime < System.currentTimeMillis()) {
removeNode(node);
cache.remove(key);
return null;
}
// 移动节点到头部(表示最近使用)
moveToHead(node);
return node.value;
}
return null;
}

public void put(K key, V value, long expireTimeMillis) {
// 如果 key 已存在,则更新 value 和过期时间
if (cache.containsKey(key)) {
Node<K, V> node = cache.get(key);
node.value = value;
node.expireTime = System.currentTimeMillis() + expireTimeMillis;
moveToHead(node);
} else {
// 新节点插入到头部
Node<K, V> newNode = new Node<>(key, value, System.currentTimeMillis() + expireTimeMillis);
cache.put(key, newNode);
addToHead(newNode);

// 如果超过容量,删除尾部节点
if (cache.size() > capacity) {
Node<K, V> tailNode = removeTail();
cache.remove(tailNode.key);
}
}
}

private void addToHead(Node<K, V> node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}

private void removeNode(Node<K, V> node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}

private Node<K, V> removeTail() {
Node<K, V> tailNode = tail.prev;
removeNode(tailNode);
return tailNode;
}

private void moveToHead(Node<K, V> node) {
removeNode(node);
addToHead(node);
}
}



public void put(K key, V value, long expireTimeMillis) {
// 如果 key 已存在,则更新 value 和过期时间
if (cache.containsKey(key)) {
Node<K, V> node = cache.get(key);
node.value = value;
node.expireTime = System.currentTimeMillis() + expireTimeMillis;
moveToHead(node);
} else {
// 新节点插入到头部
Node<K, V> newNode = new Node<>(key, value, System.currentTimeMillis() + expireTimeMillis);
cache.put(key, newNode);
addToHead(newNode);

// 如果超过容量,删除尾部节点
if (cache.size() > capacity) {
removeExpiredNodes();
if (cache.size() > capacity) {
Node<K, V> tailNode = removeTail();
cache.remove(tailNode.key);
}
}
}
}

private void removeExpiredNodes() {
Node<K, V> node = tail.prev;
while (node != head && node.expireTime < System.currentTimeMillis()) {
removeNode(node);
cache.remove(node.key);
node = tail.prev;
}
}

Kruskal

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
//1584. Min Cost to Connect All Points  https://leetcode.com/problems/min-cost-to-connect-all-points/
public int minCostConnectPoints(int[][] points) {
int n = points.length;
DisjointSetUnion dsu = new DisjointSetUnion(n);
List<Edge> edges = new ArrayList<Edge>();
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
edges.add(new Edge(dist(points, i, j), i, j));
}
}
Collections.sort(edges, new Comparator<Edge>() {
public int compare(Edge edge1, Edge edge2) {
return edge1.len - edge2.len;
}
});
int ret = 0, num = 1;
for (Edge edge : edges) {
int len = edge.len, x = edge.x, y = edge.y;
if (dsu.unionSet(x, y)) {
ret += len;
num++;
if (num == n) {
break;
}
}
}
return ret;
}
public int dist(int[][] points, int x, int y) {
return Math.abs(points[x][0] - points[y][0]) + Math.abs(points[x][1] - points[y][1]);
}

class DisjointSetUnion {
int[] f;
int[] rank;
int n;

public DisjointSetUnion(int n) {
this.n = n;
this.rank = new int[n];
Arrays.fill(this.rank, 1);
this.f = new int[n];
for (int i = 0; i < n; i++) {
this.f[i] = i;
}
}

public int find(int x) {
return f[x] == x ? x : (f[x] = find(f[x]));
}

public boolean unionSet(int x, int y) {
int fx = find(x), fy = find(y);
if (fx == fy) {
return false;
}
if (rank[fx] < rank[fy]) {
int temp = fx;
fx = fy;
fy = temp;
}
rank[fx] += rank[fy];
f[fy] = fx;
return true;
}
}
class Edge {
int len, x, y;

public Edge(int len, int x, int y) {
this.len = len;
this.x = x;
this.y = y;
}
}

Dijkstra

这里写图片描述

这里写图片描述

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
//743. Network Delay Time  https://leetcode.com/problems/network-delay-time/
public int networkDelayTime(int[][] times, int n, int k) {
final int INF = Integer.MAX_VALUE / 2;
int[][] g = new int[n][n];
for (int i = 0; i < n; ++i) {
Arrays.fill(g[i], INF);
}
for (int[] t : times) {
int x = t[0] - 1, y = t[1] - 1;
g[x][y] = t[2];
}
int[] dist = new int[n];
Arrays.fill(dist, INF);
dist[k - 1] = 0;
boolean[] used = new boolean[n];
for (int i = 0; i < n; i++) {
int x=-1;
for (int y = 0; y < n; y++) {
if(!used[y]&&(x==-1||dist[y]<dist[x])){
x=y;
}
}
used[x]=true;
for (int y = 0; y < n; y++) {
dist[y]=Math.min(dist[y],dist[x]+g[x][y]);
}
}
int ans = Arrays.stream(dist).max().getAsInt();
return ans == INF ? -1 : ans;
}

1976. 到达目的地的方案数

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
public int countPaths(int n, int[][] roads) {
int mod = 1000000007;
List<int[]>[] e = new List[n];
for (int i = 0; i < n; i++) {
e[i] = new ArrayList<int[]>();
}
for (int[] road : roads) {
int x = road[0], y = road[1], t = road[2];
e[x].add(new int[]{y, t});
e[y].add(new int[]{x, t});
}
long[] dis = new long[n];
Arrays.fill(dis, Long.MAX_VALUE);
int[] ways = new int[n];

PriorityQueue<long[]> pq = new PriorityQueue<long[]>((a, b) -> Long.compare(a[0], b[0]));
pq.offer(new long[]{0, 0});
dis[0] = 0;
ways[0] = 1;

while (!pq.isEmpty()) {
long[] arr = pq.poll();
long t = arr[0];
int u = (int) arr[1];
if (t > dis[u]) {
continue;
}
for (int[] next : e[u]) {
int v = next[0], w = next[1];
if (t + w < dis[v]) {
dis[v] = t + w;
ways[v] = ways[u];
pq.offer(new long[]{t + w, v});
} else if (t + w == dis[v]) {
ways[v] = (ways[u] + ways[v]) % mod;
}
}
}
return ways[n - 1];
}

Floyd

画每个节点的距离表格

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
class Solution {
int N = 110, M = 6010;
// 邻接矩阵数组:w[a][b] = c 代表从 a 到 b 有权重为 c 的边
int[][] w = new int[N][N];
int INF = 0x3f3f3f3f;
int n, k;
public int networkDelayTime(int[][] ts, int _n, int _k) {
n = _n; k = _k;
// 初始化邻接矩阵
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
w[i][j] = w[j][i] = i == j ? 0 : INF;
}
}
// 存图
for (int[] t : ts) {
int u = t[0], v = t[1], c = t[2];
w[u][v] = c;
}
// 最短路
floyd();
// 遍历答案
int ans = 0;
for (int i = 1; i <= n; i++) {
ans = Math.max(ans, w[k][i]);
}
return ans >= INF / 2 ? -1 : ans;
}
void floyd() {
// floyd 基本流程为三层循环:
// 枚举中转点 - 枚举起点 - 枚举终点 - 松弛操作
for (int p = 1; p <= n; p++) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
w[i][j] = Math.min(w[i][j], w[i][p] + w[p][j]);
}
}
}
}
}

BellmanFord

Dijstra不能运用在含负权边的图

利用Dijstra算法,可以得到最短路径为:1 -> 2 -> 4 -> 5,因此算出最短路径为2+2+1 = 5。然而还存在一条路径即1 -> 3 -> 4 -> 5,他的最短路径长度为5+(-2)+1=4,因此Dijstra算法失效。

1
2
3
for n 次:
  for 所有边 a , b , w (松弛操作)
    dist[b] = min(dist[b], backup[a] + c)

下面有一个有边数限制的最短路问题:

给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。

请你求出从 1 号点到 n 号点的最多经过 k 条边的最短距离,如果无法从 1 号点走到 n 号点,输出 impossible。

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
57
58
59
60
61
62
63
64
65
66
67
68
import java.io.*;
import java.util.*;

class Main {
private static int N = 510;
private static int M = 10010;
private static int[] backup = new int[N]; // 用于备份之前迭代的dist数组
private static int[] dist = new int[N]; // 从1号点到 n号点的距离
private static Node[] list = new Node[M]; // 结构体
private static int n; // 总点数
private static int m; // 总边数
private static int k; // 最多经过k条边
private static int INF = 0x3f3f3f3f;

public static void main(String[] args) throws IOException {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
String[] str1 = bufferedReader.readLine().split(" ");
n = Integer.parseInt(str1[0]);
m = Integer.parseInt(str1[1]);
k = Integer.parseInt(str1[2]);

for (int i = 0; i < m; i ++) {
String[] str2 = bufferedReader.readLine().split(" ");
int x = Integer.parseInt(str2[0]);
int y = Integer.parseInt(str2[1]);
int z = Integer.parseInt(str2[2]);
list[i] = new Node(x, y, z);
}

bellman_ford();
}

public static void bellman_ford() {
Arrays.fill(dist, INF);
dist[1] = 0;

for (int i = 0; i < k; i ++) {
//备份dist数组
backup = Arrays.copyOf(dist, n + 1);
for (int j = 0; j < m; j ++) {
Node node = list[j];
int x = node.x;
int y = node.y;
int z = node.z;
dist[y] = Math.min(dist[y], backup[x] + z);
}
}

if (dist[n] > INF / 2) {
System.out.println("impossible");
} else {
System.out.println(dist[n]);
}
}
}

class Node
{
int x, y, z;
public Node(int x,int y,int z)
{
this.x = x;
this.y = y;
this.z = z;
}
}


dp题目

一、入门 DP

二、网格图 DP

问:如何思考循环顺序?什么时候要正序枚举,什么时候要倒序枚举?

答:这里有一个通用的做法:盯着状态转移方程,想一想,要计算 f[i] [j],必须先把 f[i+1] [⋅]算出来,那么只有 i 从大到小枚举才能做到。对于 j来说,由于在计算 f[i] [j]的时候, f[i+1] [⋅]已经全部计算完毕,所以 j无论是正序还是倒序枚举都可以。

2435. 矩阵中和能被 K 整除的路径

定义 f[i] [j] [v] 表示从左上走到 (i,j),且路径和模 k 的结果为 v 时的路径数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public int numberOfPaths(int[][] grid, int k) {
final int mod = (int) 1e9 + 7;
int m = grid.length, n = grid[0].length;
int[][][] f = new int[m + 1][n + 1][k];
f[0][1][0] = 1;
for(int i=0;i<m;i++){
for (int j = 0; j < n; j++) {
for(int v=0;v<k;v++){
f[i+1][j+1][(v+grid[i][j])%k]=(f[i + 1][j][v] + f[i][j + 1][v]) % mod;
}
}
}
return f[m][n][0];
}

174. 地下城游戏

一句话,本题的难点在于怎么处理血量增加的问题, 增加血量不能为之前的损失提供帮助,只会对后续有帮助。
这意味着从王子救“***”的思路想dp是困难的,但是“***”救王子的思路dp很好做,从后往前推,当前如果可以治愈,那么当前的最小初始血量就是已经扣除的血量减去治疗量,注意不可以<1。 这意味着过量治疗。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public int calculateMinimumHP(int[][] dungeon) {
int n = dungeon.length, m = dungeon[0].length;
int[][] dp = new int[n + 1][m + 1];
for (int i = 0; i <= n; ++i) {
Arrays.fill(dp[i], Integer.MAX_VALUE);
}
dp[n][m - 1] = dp[n - 1][m] = 1;
for (int i = n - 1; i >= 0; --i) {
for (int j = m - 1; j >= 0; --j) {
int minn = Math.min(dp[i + 1][j], dp[i][j + 1]);
dp[i][j] = Math.max(minn - dungeon[i][j], 1);
}
}
return dp[0][0];
}

741. 摘樱桃

image-20240428164330973

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
public int cherryPickup(int[][] grid) {
int N = 51, INF = Integer.MIN_VALUE;
int [][][]dp=new int[2*N][N][N];
int n=grid.length;
for (int k = 0; k <=2*n ; k++) {
for (int i=0;i<=n;i++){
for (int j = 0; j <=n; j++) {
dp[k][i][j]=INF;
}
}
}
dp[2][1][1]=grid[0][0];
for(int k=3;k<=2*n;k++){
for (int i1 = 1; i1 <= n; i1++) {
for (int i2 = 1; i2 <= n; i2++) {
int j1 = k - i1, j2 = k - i2;
if (j1 <= 0 || j1 > n || j2 <= 0 || j2 > n) continue;
int A=grid[i1-1][j1-1],B=grid[i2-1][j2-1];
if(A==-1|B==-1)continue;
int a = dp[k - 1][i1 - 1][i2], b = dp[k - 1][i1 - 1][i2 - 1], c = dp[k - 1][i1][i2 - 1], d = dp[k - 1][i1][i2];
int t = Math.max(Math.max(a, b), Math.max(c, d)) + A;
if (i1 != i2) t += B;
dp[k][i1][i2] = t;
}
}
}
return dp[2 * n][n][n] <= 0 ? 0 : dp[2 * n][n][n];
}

三、背包

01背包理论基础

动态规划-背包问题2

对于背包问题,有一种写法, 是使用二维数组,即dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少

1
状态转移方程 dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 可以看出i 是由 i-1 推导出来,那么i为0的时候就一定要初始化。
1
2
3
4
5
6
for(int i = 1; i < weight.size(); i++) { // 遍历物品
for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
if (j < weight[i]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}

动态规划-背包问题5

1
2
3
4
5
6
7
// weight数组的大小 就是物品个数
for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
for(int i = 1; i < weight.size(); i++) { // 遍历物品
if (j < weight[i]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}

先遍历背包再遍历物品

动态规划-背包问题6

一维dp数组(滚动数组)
1
2
3
4
5
6
7
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

}
}

这里大家发现和二维dp的写法中,遍历背包的顺序是不一样的!

二维dp遍历的时候,背包容量是从小到大,而一维dp遍历的时候,背包是从大到小。

为什么呢?

倒序遍历是为了保证物品i只被放入一次!。但如果一旦正序遍历了,那么物品0就会被重复加入多次!

再来看看两个嵌套for循环的顺序,代码中是先遍历物品嵌套遍历背包容量,那可不可以先遍历背包容量嵌套遍历物品呢?

不可以!

因为一维dp的写法,背包容量一定是要倒序遍历(原因上面已经讲了),如果遍历背包容量放在上一层,那么每个dp[j]就只会放入一个物品,即:背包里只放入了一个物品。

倒序遍历的原因是,本质上还是一个对二维数组的遍历,并且右下角的值依赖上一层左上角的值,因此需要保证左边的值仍然是上一层的,从右向左覆盖。

完全背包理论基础

每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。

完全背包和01背包问题唯一不同的地方就是,每种物品有无限件

而完全背包的物品是可以添加多次的,所以要从小到大去遍历,即:

1
2
3
4
5
6
7
// 先遍历物品,再遍历背包
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = weight[i]; j <= bagWeight ; j++) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

}
}

在完全背包中,对于一维dp数组来说,其实两个for循环嵌套顺序是无所谓的!

如果求组合数就是外层for循环遍历物品,内层for遍历背包

如果求排列数就是外层for遍历背包,内层for循环遍历物品

多重背包

很少出现

四、经典线性 DP

§4.1 最长公共子序列(LCS)

1143. 最长公共子序列

712. 两个字符串的最小ASCII删除和

1035. 不相交的线

72. 编辑距离

§4.2 最长递增子序列(LIS)

300. 最长递增子序列

image-20240513232256413

关键在于d[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
31
32
33
34
35
36
37
38
39
40
41
//常规做法
public int lengthOfLIS(int[] nums) {
int result=0;
int []dp=new int[nums.length];
Arrays.fill(dp, 1);
for (int i=0;i<nums.length;i++){
for (int j=0;j<i;j++){
if(nums[i]>nums[j])dp[i]=Math.max(dp[i],dp[j]+1);
}
if(result<dp[i])result=dp[i];
}
return result;
}

//二分加贪心做法
public int lengthOfLIS(int[] nums) {
int len = 1, n = nums.length;
if (n == 0) {
return 0;
}
int[] d = new int[n + 1];
d[len] = nums[0];
for (int i = 1; i < n; ++i) {
if (nums[i] > d[len]) {
d[++len] = nums[i];
} else {
int l = 1, r = len, pos = 0; // 如果找不到说明所有的数都比 nums[i] 大,此时要更新 d[1],所以这里将 pos 设为 0
while (l <= r) {
int mid = (l + r) >> 1;
if (d[mid] < nums[i]) {
pos = mid;
l = mid + 1;
} else {
r = mid - 1;
}
}
d[pos + 1] = nums[i];
}
}
return len;
}

2111. 使数组 K 递增的最少操作次数

最长递增子序列变式 ,首先根据k进行分组,然后问题转化为求每一组中的最长非递减子序列的长度,用总长度减去子序列的长度即为调整次数。

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
public int kIncreasing(int[] arr, int k) {
int ans=0;
for (int i = 0; i < k; i++) {
List<Integer> temp=new ArrayList<>();
int j=i;
while (j<arr.length){ //分组
temp.add(arr[j]);
j+=k;
}
if(temp.size()==1) continue;
int dp[]=new int[temp.size()];
int maxLen=0;
for(int num : temp) { //二分查找
int low = 0, high = maxLen;
while(low < high) {
int mid = low+(high-low)/2;
if(dp[mid] <= num)
low = mid+1;
else
high = mid;
}
dp[low] = num; //更新dp
if(low == maxLen)
maxLen++;
}

ans+=temp.size()-maxLen;
}
return ans;
}

673. 最长递增子序列的个数

两个dp来分别维护最长数组长度 与到第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
int []dp=new int[nums.length];//最长数组长度
int []count=new int[nums.length];//到第i个结尾时一共有多少个最长的数组
if (nums.length <= 1) return nums.length;
for(int i = 0; i < dp.length; i++) dp[i] = 1;
for(int i = 0; i < count.length; i++) count[i] = 1;
int maxCount=0;
for(int i=1;i<nums.length;i++){
for(int j=0;j<i;j++){
if(nums[i]>nums[j]){
//更新count数组
if(dp[j] + 1 > dp[i]){
dp[i] = dp[j] + 1;
count[i]=count[j];
}
else if(dp[j]+1==dp[i]){
count[i]+=count[j];
}
// dp[i]=Math.max(dp[i],dp[j]+1);
}
if (dp[i] > maxCount) maxCount = dp[i]; // 记录最长长度
}
}
int result = 0; // 统计结果
for (int i = 0; i < nums.length; i++) {
if (maxCount == dp[i]) result += count[i];
}
return result;

1671. 得到山形数组的最少删除次数

image-20240513225431458
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
public int minimumMountainRemovals(int[] nums) {
int n = nums.length;
int[] pre = getLISArray(nums);
int[] reversed = reverse(nums);
int[] suf = getLISArray(reversed);
suf = reverse(suf);

int ans = 0;
for (int i = 0; i < n; ++i) {
if (pre[i] > 1 && suf[i] > 1) {
ans = Math.max(ans, pre[i] + suf[i] - 1);
}
}

return n - ans;
}

public int[] getLISArray(int[] nums) {
int n = nums.length;
int[] dp = new int[n];
Arrays.fill(dp, 1);
for (int i = 0; i < n; ++i) {
for (int j = 0; j < i; ++j) {
if (nums[j] < nums[i]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
}
return dp;
}

public int[] reverse(int[] nums) {
int n = nums.length;
int[] reversed = new int[n];
for (int i = 0; i < n; i++) {
reversed[i] = nums[n - 1 - i];
}
return reversed;
}

845. 数组中的最长山脉

1964. 找出到每个位置为止最长的有效障碍赛跑路线

此题为最长递增子序列的二分法的应用,找到每次最长的index将其加入res 并实时维护a数组来表示第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
public int[] longestObstacleCourseAtEachPosition(int[] obstacles) {
int n = obstacles.length;
List<Integer> a = new ArrayList<>();
int [] res = new int [n];
for(int i=0;i<n;i++){
int num=obstacles[i];
int index=binarysearch(a,num);
if(index==a.size())a.add(num);
else{
a.set(index,num);
}
res[i]=index+1;
}
return res;
}
public int binarysearch(List<Integer> a ,int t){
int l=0,r=a.size();
while(l<r){
int mid=(r-l)/2+l;
if(a.get(mid)>t){
r=mid;
}
else{
l=mid+1;
}
}
return l;
}

2407. 最长递增子序列 II

在求解「上升子序列(IS)」问题时,一般有两种优化方法:

  1. 维护固定长度的 IS 的末尾元素的最小值 + 二分优化;

  2. 基于值域的线段树、平衡树等数据结构优化。

    此题使用线段树优化

五、状态机 DP

121. 买卖股票的最佳时机

122. 买卖股票的最佳时机 II

123. 买卖股票的最佳时机 III

188. 买卖股票的最佳时机 IV

309. 买卖股票的最佳时机含冷冻期

714. 买卖股票的最佳时机含手续费

六、划分型 DP

§6.1 判定能否划分

2369. 检查数组是否存在有效划分

1
2
3
4
5
6
7
8
9
10
11
12
13
public boolean validPartition(int[] nums) {
boolean dp[]=new boolean[nums.length+1];
dp[0]=true;
for (int i = 1; i < nums.length; i++) {
if (dp[i - 1] && nums[i] == nums[i - 1] ||
i > 1 && dp[i - 2] && (nums[i] == nums[i - 1] && nums[i] == nums[i - 2] ||
nums[i] == nums[i - 1] + 1 && nums[i] == nums[i - 2] + 2))
{
dp[i+1]=true;
}
}
return dp[nums.length];
}

§6.2 计算划分个数

132. 分割回文串 II

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
public int minCut(String s) {
boolean[][]array=new boolean[s.length()][s.length()];
for(int i=s.length();i>=0;i--){
for (int j=i;j<s.length();j++){
if(s.charAt(i)==s.charAt(j)&&(j-i<=1||array[i+1][j-1])){
array[i][j]=true;
}
}
}
int []dp=new int[s.length()];
for(int i=0;i<s.length();i++){
dp[i]=i;
}
for(int i=1;i<s.length();i++){
if(array[0][i]){
dp[i]=0;
continue;
}
for(int j=0;j<i;j++){
if(array[j+1][i]){
dp[i]=Math.min(dp[i],dp[j]+1);
}
}
}
return dp[s.length()-1];
}

2707. 字符串中的额外字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public int minExtraChar(String s, String[] dictionary) {
int n= s.length();
int dp[]=new int[n+1];
Arrays.fill(dp, Integer.MAX_VALUE);
Map<String,Integer> map=new HashMap<String,Integer>();
for (String d:
dictionary) {
map.put(d,map.getOrDefault(d,0)+1);
}
dp[0]=0;
for (int i = 1; i <=n ; i++) {
dp[i]=dp[i-1]+1;
for (int j = i-1; j >=0 ; j--) {
if(map.containsKey(s.substring(j,i))){
dp[i]=Math.min(dp[i],dp[j]);
}
}
}
return dp[n];
}

91. 解码方法

其他细节:由于题目存在前导零,而前导零属于无效 item。可以进行特判,但个人习惯往字符串头部追加空格作为哨兵,追加空格既可以避免讨论前导零,也能使下标从 1 开始,简化 f[i-1] 等负数下标的判断。

1
2
3
4
5
6
7
8
9
10
11
12
public int numDecodings(String s) {
s = " " + s;
char c[]=s.toCharArray();
int dp[]=new int [s.length()];
dp[0]=1;
for(int i=1;i<s.length();i++){
int a=c[i]-'0',b=(c[i-1]-'0')*10+(c[i]-'0');
if(1<=a&&a<=9)dp[i]=dp[i-1];
if(10<=b&&b<=26)dp[i]+=dp[i-2];
}
return dp[s.length()-1];
}

§6.3 约束划分个数

410. 分割数组的最大值

image-20240522030445984
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
public int splitArray(int[] nums, int m) {
int max = 0;
int sum = 0;

// 计算「子数组各自的和的最大值」的上下界
for (int num : nums) {
max = Math.max(max, num);
sum += num;
}

// 使用「二分查找」确定一个恰当的「子数组各自的和的最大值」,
// 使得它对应的「子数组的分割数」恰好等于 m
int left = max;
int right = sum;
while (left < right) {
int mid = left + (right - left) / 2;

int splits = split(nums, mid);
if (splits > m) {
// 如果分割数太多,说明「子数组各自的和的最大值」太小,此时需要将「子数组各自的和的最大值」调大
// 下一轮搜索的区间是 [mid + 1, right]
left = mid + 1;
} else {
// 下一轮搜索的区间是上一轮的反面区间 [left, mid]
right = mid;
}
}
return left;
}

/***
*
* @param nums 原始数组
* @param maxIntervalSum 子数组各自的和的最大值
* @return 满足不超过「子数组各自的和的最大值」的分割数
*/
private int split(int[] nums, int maxIntervalSum) {
// 至少是一个分割
int splits = 1;
// 当前区间的和
int curIntervalSum = 0;
for (int num : nums) {
// 尝试加上当前遍历的这个数,如果加上去超过了「子数组各自的和的最大值」,就不加这个数,另起炉灶
if (curIntervalSum + num > maxIntervalSum) {
curIntervalSum = 0;
splits++;
}
curIntervalSum += num;
}
return splits;
}

1043. 分隔数组以得到最大和

1
2
3
4
5
6
7
8
9
10
11
public int maxSumAfterPartitioning(int[] arr, int k) {
int n=arr.length;
int dp[]=new int [n+1];
for(int i=0;i<n;i++){
for (int j=i,max=0;j>i-k&&j>=0;j--){
max=Math.max(max,arr[j]);
dp[i+1]=Math.max(dp[i+1],dp[j]+(i-j+1)*max);
}
}
return dp[n];
}

1745. 分割回文串 IV

此题思路和回文子串相同,用中心扩展法记录有多少个回文子串,然后遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public boolean checkPartitioning(String s) {
int n=s.length();
boolean dp[][]=new boolean [n][n];
for (int r = 0; r < n; r++) {
for (int l = 0; l <=r; l++) {
if (s.charAt(l) == s.charAt(r) && (r - l <= 2 || dp[l + 1][r - 1])) {
dp[l][r]=true;
}
}
}
for(int i=0;i<n;i++){
if(dp[0][i]){
for(int j=i+1;j<n-1;j++){
if(dp[i+1][j]&&dp[j+1][n-1]){
return true;
}
}
}
}

return false;
}

813. 最大平均值和的分组

image-20240522043611599
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public double largestSumOfAverages(int[] nums, int k) {
int n=nums.length;
double sum[]=new double [n+1];
double dp[][]=new double [n+1][k+1];
for(int i=1;i<=n;i++){
sum[i]=sum[i-1]+nums[i-1];
}
for(int i=1;i<=n;i++){
for(int j=1;j<=Math.min(i,k);j++){
if(j==1){
dp[i][1]=sum[i]/i;
}
else{
for(int l=2;l<=i;l++){
dp[i][j]=Math.max(dp[i][j],dp[l-1][j-1]+(sum[i]-sum[l-1])/(i-l+1));
}
}
}
}
return dp[n][k];
}

§6.4 不相交区间

2830. 销售利润最大化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public int maximizeTheProfit(int n, List<List<Integer>> offers) {
List<int[]>[] groups = new ArrayList[n];
Arrays.setAll(groups, e -> new ArrayList<>());
for(List<Integer> offer:offers){
groups[offer.get(1)].add(new int[]{offer.get(0),offer.get(2)});
}
int dp[]=new int [n+1];
for(int end=0;end<n;end++){
dp[end+1]=dp[end];
for(int[]group:groups[end]){
dp[end+1]=Math.max(dp[end+1],dp[group[0]]+group[1]);
}
}
return dp[n];
}

2008. 出租车的最大盈利

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public long maxTaxiEarnings(int n, int[][] rides) {
List<int[]>[] groups = new ArrayList[n + 1];
for (int[] r : rides) {
int start = r[0], end = r[1], tip = r[2];
if (groups[end] == null) {
groups[end] = new ArrayList<>();
}
groups[end].add(new int[]{start, end - start + tip});
}

long[] f = new long[n + 1];
for (int i = 2; i <= n; i++) {
f[i] = f[i - 1];
if (groups[i] != null) {
for (int[] p : groups[i]) {
f[i] = Math.max(f[i], f[p[0]] + p[1]);
}
}
}
return f[n];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public long maxTaxiEarnings(int n, int[][] rides) {
Arrays.sort(rides, (a, b) -> a[1] - b[1]);
int m = rides.length;
long[] dp = new long[m + 1];
for (int i = 0; i < m; i++) {
int j = binarySearch1(rides, i, rides[i][0]);
dp[i + 1] = Math.max(dp[i], dp[j] + rides[i][1] - rides[i][0] + rides[i][2]);
}
return dp[m];
}
private int binarySearch1(int[][] rides, int right, int target){
int low =0;
while (low<right){
int mid=low+(right-low)/2;
if(rides[mid][1]>target){
right=mid;
}
else low=mid+1;
}
return low;
}

1235. 规划兼职工作

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
public int jobScheduling(int[] startTime, int[] endTime, int[] profit) {
int n = startTime.length;
int[][] jobs = new int[n][];
for (int i = 0; i < n; ++i)
jobs[i] = new int[]{startTime[i], endTime[i], profit[i]};
Arrays.sort(jobs, (a, b) -> a[1] - b[1]); // 按照结束时间排序
int[] dp = new int[n + 1];
for (int i = 1; i <= n; i++) {
int k = Search(jobs, i - 1, jobs[i - 1][0]);
dp[i] = Math.max(dp[i - 1], dp[k] + jobs[i - 1][2]);
}
return dp[n];
}
// 返回 endTime <= upper 的最大下标
public int Search(int[][] jobs, int right, int target) {
int left = 0;
while (left < right) {
int mid = left + (right - left) / 2;
if (jobs[mid][1] > target) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
}

七、其它线性 DP

§7.1 一维

§7.2 特殊子序列

§7.3 矩阵快速幂优化

1137. 第 N 个泰波那契数

552. 学生出勤记录 II

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
int N = 6;
int mod = (int)1e9+7;
long[][] mul(long[][] a, long[][] b) {
int r = a.length, c = b[0].length, z = b.length;
long[][] ans = new long[r][c];
for (int i = 0; i < r; i++) {
for (int j = 0; j < c; j++) {
for (int k = 0; k < z; k++) {
ans[i][j] += a[i][k] * b[k][j];
}
}
return ans;
}
public int checkRecord(int n) {
long[][] ans = new long[][]{
{1}, {0}, {0}, {0}, {0}, {0}
};
long[][] mat = new long[][]{
{1, 1, 1, 0, 0, 0},
{1, 0, 0, 0, 0, 0},
{0, 1, 0, 0, 0, 0},
{1, 1, 1, 1, 1, 1},
{0, 0, 0, 1, 0, 0},
{0, 0, 0, 0, 1, 0}
};
while (n != 0) {
if ((n & 1) != 0) ans = mul(mat, ans);
mat = mul(mat, mat);
n >>= 1;
}
int res = 0;
for (int i = 0; i < N; i++) {
res += ans[i][0];
res %= mod;
}
return res;
} ans[i][j] %= mod;
}

2851. 字符串转换

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
public int numberOfWays(String s, String t, long k) {
int n = s.length();
int c = kmpSearch(s + s.substring(0, n - 1), t);//避免统计最后一个字母
long[][] m = {
{c - 1, c},
{n - c, n - 1 - c},
};
m = pow(m, k);
return s.equals(t) ? (int) m[0][0] : (int) m[0][1];
}

// KMP 模板
private int[] calcMaxMatch(String s) {
int[] match = new int[s.length()];
int c = 0;
for (int i = 1; i < s.length(); i++) {
char v = s.charAt(i);
while (c > 0 && s.charAt(c) != v) {
c = match[c - 1];
}
if (s.charAt(c) == v) {
c++;
}
match[i] = c;
}
return match;
}

// KMP 模板
// 返回 text 中出现了多少次 pattern(允许 pattern 重叠)
private int kmpSearch(String text, String pattern) {
int[] match = calcMaxMatch(pattern);
int lenP = pattern.length();
int matchCnt = 0;
int c = 0;
for (int i = 0; i < text.length(); i++) {
char v = text.charAt(i);
while (c > 0 && pattern.charAt(c) != v) {
c = match[c - 1];
}
if (pattern.charAt(c) == v) {
c++;
}
if (c == lenP) {
matchCnt++;
c = match[c - 1];
}
}
return matchCnt;
}

private static final long MOD = (long) 1e9 + 7;

// 矩阵乘法
private long[][] multiply(long[][] a, long[][] b) {
long[][] c = new long[2][2];
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 2; j++) {
c[i][j] = (a[i][0] * b[0][j] + a[i][1] * b[1][j]) % MOD;
}
}
return c;
}

// 矩阵快速幂
private long[][] pow(long[][] a, long n) {
long[][] res = {{1, 0}, {0, 1}};
for (; n > 0; n /= 2) {//二分法求幂
if (n % 2 > 0) {
res = multiply(res, a);
}
a = multiply(a, a);
}
return res;
}

八、区间 DP

§8.1 最长回文子序列

516. 最长回文子序列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public int longestPalindromeSubseq(String s) {
int [][]dp=new int[s.length()][s.length()];
for (int i = 0; i < s.length(); i++) dp[i][i] = 1;
for(int i=s.length();i>=0;i--){
for(int j=i+1;j<s.length();j++){
if(s.charAt(i)==s.charAt(j))
dp[i][j]=dp[i+1][j-1]+2;
else{
dp[i][j]=Math.max(dp[i+1][j],dp[i][j-1]);
}
}
}
return dp[0][s.length()-1];
}

730. 统计不同回文子序列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int MOD = (int)1e9+7;
public int countPalindromicSubsequences(String s) {
char[] cs = s.toCharArray();
int n = cs.length;
int[][] f = new int[n][n];
int[] L = new int[4], R = new int[4];
Arrays.fill(L, -1);
for (int i = n - 1; i >= 0; i--) {
L[cs[i] - 'a'] = i;
Arrays.fill(R, -1);
for (int j = i; j < n; j++) {
R[cs[j] - 'a'] = j;
for (int k = 0; k < 4; k++) {
if (L[k] == -1 || R[k] == -1) continue;
int l = L[k], r = R[k];
if (l == r) f[i][j] = (f[i][j] + 1) % MOD;
else if (l == r - 1) f[i][j] = (f[i][j] + 2) % MOD;
else f[i][j] = (f[i][j] + f[l + 1][r - 1] + 2) % MOD;
}
}
}
return f[0][n - 1];
}

1312. 让字符串成为回文串的最少插入次数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public int minInsertions(String s) {
int n =s.length();
int [][]dp=new int [n][n];
for(int i=n-1;i>=0;i--){
for(int j=i+1;j<n;j++){
if(s.charAt(i)==s.charAt(j)){
dp[i][j]=dp[i+1][j-1];
}
else{
dp[i][j]=Math.min(dp[i+1][j],dp[i][j-1])+1;
}
}
}
return dp[0][n-1];
}

§8.2 其它区间 DP

子串类型可以用中心扩散法优化时间

5. 最长回文子串

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
public String longestPalindrome(String s) {
if (s == null || s.length() < 2) {
return s;
}
int strLen = s.length();
int maxStart = 0; //最长回文串的起点
int maxEnd = 0; //最长回文串的终点
int maxLen = 1; //最长回文串的长度

boolean[][] dp = new boolean[strLen][strLen];

for (int r = 0; r < strLen; r++) {
for (int l = 0; l <=r; l++) {
if (s.charAt(l) == s.charAt(r) && (r - l <= 2 || dp[l + 1][r - 1])) {
dp[l][r] = true;
if (r - l + 1 > maxLen) {
maxLen = r - l + 1;
maxStart = l;
maxEnd = r;
}
}

}
}
return s.substring(maxStart, maxEnd + 1);

}

96. 不同的二叉搜索树

image-20240526000548963
1
2
3
4
5
6
7
8
9
10
11
public int numTrees(int n) {
int dp[]=new int[n+1];
dp[0]=1;
dp[1]=1;
for(int i=2;i<=n;i++){
for(int j=1;j<=i;j++){
dp[i]+=dp[j-1]*dp[i-j];
}
}
return dp[n];
}

375. 猜数字大小 II

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public int getMoneyAmount(int n) {
int[][] f = new int[n + 10][n + 10];
for (int len = 2; len <= n; len++) {
for (int l = 1; l + len - 1 <= n; l++) {
int r = l + len - 1;
f[l][r] = 0x3f3f3f3f;
for (int x = l; x <= r; x++) {
int cur = Math.max(f[l][x - 1], f[x + 1][r]) + x;
f[l][r] = Math.min(f[l][r], cur);
}
}
}
return f[1][n];
}

312. 戳气球

image-20240526013617304
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public int maxCoins(int[] nums) {
int n = nums.length;
int[] arr = new int[n + 2];
arr[0] = arr[n + 1] = 1;
for (int i = 1; i <= n; i++) arr[i] = nums[i - 1];
int[][] f = new int[n + 2][n + 2];
for (int len = 3; len <= n + 2; len++) {
for (int l = 0; l + len - 1 <= n + 1; l++) {
int r = l + len - 1;
for (int k = l + 1; k <= r - 1; k++) {
f[l][r] = Math.max(f[l][r], f[l][k] + f[k][r] + arr[l] * arr[k] * arr[r]);
}
}
}
return f[0][n + 1];
}

1000. 合并石头的最低成本

1000-3d-cut.png 1000-2d.png
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public int mergeStones(int[] stones, int k) {
int n = stones.length;
if ((n - 1) % (k - 1) > 0) // 无法合并成一堆
return -1;

var s = new int[n + 1];
for (int i = 0; i < n; i++)
s[i + 1] = s[i] + stones[i]; // 前缀和

var f = new int[n][n];
for (int i = n - 1; i >= 0; --i)
for (int j = i + 1; j < n; ++j) {
f[i][j] = Integer.MAX_VALUE;
for (int m = i; m < j; m += k - 1)
f[i][j] = Math.min(f[i][j], f[i][m] + f[m + 1][j]);
if ((j - i) % (k - 1) == 0) // 可以合并成一堆
f[i][j] += s[j + 1] - s[i];
}
return f[0][n - 1];
}

九、状态压缩 DP(状压 DP)

§9.1 排列型 ① 相邻无关

§9.2 排列型 ② 相邻相关

§9.3 旅行商问题(TSP)

§9.4 枚举子集的子集

§9.5 其它状压 D

十、数位 DP

2719. 统计整数数目

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
public int count(String num1, String num2, int minSum, int maxSum) {
int n = num2.length();
num1 = "0".repeat(n - num1.length()) + num1; // 补前导零,和 num2 对齐

int[][] memo = new int[n][Math.min(9 * n, maxSum) + 1];
for (int[] row : memo) {
Arrays.fill(row, -1);
}

return dfs(0, 0, true, true, num1.toCharArray(), num2.toCharArray(), minSum, maxSum, memo);
}

private int dfs(int i, int sum, boolean limitLow, boolean limitHigh, char[] num1, char[] num2, int minSum, int maxSum, int[][] memo) {
if (sum > maxSum) { // 非法
return 0;
}
if (i == num2.length) {
return sum >= minSum ? 1 : 0;
}
if (!limitLow && !limitHigh && memo[i][sum] != -1) {
return memo[i][sum];
}

int lo = limitLow ? num1[i] - '0' : 0;
int hi = limitHigh ? num2[i] - '0' : 9;

int res = 0;
for (int d = lo; d <= hi; d++) { // 枚举当前数位填 d
res = (res + dfs(i + 1, sum + d, limitLow && d == lo, limitHigh && d == hi,
num1, num2, minSum, maxSum, memo)) % 1_000_000_007;
}

if (!limitLow && !limitHigh) {
memo[i][sum] = res;
}
return res;
}

面试题 17.06. 2出现的次数

image-20240530064021805
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
char s[];
int dp[][];
public int numberOf2sInRange(int n) {
s=Integer.toString(n).toCharArray();
int m=s.length;
dp=new int [m][m];
for(int i=0;i<m;i++)Arrays.fill(dp[i],-1);
return dfs(0,0,true);
}
public int dfs(int i,int count,boolean islimit){
if(i==s.length)return count;
if(!islimit&&dp[i][count]>=0)return dp[i][count];
int res=0;
for(int j=0,up=islimit?s[i]-'0':9;j<=up;j++){
res+=dfs(i+1,count+(j==2?1:0),islimit&&j==up);
}
if(!islimit)dp[i][count]=res;
return res;
}

902. 最大为 N 的数字组合

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
private String[] digits;
private char s[];
private int dp[];

public int atMostNGivenDigitSet(String[] digits, int n) {
this.digits = digits;
s = Integer.toString(n).toCharArray();
dp = new int[s.length];
Arrays.fill(dp, -1); // dp[i] = -1 表示 i 这个状态还没被计算出来
return f(0, true, false);
}

private int f(int i, boolean isLimit, boolean isNum) {
if (i == s.length) return isNum ? 1 : 0; // 如果填了数字,则为 1 种合法方案
if (!isLimit && isNum && dp[i] >= 0) return dp[i]; // 在不受到任何约束的情况下,返回记录的结果,避免重复运算
var res = 0;
if (!isNum) // 前面不填数字,那么可以跳过当前数位,也不填数字
// isLimit 改为 false,因为没有填数字,位数都比 n 要短,自然不会受到 n 的约束
// isNum 仍然为 false,因为没有填任何数字
res = f(i + 1, false, false);
var up = isLimit ? s[i] : '9'; // 根据是否受到约束,决定可以填的数字的上限
// 注意:对于一般的题目而言,如果此时 isNum 为 false,则必须从 1 开始枚举,由于本题 digits 没有 0,所以无需处理这种情况
for (var d : digits) { // 枚举要填入的数字 d
if (d.charAt(0) > up) break; // d 超过上限,由于 digits 是有序的,后面的 d 都会超过上限,故退出循环
// isLimit:如果当前受到 n 的约束,且填的数字等于上限,那么后面仍然会受到 n 的约束
// isNum 为 true,因为填了数字
res += f(i + 1, isLimit && d.charAt(0) == up, true);
}
if (!isLimit && isNum) dp[i] = res; // 在不受到任何约束的情况下,记录结果
return res;
}

十一、数据结构优化 DP

十二、树形 DP

§12.1 树的直径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int max = 0;
public int diameterOfBinaryTree(TreeNode root) {
if(root==null){
return 0;
}
dfs1(root);
return max;
}
public int dfs1(TreeNode root){
if (root.left == null && root.right == null) {
return 0;
}
int leftSize = root.left == null? 0: dfs1(root.left) + 1;
int rightSize = root.right == null? 0: dfs1(root.right) + 1;
max=Math.max(max,leftSize+rightSize);
return Math.max(leftSize,rightSize);
}

124. 二叉树中的最大路径和

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int maxsum=Integer.MIN_VALUE;;
public int maxPathSum(TreeNode root) {
dfs(root);
return maxsum;

}

public int dfs(TreeNode root){
if(root==null)
return 0;
int leftgain=Math.max(dfs(root.left),0);
int rightgain=Math.max(dfs(root.right),0);
int sum=root.val+leftgain+rightgain;
maxsum=Math.max(sum,maxsum);
return root.val + Math.max(leftgain, rightgain);
}


2246. 相邻字符不同的最长路径

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
List<Integer>[] g;
String s;
int ans;

public int longestPath(int[] parent, String s) {
this.s = s;
var n = parent.length;
g = new ArrayList[n];
Arrays.setAll(g, e -> new ArrayList<>());
for (var i = 1; i < n; i++) g[parent[i]].add(i);

dfs(0);
return ans + 1;
}

int dfs(int x) {
var maxLen = 0;
for (var y : g[x]) {
var len = dfs(y) + 1;
if (s.charAt(y) != s.charAt(x)) {
ans = Math.max(ans, maxLen + len);
maxLen = Math.max(maxLen, len);
}
}
return maxLen;
}

§12.2 树上最大独立集

337. 打家劫舍 III

1
2
3
4
5
6
7
8
9
10
11
12
13
public int rob(TreeNode root) {
int[] res = robAction1(root);
return Math.max(res[0],res[1]);
}
int[] robAction1(TreeNode root) {
int []res=new int[2];
if(root==null)return res;
int []left=robAction1(root.left);
int []right=robAction1(root.right);
res[0]=Math.max(left[0],left[1])+Math.max(right[0],right[1]);
res[1]=root.val+left[0]+right[0];
return res;
}

§12.3 树上最小支配集

0:安装摄像头

1:不安装摄像头但是父节点安装摄像头
2:不安装摄像头但是儿子节点安装摄像头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public int minCameraCover(TreeNode root) {
int[] res = dfs(root);
return Math.min(res[0], res[2]);
}

private int[] dfs(TreeNode node) {
if (node == null) {
return new int[]{Integer.MAX_VALUE / 2, 0, 0}; // 除 2 防止加法溢出
}
int[] left = dfs(node.left);
int[] right = dfs(node.right);
int choose = Math.min(left[0], left[1]) + Math.min(right[0], right[1]) + 1;
int byFa = Math.min(left[0], left[2]) + Math.min(right[0], right[2]);
int byChildren = Math.min(Math.min(left[0] + right[2], left[2] + right[0]), left[0] + right[0]);
return new int[]{choose, byFa, byChildren};
}

SDOI2006保安站岗

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
using namespace std;
typedef long long ll;
const int inf=1e9+7;
inline int read()
{
int p=0,f=1;char c=getchar();
while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
while(c>='0'&&c<='9'){p=p*10+c-'0';c=getchar();}
return f*p;}
const int maxn=1503;
struct Edge
{
int from,to;
}p[maxn<<1];
int n,cnt,head[maxn<<1],val[maxn];
int f[maxn][4];
//设状态f[x][0],f[x][1],f[x][2]分别表示
//对于x点,被自己覆盖,被自己的儿子覆盖,被自己的父亲覆盖时
//满足以x为根的子树所有点都被覆盖的最小代价
inline void add_edge(int x,int y)//加边
{
cnt++;
p[cnt].from=head[x];
head[x]=cnt;
p[cnt].to=y;
}
inline void TreeDP(int x,int fa)//树形DP
{
f[x][0]=val[x];//初值:选择x点
int sum=0,must_need_mincost=inf;
for(int i=head[x];i;i=p[i].from)
{
int y=p[i].to;
if(y==fa)continue;
TreeDP(y,x);
int t=min(f[y][0],f[y][1]);
f[x][0]+=min(t,f[y][2]);
//自己被自己覆盖:儿子怎么样都行
f[x][2]+=t;
//自己被父节点覆盖:儿子必须合法,要么选择儿子,要么是儿子被儿子的儿子覆盖
//以下是对f[x][1]的转移,请好好理解
if(f[y][0]<f[y][1])sum++;
//如果选择儿子节点更优,选上,计数器sum++,证明选过f[y][0]
else must_need_mincost=min(must_need_mincost,f[y][0]-f[y][1]);
//否则记录一个最小的必须支付代价
//因为最后要保证x点被y覆盖,必须要找差值最小的,这样才最优
f[x][1]+=t;//自己被儿子覆盖,那么儿子必须合法
}
if(!sum)f[x][1]+=must_need_mincost;
//对于f[x][1]转移:如果一个f[y][0]都没选过,那么必须从差值最小的儿子里面选择一个
}
int main()
{
n=read();
for(int i=1;i<=n;i++)
{
int x=read();
val[x]=read();
int num=read();
while(num>0)
{
int y=read();
add_edge(x,y);
add_edge(y,x);
num--;
}
}
TreeDP(1,0);
printf("%d",min(f[1][0],f[1][1]));
//由于根节点没有父节点,最后答案就是min(f[1][0],f[1][1])
//即1节点被自己覆盖或者被自己的儿子覆盖
return 0;
}

§12.4 换根 DP

lc834.png

834. 树中距离之和

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
   private List<Integer>[] g;
private int[] ans, size;
public int[] sumOfDistancesInTree(int n, int[][] edges) {
g = new ArrayList[n]; // g[x] 表示 x 的所有邻居
Arrays.setAll(g, e -> new ArrayList<>());
for (int [] e : edges) {
int x = e[0], y = e[1];
g[x].add(y);
g[y].add(x);
}
ans = new int[n];
size = new int[n];
dfs(0, -1, 0); // 0 没有父节点
reroot(0, -1); // 0 没有父节点
return ans;
}

private void dfs(int x, int fa, int depth) {
ans[0] += depth; // depth 为 0 到 x 的距离
size[x] = 1;
for (int y : g[x]) { // 遍历 x 的邻居 y
if (y != fa) { // 避免访问父节点
dfs(y, x, depth + 1); // x 是 y 的父节点
size[x] += size[y]; // 累加 x 的儿子 y 的子树大小
}
}
}

private void reroot(int x, int fa) {
for (int y : g[x]) { // 遍历 x 的邻居 y
if (y != fa) { // 避免访问父节点
ans[y] = ans[x] + g.length - 2 * size[y];
reroot(y, x); // x 是 y 的父节点
}
}
}

2581. 统计可能的树根数目

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
private List<Integer>[] times;
private Set<Long> s = new HashSet<>();
private int k, res, cnt0;

public int rootCount(int[][] edges, int[][] guesses, int k) {
this.k = k;
times = new ArrayList[edges.length + 1];
Arrays.setAll(times, i -> new ArrayList<>());
for (int[] e : edges) {
int x = e[0];
int y = e[1];
times[x].add(y);
times[y].add(x); // 建图
}

for (int[] e : guesses) { // guesses 转成哈希表
s.add((long) e[0] << 32 | e[1]); // 两个 4 字节 int 压缩成一个 8 字节 long
}

dfs(0, -1);
reroot(0, -1, cnt0);
return res;
}

private void dfs(int x, int fa) {
for (int y : times[x]) {
if (y != fa) {
if (s.contains((long) x << 32 | y)) { // 以 0 为根时,猜对了
cnt0++;
}
dfs(y, x);
}
}
}

private void reroot(int x, int fa, int cnt) {
if (cnt >= k) { // 此时 cnt 就是以 x 为根时的猜对次数
res++;
}
for (int y : times[x]) {
if (y != fa) {
int c = cnt;
if (s.contains((long) x << 32 | y)) c--; // 原来是对的,现在错了
if (s.contains((long) y << 32 | x)) c++; // 原来是错的,现在对了
reroot(y, x, c);
}
}
}

图的建立

链式前向星——最完美图解

图的存储方法很多,最常见的除了邻接矩阵、邻接表和边集数组外,还有链式前向星。链式前向星是一种静态链表存储,用边集数组和邻接表相结合,可以快速访问一个顶点的所有邻接点,在算法竞赛中广泛应用。

链式前向星存储包括两种结构:

  1. 边集数组:edge[ ],edge[i]表示第i条边;
  2. 头结点数组:head[ ],head[i]存以i为起点的第一条边的下标(在edge[]中的下标)
1
2
3
4
5
struct node{
int to,next,w;
}edge[maxe];//边集数组,边数一般要设置比maxn*maxn大的数,如果题目有要求除外

int head[maxn];//头结点数组

复制

每一条边的结构,如图所示。

img

例如,一个无向图,如图所示。

img

按以下顺序输入每条边的两个端点,建立的链式前向星,过程如下。

  1. 输入 1 2 5

创建一条边1—2,权值为5,创建第一条边edge[0],如图所示。

img

然后将该边链接到1号结点的头结点中。(初始时head[]数组全部初始化为-1)

即edge[0].next=head[1]; head[1]=0; 表示1号结点关联的第一个条边为0号边,如图所示。图中的虚线箭头仅表示他们之间的链接关系,不是指针。

img

因为是无向图,还需要添加它的反向边,2—1,权值为5。创建第二条边edge[1],如图所示。

img

然后将该边链接到2号结点的头结点中。

即edge[1].next=head[2]; head[2]=1; 表示2号结点关联的第一个条边为1号边,如图所示。

img

  1. 输入 1 4 3

创建一条边1—4,权值为3,创建第3条边edge[2],如图所示。

img

然后将该边链接到1号结点的头结点中(头插法)。

即edge[2].next=head[1]; head[1]=2; 表示1号结点关联的第一个条边为2号边,如图所示。

img

因为是无向图,还需要添加它的反向边,4—1,权值为3。创建第4条边edge[3],如图所示。

img

然后将该边链接到4号结点的头结点中。

即edge[3].next=head[4]; head[4]=3; 表示4号结点关联的第一个条边为3号边,如图所示。

img

  1. 依次输入以下三条边,创建的链式前向星,如图所示。

​ 2 3 8

​ 2 4 12

​ 3 4 9

img

添加一条边u v w的代码如下:

1
2
3
4
5
6
void add(int u,int v,int w){//添加一条边
edge[cnt].to=v;
edge[cnt].w=w;
edge[cnt].next=head[u];
head[u]=cnt++;
}

复制

如果是有向图,每输入一条边,执行一次add(u,v,w)即可;如果是无向图,则需要执行两次add(u,v,w); add(v,u,w)。

如何使用链式前向星访问一个结点u的所有邻接点呢?

1
2
3
4
5
for(int i=head[u];i!=-1;i=edge[i].next){
int v=edge[i].to; //u的邻接点
int w=edge[i].w; //u—v的权值

}

复制

链式前向星的特性:

  1. 和邻接表一样,因为采用头插法进行链接,所以边输入顺序不同,创建的链式前向星也不同。
  2. 对于无向图,每输入一条边,需要添加两条边,互为反向边。例如,输入第一条边1 2 5,实际上添加了两条边,如图所示。

img

这两条边可以通过互为反向边,可以通过与1的异或运算得到其反向边,0^1=1,1^1=0。也就是说如果一条边的下标为i,则其反向边为i^1。这个特性应用在网络流中非常方便。

3.链式前向星具有边集数组和邻接表的功能,属于静态链表,不需要频繁地创建结点,应用十分灵活。

总代码:

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
#include<iostream>//创建无向网的链式前向星 
#include<cstring>
using namespace std;
const int maxn=100000+5;
int maxx[maxn],head[maxn];
int n,m,x,y,w,cnt;

struct Edge{
int to,w,next;
}e[maxn];

void add(int u,int v,int w){//添加一条边u--v
e[cnt].to=v;
e[cnt].w=w;
e[cnt].next=head[u];
head[u]=cnt++;
}

void printg(){//输出链式前向星
cout<<"----------链式前向星如下:----------"<<endl;
for(int v=1;v<=n;v++){
cout<<v<<": ";
for(int i=head[v];~i;i=e[i].next){
int v1=e[i].to,w1=e[i].w;
cout<<"["<<v1<<" "<<w1<<"]\t";
}
cout<<endl;
}
}

int main(){
cin>>n>>m;
memset(head,-1,sizeof(head));
cnt=0;
for(int i=1;i<=m;i++){
cin>>x>>y>>w;
add(x,y,w);//添加边
add(y,x,w);//添加反向边
}
printg();
return 0;
}
/*输入样例
4 5
1 2 5
1 4 3
2 3 8
2 4 12
3 4 9
*/

线段树

线段树的建立

如果题目中给了具体的区间范围,我们根据该范围建立线段树。见题目 区域和检索 - 数组可修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void buildTree(Node node, int start, int end) {
// 到达叶子节点
if (start == end) {
node.val = arr[start];
return ;
}
int mid = (start + end) >> 1;
buildTree(node.left, start, mid);
buildTree(node.right, mid + 1, end);
// 向上更新
pushUp(node);
}
// 向上更新
private void pushUp(Node node) {
node.val = node.left.val + node.right.val;
}

但是很多时候,题目中都没有给出很具体的范围,只有数据的取值范围,一般都很大,所以我们更常用的是「动态开点」

下面我们手动模拟一下「动态开点」的过程。同样的,也是基于上面的例子 nums = [1, 2, 3, 4, 5]

假设一种情况,最开始只知道数组的长度 5,而不知道数组内每个元素的大小,元素都是后面添加进去的。所以线段树的初始状态如下图所示:(只有一个节点,很孤独!!)

1.svg

假设此时,我们添加了一个元素 [2, 2]; val = 3。现在线段树的结构如下图所示:

2.svg

这里需要解释一下,如果一个节点没有左右孩子,会一下子把左右孩子节点都给创建出来,如上图橙色节点所示,具体代码可见方法 pushDown()

两个橙色的叶子节点仅仅只是被创建出来了,并无实际的值,均为 0;而另外一个橙色的非叶子节点,值为 3 的原因是下面的孩子节点的值向上更新得到的

下面给出依次添加剩余节点的过程:(注意观察值的变化!!)

3.svg

线段树的更新
我看大多数教程都是把更新分为两种:「点更新」和「区间更新」。其实这两种可以合并成一种,「点更新」不就是更新长度为 1 的区间嘛!!

更新区间的前提是找到需要更新的区间,所以和查询的思路很相似

如果我们要把区间 [2, 4] 内的元素都「➕1」

3.svg

当我们向孩子节点遍历的时候会把「懒惰标记」下推给孩子节点

我们需要稍微修改一下 Node 的数据结构

1
2
3
4
5
6
7
8
class Node {
// 左右孩子节点
Node left, right;
// 当前节点值
int val;
// 懒惰标记
int add;
}

基于「动态开点」的前提,我们下推懒惰标记的时候,如果节点不存在左右孩子节点,那么我们就创建左右孩子节点

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84


* @Description: 线段树(动态开点)

* @Author: LFool
* @Date 2022/6/7 09:15
**/
public class SegmentTreeDynamic {
class Node {
Node left, right;
int val, add;
}
private int N = (int) 1e9;
private Node root = new Node();
// 下面来实现更新的函数:
// 在区间 [start, end] 中更新区间 [l, r] 的值,将区间 [l, r] ➕ val
// 对于上面的例子,应该这样调用该函数:update(root, 0, 4, 2, 4, 1)
public void update(Node node, int start, int end, int l, int r, int val) {
// 找到满足要求的区间
if (l <= start && end <= r) {
// 区间节点加上更新值
// 注意:需要✖️该子树所有叶子节点
node.val += (end - start + 1) * val;
// 添加懒惰标记
// 对区间进行「加减」的更新操作,懒惰标记需要累加,不能直接覆盖
node.add += val;
return ;
}
int mid = (start + end) >> 1;
// 下推标记
// mid - start + 1:表示左孩子区间叶子节点数量
// end - mid:表示右孩子区间叶子节点数量
pushDown(node, mid - start + 1, end - mid);
// [start, mid] 和 [l, r] 可能有交集,遍历左孩子区间
if (l <= mid) update(node.left, start, mid, l, r, val);
// [mid + 1, end] 和 [l, r] 可能有交集,遍历右孩子区间
if (r > mid) update(node.right, mid + 1, end, l, r, val);
// 向上更新
pushUp(node);
}
//线段树的查询
// 在区间 [start, end] 中查询区间 [l, r] 的结果,即 [l ,r] 保持不变
// 对于上面的例子,应该这样调用该函数:query(root, 0, 4, 2, 4)
public int query(Node node, int start, int end, int l, int r) {
// 区间 [l ,r] 完全包含区间 [start, end]
// 例如:[2, 4] = [2, 2] + [3, 4],当 [start, end] = [2, 2] 或者 [start, end] = [3, 4],直接返回
if (l <= start && end <= r) return node.val;
// 把当前区间 [start, end] 均分得到左右孩子的区间范围
// node 左孩子区间 [start, mid]
// node 左孩子区间 [mid + 1, end]
int mid = (start + end) >> 1, ans = 0;
// 下推标记
pushDown(node, mid - start + 1, end - mid);
// [start, mid] 和 [l, r] 可能有交集,遍历左孩子区间
if (l <= mid) ans += query(node.left, start, mid, l, r);
// [mid + 1, end] 和 [l, r] 可能有交集,遍历右孩子区间
if (r > mid) ans += query(node.right, mid + 1, end, l, r);
// ans 把左右子树的结果都累加起来了,与树的后续遍历同理
return ans;
}
// 向上更新
private void pushUp(Node node) {
node.val = node.left.val + node.right.val;
}
//先来实现下推懒惰标记的函数:
// leftNum 和 rightNum 表示左右孩子区间的叶子节点数量
// 因为如果是「加减」更新操作的话,需要用懒惰标记的值✖️叶子节点的数量
private void pushDown(Node node, int leftNum, int rightNum) {
// 动态开点
if (node.left == null) node.left = new Node();
if (node.right == null) node.right = new Node();
// 如果 add 为 0,表示没有标记
if (node.add == 0) return ;
// 注意:当前节点加上标记值✖️该子树所有叶子节点的数量
node.left.val += node.add * leftNum;
node.right.val += node.add * rightNum;
// 把标记下推给孩子节点
// 对区间进行「加减」的更新操作,下推懒惰标记时需要累加起来,不能直接覆盖
node.left.add += node.add;
node.right.add += node.add;
// 取消当前节点标记
node.add = 0;
}
}

2407. 最长递增子序列 II - 力扣(LeetCode)

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
class Solution {
public int lengthOfLIS(int[] nums, int k) {
int ans = 0;
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
// 查询区间 [nums[i] - k, nums[i] - 1] 的最值
int cnt = query(root, 0, N, Math.max(0, nums[i] - k), nums[i] - 1) + 1;
// 更新,注意这里是覆盖更新,对应的模版中覆盖更新不需要累加,已在下方代码中标注
update(root, 0, N, nums[i], nums[i], cnt);
ans = Math.max(ans, cnt);
}
return ans;
}
// *************** 下面是模版 ***************
class Node {
Node left, right;
int val, add;
}
private int N = (int) 1e5;
private Node root = new Node();
public void update(Node node, int start, int end, int l, int r, int val) {
if (l <= start && end <= r) {
node.val = val; // 不需要累加
node.add = val; // 不需要累加
return ;
}
pushDown(node);
int mid = (start + end) >> 1;
if (l <= mid) update(node.left, start, mid, l, r, val);
if (r > mid) update(node.right, mid + 1, end, l, r, val);
pushUp(node);
}
public int query(Node node, int start, int end, int l, int r) {
if (l <= start && end <= r) return node.val;
pushDown(node);
int mid = (start + end) >> 1, ans = 0;
if (l <= mid) ans = query(node.left, start, mid, l, r);
if (r > mid) ans = Math.max(ans, query(node.right, mid + 1, end, l, r));
return ans;
}
private void pushUp(Node node) {
node.val = Math.max(node.left.val, node.right.val);
}
private void pushDown(Node node) {
if (node.left == null) node.left = new Node();
if (node.right == null) node.right = new Node();
if (node.add == 0) return ;
node.left.val = node.add; // 不需要累加
node.right.val = node.add; // 不需要累加
node.left.add = node.add; // 不需要累加
node.right.add = node.add; // 不需要累加
node.add = 0;
}
}

树状数组

image.png

image.png

307. 区域和检索 - 数组可修改

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
57
58
59
60
61
62
63
64
65
66
67
68
69
class NumArray {
int[] tree;
int lowbit(int x) {
return x & -x;
}
int query(int x) {
int ans = 0;
for (int i = x; i > 0; i -= lowbit(i)) ans += tree[i];
return ans;
}
void add(int x, int u) {
for (int i = x; i <= n; i += lowbit(i)) tree[i] += u;
}

int[] nums;
int n;
public NumArray(int[] _nums) {
nums = _nums;
n = nums.length;
tree = new int[n + 1];
for (int i = 0; i < n; i++) add(i + 1, nums[i]);
}

public void update(int i, int val) {
add(i + 1, val - nums[i]);
nums[i] = val;
}

public int sumRange(int l, int r) {
return query(r + 1) - query(l);
}
}

//模版
// 上来先把三个方法写出来
{
int[] tree;
int lowbit(int x) {
return x & -x;
}
// 查询前缀和的方法
int query(int x) {
int ans = 0;
for (int i = x; i > 0; i -= lowbit(i)) ans += tree[i];
return ans;
}
// 在树状数组 x 位置中增加值 u
void add(int x, int u) {
for (int i = x; i <= n; i += lowbit(i)) tree[i] += u;
}
}

// 初始化「树状数组」,要默认数组是从 1 开始
{
for (int i = 0; i < n; i++) add(i + 1, nums[i]);
}

// 使用「树状数组」:
{
void update(int i, int val) {
// 原有的值是 nums[i],要使得修改为 val,需要增加 val - nums[i]
add(i + 1, val - nums[i]);
nums[i] = val;
}

int sumRange(int l, int r) {
return query(r + 1) - query(l);
}
}

3072. 将元素分配到两个数组中 II

根据题意,要实现 greaterCount 函数,需要快速查找一个有序结构中,严格大于 val 的元素数量。我们可以使用「树状数组」来实现这个数据结构,其它「线段树」结构也可以实现相同功能。

首先,因为我们只关心数组元素的大小关系,我们可以将数组「离散化」。

然后我们根据题意进行模拟,初始化两个数组和其对应的树,依次遍历原数组中的元素,根据题目条件,将元素加入到对应数组中,并将元素离散化后的数组索引加入到树中。

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
57
58
59
60
61
62
class TreeArrays{
private int []tree;
public TreeArrays(int n){
tree=new int[n+1];
}
public void add (int i){
while(i<tree.length){
tree[i]++;
i+=i&-i;
}
}
public int get(int i){
int res=0;
while(i>0){
res+=tree[i];
i-=i&-i;
}
return res;
}
}

class Solution {

public int[] resultArray(int[] nums) {
int n = nums.length;
int[] sortedNums = Arrays.copyOf(nums, n);
Arrays.sort(sortedNums);

Map<Integer, Integer> index = new HashMap<>();
for (int i = 0; i < n; i++) {
index.put(sortedNums[i], i+1);
}

List<Integer> arr1 = new ArrayList<>(List.of(nums[0]));
List<Integer> arr2 = new ArrayList<>(List.of(nums[1]));
TreeArrays tree1 = new TreeArrays(n);
TreeArrays tree2 = new TreeArrays(n);
tree1.add(index.get(nums[0]));
tree2.add(index.get(nums[1]));

for (int i = 2; i < n; i++) {
int count1 = arr1.size() - tree1.get(index.get(nums[i]));
int count2 = arr2.size() - tree2.get(index.get(nums[i]));
if (count1 > count2 || (count1 == count2 && arr1.size() <= arr2.size())) {
arr1.add(nums[i]);
tree1.add(index.get(nums[i]));
} else {
arr2.add(nums[i]);
tree2.add(index.get(nums[i]));
}
}

int i = 0;
for (int a: arr1) {
nums[i++] = a;
}
for (int a: arr2) {
nums[i++] = a;
}
return nums;
}
}

二分强化

五、堆(优先队列)

2530. 执行 K 次操作后的最大分数

自己写一个堆排序

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
public long maxKelements(int[] nums, int k) {
buildheap(nums); // 原地堆化(最大堆)
long ans = 0;
while (k-- > 0) {
ans += nums[0]; // 堆顶
nums[0] = (nums[0] + 2) / 3;
sink(nums, 0); // 堆化(只需要把 nums[0] 下沉)
}
return ans;
}
public void buildheap(int []num){
for(int i=num.length/2-1;i>=0;i--){
sink(num,i);
}
}
public void sink(int []arr,int index){
int length=arr.length;
int leftChild = 2 * index + 1;//左子节点下标
int rightChild = 2 * index + 2;//右子节点下标
int present = index;//要调整的节点下标
//下沉左边
if (leftChild < length && arr[leftChild] > arr[present]) {
present = leftChild;
}

//下沉右边
if (rightChild < length && arr[rightChild] > arr[present]) {
present = rightChild;
}

//如果下标不相等 证明调换过了
if (present != index) {
//交换值
int temp = arr[index];
arr[index] = arr[present];
arr[present] = temp;

//继续下沉 在取出头元素后与尾元素交换继续下沉
sink(arr, present);
}
}

1834. 单线程 CPU

三个参数排序,先按照到达时间排序,再按照执行时间排序,最后再按照到达顺序排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public int[] getOrder(int[][] tasks) {
int n =tasks.length;
int [][]list=new int[n][3];
for(int i=0;i<n;i++){
list[i]=new int []{tasks[i][0],tasks[i][1],i};
}
Arrays.sort(list,(a,b)->a[0]-b[0]);
PriorityQueue<int[]>queue=new PriorityQueue<>((a,b)->{
if(a[1]!=b[1])return a[1]-b[1];
return a[2]-b[2];
});
int []res=new int[n];
for (int time = 1, j = 0, idx = 0; idx < n; ) {
while (j < n && list[j][0] <= time) queue.add(list[j++]);
if (queue.isEmpty()) {
time = list[j][0];
} else {
int[] cur = queue.poll();
res[idx++] = cur[2];
time += cur[1];
}
}
return res;
}

1792. 最大平均通过率

image-20240618001223909
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
public double maxAverageRatio(int[][] classes, int extraStudents) {
PriorityQueue<int[]> pq = new PriorityQueue<int[]>((a, b) -> {
long val1 = (long) (b[1] + 1) * b[1] * (a[1] - a[0]);
long val2 = (long) (a[1] + 1) * a[1] * (b[1] - b[0]);
if (val1 == val2) {
return 0;
}
return val1 < val2 ? 1 : -1;
});
for (int[] c : classes) {
pq.offer(new int[]{c[0], c[1]});
}

for (int i = 0; i < extraStudents; i++) {
int[] arr = pq.poll();
int pass = arr[0], total = arr[1];
pq.offer(new int[]{pass + 1, total + 1});
}

double res = 0;
for (int i = 0; i < classes.length; i++) {
int[] arr = pq.poll();
int pass = arr[0], total = arr[1];
res += 1.0 * pass / total;
}
return res / classes.length;
}

2402. 会议室 III

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public int mostBooked(int n, int[][] meetings) {
var cnt = new int[n];
var idle = new PriorityQueue<Integer>();
for (var i = 0; i < n; ++i) idle.offer(i);
var using = new PriorityQueue<Pair<Long, Integer>>((a, b) -> !Objects.equals(a.getKey(), b.getKey()) ? Long.compare(a.getKey(), b.getKey()) : Integer.compare(a.getValue(), b.getValue()));
Arrays.sort(meetings, (a, b) -> Integer.compare(a[0], b[0]));
for (var m : meetings) {
long st = m[0], end = m[1];
while (!using.isEmpty() && using.peek().getKey() <= st) {
idle.offer(using.poll().getValue()); // 维护在 st 时刻空闲的会议室
}
int id;
if (idle.isEmpty()) {
var p = using.poll(); // 没有可用的会议室,那么弹出一个最早结束的会议室(若有多个同时结束的,会弹出下标最小的)
end += p.getKey() - st; // 更新当前会议的结束时间
id = p.getValue();
} else id = idle.poll();
++cnt[id];
using.offer(new Pair<>(end, id)); // 使用一个会议室
}
var ans = 0;
for (var i = 0; i < n; ++i) if (cnt[i] > cnt[ans]) ans = i;
return ans;
}

2940. 找到 Alice 和 Bob 可以相遇的建筑

这题关键在于记录下无法跳到的的末尾的位置,然后从这个末尾位置开始使用优先队列从小到排列与每个为的高度值对比求出可以一起跳到的位置

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
public int[] leftmostBuildingQueries(int[] heights, int[][] queries) {
int[] ans = new int[queries.length];
Arrays.fill(ans, -1);
List<int[]>[] left = new ArrayList[heights.length];
Arrays.setAll(left, e -> new ArrayList<>());
for (int qi = 0; qi < queries.length; qi++) {
int i = queries[qi][0], j = queries[qi][1];
if (i > j) {
int temp = i;
i = j;
j = temp; // 保证 i <= j
}
if (i == j || heights[i] < heights[j]) {
ans[qi] = j; // i 直接跳到 j
} else {
left[j].add(new int[]{heights[i], qi}); // 离线
}
}

PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[0] - b[0]);
for (int i = 0; i < heights.length; i++) { // 从小到大枚举下标 i
while (!pq.isEmpty() && pq.peek()[0] < heights[i]) {
ans[pq.poll()[1]] = i; // 可以跳到 i(此时 i 是最小的)
}
for (int[] p : left[i]) {
pq.offer(p); // 后面再回答
}
}
return ans;
}

§5.3 重排元素

1405. 最长快乐字符串

此类题都是贪心和堆排序结合用堆排序满足贪心优先的想法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public String longestDiverseString(int a, int b, int c) {
PriorityQueue<int[]> q = new PriorityQueue<>((x,y)->y[1]-x[1]);
if (a > 0) q.add(new int[]{0, a});
if (b > 0) q.add(new int[]{1, b});
if (c > 0) q.add(new int[]{2, c});
StringBuilder sb = new StringBuilder();
while (!q.isEmpty()) {
int[] cur = q.poll();
int n = sb.length();
if (n >= 2 && sb.charAt(n - 1) - 'a' == cur[0] && sb.charAt(n - 2) - 'a' == cur[0]) {
if (q.isEmpty()) break;
int[] next = q.poll();
sb.append((char)(next[0] + 'a'));
if (--next[1] != 0) q.add(next);
q.add(cur);
} else {
sb.append((char)(cur[0] + 'a'));
if (--cur[1] != 0) q.add(cur);
}
}
return sb.toString();
}

§5.4 第 K 小/大

264. 丑数 II

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int[] nums = new int[]{2,3,5};
public int nthUglyNumber(int n) {
PriorityQueue <Long>priorityQueue=new PriorityQueue<>();
Set<Long>set=new HashSet<>();
set.add(1L);
priorityQueue.add(1L);
for(int i=1;i<=n;i++){
long temp=priorityQueue.poll();
if(i==n)return (int)temp;
for(int num:nums ){
long x=num*temp;
if(!set.contains(x)){
set.add(x);
priorityQueue.add(x);
}
}
}
return -1;
}

2386. 找出数组的第 K 大和

image-20240308210525630

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
public long kSum(int[] nums, int k) {
int n = nums.length;
long total = 0;
for (int i = 0; i < n; i++) {
if (nums[i] >= 0) {
total += nums[i];
} else {
nums[i] = -nums[i];
}
}
Arrays.sort(nums);
long ret = 0;
PriorityQueue<long[]> pq = new PriorityQueue<long[]>((a, b) -> Long.compare(a[0], b[0]));
pq.offer(new long[]{nums[0], 0});
for (int j = 2; j <= k; j++) {
long[] arr = pq.poll();
long t = arr[0];
int i = (int) arr[1];
ret = t;
if (i == n - 1) {
continue;
}
pq.offer(new long[]{t + nums[i + 1], i + 1});
pq.offer(new long[]{t - nums[i] + nums[i + 1], i + 1});
}
return total - ret;
}

§5.5 反悔堆

LCP 30. 魔塔游戏

1.初始化血量 hp=1。
2.从左到右遍历数组,把小于 0 的数丢到一个小根堆中。
3.遍历的同时,把 nums[i] 加到 hp 中。如果 hp<1,那么弹出堆顶,hp 减去堆顶,相当于把之前扣掉的血重新加回来。同时把调整次数增加一。注意如果 hp<1,那么必然是由当前这个小于 0 的 nums[i] 导致的,这一保证了此时堆不为空,二保证了 hp 减去堆顶后必然可以恢复成正数,因为堆顶不会比 nums[i] 还大。
4返回调整次数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public int magicTower(int[] nums) {
PriorityQueue<Integer>queue=new PriorityQueue<>();
long cur=1;
int back=0;
int res=0;
for(int num:nums){
if(num<0)queue.offer(num);
cur+=num;
if(cur<=0){
res++;
int n=queue.poll();
cur-=n;
back+=n;
}
}
cur+=back;
return cur>0?res:-1;
}

630. 课程表 III

看上去,找不到一个合适的贪心策略。别放弃!顺着这个思路,如果我们可以「反悔」呢?

按照 lastDay 从小到大排序,然后遍历 courses。比如先上完 duration=7 的课和 duration=10 的课,后面遍历到了 duration=4 的课,但受到 lastDay 的限制,无法上 duration=4 的课。此时,我们可以「撤销」前面 duration 最长的课,也就是 duration=10 的课,这样就可以上 duration=4 的课了!虽然能上完的课程数目没有变化,但是由于我们多出了 10−4=6 天时间,在后续的遍历中,更有机会上完更多的课程。

在上面的讨论中,我们需要维护一个数据结构,来帮助我们快速找到 duration 最长的课程。这可以用最大堆解决。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public int scheduleCourse(int[][] courses) {
Arrays.sort(courses, (a, b) -> a[1] - b[1]); // 按照 lastDay 从小到大排序
PriorityQueue<Integer> pq = new PriorityQueue<>((a, b) -> b - a); // 最大堆
int day=0;
for(int []course:courses){
int duration = course[0], lastDay = course[1];
if(day+duration<=lastDay){
day+=duration;
pq.offer(duration);
}
else if(!pq.isEmpty()&&duration<pq.peek()){
day-=pq.poll()-duration;
pq.offer(duration);
}
}
return pq.size();
}

3049. 标记所有下标的最早秒数 II

周赛题参见周赛

§5.6 懒删除堆

2349. 设计数字容器系统

用最小堆堆存储改数字曾经存在于那个数据组中,然后再判断当前数据组是否存储这个数字如果存储返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class NumberContainers {
Map<Integer,Integer>map=new HashMap<>();
Map<Integer,PriorityQueue<Integer>>ms=new HashMap<>();
public NumberContainers() {

}

public void change(int index, int number) {
map.put(index,number);
ms.computeIfAbsent(number, k -> new PriorityQueue<>()).offer(index);
}

public int find(int number) {
var q=ms.get(number);
if(q==null)return -1;
while(!q.isEmpty()&&map.get(q.peek())!=number)q.poll();
return q.isEmpty()?-1:q.peek();
}
}

§5.7 对顶堆

295. 数据流的中位数

image-20240710104545701
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
class MedianFinder {


Queue<Integer>min=new PriorityQueue<>();
Queue<Integer>max=new PriorityQueue<>();
public MedianFinder() {
min = new PriorityQueue<Integer>((a, b) -> (b - a));
max = new PriorityQueue<Integer>((a, b) -> (a - b));
}

public void addNum(int num) {
if(min.isEmpty()||num<=min.peek()){
min.offer(num);
if(max.size()+1<min.size()){
max.offer(min.poll());
}
}
else {
max.offer(num);
if(min.size()<max.size()){
min.offer(max.poll());
}
}
}

public double findMedian() {
if(max.size()<min.size()){
return min.peek();
}
return (max.peek()+min.peek())/2.0;
}
}

2102. 序列顺序查询

  1. 数据流的中位数的变种题
    小根堆的最小值 >= 大根堆的最大值
    295题求中间的一个或两个数,保证两个堆的大小差距不超过1即可
    本题求第i个数(i等于get()的次数),保证小根堆的大小等于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
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
57
58
59
60
61
62
63
64
class SORTracker {
List<String> names = new ArrayList<>();
List<Integer> scores = new ArrayList<>();
Comparator<Integer> comparator = Comparator.comparing(scores::get)
.thenComparing(Comparator.comparing(names::get).reversed());
Queue<Integer> minHeap = new PriorityQueue<>(comparator);
Queue<Integer> maxHeap = new PriorityQueue<>(comparator.reversed());

/*
//使用 Lambda 表达式改写 Comparator
Comparator<Integer> comparator = (i1, i2) -> {
int scoreComparison = scores.get(i1).compareTo(scores.get(i2));
if (scoreComparison != 0) {
return scoreComparison;
}
// If scores are the same, compare names in reversed order
return names.get(i2).compareTo(names.get(i1));
};

//重写 compare 方法
Comparator<Integer> comparator = new Comparator<Integer>() {
@Override
public int compare(Integer i1, Integer i2) {
int scoreComparison = scores.get(i1).compareTo(scores.get(i2));
if (scoreComparison != 0) {
return scoreComparison;
}
// If scores are the same, compare names in reversed order
return names.get(i2).compareTo(names.get(i1));
}
};
*/

Queue<Integer> minHeap = new PriorityQueue<>(comparator);
Queue<Integer> maxHeap = new PriorityQueue<>(comparator.reversed()); Comparator<Integer> comparator = new Comparator<Integer>() {
@Override
public int compare(Integer i1, Integer i2) {
int scoreComparison = scores.get(i1).compareTo(scores.get(i2));
if (scoreComparison != 0) {
return scoreComparison;
}
// If scores are the same, compare names in reversed order
return names.get(i2).compareTo(names.get(i1));
}
};

Queue<Integer> minHeap = new PriorityQueue<>(comparator);
Queue<Integer> maxHeap = new PriorityQueue<>(comparator.reversed());
public SORTracker() {
}

public void add(String name, int score) {
names.add(name);
scores.add(score);
minHeap.offer(names.size() - 1);
maxHeap.offer(minHeap.poll());
}

public String get() {
minHeap.offer(maxHeap.poll());
return names.get(minHeap.peek());
}
}

六、字典树(trie)

212. 单词搜索 II

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
57
58
59
60
61
62
63
64
65
66
67
 int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};

public List<String> findWords(char[][] board, String[] words) {
Trie trie = new Trie();
for (String word : words) {
trie.insert(word);
}

Set<String> ans = new HashSet<String>();
for (int i = 0; i < board.length; ++i) {
for (int j = 0; j < board[0].length; ++j) {
dfs(board, trie, i, j, ans);
}
}

return new ArrayList<String>(ans);
}

public void dfs(char[][] board, Trie now, int i1, int j1, Set<String> ans) {
if (!now.children.containsKey(board[i1][j1])) {
return;
}
char ch = board[i1][j1];
Trie nxt = now.children.get(ch);
if (!"".equals(nxt.word)) {
ans.add(nxt.word);
nxt.word = "";
}

if (!nxt.children.isEmpty()) {
board[i1][j1] = '#';
for (int[] dir : dirs) {
int i2 = i1 + dir[0], j2 = j1 + dir[1];
if (i2 >= 0 && i2 < board.length && j2 >= 0 && j2 < board[0].length) {
dfs(board, nxt, i2, j2, ans);
}
}
board[i1][j1] = ch;
}

if (nxt.children.isEmpty()) {
now.children.remove(ch);
}
}
}

class Trie {
String word;
Map<Character, Trie> children;
boolean isWord;

public Trie() {
this.word = "";
this.children = new HashMap<Character, Trie>();
}

public void insert(String word) {
Trie cur = this;
for (int i = 0; i < word.length(); ++i) {
char c = word.charAt(i);
if (!cur.children.containsKey(c)) {
cur.children.put(c, new Trie());
}
cur = cur.children.get(c);
}
cur.word = word;
}

3045. 统计前后缀下标对 II

将字符的前n位和倒数n位一起拼成一个组,这个就可以将该组作为一个前缀树节点,然后依次判断该组出现的次数累加即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Trie{
Map<Integer,Trie>son=new HashMap<>();
int cnt;
}

class Solution {
public long countPrefixSuffixPairs(String[] words) {
long res=0;
Trie t=new Trie();
for(String s :words){
char c[]=s.toCharArray();
int n=c.length;
Trie cur=t;
for(int i=0;i<n;i++){
int pos=(c[i]-'a')<<5|(c[n-1-i]-'a');
cur=cur.son.computeIfAbsent(pos, k->new Trie());
res+=cur.cnt;
}
cur.cnt++;
}
return res;
}
}

§6.3 字典树优化 DP

140. 单词拆分 II

image.png

此题先将类似于单词拆分1 记录这个字符串能否被拆分成对应的单词set,然后再从后往前回溯便利每个dp[i]==true 的单词求得集合

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
public List<String> wordBreak(String s, List<String> wordDict) {
Set<String>set=new HashSet<>(wordDict);
int n=s.length();
boolean dp[]=new boolean[n+1];
dp[0]=true;
for(int i=1;i<=n;i++){
for(int j=i-1;j>=0;j--){
if(set.contains(s.substring(j,i))&&dp[j]){
dp[i]=true;
break;
}
}
}
List<String> res = new ArrayList<>();
if (dp[n]) {
Deque<String> path = new ArrayDeque<>();
dfs(s, n, set, dp, path, res);
return res;
}
return res;
}
private void dfs(String s ,int n,Set<String>set,boolean dp[],
Deque<String>path,List<String> res){
if(n==0){
res.add(String.join(" ", path));
return;
}
for(int j=n-1;j>=0;j--){
String word=s.substring(j,n);
if(set.contains(word)&&dp[j]){
path.addFirst(word);
dfs(s, j, set, dp, path, res);
path.removeFirst();
}
}
}

七、并查集

737. 句子相似性 II

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
public boolean areSentencesSimilarTwo(String[] sentence1, String[] sentence2, List<List<String>> similarPairs) {   
if (sentence1.length != sentence2.length) return false;
Map<String, Integer> index = new HashMap();
int count = 0;
sim s=new sim(2*similarPairs.size());
for(List<String>pair:similarPairs){
for(String p:pair){
if(!index.containsKey(p)){
index.put(p,count++);
}
}
sim.union(index.get(pair.get(0)),index.get(pair.get(1)));
}
for(int i=0;i<sentence1.length;i++){
String s1=sentence1[i],s2=sentence2[i];
if(s1.equals(s2))continue;
if(!index.containsKey(s1)||!index.containsKey(s2)
||sim.find(index.get(s1))!=sim.find(index.get(s2)))
return false;
}
return true;
}
}

class sim {
static int parent [];
public sim(int n){
parent=new int[n];
for(int i=0;i<n;i++){
parent[i]=i;
}
}
public static int find(int x){
if(parent[x]!=x)parent[x]=find(parent[x]);
return parent[x];
}
public static void union(int x,int y){
parent[find(x)]=find(y);
}
}

765. 情侣牵手

首先,我们总是以「情侣对」为单位进行设想:

当有两对情侣相互坐错了位置,ta们两对之间形成了一个环。需要进行一次交换,使得每队情侣独立(相互牵手)

如果三对情侣相互坐错了位置,ta们三对之间形成了一个环,需要进行两次交换,使得每队情侣独立(相互牵手)

如果四对情侣相互坐错了位置,ta们四对之间形成了一个环,需要进行三次交换,使得每队情侣独立(相互牵手)

也就是说,如果我们有 k 对情侣形成了错误环,需要交换 k - 1 次才能让情侣牵手。

于是问题转化成 n / 2 对情侣中,有多少个这样的环。

可以直接使用「并查集」来做。

由于 0和1配对、2和3配对 … 因此互为情侣的两个编号除以 2 对应同一个数字,可直接作为它们的「情侣组」编号:

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
57
58
59
60
61
public int minSwapsCouples(int[] row) {
int n=row.length;
int t=n/2;
int f[]=new int[t];

for (int i = 0; i < t; i++) {
f[i]=i;
}
for (int i = 0; i < n; i+=2) {
int x=row[i]/2;
int y=row[i+1]/2;
add(f,x,y);
}
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
for (int i = 0; i < t; i++) {
int fx = getf(f, i);
map.put(fx, map.getOrDefault(fx, 0) + 1);
}
int res=0;
for (Map.Entry<Integer,Integer> entry:map.entrySet()) {
res+=entry.getValue()-1;
}
return res;
}
public int getf(int[] f, int x) {
if (f[x] == x) {
return x;
}
f[x]=getf(f,f[x]);
return f[x];
}
public void add(int[] f,int x,int y){
int fx=getf(f,x);
int fy=getf(f,y);
f[fx]=fy;
}


//简化写法
int[] p = new int[70];
public int minSwapsCouples(int[] row) {
int n=row.length, m=n/2;
for(int i=0;i<m;i++){
p[i]=i;
}
for(int i=0;i<n;i+=2){
union(row[i]/2, row[i+1]/2);
}
int res=0;
for(int i=0;i<m;i++){
if(i!=find(i))res++;
}
return res;
}
public int find(int x){
if(p[x]!=x)p[x]=find(p[x]);
return p[x];
}
public void union(int x,int y){
p[find(x)]=find(y);
}

2709. 最大公约数遍历

待定

§7.3 公因数并查集

2709. 最大公约数遍历

将每个数与自己的公因数相连,再遍历每个数,如果不相同则无法连接

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
57
58
59
60
61
62
63
64
65
66
67
68
class Solution {
public boolean canTraverseAllPairs(int[] nums) {
if (nums.length == 1) {
return true;
}
int maxNum = 0;
for (int num : nums) {
if (num == 1) {
return false;
}
maxNum = Math.max(maxNum, num);
}
UnionFind uf = new UnionFind(maxNum + 1);
for (int num : nums) {
for (int i = 2; i * i <= num; i++) {
if (num % i == 0) {
uf.union(num, i);
uf.union(num, num / i);
}
}
}
int root = 0;
for (int num : nums) {
int currRoot = uf.find(num);
if (root == 0) {
root = currRoot;
} else if (currRoot != root) {
return false;
}
}
return true;
}

}
class UnionFind {
private int[] parent;
private int[] rank;

public UnionFind(int n) {
parent = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
}
rank = new int[n];
}

public void union(int x, int y) {
int rootx = find(x);
int rooty = find(y);
if (rootx != rooty) {
if (rank[rootx] > rank[rooty]) {
parent[rooty] = rootx;
} else if (rank[rootx] < rank[rooty]) {
parent[rootx] = rooty;
} else {
parent[rooty] = rootx;
rank[rootx]++;
}
}
}

public int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
}

§7.4 数组上的并查集

1488. 避免洪水泛滥

从集合论到位运算,常见位运算技巧分类总结

image-20240131223108107

image-20240131223636659

三、遍历集合

1
2
3
4
5
for (int i = 0; i < n; i++) {
if (((s >> i) & 1) == 1) { // i 在 s 中
// 处理 i 的逻辑
}
}

四、枚举集合

1
2
3
for (int s = 0; s < (1 << n); s++) {
// 处理 s 的逻辑
}
1
2
3
for (int sub = s; sub > 0; sub = (sub - 1) & s) {
// 处理 sub 的逻辑
}

八、差分数组

image-20240915135808848

1094. 拼车

image-20240915140242492

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
   public boolean carPooling(int[][] trips, int capacity) {
int[] d = new int[1001];
for (int[] t : trips) {
int num = t[0], from = t[1], to = t[2];
d[from] += num;
d[to] -= num;
}
int s = 0;
for (int v : d) {
s += v;
if (s > capacity) {
return false;
}
}
return true;
}

2848. 与车相交的点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public int numberOfPoints(List<List<Integer>> nums) {
int maxend=0;
for(List<Integer>list:nums){
maxend =Math.max(maxend,list.get(1));
}
int diff[]=new int[maxend+2];
for(List<Integer>list:nums){
diff[list.get(0)]++;
diff[list.get(1)+1]--;
}
int res=0;
int sum=0;
for(int d:diff){
sum+=d;
if(sum>0){
res++;
}
}
return res;
}

一维差分的思想可以推广至二维,请点击图片放大查看:

LC2132-c.png

2132. 用邮票贴满网格图

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
class Solution {
public boolean possibleToStamp(int[][] grid, int stampHeight, int stampWidth) {
int m = grid.length;
int n = grid[0].length;

// 1. 计算 grid 的二维前缀和
int[][] s = new int[m + 1][n + 1];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
s[i + 1][j + 1] = s[i + 1][j] + s[i][j + 1] - s[i][j] + grid[i][j];
}
}

// 2. 计算二维差分
// 为方便第 3 步的计算,在 d 数组的最上面和最左边各加了一行(列),所以下标要 +1
int[][] d = new int[m + 2][n + 2];
for (int i2 = stampHeight; i2 <= m; i2++) {
for (int j2 = stampWidth; j2 <= n; j2++) {
int i1 = i2 - stampHeight + 1;
int j1 = j2 - stampWidth + 1;
if (s[i2][j2] - s[i2][j1 - 1] - s[i1 - 1][j2] + s[i1 - 1][j1 - 1] == 0) {
d[i1][j1]++;
d[i1][j2 + 1]--;
d[i2 + 1][j1]--;
d[i2 + 1][j2 + 1]++;
}
}
}

// 3. 还原二维差分矩阵对应的计数矩阵(原地计算)
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
d[i + 1][j + 1] += d[i + 1][j] + d[i][j + 1] - d[i][j];
if (grid[i][j] == 0 && d[i + 1][j + 1] == 0) {
return false;
}
}
}
return true;
}
}

Spring 微服务

从单体架构过度到微服务架构,需要一系列中间技术支撑,其中重要的部分包括:

注册中心:Eureka 、Zookeeper、Nacos
服务网关:Zuul 、Gateway
微服务远程调用:RestTemplate、Feign
容器化技术 Docker
消息队列 MQ(多种实现方式)
负载均衡 Ribbon 、 Nginx
分布式搜索技术:ElasticSearch

2.Eureka注册中心

假如我们的服务提供者user-service部署了多个实例,如图:

img

3.1.Eureka的结构和作用

这些问题都需要利用SpringCloud中的注册中心来解决,其中最广为人知的注册中心就是Eureka,其结构如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a3suA6Zv-1662516079572)(https://an-menghe.gitee.io/imgs/202203010024704.png)]

回答之前的各个问题。

问题1:order-service如何得知user-service实例地址?

获取地址信息的流程如下:

user-service服务实例启动后,将自己的信息注册到eureka-server(Eureka服务端)。这个叫服务注册

eureka-server保存服务名称到服务实例地址列表的映射关系

order-service根据服务名称,拉取实例地址列表。这个叫服务发现或服务拉取

问题2:order-service如何从多个user-service实例中选择具体的实例?

order-service从实例列表中利用负载均衡算法选中一个实例地址

向该实例地址发起远程调用

问题3:order-service如何得知某个user-service实例是否依然健康,是不是已经宕机?

user-service会每隔一段时间(默认30秒)向eureka-server发起请求,报告自己状态,称为心跳

当超过一定时间没有发送心跳时,eureka-server会认为微服务实例故障,将该实例从服务列表中剔除

order-service拉取服务时,就能将故障实例排除了

1
2
3
4
5
6
7
8
9
10
11
12
13
package cn.itcast.eureka;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
server:
port: 10086
spring:
application:
name: eureka-server #eureka服务名称
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka #eureka地址信息
instance:
prefer-ip-address: true
instance-id: 127.0.0.1:${server.port}

3.3.服务注册

下面,我们将user-service注册到eureka-server中去。

1)引入依赖

在user-service的pom文件中,引入下面的eureka-client依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

2)配置文件

在user-service中,修改application.yml文件,添加服务名称、eureka地址:

1
2
3
4
5
6
7
8
9
10
spring:
application:
name: userservice
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
instance:
prefer-ip-address: true #以IP地址注册到服务中心,相互注册使用IP地址,如果不配置就是机器的主机名
instance-id: 127.0.0.1:${server.port} # instanceID默认值为主机名+服务名+端口

3)启动多个user-service实例

为了演示一个服务有多个实例的场景,我们添加一个SpringBoot的启动配置,再启动一个user-service。

首先,复制原来的user-service启动配置:

img

然后,在弹出的窗口中,填写信息:

img

现在,SpringBoot窗口会出现两个user-service启动配置:

img

不过,第一个是8081端口,第二个是8082端口。

启动两个user-service实例:

img

查看eureka-server管理页面:

在这里插入图片描述

3.4.服务发现

下面,我们将order-service的逻辑修改:向eureka-server拉取user-service的信息,实现服务发现。

1)引入依赖

之前说过,服务发现、服务注册统一都封装在eureka-client依赖,因此这一步与服务注册时一致。

在order-service的pom文件中,引入下面的eureka-client依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

2)配置文件

服务发现也需要知道eureka地址,因此第二步与服务注册一致,都是配置eureka信息:

在order-service中,修改application.yml文件,添加服务名称、eureka地址:

1
2
3
4
5
6
7
8
9
10
spring:
application:
name: orderservice
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
instance:
prefer-ip-address: true
instance-id: 127.0.0.1:${server.port}

3)服务拉取和负载均衡
最后,我们要去eureka-server中拉取user-service服务的实例列表,并且实现负载均衡。

不过这些动作不用我们去做,只需要添加一些注解即可。

在order-service的OrderApplication中,给RestTemplate这个Bean添加一个@LoadBalanced注解:

img

修改order-service服务中的cn.itcast.order.service包下的OrderService类中的queryOrderById方法。修改访问的url路径,用服务名代替ip、端口:

img

4.Ribbon负载均衡

上一节中,我们添加了@LoadBalanced注解,即可实现负载均衡功能,这是什么原理呢?

4.1.负载均衡原理

SpringCloud底层其实是利用了一个名为Ribbon的组件,来实现负载均衡功能的。

在这里插入图片描述

那么我们发出的请求明明是http://userservice/user/1,怎么变成了http://localhost:8081的呢?

可以看到这里的intercept方法,拦截了用户的HttpRequest请求,然后做了几件事:

request.getURI():获取请求uri,本例中就是 http://user-service/user/8

originalUri.getHost():获取uri路径的主机名,其实就是服务id,user-service

this.loadBalancer.execute():处理服务id,和用户请求。

这里的this.loadBalancer是LoadBalancerClient类型,我们继续跟入。
4)总结
SpringCloudRibbon的底层采用了一个拦截器,拦截了RestTemplate发出的请求,对地址做了修改。用一幅图来总结一下:

img

基本流程如下:

拦截我们的RestTemplate请求http://userservice/user/1

RibbonLoadBalancerClient会从请求url中获取服务名称,也就是user-service

DynamicServerListLoadBalancer根据user-service到eureka拉取服务列表

eureka返回列表,localhost:8081、localhost:8082

IRule利用内置负载均衡规则,从列表中选择一个,例如localhost:8081

RibbonLoadBalancerClient修改请求地址,用localhost:8081替代userservice,得到http://localhost:8081/user/1,发起真实请求4.3.负载均衡策略

4.3.负载均衡策略

4.3.1.负载均衡策略

负载均衡的规则都定义在IRule接口中,而IRule有很多不同的实现类:

image-20210713225653000

image-20230308225117696

4.3.2.自定义负载均衡策略
通过定义IRule实现可以修改负载均衡规则,有两种方式:

代码方式:在order-service中的OrderApplication类中,定义一个新的IRule:

1
2
3
@Bean
public IRule randomRule(){
return new RandomRule();

配置文件方式:在order-service的application.yml文件中,添加新的配置也可以修改规则:

1
2
3
userservice: # 给某个微服务配置负载均衡规则,这里是userservice服务
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则

注意,一般用默认的负载均衡规则,不做修改。

4.4.饥饿加载

Ribbon默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient,请求时间会很长。

而饥饿加载则会在项目启动时创建,降低第一次访问的耗时,通过下面配置开启饥饿加载:

1
2
3
4
5
6
ribbon:
eager-load:
enabled: true # 开启饥饿加载
clients:
- userservice # 指定饥饿加载的服务名称
- xxxxservice # 如果需要指定多个,需要这么写

5.Nacos注册中心

5.2.服务注册到nacos

Nacos是SpringCloudAlibaba的组件,而SpringCloudAlibaba也遵循SpringCloud中定义的服务注册、服务发现规范。因此使用Nacos和使用Eureka对于微服务来说,并没有太大区别。

主要差异在于:

依赖不同
服务地址不同
1)引入依赖
在cloud-demo父工程的pom文件中的中引入SpringCloudAlibaba的依赖:

1
2
3
4
5
6
7
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.6.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>

然后在user-service和order-service中的pom文件中引入nacos-discovery依赖:

1
2
3
4
5
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
注意:不要忘了注释掉eureka的依赖。

2)配置nacos地址

在user-service和order-service的application.yml中添加nacos地址:

1
2
3
4
5
spring:
cloud:
nacos:
server-addr: localhost:8848
#注意:不要忘了注释掉eureka的地址

5.3.服务分级存储模型

微服务互相访问时,应该尽可能访问同集群实例,因为本地访问速度更快。当本集群内不可用时,才访问其它集群。例如:

在这里插入图片描述

杭州机房内的order-service应该优先访问同机房的user-service。

5.3.1.给user-service配置集群

修改user-service的application.yml文件,添加集群配置:

1
2
3
4
5
6
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: HZ # 集群名称

重启两个user-service实例后,我们可以在nacos控制台看到下面结果:
img

我们再次复制一个user-service启动配置,添加属性:

-Dserver.port=8083 -Dspring.cloud.nacos.discovery.cluster-name=SH
1img

启动UserApplication3后再次查看nacos控制台:

img

5.3.2.同集群优先的负载均衡

认的ZoneAvoidanceRule并不能实现根据同集群优先来实现负载均衡。

因此Nacos中提供了一个NacosRule的实现,可以优先从同集群中挑选实例。

1)给order-service配置集群信息

修改order-service的application.yml文件,添加集群配置:

1
2
3
4
5
6
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: HZ # 集群名称

2)修改负载均衡规则

修改order-service的application.yml文件,修改负载均衡规则:

1
2
3
4
userservice:
ribbon:
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # 负载均衡规则

5.4.权重配置

实际部署中会出现这样的场景:

服务器设备性能有差异,部分实例所在机器性能较好,另一些较差,我们希望性能好的机器承担更多的用户请求。

但默认情况下NacosRule是同集群内随机挑选,不会考虑机器的性能问题。

因此,Nacos提供了权重配置来控制访问频率,权重越大则访问频率越高。

在nacos控制台,找到user-service的实例列表,点击编辑,即可修改权重:
img

image-20210713235235219

5.5.环境隔离

Nacos提供了namespace来实现环境隔离功能。

  • nacos中可以有多个namespace
  • namespace下可以有group、service等
  • 不同namespace之间相互隔离,例如不同namespace的服务互相不可见

img

5.5.2.给微服务配置namespace

给微服务配置namespace只能通过修改配置来实现。

例如,修改order-service的application.yml文件:

1
2
3
4
5
6
7
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: HZ
namespace: 492a7d5d-237b-46a1-a99a-fa8e98e4b0f9 # 命名空间,填ID

PS:遇到的bug

端口8082被占用了。
原因可能如下:
1电脑中其他进程占用8080端口;
2其他Spring Boot项目占用8080端口;
3自己要运行的项目重复生成占用了端口。
解决方法:
打开cmd,输入如下命令,查找8082端口对应的进程ID PID:

1
netstat -ano

img

这里PID为10400,再输入如下命令杀死进程:

1
taskkill /F /pid 10400

6.Nacos配置管理

Nacos除了可以做注册中心,同样可以做配置管理来使用。

如何在nacos中管理配置呢?

6.1.统一配置管理

image-20210714164742924

然后在弹出的表单中,填写配置信息:

image-20210714164856664

注意:项目的核心配置,需要热更新的配置才有放到nacos管理的必要。基本不会变更的一些配置还是保存在微服务本地比较好。

6.1.2.从微服务拉取配置
微服务要拉取nacos中管理的配置,并且与本地的application.yml配置合并,才能完成项目启动。

但如果尚未读取application.yml,又如何得知nacos地址呢?

因此spring引入了一种新的配置文件:bootstrap.yaml文件,会在application.yml之前被读取,流程如下:

1)引入nacos-config依赖

首先,在user-service服务中,引入nacos-config的客户端依赖:

1
2
3
4
5
6
<!--nacos配置管理依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

2)添加bootstrap.yaml

然后,在user-service中添加一个bootstrap.yaml文件,内容如下:

1
2
3
4
5
6
7
8
9
10
spring:
application:
name: userservice # 服务名称
profiles:
active: dev #开发环境,这里是dev
cloud:
nacos:
server-addr: localhost:8848 # Nacos地址
config:
file-extension: yaml # 文件后缀名

这里会根据spring.cloud.nacos.server-addr获取nacos地址,再根据

s p r i n g . a p p l i c a t i o n . n a m e − {spring.application.name}-spring.application.name−{spring.profiles.active}.${spring.cloud.nacos.config.file-extension}作为文件id,来读取配置。

本例中,就是去读取userservice-dev.yaml:

img

img

6.2.配置热更新

我们最终的目的,是修改nacos中的配置后,微服务中无需重启即可让配置生效,也就是配置热更新

要实现配置热更新,可以使用两种方式:

6.2.1.方式一
在@Value注入的变量所在类上添加注解@RefreshScope:

img

6.2.2.方式二
使用@ConfigurationProperties注解代替@Value注解。

在user-service服务中,添加一个类,读取patterrn.dateformat属性:

1
2
3
4
5
6
7
8
9
10
11
12
package cn.itcast.user.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@Data
@ConfigurationProperties(prefix = "pattern")
public class PatternProperties {
private String dateformat;
}

在UserController中使用这个类代替@Value:

在这里插入图片描述

6.3.配置共享

其实微服务启动时,会去nacos读取多个配置文件,例如:

[spring.application.name]-[spring.profiles.active].yaml,例如:userservice-dev.yaml

[spring.application.name].yaml,例如:userservice.yaml而[spring.application.name].yaml不包含环境,因此可以被多个环境共享。

下面我们通过案例来测试配置共享

img

2)在user-service中读取共享配置

在user-service服务中,修改PatternProperties类,读取新添加的属性:

image-20210714173324231

img

img

这样,UserApplication(8081)使用的profile是dev,UserApplication2(8082)使用的profile是test。

启动UserApplication和UserApplication2

访问http://localhost:8081/user/prop,结果:

img

访问http://localhost:8082/user/prop,结果:

img

可以看出来,不管是dev,还是test环境,都读取到了envSharedValue这个属性的值。

4)配置共享的优先级

当nacos、服务本地同时出现相同属性时,优先级有高低之分:
在这里插入图片描述

6.4.搭建Nacos集群

Nacos生产环境下一定要部署为集群状态,部署方式参考课前资料中的文档:

1.集群结构图

官方给出的Nacos集群图:

img

其中包含3个nacos节点,然后一个负载均衡器代理3个Nacos。这里负载均衡器可以使用nginx。

我们计划的集群结构:

image-20210409211355037

三个nacos节点的地址:

节点 ip port
nacos1 192.168.150.1 8845
nacos2 192.168.150.1 8846
nacos3 192.168.150.1 8847

2.搭建集群

搭建集群的基本步骤:

  • 搭建数据库,初始化数据库表结构
  • 下载nacos安装包
  • 配置nacos
  • 启动nacos集群
  • nginx反向代理

2.1.初始化数据库

Nacos默认数据存储在内嵌数据库Derby中,不属于生产可用的数据库。

官方推荐的最佳实践是使用带有主从的高可用数据库集群,主从模式的高可用数据库可以参考传智教育的后续高手课程。

这里我们以单点的数据库为例来讲解。

首先新建一个数据库,命名为nacos,而后导入下面的SQL:

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
CREATE TABLE `config_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(255) DEFAULT NULL,
`content` longtext NOT NULL COMMENT 'content',
`md5` varchar(32) DEFAULT NULL COMMENT 'md5',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
`src_user` text COMMENT 'source user',
`src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
`app_name` varchar(128) DEFAULT NULL,
`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
`c_desc` varchar(256) DEFAULT NULL,
`c_use` varchar(64) DEFAULT NULL,
`effect` varchar(64) DEFAULT NULL,
`type` varchar(64) DEFAULT NULL,
`c_schema` text,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfo_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info';

/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_info_aggr */
/******************************************/
CREATE TABLE `config_info_aggr` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(255) NOT NULL COMMENT 'group_id',
`datum_id` varchar(255) NOT NULL COMMENT 'datum_id',
`content` longtext NOT NULL COMMENT '内容',
`gmt_modified` datetime NOT NULL COMMENT '修改时间',
`app_name` varchar(128) DEFAULT NULL,
`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfoaggr_datagrouptenantdatum` (`data_id`,`group_id`,`tenant_id`,`datum_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='增加租户字段';


/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_info_beta */
/******************************************/
CREATE TABLE `config_info_beta` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(128) NOT NULL COMMENT 'group_id',
`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
`content` longtext NOT NULL COMMENT 'content',
`beta_ips` varchar(1024) DEFAULT NULL COMMENT 'betaIps',
`md5` varchar(32) DEFAULT NULL COMMENT 'md5',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
`src_user` text COMMENT 'source user',
`src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfobeta_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_beta';

/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_info_tag */
/******************************************/
CREATE TABLE `config_info_tag` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(128) NOT NULL COMMENT 'group_id',
`tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',
`tag_id` varchar(128) NOT NULL COMMENT 'tag_id',
`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
`content` longtext NOT NULL COMMENT 'content',
`md5` varchar(32) DEFAULT NULL COMMENT 'md5',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
`src_user` text COMMENT 'source user',
`src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfotag_datagrouptenanttag` (`data_id`,`group_id`,`tenant_id`,`tag_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_tag';

/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_tags_relation */
/******************************************/
CREATE TABLE `config_tags_relation` (
`id` bigint(20) NOT NULL COMMENT 'id',
`tag_name` varchar(128) NOT NULL COMMENT 'tag_name',
`tag_type` varchar(64) DEFAULT NULL COMMENT 'tag_type',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(128) NOT NULL COMMENT 'group_id',
`tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',
`nid` bigint(20) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`nid`),
UNIQUE KEY `uk_configtagrelation_configidtag` (`id`,`tag_name`,`tag_type`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_tag_relation';

/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = group_capacity */
/******************************************/
CREATE TABLE `group_capacity` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`group_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Group ID,空字符表示整个集群',
`quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',
`usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
`max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',
`max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数,,0表示使用默认值',
`max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',
`max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_group_id` (`group_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='集群、各Group容量信息表';

/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = his_config_info */
/******************************************/
CREATE TABLE `his_config_info` (
`id` bigint(64) unsigned NOT NULL,
`nid` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`data_id` varchar(255) NOT NULL,
`group_id` varchar(128) NOT NULL,
`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
`content` longtext NOT NULL,
`md5` varchar(32) DEFAULT NULL,
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`src_user` text,
`src_ip` varchar(50) DEFAULT NULL,
`op_type` char(10) DEFAULT NULL,
`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
PRIMARY KEY (`nid`),
KEY `idx_gmt_create` (`gmt_create`),
KEY `idx_gmt_modified` (`gmt_modified`),
KEY `idx_did` (`data_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='多租户改造';


/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = tenant_capacity */
/******************************************/
CREATE TABLE `tenant_capacity` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`tenant_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Tenant ID',
`quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',
`usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
`max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',
`max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数',
`max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',
`max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='租户容量信息表';


CREATE TABLE `tenant_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`kp` varchar(128) NOT NULL COMMENT 'kp',
`tenant_id` varchar(128) default '' COMMENT 'tenant_id',
`tenant_name` varchar(128) default '' COMMENT 'tenant_name',
`tenant_desc` varchar(256) DEFAULT NULL COMMENT 'tenant_desc',
`create_source` varchar(32) DEFAULT NULL COMMENT 'create_source',
`gmt_create` bigint(20) NOT NULL COMMENT '创建时间',
`gmt_modified` bigint(20) NOT NULL COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_info_kptenantid` (`kp`,`tenant_id`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='tenant_info';

CREATE TABLE `users` (
`username` varchar(50) NOT NULL PRIMARY KEY,
`password` varchar(500) NOT NULL,
`enabled` boolean NOT NULL
);

CREATE TABLE `roles` (
`username` varchar(50) NOT NULL,
`role` varchar(50) NOT NULL,
UNIQUE INDEX `idx_user_role` (`username` ASC, `role` ASC) USING BTREE
);

CREATE TABLE `permissions` (
`role` varchar(50) NOT NULL,
`resource` varchar(255) NOT NULL,
`action` varchar(8) NOT NULL,
UNIQUE INDEX `uk_role_permission` (`role`,`resource`,`action`) USING BTREE
);

INSERT INTO users (username, password, enabled) VALUES ('nacos', '$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu', TRUE);

INSERT INTO roles (username, role) VALUES ('nacos', 'ROLE_ADMIN');

2.2.下载nacos

nacos在GitHub上有下载地址:https://github.com/alibaba/nacos/tags,可以选择任意版本下载。

本例中才用1.4.1版本:

image-20210409212119411

2.3.配置Nacos

将这个包解压到任意非中文目录下,如图:

image-20210402161843337

目录说明:

  • bin:启动脚本
  • conf:配置文件

进入nacos的conf目录,修改配置文件cluster.conf.example,重命名为cluster.conf:

image-20210409212459292

然后添加内容:

1
2
3
127.0.0.1:8845
127.0.0.1.8846
127.0.0.1.8847

然后修改application.properties文件,添加数据库配置

1
2
3
4
5
6
7
spring.datasource.platform=mysql

db.num=1

db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=root
db.password.0=123

2.4.启动

将nacos文件夹复制三份,分别命名为:nacos1、nacos2、nacos3

image-20210409213335538

然后分别修改三个文件夹中的application.properties,

nacos1:

1
server.port=8845

nacos2:

1
server.port=8846

nacos3:

1
server.port=8847

然后分别启动三个nacos节点:

1
startup.cmd

2.5.nginx反向代理

找到课前资料提供的nginx安装包:

image-20210410103253355

解压到任意非中文目录下:

image-20210410103322874

修改conf/nginx.conf文件,配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
upstream nacos-cluster {
server 127.0.0.1:8845;
server 127.0.0.1:8846;
server 127.0.0.1:8847;
}

server {
listen 80;
server_name localhost;

location /nacos {
proxy_pass http://nacos-cluster;
}
}

而后在浏览器访问:http://localhost/nacos即可。

代码中application.yml文件配置如下:

1
2
3
4
spring:
cloud:
nacos:
server-addr: localhost:80 # Nacos地址

2.6.优化

  • 实际部署时,需要给做反向代理的nginx服务器设置一个域名,这样后续如果有服务器迁移nacos的客户端也无需更改配置.
  • Nacos的各个节点应该部署到多个不同服务器,做好容灾和隔离

7.Feign远程调用

先来看我们以前利用RestTemplate发起远程调用的代码:

在这里插入图片描述

7.1.Feign替代RestTemplate

Fegin的使用步骤如下:

1)引入依赖
我们在order-service服务的pom文件中引入feign的依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

2)添加注解
在order-service的启动类添加注解开启Feign的功能:
img

3)编写Feign的客户端
在order-service中新建一个接口,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
package cn.itcast.order.client;

import cn.itcast.order.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient("userservice")
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}

4)测试

修改order-service中的OrderService类中的queryOrderById方法,使用Feign客户端代替RestTemplate:

在这里插入图片描述

是不是看起来优雅多了。

5)总结

使用Feign的步骤:

① 引入依赖

② 添加@EnableFeignClients注解

③ 编写FeignClient接口

④ 使用FeignClient中定义的方法代替RestTemplate

7.2.自定义配置

Feign可以支持很多的自定义配置,如下表所示:

类型 作用 说明
feign.Logger.Level 修改日志级别 包含四种不同的级别:NONE、BASIC、HEADERS、FULL
feign.codec.Decoder 响应结果的解析器 http远程调用的结果做解析,例如解析json字符串为java对象
feign.codec.Encoder 请求参数编码 将请求参数编码,便于通过http请求发送
feign. Contract 支持的注解格式 默认是SpringMVC的注解
feign. Retryer 失败重试机制 请求失败的重试机制,默认是没有,不过会使用Ribbon的重试
一般情况下,默认值就能满足我们使用,如果要自定义时,只需要创建自定义的@Bean覆盖默认Bean即可。
基于配置文件修改feign的日志级别可以针对单个服务:

1
2
3
4
5
feign:  
client:
config:
userservice: # 针对某个微服务的配置
loggerLevel: FULL # 日志级别

也可以针对所有服务:

1
2
3
4
5
feign:  
client:
config:
default: # 这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
loggerLevel: FULL # 日志级别

而日志的级别分为四种:

  • NONE:不记录任何日志信息,这是默认值。
  • BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
  • HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
  • FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。

7.2.2.Java代码方式
也可以基于Java代码来修改日志级别,先声明一个类,然后声明一个Logger.Level的对象:

1
2
3
4
5
6
7
public class DefaultFeignConfiguration  {
@Bean
public Logger.Level feignLogLevel(){
return Logger.Level.BASIC; // 日志级别为BASIC
}
}
如果要全局生效,将其放到启动类的@EnableFeignClients这个注解中:

@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration .class)
1
如果是局部生效,则把它放到接口对应的@FeignClient这个注解中:

@FeignClient(value = “userservice”, configuration = DefaultFeignConfiguration .class)
1
7.3.Feign使用优化
Feign底层发起http请求,依赖于其它的框架。其底层客户端实现包括:

•URLConnection:默认实现,不支持连接池

•Apache HttpClient :支持连接池

•OKHttp:支持连接池

因此提高Feign的性能主要手段就是使用连接池代替默认的URLConnection。

这里我们用Apache的HttpClient来演示。

1)引入依赖

在order-service的pom文件中引入Apache的HttpClient依赖:

1
2
3
4
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>

2)配置连接池

在order-service的application.yml中添加配置:

1
2
3
4
5
6
7
8
9
feign:
client:
config:
default: # default全局的配置
loggerLevel: BASIC # 日志级别,BASIC就是基本的请求和响应信息
httpclient:
enabled: true # 开启feign对HttpClient的支持
max-connections: 200 # 最大的连接数
max-connections-per-route: 50 # 每个路径的最大连接数

总结,Feign的优化:

1.日志级别尽量用basic

2.使用HttpClient或OKHttp代替URLConnection

① 引入feign-httpClient依赖

② 配置文件开启httpClient功能,设置连接池参数

7.4.最佳实践

所谓最近实践,就是使用过程中总结的经验,最好的一种使用方式。

自习观察可以发现,Feign的客户端与服务提供者的controller代码非常相似:

feign客户端:
在这里插入图片描述

UserController:

在这里插入图片描述

有没有一种办法简化这种重复的代码编写呢?

7.4.1.继承方式

一样的代码可以通过继承来共享:

1)定义一个API接口,利用定义方法,并基于SpringMVC注解做声明。

2)Feign客户端和Controller都集成改接口

在这里插入图片描述

优点:

  • 简单
  • 实现了代码共享

缺点:

  • 服务提供方、服务消费方紧耦合
  • 参数列表中的注解映射并不会继承,因此Controller中必须再次声明方法、参数列表、注解

7.4.2.抽取方式

将Feign的Client抽取为独立模块,并且把接口有关的POJO、默认的Feign配置都放到这个模块中,提供给所有消费者使用。

例如,将UserClient、User、Feign的默认配置都抽取到一个feign-api包中,所有微服务引用该依赖包,即可直接使用。

img

7.4.3.实现基于抽取的最佳实践

1)抽取

首先创建一个module,命名为feign-api:

image-20210714204557771

项目结构:在这里插入图片描述

在feign-api中然后引入feign的starter依赖

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

然后,order-service中编写的UserClient、User、DefaultFeignConfiguration都复制到feign-api项目中

在这里插入图片描述

2)在order-service中使用feign-api

首先,删除order-service中的UserClient、User、DefaultFeignConfiguration等类或接口。

在order-service的pom文件中中引入feign-api的依赖:

1
2
3
4
5
<dependency>
<groupId>cn.itcast.demo</groupId>
<artifactId>feign-api</artifactId>
<version>1.0</version>
</dependency>

修改order-service中的所有与上述三个组件有关的导包部分,改成导入feign-api中的包

3)重启测试

重启后,发现服务报错了:

image-20210714205623048

这是因为UserClient现在在cn.itcast.feign.clients包下,

而order-service的@EnableFeignClients注解是在cn.itcast.order包下,不在同一个包,无法扫描到UserClient。

4)解决扫描包问题

方式一:

指定Feign应该扫描的包:

1
@EnableFeignClients(basePackages = "cn.itcast.feign.clients")

方式二:

指定需要加载的Client接口:

1
@EnableFeignClients(clients = {UserClient.class})

8.Gateway服务网关

Spring Cloud Gateway 是 Spring Cloud 的一个全新项目,该项目是基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等响应式编程和事件流技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。

8.1.为什么需要网关

Gateway网关是我们服务的守门神,所有微服务的统一入口。

网关的核心功能特性:

  • 请求路由
  • 权限控制
  • 限流

权限控制:网关作为微服务入口,需要校验用户是是否有请求资格,如果没有则进行拦截。

路由和负载均衡:一切请求都必须先经过gateway,但网关不处理业务,而是根据某种规则,把请求转发到某个微服务,这个过程叫做路由。当然路由的目标服务有多个时,还需要做负载均衡。

限流:当请求流量过高时,在网关中按照下流的微服务能够接受的速度来放行请求,避免服务压力过大。

在SpringCloud中网关的实现包括两种:

gateway

zuul

Zuul是基于Servlet的实现,属于阻塞式编程。而SpringCloudGateway则是基于Spring5中提供的WebFlux,属于响应式编程的实现,具备更好的性能。

8.2.gateway快速入门
下面,我们就演示下网关的基本路由功能。基本步骤如下:

创建SpringBoot工程gateway,引入网关依赖
编写启动类
编写基础配置和路由规则
启动网关服务进行测试
1)创建gateway服务,引入依赖创建服务:

在这里插入图片描述

引入依赖:

1
2
3
4
5
6
7
8
9
10
<!--网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--nacos服务发现依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
package cn.itcast.gateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class GatewayApplication {

public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
server:
port: 10010 # 网关端口
spring:
application:
name: gateway # 服务名称
cloud:
nacos:
server-addr: localhost:8848 # nacos地址
gateway:
routes: # 网关路由配置
- id: user-service # 路由id,自定义,只要唯一即可
# uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
uri: lb://userservice # 路由的目标地址 lb就是负载均衡,后面跟服务名称
predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
- Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求

我们将符合Path 规则的一切请求,都代理到 uri参数指定的地址。

本例中,我们将 /user/**开头的请求,代理到lb://userservice,lb是负载均衡,根据服务名拉取服务列表,实现负载均衡。

4)重启测试

重启网关,访问http://localhost:10010/user/1时,符合`/user/**`规则,请求转发到uri:http://userservice/user/1,得到了结果:

image-20210714211908341

5)网关路由的流程图

整个访问的流程如下:

img

总结:网关搭建步骤:

创建项目,引入nacos服务发现和gateway依赖

配置application.yml,包括服务基本信息、nacos地址、路由

路由配置包括:

路由id:路由的唯一标示

路由目标(uri):路由的目标地址,http代表固定地址,lb代表根据服务名负载均衡

路由断言(predicates):判断路由的规则,

路由过滤器(filters):对请求或响应做处理

8.3.断言工厂

我们在配置文件中写的断言规则只是字符串,这些字符串会被Predicate Factory读取并处理,转变为路由判断的条件

例如Path=/user/**是按照路径匹配,这个规则是由

org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory类来

处理的,像这样的断言工厂在SpringCloudGateway还有十几个:
image-20230314112157208

8.4.过滤器工厂

GatewayFilter是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理:

image-20210714212312871

8.4.1.路由过滤器的种类

image-20230314112227794

8.4.2.请求头过滤器

下面我们以AddRequestHeader 为例来讲解。

需求:给所有进入userservice的请求添加一个请求头:Truth=itcast is freaking awesome!

只需要修改gateway服务的application.yml文件,添加路由过滤即可:

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://userservice
predicates:
- Path=/user/**
filters: # 过滤器
- AddRequestHeader=Truth, Itcast is freaking awesome! # 添加请求头

8.4.3.默认过滤器

如果要对所有的路由都生效,则可以将过滤器工厂写到default下。格式如下:

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://userservice
predicates:
- Path=/user/**
default-filters: # 默认过滤项
- AddRequestHeader=Truth, Itcast is freaking awesome!

8.4.4.总结

过滤器的作用是什么?

① 对路由的请求或响应做加工处理,比如添加请求头

② 配置在路由下的过滤器只对当前路由的请求生效

defaultFilters的作用是什么?

① 对所有路由都生效的过滤器

8.5.全局过滤器

上一节学习的过滤器,网关提供了31种,但每一种过滤器的作用都是固定的。如果我们希望拦截请求,做自己的业务逻辑则没办法实现。

8.5.1.全局过滤器作用

全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与GatewayFilter的作用一样。区别在于GatewayFilter通过配置定义,处理逻辑是固定的;而GlobalFilter的逻辑需要自己写代码实现。

定义方式是实现GlobalFilter接口。

1
2
3
4
5
6
7
8
9
10
public interface GlobalFilter {
/**
* 处理当前请求,有必要的话通过{@link GatewayFilterChain}将请求交给下一个过滤器处理
*
* @param exchange 请求上下文,里面可以获取Request、Response等信息
* @param chain 用来把请求委托给下一个过滤器
* @return {@code Mono<Void>} 返回标示当前过滤器业务结束
*/
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
}

在filter中编写自定义逻辑,可以实现下列功能:

登录状态判断
权限校验
请求限流等

8.5.2.自定义全局过滤器

需求:定义全局过滤器,拦截请求,判断请求的参数是否满足下面条件:

参数中是否有authorization,
authorization参数值是否为admin
如果同时满足则放行,否则拦截

实现:

在gateway中定义一个过滤器:

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
package cn.itcast.gateway.filters;

import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Order(-1)
@Component
public class AuthorizeFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1.获取请求参数
MultiValueMap<String, String> params = exchange.getRequest().getQueryParams();
// 2.获取authorization参数
String auth = params.getFirst("authorization");
// 3.校验
if ("admin".equals(auth)) {
// 放行
return chain.filter(exchange);
}
// 4.拦截
// 4.1.禁止访问,设置状态码
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
// 4.2.结束处理
return exchange.getResponse().setComplete();
}
}

8.5.3.过滤器执行顺序

请求进入网关会碰到三类过滤器:当前路由的过滤器、DefaultFilter、GlobalFilter
请求路由后,会将当前路由过滤器和DefaultFilter、GlobalFilter,合并到一个过滤器链(集合)中,排序后依次执行每个过滤器:

在这里插入图片描述

排序的规则是什么呢?

每一个过滤器都必须指定一个int类型的order值,order值越小,优先级越高,执行顺序越靠前。

GlobalFilter通过实现Ordered接口,或者添加@Order注解来指定order值,由我们自己指定

路由过滤器和defaultFilter的order由Spring指定,默认是按照声明顺序从1递增。

当过滤器的order值一样时,会按照 defaultFilter > 路由过滤器 > GlobalFilter的顺序执行。

8.6.跨域问题

8.6.1.什么是跨域问题

跨域:域名不一致就是跨域,主要包括:

域名不同: www.taobao.comwww.taobao.orgwww.jd.com 和 miaosha.jd.com

域名相同,端口不同:localhost:8080和localhost8081

跨域问题:浏览器禁止请求的发起者与服务端发生跨域ajax请求,请求被浏览器拦截的问题
解决方案:CORS,这个以前应该学习过,这里不再赘述了。放入tomcat或者nginx这样的web服务器中,启动并访问。

8.6.2.模拟跨域问题

可以在浏览器控制台看到下面的错误:

img

从localhost:8090访问localhost:10010,端口不同,显然是跨域的请求。

8.6.3.解决跨域问题

在gateway服务的application.yml文件中,添加下面的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
spring:
cloud:
gateway:

globalcors: # 全局的跨域处理
add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
corsConfigurations:
'[/**]':
allowedOrigins: # 允许哪些网站的跨域请求
- "http://localhost:8090"
allowedMethods: # 允许的跨域ajax的请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" # 允许在请求中携带的头信息
allowCredentials: true # 是否允许携带cookie
maxAge: 360000 # 这次跨域检测的有效期

10、MQ(Message Queue)消息队列

10.1 概述

事件驱动架构的概念:
MQ是事件驱动架构的实现形式,MQ其实就是事件驱动架构的Broker。

异步应用场景:
如果是传统软件行业:虽然不需要太高并发,但是涉及到和其它系统做对接,我方系统处理速度(50ms)远快于对方系统处理速度(1-3s),为了兼顾用户的体验,加快单据处理速度,故引入MQ。
用户只用点击我方系统的按钮,我方按钮发送到MQ即可给用户返回处理成功信息。背后交由对方系统做处理即可。至于处理失败,补偿机制就不是用户体验要考虑的事情了,这样可以大大提升用户体验。

异步通讯优缺点:

  • 优点:

    • 耦合度低

    • 吞吐量提升

    • 故障隔离

    • 流量削峰

    • 缺点:

      • 依赖于MQ的可靠性,安全性,吞吐能力(因为加了一层MQ,当然高度依赖它)
      • 业务复杂了,业务没有明显的流程线,不好追踪管理
  • MQ常见技术介绍:

img

10.3 常见消息模型

10.3.1 简单队列模型

核心代码位置:下图所示
在这里插入图片描述

执行MQ容器的命令和简单说明:

1
2
3
4
5
6
7
8
9
docker run \
-e RABBITMQ_DEFAULT_USER=root \ #用户名
-e RABBITMQ_DEFAULT_PASS=root \ # 密码
--name mq \
--hostname mq1 \ # 主机名,将来做集群部署要用
-p 15672:15672 \ # 端口映射,映射RabbitMQ管理平台端口
-p 5672:5672 \ # 端口映射,消息通信端口
-d \ # 后台运行
rabbitmq:3-management # 镜像名称

最后在浏览器地址栏输入:你的端口号:15672
在这里插入图片描述

10.4 Spring AMQP

概述

AMQP(Advanced Message Queuing Protocol),是用于在应用程序之间传递业务信息的开放标准,该协议与语言和平台无关,更符合微服务中独立性的要求

SpringAMQP就是Spring基于AMQP定义的一套API规范。

使用Spring AMQP实现简单队列模型步骤:

以生产者为例:

由于这玩意已被spring托管了,所以对比之前rabbitmq demo的方式,不需要在代码里写配置了,直接在spring的application.yml里写配置文件即可.

配置如下:

1
2
3
4
5
6
7
8
9
1.1.设置连接参数,分别是:主机名、端口号、用户名、密码、vhost

spring:
rabbitmq:
host: 127.0.0.1
port: 5672
username: root
password: root
virtual-host: /

然后编写测试类,以及测试代码,位置如下图所示:

在这里插入图片描述
消费者一侧,和生产者类似。不再赘述,如下图进行配置即可:

在这里插入图片描述

至于如何启动消费者 一侧?如下图所示:
在这里插入图片描述

10.3.2 WorkQueue模型

之所以 10.3.2 放在 10.4章,因为demo模型的演示,今后就是以 Spring AMQP为例了

概述
其实就是一个队列,绑定了多个消费者,一条消息只能由一个消费者进行消费,默认情况下,每个消费者是轮询消费的。区别于下文的发布-订阅模型(该模型允许将同一消息发给多消费者)

10.3.3 发布-订阅模型

概念
允许将同一个消息发给多个消费者。
其实就是加了一层交换机而已,如下图所示:许将同一消息发给多消费者)
在这里插入图片描述

在这里插入图片描述

最后,交换机只能做消息的转发而不是存储,如果将来路由(交换机和消息队列queue的连接称作路由)没有成功,消息会丢失

A. Fanout Exchange

在这里插入图片描述

生产者添加代码位置如下图:

在这里插入图片描述

队列绑定成功后,打开mq可视化页面,会看到如下图所示:

在这里插入图片描述

概念
这种模型中生产者发送的消息所有消费者都可以消费。

案例:
在这里插入图片描述

总结:workQueue模式和FanoutQueue模式区别:
P代表生产者,C代表消费者 X代表交换机,红色部分代表消息队列
workQueue:

在这里插入图片描述

FanoutQUeue:
在这里插入图片描述
可以发现,FanoutQueue增加了一层交换机,可以多个队列对应多个消费者。而且比起WorkQueue,FanoutQueue生产者是先发送到交换机; 而WorkQueue是直接发送到队列

B. Direct Exchange
概念:DirectExchange 会将接收到的消息根据规则路由到指定的queue,因此称为路由模式,如下图所示:

在这里插入图片描述

P代表生产者,C代表消费者 X代表交换机,红色部分代表消息队列

每一个queue都会与Exchange设置一个BindingKey
将来发布者发布消息时,会指定消息的RoutingKey
Exchange将消息路由到BingingKey与RoutingKey一致的队列
实际应用时,可以绑定多个key。
如果所有queue和所有Exchange绑定了一样的key,那生产者所有符合key的消息消费者都会消费。如果这样做,那DirectExchange就相当于FanoutExchange了(Direct可以模拟Fanout的全部功能)
案例如图:
在这里插入图片描述

这次的案例,我们用注解的方式声明队列和绑定交换机,之前Fanout的Demo是手写了个配置类。 直接在监听队列里面声明如下图注解即可:
在这里插入图片描述
上图的@QueueBinding点进去:
在这里插入图片描述
上面的key是个数组,可以写多个key。

写完代码后启动消费者的SpiringBoot主启动类(报错信息不用管),然后进入rabbitMQ可视化控制台,出现下图则说明配置成功:
在这里插入图片描述
随后运行发送队列的Test代码,打开消费者的控制台,出现如下图输出,则说明案例测试通过:

C. Topic Exchange
概念: 和上面的Direct Exchange及其相似:

在这里插入图片描述

(下图来源于Java旅途 ,作者大尧)

案例:在这里插入图片描述

发送队列、消费者的添加代码位置和上面的DirectExchange位置一致,就在DirectExchange代码下面。

写完代码后启动消费者的SpiringBoot主启动类(报错信息不用管),然后进入rabbitMQ可视化控制台,出现下图则说明配置成功:

在这里插入图片描述

10.3.4 消息转换器

引入:

在之前的案例中,我们发送到队列的都是String类型,但是实际上,我们可以往消息队列中扔进去任何类型。我们看下图,convertAndSend这个方法,第三个参数也是Object。这说明可以发送任何类型给消息队列:
在这里插入图片描述

案例:

创建一个队列,向该队列扔一个任意对象(Object类型)

创建队列位置、发送队列的添加代码位置如下图
创建队列位置:
在这里插入图片描述
发送:

在这里插入图片描述

写完代码后启动发送的Test,去看RabbitMQ控制台,发现我们发过来的对象在内部被序列化(ObjectOutPutStream)了,如下图所示:

在这里插入图片描述

上面说的ObjectOutPutStream这个序列化方式,缺点很多(性能差、长度太长、安全性有问题)。我们可以在这里调优一下,推荐JSON的序列化方式。于是引出了这一节的正文:自定义消息转换器(覆盖了原有的Bean配置):

在这里插入图片描述

声明配置位置如下图

在这里插入图片描述
配置了消息转换器转换成json,然后重复之前的步骤,使用发送者发送一条消息到队列,发送完成后打开RabbitMQ控制台,出现如下图所示:

在这里插入图片描述

该对象被成功序列为json格式了!!!!!

  • 对刚才发送过来的json格式消息进行接收,需要修改消费者一侧的代码。并不复杂,如下图所示:
    在这里插入图片描述
    消费者配置、监听消息位置如下2图:在这里插入图片描述

总结:

  • 消息序列化和反序列化使用MessageConverter实现
  • SpringAMQP的消息序列化默认底层是使用JDK的序列化
  • 我们可以手动配置成其它的序列化方式(覆盖MessageConverter配置Bean),推荐json
  • 发送方和接收方必须使用相同的MessageConverter

11、分布式搜索引擎01

– elasticsearch基础

1.1.了解ES

1.1.1.elasticsearch的作用

elasticsearch是一款非常强大的开源搜索引擎,具备非常多强大功能,可以帮助我们从海量数据中快速找到需要的内容

1.1.2.ELK技术栈

elasticsearch结合kibana、Logstash、Beats,也就是elastic stack(ELK)。被广泛应用在日志数据分析、实时监控等领域:

image-20210720194008781

而elasticsearch是elastic stack的核心,负责存储、搜索、分析数据。

image-20210720194230265

1.1.3.elasticsearch和lucene

elasticsearch底层是基于lucene来实现的。

1.1.4.总结

什么是elasticsearch?

  • 一个开源的分布式搜索引擎,可以用来实现搜索、日志统计、分析、系统监控等功能

什么是elastic stack(ELK)?

  • 是以elasticsearch为核心的技术栈,包括beats、Logstash、kibana、elasticsearch

什么是Lucene?

  • 是Apache的开源搜索引擎类库,提供了搜索引擎的核心API

1.2.倒排索引

倒排索引的概念是基于MySQL这样的正向索引而言的。

1.2.1.正向索引

那么什么是正向索引呢?例如给下表(tb_goods)中的id创建索引:

image-20210720195531539

如果是根据id查询,那么直接走索引,查询速度非常快。

但如果是基于title做模糊查询,只能是逐行扫描数据,流程如下:

1)用户搜索数据,条件是title符合"%手机%"

2)逐行获取数据,比如id为1的数据

3)判断数据中的title是否符合用户搜索条件

4)如果符合则放入结果集,不符合则丢弃。回到步骤1

逐行扫描,也就是全表扫描,随着数据量增加,其查询效率也会越来越低。当数据量达到数百万时,就是一场灾难。

1.2.2.倒排索引

倒排索引中有两个非常重要的概念:

  • 文档(Document):用来搜索的数据,其中的每一条数据就是一个文档。例如一个网页、一个商品信息
  • 词条(Term):对文档数据或用户搜索数据,利用某种算法分词,得到的具备含义的词语就是词条。例如:我是中国人,就可以分为:我、是、中国人、中国、国人这样的几个词条

创建倒排索引是对正向索引的一种特殊处理,流程如下:

  • 将每一个文档的数据利用算法分词,得到一个个词条
  • 创建表,每行数据包括词条、词条所在文档id、位置等信息
  • 因为词条唯一性,可以给词条创建索引,例如hash表结构索引

如图:

image-20210720200457207

倒排索引的搜索流程如下(以搜索”华为手机”为例):

1)用户输入条件"华为手机"进行搜索。

2)对用户输入内容分词,得到词条:华为手机

3)拿着词条在倒排索引中查找,可以得到包含词条的文档id:1、2、3。

4)拿着文档id到正向索引中查找具体文档。

如图:

image-20210720201115192

虽然要先查询倒排索引,再查询倒排索引,但是无论是词条、还是文档id都建立了索引,查询速度非常快!无需全表扫描。

1.2.3.正向和倒排

那么为什么一个叫做正向索引,一个叫做倒排索引呢?

  • 正向索引是最传统的,根据id索引的方式。但根据词条查询时,必须先逐条获取每个文档,然后判断文档中是否包含所需要的词条,是根据文档找词条的过程

  • 倒排索引则相反,是先找到用户要搜索的词条,根据词条得到保护词条的文档的id,然后根据id获取文档。是根据词条找文档的过程

是不是恰好反过来了?

那么两者方式的优缺点是什么呢?

正向索引

  • 优点:
    • 可以给多个字段创建索引
    • 根据索引字段搜索、排序速度非常快
  • 缺点:
    • 根据非索引字段,或者索引字段中的部分词条查找时,只能全表扫描。

倒排索引

  • 优点:
    • 根据词条搜索、模糊搜索时,速度非常快
  • 缺点:
    • 只能给词条创建索引,而不是字段
    • 无法根据字段做排序

1.3.es的一些概念

elasticsearch中有很多独有的概念,与mysql中略有差别,但也有相似之处。

1.3.1.文档和字段

elasticsearch是面向文档(Document)存储的,可以是数据库中的一条商品数据,一个订单信息。文档数据会被序列化为json格式后存储在elasticsearch中:

image-20210720202707797

而Json文档中往往包含很多的字段(Field),类似于数据库中的列。

1.3.2.索引和映射

索引(Index),就是相同类型的文档的集合。

例如:

  • 所有用户文档,就可以组织在一起,称为用户的索引;
  • 所有商品的文档,可以组织在一起,称为商品的索引;
  • 所有订单的文档,可以组织在一起,称为订单的索引;

image-20210720203022172

因此,我们可以把索引当做是数据库中的表。

数据库的表会有约束信息,用来定义表的结构、字段的名称、类型等信息。因此,索引库中就有映射(mapping),是索引中文档的字段约束信息,类似表的结构约束。

1.3.3.mysql与elasticsearch

我们统一的把mysql与elasticsearch的概念做一下对比:

MySQL Elasticsearch 说明
Table Index 索引(index),就是文档的集合,类似数据库的表(table)
Row Document 文档(Document),就是一条条的数据,类似数据库中的行(Row),文档都是JSON格式
Column Field 字段(Field),就是JSON文档中的字段,类似数据库中的列(Column)
Schema Mapping Mapping(映射)是索引中文档的约束,例如字段类型约束。类似数据库的表结构(Schema)
SQL DSL DSL是elasticsearch提供的JSON风格的请求语句,用来操作elasticsearch,实现CRUD

是不是说,我们学习了elasticsearch就不再需要mysql了呢?

并不是如此,两者各自有自己的擅长支出:

  • Mysql:擅长事务类型操作,可以确保数据的安全和一致性

  • Elasticsearch:擅长海量数据的搜索、分析、计算

因此在企业中,往往是两者结合使用:

  • 对安全性要求较高的写操作,使用mysql实现
  • 对查询性能要求较高的搜索需求,使用elasticsearch实现
  • 两者再基于某种方式,实现数据的同步,保证一致性

image-20210720203534945

1.4.总结

分词器的作用是什么?

  • 创建倒排索引时对文档分词
  • 用户搜索时,对输入的内容分词

IK分词器有几种模式?

  • ik_smart:智能切分,粗粒度
  • ik_max_word:最细切分,细粒度

IK分词器如何拓展词条?如何停用词条?

  • 利用config目录的IkAnalyzer.cfg.xml文件添加拓展词典和停用词典
  • 在词典中添加拓展词条或者停用词条

2.索引库操作

索引库就类似数据库表,mapping映射就类似表的结构。

我们要向es中存储数据,必须先创建“库”和“表”。

2.1.mapping映射属性

mapping是对索引库中文档的约束,常见的mapping属性包括:

  • type:字段数据类型,常见的简单类型有:
    • 字符串:text(可分词的文本)、keyword(精确值,例如:品牌、国家、ip地址)
    • 数值:long、integer、short、byte、double、float、
    • 布尔:boolean
    • 日期:date
    • 对象:object
  • index:是否创建索引,默认为true
  • analyzer:使用哪种分词器
  • properties:该字段的子字段

例如下面的json文档:

1
2
3
4
5
6
7
8
9
10
11
12
{
    "age": 21,
    "weight": 52.1,
    "isMarried": false,
    "info": "黑马程序员Java讲师",
"email": "zy@itcast.cn",
"score": [99.1, 99.5, 98.9],
    "name": {
        "firstName": "云",
        "lastName": "赵"
    }
}

对应的每个字段映射(mapping):

  • age:类型为 integer;参与搜索,因此需要index为true;无需分词器
  • weight:类型为float;参与搜索,因此需要index为true;无需分词器
  • isMarried:类型为boolean;参与搜索,因此需要index为true;无需分词器
  • info:类型为字符串,需要分词,因此是text;参与搜索,因此需要index为true;分词器可以用ik_smart
  • email:类型为字符串,但是不需要分词,因此是keyword;不参与搜索,因此需要index为false;无需分词器
  • score:虽然是数组,但是我们只看元素的类型,类型为float;参与搜索,因此需要index为true;无需分词器
  • name:类型为object,需要定义多个子属性
    • name.firstName;类型为字符串,但是不需要分词,因此是keyword;参与搜索,因此需要index为true;无需分词器
    • name.lastName;类型为字符串,但是不需要分词,因此是keyword;参与搜索,因此需要index为true;无需分词器

2.2.索引库的CRUD

这里我们统一使用Kibana编写DSL的方式来演示。

2.2.1.创建索引库和映射

基本语法:

  • 请求方式:PUT
  • 请求路径:/索引库名,可以自定义
  • 请求参数:mapping映射

格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
PUT /索引库名称
{
  "mappings": {
    "properties": {
      "字段名":{
        "type": "text",
        "analyzer": "ik_smart"
      },
      "字段名2":{
        "type": "keyword",
        "index": "false"
      },
      "字段名3":{
        "properties": {
          "子字段": {
            "type": "keyword"
          }
        }
      },
// ...略
    }
  }
}

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
PUT /heima
{
  "mappings": {
    "properties": {
      "info":{
        "type""text",
        "analyzer""ik_smart"
      },
      "email":{
        "type""keyword",
        "index""falsae"
      },
      "name":{
        "properties": {
          "firstName": {
            "type""keyword"
          }
        }
      },
// ... 略
    }
  }
}

2.2.2.查询索引库

基本语法

  • 请求方式:GET

  • 请求路径:/索引库名

  • 请求参数:无

格式

1
GET /索引库名

示例

image-20210720211019329

2.2.3.修改索引库

倒排索引结构虽然不复杂,但是一旦数据结构改变(比如改变了分词器),就需要重新创建倒排索引,这简直是灾难。因此索引库一旦创建,无法修改mapping

虽然无法修改mapping中已有的字段,但是却允许添加新的字段到mapping中,因为不会对倒排索引产生影响。

语法说明

1
2
3
4
5
6
7
8
PUT /索引库名/_mapping
{
  "properties": {
    "新字段名":{
      "type": "integer"
    }
  }
}

示例

image-20210720212357390

2.2.4.删除索引库

语法:

  • 请求方式:DELETE

  • 请求路径:/索引库名

  • 请求参数:无

格式:

1
DELETE /索引库名

在kibana中测试:

image-20210720212123420

2.2.5.总结

索引库操作有哪些?

  • 创建索引库:PUT /索引库名
  • 查询索引库:GET /索引库名
  • 删除索引库:DELETE /索引库名
  • 添加字段:PUT /索引库名/_mapping

3.文档操作

3.1.新增文档

语法:

1
2
3
4
5
6
7
8
9
10
POST /索引库名/_doc/文档id
{
    "字段1": "值1",
    "字段2": "值2",
    "字段3": {
        "子属性1": "值3",
        "子属性2": "值4"
    },
// ...
}

示例:

1
2
3
4
5
6
7
8
9
POST /heima/_doc/1
{
    "info": "黑马程序员Java讲师",
    "email": "zy@itcast.cn",
    "name": {
        "firstName": "云",
        "lastName": "赵"
    }
}

响应:

image-20210720212933362

3.2.查询文档

根据rest风格,新增是post,查询应该是get,不过查询一般都需要条件,这里我们把文档id带上。

语法:

1
GET /{索引库名称}/_doc/{id}

通过kibana查看数据:

1
GET /heima/_doc/1

查看结果:

image-20210720213345003

3.3.删除文档

删除使用DELETE请求,同样,需要根据id进行删除:

语法:

1
DELETE /{索引库名}/_doc/id值

示例:

1
2
# 根据id删除数据
DELETE /heima/_doc/1

结果:

image-20210720213634918

3.4.修改文档

修改有两种方式:

  • 全量修改:直接覆盖原来的文档
  • 增量修改:修改文档中的部分字段
3.4.1.全量修改

全量修改是覆盖原来的文档,其本质是:

  • 根据指定的id删除文档
  • 新增一个相同id的文档

注意:如果根据id删除时,id不存在,第二步的新增也会执行,也就从修改变成了新增操作了。

语法:

1
2
3
4
5
6
7
PUT /{索引库名}/_doc/文档id
{
    "字段1": "值1",
    "字段2": "值2",
// ... 略
}

示例:

1
2
3
4
5
6
7
8
9
PUT /heima/_doc/1
{
    "info": "黑马程序员高级Java讲师",
    "email": "zy@itcast.cn",
    "name": {
        "firstName": "云",
        "lastName": "赵"
    }
}
3.4.2.增量修改

增量修改是只修改指定id匹配的文档中的部分字段。

语法:

1
2
3
4
5
6
POST /{索引库名}/_update/文档id
{
    "doc": {
"字段名": "新的值",
}
}

示例:

1
2
3
4
5
6
POST /heima/_update/1
{
  "doc": {
    "email": "ZhaoYun@itcast.cn"
  }
}

3.5.总结

文档操作有哪些?

  • 创建文档:POST /{索引库名}/_doc/文档id { json文档 }
  • 查询文档:GET /{索引库名}/_doc/文档id
  • 删除文档:DELETE /{索引库名}/_doc/文档id
  • 修改文档:
    • 全量修改:PUT /{索引库名}/_doc/文档id { json文档 }
    • 增量修改:POST /{索引库名}/_update/文档id { “doc”: {字段}}

4.RestAPI

ES官方提供了各种不同语言的客户端,用来操作ES。这些客户端的本质就是组装DSL语句,通过http请求发送给ES。官方文档地址:https://www.elastic.co/guide/en/elasticsearch/client/index.html

其中的Java Rest Client又包括两种:

  • Java Low Level Rest Client
  • Java High Level Rest Client

image-20210720214555863

我们学习的是Java HighLevel Rest Client客户端API

4.0.导入Demo工程

4.0.1.导入数据

首先导入课前资料提供的数据库数据:

image-20210720220400297

数据结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CREATE TABLE `tb_hotel` (
  `id` bigint(20NOT NULL COMMENT '酒店id',
  `name` varchar(255NOT NULL COMMENT '酒店名称;例:7天酒店',
  `address` varchar(255NOT NULL COMMENT '酒店地址;例:航头路',
  `price` int(10NOT NULL COMMENT '酒店价格;例:329',
  `score` int(2NOT NULL COMMENT '酒店评分;例:45,就是4.5分',
  `brand` varchar(32NOT NULL COMMENT '酒店品牌;例:如家',
  `city` varchar(32NOT NULL COMMENT '所在城市;例:上海',
  `star_name` varchar(16DEFAULT NULL COMMENT '酒店星级,从低到高分别是:1星到5星,1钻到5钻',
  `business` varchar(255DEFAULT NULL COMMENT '商圈;例:虹桥',
  `latitude` varchar(32NOT NULL COMMENT '纬度;例:31.2497',
  `longitude` varchar(32NOT NULL COMMENT '经度;例:120.3925',
  `pic` varchar(255DEFAULT NULL COMMENT '酒店图片;例:/img/1.jpg',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
4.0.2.导入项目

然后导入课前资料提供的项目:

image-20210720220503411

项目结构如图:

image-20210720220647541

4.0.3.mapping映射分析

创建索引库,最关键的是mapping映射,而mapping映射要考虑的信息包括:

  • 字段名
  • 字段数据类型
  • 是否参与搜索
  • 是否需要分词
  • 如果分词,分词器是什么?

其中:

  • 字段名、字段数据类型,可以参考数据表结构的名称和类型
  • 是否参与搜索要分析业务来判断,例如图片地址,就无需参与搜索
  • 是否分词呢要看内容,内容如果是一个整体就无需分词,反之则要分词
  • 分词器,我们可以统一使用ik_max_word

来看下酒店数据的索引库结构:

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
PUT /hotel
{
"mappings": {
"properties": {
"id": {
"type": "keyword"
},
"name":{
"type": "text",
"analyzer": "ik_max_word",
"copy_to": "all"
},
"address":{
"type": "keyword",
"index": false
},
"price":{
"type": "integer"
},
"score":{
"type": "integer"
},
"brand":{
"type": "keyword",
"copy_to": "all"
},
"city":{
"type": "keyword",
"copy_to": "all"
},
"starName":{
"type": "keyword"
},
"business":{
"type": "keyword"
},
"location":{
"type": "geo_point"
},
"pic":{
"type": "keyword",
"index": false
},
"all":{
"type": "text",
"analyzer": "ik_max_word"
}
}
}
}

几个特殊字段说明:

  • location:地理坐标,里面包含精度、纬度
  • all:一个组合字段,其目的是将多字段的值 利用copy_to合并,提供给用户搜索

地理坐标说明:

image-20210720222110126

copy_to说明:

image-20210720222221516

4.0.4.初始化RestClient

在elasticsearch提供的API中,与elasticsearch一切交互都封装在一个名为RestHighLevelClient的类中,必须先完成这个对象的初始化,建立与elasticsearch的连接。

分为三步:

1)引入es的RestHighLevelClient依赖:

1
2
3
4
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>

2)因为SpringBoot默认的ES版本是7.6.2,所以我们需要覆盖默认的ES版本:

1
2
3
4
<properties>
<java.version>1.8</java.version>
<elasticsearch.version>7.12.1</elasticsearch.version>
</properties>

3)初始化RestHighLevelClient:

初始化的代码如下:

1
2
3
RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.150.101:9200")
));

这里为了单元测试方便,我们创建一个测试类HotelIndexTest,然后将初始化的代码编写在@BeforeEach方法中:

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
package cn.itcast.hotel;

import org.apache.http.HttpHost;
import org.elasticsearch.client.RestHighLevelClient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.io.IOException;

public class HotelIndexTest {
private RestHighLevelClient client;

@BeforeEach
void setUp() {
this.client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.150.101:9200")
));
}

@AfterEach
void tearDown() throws IOException {
this.client.close();
}
}

4.1.创建索引库

4.1.1.代码解读

创建索引库的API如下:

image-20210720223049408

代码分为三步:

  • 1)创建Request对象。因为是创建索引库的操作,因此Request是CreateIndexRequest。
  • 2)添加请求参数,其实就是DSL的JSON参数部分。因为json字符串很长,这里是定义了静态字符串常量MAPPING_TEMPLATE,让代码看起来更加优雅。
  • 3)发送请求,client.indices()方法的返回值是IndicesClient类型,封装了所有与索引库操作有关的方法。
4.1.2.完整示例

在hotel-demo的cn.itcast.hotel.constants包下,创建一个类,定义mapping映射的JSON字符串常量:

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
package cn.itcast.hotel.constants;

public class HotelConstants {
public static final String MAPPING_TEMPLATE = "{\n" +
" \"mappings\": {\n" +
" \"properties\": {\n" +
" \"id\": {\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"name\":{\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_max_word\",\n" +
" \"copy_to\": \"all\"\n" +
" },\n" +
" \"address\":{\n" +
" \"type\": \"keyword\",\n" +
" \"index\": false\n" +
" },\n" +
" \"price\":{\n" +
" \"type\": \"integer\"\n" +
" },\n" +
" \"score\":{\n" +
" \"type\": \"integer\"\n" +
" },\n" +
" \"brand\":{\n" +
" \"type\": \"keyword\",\n" +
" \"copy_to\": \"all\"\n" +
" },\n" +
" \"city\":{\n" +
" \"type\": \"keyword\",\n" +
" \"copy_to\": \"all\"\n" +
" },\n" +
" \"starName\":{\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"business\":{\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"location\":{\n" +
" \"type\": \"geo_point\"\n" +
" },\n" +
" \"pic\":{\n" +
" \"type\": \"keyword\",\n" +
" \"index\": false\n" +
" },\n" +
" \"all\":{\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_max_word\"\n" +
" }\n" +
" }\n" +
" }\n" +
"}";
}

在hotel-demo中的HotelIndexTest测试类中,编写单元测试,实现创建索引:

1
2
3
4
5
6
7
8
9
@Test
void createHotelIndex() throws IOException {
// 1.创建Request对象
CreateIndexRequest request = new CreateIndexRequest("hotel");
// 2.准备请求的参数:DSL语句
request.source(MAPPING_TEMPLATE, XContentType.JSON);
// 3.发送请求
client.indices().create(request, RequestOptions.DEFAULT);
}

4.2.删除索引库

删除索引库的DSL语句非常简单:

1
DELETE /hotel

与创建索引库相比:

  • 请求方式从PUT变为DELTE
  • 请求路径不变
  • 无请求参数

所以代码的差异,注意体现在Request对象上。依然是三步走:

  • 1)创建Request对象。这次是DeleteIndexRequest对象
  • 2)准备参数。这里是无参
  • 3)发送请求。改用delete方法

在hotel-demo中的HotelIndexTest测试类中,编写单元测试,实现删除索引:

1
2
3
4
5
6
7
@Test
void testDeleteHotelIndex() throws IOException {
// 1.创建Request对象
DeleteIndexRequest request = new DeleteIndexRequest("hotel");
// 2.发送请求
client.indices().delete(request, RequestOptions.DEFAULT);
}

4.3.判断索引库是否存在

判断索引库是否存在,本质就是查询,对应的DSL是:

1
GET /hotel

因此与删除的Java代码流程是类似的。依然是三步走:

  • 1)创建Request对象。这次是GetIndexRequest对象
  • 2)准备参数。这里是无参
  • 3)发送请求。改用exists方法
1
2
3
4
5
6
7
8
9
@Test
void testExistsHotelIndex() throws IOException {
// 1.创建Request对象
GetIndexRequest request = new GetIndexRequest("hotel");
// 2.发送请求
boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
// 3.输出
System.err.println(exists ? "索引库已经存在!" : "索引库不存在!");
}

4.4.总结

JavaRestClient操作elasticsearch的流程基本类似。核心是client.indices()方法来获取索引库的操作对象。

索引库操作的基本步骤:

  • 初始化RestHighLevelClient
  • 创建XxxIndexRequest。XXX是Create、Get、Delete
  • 准备DSL( Create时需要,其它是无参)
  • 发送请求。调用RestHighLevelClient#indices().xxx()方法,xxx是create、exists、delete

5.RestClient操作文档

为了与索引库操作分离,我们再次参加一个测试类,做两件事情:

  • 初始化RestHighLevelClient
  • 我们的酒店数据在数据库,需要利用IHotelService去查询,所以注入这个接口
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
package cn.itcast.hotel;

import cn.itcast.hotel.pojo.Hotel;
import cn.itcast.hotel.service.IHotelService;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.io.IOException;
import java.util.List;

@SpringBootTest
public class HotelDocumentTest {
@Autowired
private IHotelService hotelService;

private RestHighLevelClient client;

@BeforeEach
void setUp() {
this.client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.150.101:9200")
));
}

@AfterEach
void tearDown() throws IOException {
this.client.close();
}
}

5.1.新增文档

我们要将数据库的酒店数据查询出来,写入elasticsearch中。

5.1.1.索引库实体类

数据库查询后的结果是一个Hotel类型的对象。结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Data
@TableName("tb_hotel")
public class Hotel {
@TableId(type = IdType.INPUT)
private Long id;
private String name;
private String address;
private Integer price;
private Integer score;
private String brand;
private String city;
private String starName;
private String business;
private String longitude;
private String latitude;
private String pic;
}

与我们的索引库结构存在差异:

  • longitude和latitude需要合并为location

因此,我们需要定义一个新的类型,与索引库结构吻合:

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
package cn.itcast.hotel.pojo;

import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
public class HotelDoc {
private Long id;
private String name;
private String address;
private Integer price;
private Integer score;
private String brand;
private String city;
private String starName;
private String business;
private String location;
private String pic;

public HotelDoc(Hotel hotel) {
this.id = hotel.getId();
this.name = hotel.getName();
this.address = hotel.getAddress();
this.price = hotel.getPrice();
this.score = hotel.getScore();
this.brand = hotel.getBrand();
this.city = hotel.getCity();
this.starName = hotel.getStarName();
this.business = hotel.getBusiness();
this.location = hotel.getLatitude() + ", " + hotel.getLongitude();
this.pic = hotel.getPic();
}
}

5.1.2.语法说明

新增文档的DSL语句如下:

1
2
3
4
5
POST /{索引库名}/_doc/1
{
"name": "Jack",
"age": 21
}

对应的java代码如图:

image-20210720230027240

可以看到与创建索引库类似,同样是三步走:

  • 1)创建Request对象
  • 2)准备请求参数,也就是DSL中的JSON文档
  • 3)发送请求

变化的地方在于,这里直接使用client.xxx()的API,不再需要client.indices()了。

5.1.3.完整代码

我们导入酒店数据,基本流程一致,但是需要考虑几点变化:

  • 酒店数据来自于数据库,我们需要先查询出来,得到hotel对象
  • hotel对象需要转为HotelDoc对象
  • HotelDoc需要序列化为json格式

因此,代码整体步骤如下:

  • 1)根据id查询酒店数据Hotel
  • 2)将Hotel封装为HotelDoc
  • 3)将HotelDoc序列化为JSON
  • 4)创建IndexRequest,指定索引库名和id
  • 5)准备请求参数,也就是JSON文档
  • 6)发送请求

在hotel-demo的HotelDocumentTest测试类中,编写单元测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
void testAddDocument() throws IOException {
// 1.根据id查询酒店数据
Hotel hotel = hotelService.getById(61083L);
// 2.转换为文档类型
HotelDoc hotelDoc = new HotelDoc(hotel);
// 3.将HotelDoc转json
String json = JSON.toJSONString(hotelDoc);

// 1.准备Request对象
IndexRequest request = new IndexRequest("hotel").id(hotelDoc.getId().toString());
// 2.准备Json文档
request.source(json, XContentType.JSON);
// 3.发送请求
client.index(request, RequestOptions.DEFAULT);
}

5.2.查询文档

5.2.1.语法说明

查询的DSL语句如下:

1
GET /hotel/_doc/{id}

非常简单,因此代码大概分两步:

  • 准备Request对象
  • 发送请求

不过查询的目的是得到结果,解析为HotelDoc,因此难点是结果的解析。完整代码如下:

image-20210720230811674

可以看到,结果是一个JSON,其中文档放在一个_source属性中,因此解析就是拿到_source,反序列化为Java对象即可。

与之前类似,也是三步走:

  • 1)准备Request对象。这次是查询,所以是GetRequest
  • 2)发送请求,得到结果。因为是查询,这里调用client.get()方法
  • 3)解析结果,就是对JSON做反序列化
5.2.2.完整代码

在hotel-demo的HotelDocumentTest测试类中,编写单元测试:

1
2
3
4
5
6
7
8
9
10
11
12
@Test
void testGetDocumentById() throws IOException {
// 1.准备Request
GetRequest request = new GetRequest("hotel", "61082");
// 2.发送请求,得到响应
GetResponse response = client.get(request, RequestOptions.DEFAULT);
// 3.解析响应结果
String json = response.getSourceAsString();

HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
System.out.println(hotelDoc);
}

5.3.删除文档

删除的DSL为是这样的:

1
DELETE /hotel/_doc/{id}

与查询相比,仅仅是请求方式从DELETE变成GET,可以想象Java代码应该依然是三步走:

  • 1)准备Request对象,因为是删除,这次是DeleteRequest对象。要指定索引库名和id
  • 2)准备参数,无参
  • 3)发送请求。因为是删除,所以是client.delete()方法

在hotel-demo的HotelDocumentTest测试类中,编写单元测试:

1
2
3
4
5
6
7
@Test
void testDeleteDocument() throws IOException {
// 1.准备Request
DeleteRequest request = new DeleteRequest("hotel", "61083");
// 2.发送请求
client.delete(request, RequestOptions.DEFAULT);
}

5.4.修改文档

5.4.1.语法说明

修改我们讲过两种方式:

  • 全量修改:本质是先根据id删除,再新增
  • 增量修改:修改文档中的指定字段值

在RestClient的API中,全量修改与新增的API完全一致,判断依据是ID:

  • 如果新增时,ID已经存在,则修改
  • 如果新增时,ID不存在,则新增

这里不再赘述,我们主要关注增量修改。

代码示例如图:

image-20210720231040875

与之前类似,也是三步走:

  • 1)准备Request对象。这次是修改,所以是UpdateRequest
  • 2)准备参数。也就是JSON文档,里面包含要修改的字段
  • 3)更新文档。这里调用client.update()方法
5.4.2.完整代码

在hotel-demo的HotelDocumentTest测试类中,编写单元测试:

1
2
3
4
5
6
7
8
9
10
11
12
@Test
void testUpdateDocument() throws IOException {
// 1.准备Request
UpdateRequest request = new UpdateRequest("hotel", "61083");
// 2.准备请求参数
request.doc(
"price", "952",
"starName", "四钻"
);
// 3.发送请求
client.update(request, RequestOptions.DEFAULT);
}

5.5.批量导入文档

案例需求:利用BulkRequest批量将数据库数据导入到索引库中。

步骤如下:

  • 利用mybatis-plus查询酒店数据

  • 将查询到的酒店数据(Hotel)转换为文档类型数据(HotelDoc)

  • 利用JavaRestClient中的BulkRequest批处理,实现批量新增文档

5.5.1.语法说明

批量处理BulkRequest,其本质就是将多个普通的CRUD请求组合在一起发送。

其中提供了一个add方法,用来添加其他请求:

image-20210720232105943

可以看到,能添加的请求包括:

  • IndexRequest,也就是新增
  • UpdateRequest,也就是修改
  • DeleteRequest,也就是删除

因此Bulk中添加了多个IndexRequest,就是批量新增功能了。示例:

image-20210720232431383

其实还是三步走:

  • 1)创建Request对象。这里是BulkRequest
  • 2)准备参数。批处理的参数,就是其它Request对象,这里就是多个IndexRequest
  • 3)发起请求。这里是批处理,调用的方法为client.bulk()方法

我们在导入酒店数据时,将上述代码改造成for循环处理即可。

5.5.2.完整代码

在hotel-demo的HotelDocumentTest测试类中,编写单元测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
void testBulkRequest() throws IOException {
// 批量查询酒店数据
List<Hotel> hotels = hotelService.list();

// 1.创建Request
BulkRequest request = new BulkRequest();
// 2.准备参数,添加多个新增的Request
for (Hotel hotel : hotels) {
// 2.1.转换为文档类型HotelDoc
HotelDoc hotelDoc = new HotelDoc(hotel);
// 2.2.创建新增文档的Request对象
request.add(new IndexRequest("hotel")
.id(hotelDoc.getId().toString())
.source(JSON.toJSONString(hotelDoc), XContentType.JSON));
}
// 3.发送请求
client.bulk(request, RequestOptions.DEFAULT);
}
5.6.小结

文档操作的基本步骤:

  • 初始化RestHighLevelClient
  • 创建XxxRequest。XXX是Index、Get、Update、Delete、Bulk
  • 准备参数(Index、Update、Bulk时需要)
  • 发送请求。调用RestHighLevelClient#.xxx()方法,xxx是index、get、update、delete、bulk
  • 解析结果(Get时需要)

1.DSL查询文档

elasticsearch的查询依然是基于JSON风格的DSL来实现的。

1.1.DSL查询分类

Elasticsearch提供了基于JSON的DSL(Domain Specific Language)来定义查询。常见的查询类型包括:

  • 查询所有:查询出所有数据,一般测试用。例如:match_all

  • 全文检索(full text)查询:利用分词器对用户输入内容分词,然后去倒排索引库中匹配。例如:

    • match_query
    • multi_match_query
  • 精确查询:根据精确词条值查找数据,一般是查找keyword、数值、日期、boolean等类型字段。例如:

    • ids
    • range
    • term
  • 地理(geo)查询:根据经纬度查询。例如:

    • geo_distance
    • geo_bounding_box
  • 复合(compound)查询:复合查询可以将上述各种查询条件组合起来,合并查询条件。例如:

    • bool
    • function_score

查询的语法基本一致:

1
2
3
4
5
6
7
8
GET /indexName/_search
{
  "query": {
    "查询类型": {
      "查询条件": "条件值"
    }
  }
}

我们以查询所有为例,其中:

  • 查询类型为match_all
  • 没有查询条件
1
2
3
4
5
6
7
8
// 查询所有
GET /indexName/_search
{
  "query": {
    "match_all": {
}
  }
}

其它查询无非就是查询类型查询条件的变化。

1.2.全文检索查询

1.2.1.使用场景

全文检索查询的基本流程如下:

  • 对用户搜索的内容做分词,得到词条
  • 根据词条去倒排索引库中匹配,得到文档id
  • 根据文档id找到文档,返回给用户

比较常用的场景包括:

  • 商城的输入框搜索
  • 百度输入框搜索

例如京东:

image-20210721165326938

因为是拿着词条去匹配,因此参与搜索的字段也必须是可分词的text类型的字段。

1.2.2.基本语法

常见的全文检索查询包括:

  • match查询:单字段查询
  • multi_match查询:多字段查询,任意一个字段符合条件就算符合查询条件

match查询语法如下:

1
2
3
4
5
6
7
8
GET /indexName/_search
{
  "query": {
    "match": {
      "FIELD": "TEXT"
    }
  }
}

mulit_match语法如下:

1
2
3
4
5
6
7
8
9
GET /indexName/_search
{
  "query": {
    "multi_match": {
      "query": "TEXT",
      "fields": ["FIELD1", " FIELD12"]
    }
  }
}

1.2.3.示例

match查询示例:

image-20210721170455419

multi_match查询示例:

image-20210721170720691

可以看到,两种查询结果是一样的,为什么?

因为我们将brand、name、business值都利用copy_to复制到了all字段中。因此你根据三个字段搜索,和根据all字段搜索效果当然一样了。

但是,搜索字段越多,对查询性能影响越大,因此建议采用copy_to,然后单字段查询的方式。

1.2.4.总结

match和multi_match的区别是什么?

  • match:根据一个字段查询
  • multi_match:根据多个字段查询,参与查询字段越多,查询性能越差

1.3.精准查询

精确查询一般是查找keyword、数值、日期、boolean等类型字段。所以不会对搜索条件分词。常见的有:

  • term:根据词条精确值查询
  • range:根据值的范围查询

1.3.1.term查询

因为精确查询的字段搜是不分词的字段,因此查询的条件也必须是不分词的词条。查询时,用户输入的内容跟自动值完全匹配时才认为符合条件。如果用户输入的内容过多,反而搜索不到数据。

语法说明:

1
2
3
4
5
6
7
8
9
10
11
// term查询
GET /indexName/_search
{
  "query": {
    "term": {
      "FIELD": {
        "value": "VALUE"
      }
    }
  }
}

示例:

当我搜索的是精确词条时,能正确查询出结果:

image-20210721171655308

但是,当我搜索的内容不是词条,而是多个词语形成的短语时,反而搜索不到:

image-20210721171838378

1.3.2.range查询

范围查询,一般应用在对数值类型做范围过滤的时候。比如做价格范围过滤。

基本语法:

1
2
3
4
5
6
7
8
9
10
11
12
// range查询
GET /indexName/_search
{
  "query": {
    "range": {
      "FIELD": {
        "gte": 10, // 这里的gte代表大于等于,gt则代表大于
        "lte": 20 // lte代表小于等于,lt则代表小于
      }
    }
  }
}

示例:

image-20210721172307172

1.3.3.总结

精确查询常见的有哪些?

  • term查询:根据词条精确匹配,一般搜索keyword类型、数值类型、布尔类型、日期类型字段
  • range查询:根据数值范围查询,可以是数值、日期的范围

1.4.地理坐标查询

所谓的地理坐标查询,其实就是根据经纬度查询,官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-queries.html

常见的使用场景包括:

  • 携程:搜索我附近的酒店
  • 滴滴:搜索我附近的出租车
  • 微信:搜索我附近的人

附近的酒店:

image-20210721172645103

附近的车:

image-20210721172654880

1.4.1.矩形范围查询

矩形范围查询,也就是geo_bounding_box查询,查询坐标落在某个矩形范围的所有文档:

DKV9HZbVS6

查询时,需要指定矩形的左上右下两个点的坐标,然后画出一个矩形,落在该矩形内的都是符合条件的点。

语法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// geo_bounding_box查询
GET /indexName/_search
{
  "query": {
    "geo_bounding_box": {
      "FIELD": {
        "top_left": { // 左上点
          "lat": 31.1,
          "lon": 121.5
        },
        "bottom_right": { // 右下点
          "lat": 30.9,
          "lon": 121.7
        }
      }
    }
  }
}

这种并不符合“附近的人”这样的需求,所以我们就不做了。

1.4.2.附近查询

附近查询,也叫做距离查询(geo_distance):查询到指定中心点小于某个距离值的所有文档。

换句话来说,在地图上找一个点作为圆心,以指定距离为半径,画一个圆,落在圆内的坐标都算符合条件:

vZrdKAh19C

语法说明:

1
2
3
4
5
6
7
8
9
10
// geo_distance 查询
GET /indexName/_search
{
  "query": {
    "geo_distance": {
      "distance": "15km", // 半径
      "FIELD": "31.21,121.5" // 圆心
    }
  }
}

示例:

我们先搜索陆家嘴附近15km的酒店:

image-20210721175443234

发现共有47家酒店。

然后把半径缩短到3公里:

image-20210721182031475

可以发现,搜索到的酒店数量减少到了5家。

1.5.复合查询

复合(compound)查询:复合查询可以将其它简单查询组合起来,实现更复杂的搜索逻辑。常见的有两种:

  • fuction score:算分函数查询,可以控制文档相关性算分,控制文档排名
  • bool query:布尔查询,利用逻辑关系组合多个其它的查询,实现复杂搜索

1.5.1.相关性算分

当我们利用match查询时,文档结果会根据与搜索词条的关联度打分(_score),返回结果时按照分值降序排列。

例如,我们搜索 “虹桥如家”,结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[
  {
    "_score" : 17.850193,
    "_source" : {
      "name" : "虹桥如家酒店真不错",
    }
  },
  {
    "_score" : 12.259849,
    "_source" : {
      "name" : "外滩如家酒店真不错",
    }
  },
  {
    "_score" : 11.91091,
    "_source" : {
      "name" : "迪士尼如家酒店真不错",
    }
  }
]

在elasticsearch中,早期使用的打分算法是TF-IDF算法,公式如下:

image-20210721190152134

在后来的5.1版本升级中,elasticsearch将算法改进为BM25算法,公式如下:

image-20210721190416214

TF-IDF算法有一各缺陷,就是词条频率越高,文档得分也会越高,单个词条对文档影响较大。而BM25则会让单个词条的算分有一个上限,曲线更加平滑:

image-20210721190907320

小结:elasticsearch会根据词条和文档的相关度做打分,算法由两种:

  • TF-IDF算法
  • BM25算法,elasticsearch5.1版本后采用的算法

1.5.2.算分函数查询

根据相关度打分是比较合理的需求,但合理的不一定是产品经理需要的。

以百度为例,你搜索的结果中,并不是相关度越高排名越靠前,而是谁掏的钱多排名就越靠前。如图:

image-20210721191144560

要想认为控制相关性算分,就需要利用elasticsearch中的function score 查询了。

1)语法说明

image-20210721191544750

function score 查询中包含四部分内容:

  • 原始查询条件:query部分,基于这个条件搜索文档,并且基于BM25算法给文档打分,原始算分(query score)
  • 过滤条件:filter部分,符合该条件的文档才会重新算分
  • 算分函数:符合filter条件的文档要根据这个函数做运算,得到的函数算分(function score),有四种函数
    • weight:函数结果是常量
    • field_value_factor:以文档中的某个字段值作为函数结果
    • random_score:以随机数作为函数结果
    • script_score:自定义算分函数算法
  • 运算模式:算分函数的结果、原始查询的相关性算分,两者之间的运算方式,包括:
    • multiply:相乘
    • replace:用function score替换query score
    • 其它,例如:sum、avg、max、min

function score的运行流程如下:

  • 1)根据原始条件查询搜索文档,并且计算相关性算分,称为原始算分(query score)
  • 2)根据过滤条件,过滤文档
  • 3)符合过滤条件的文档,基于算分函数运算,得到函数算分(function score)
  • 4)将原始算分(query score)和函数算分(function score)基于运算模式做运算,得到最终结果,作为相关性算分。

因此,其中的关键点是:

  • 过滤条件:决定哪些文档的算分被修改
  • 算分函数:决定函数算分的算法
  • 运算模式:决定最终算分结果

2)示例

需求:给“如家”这个品牌的酒店排名靠前一些

翻译一下这个需求,转换为之前说的四个要点:

  • 原始条件:不确定,可以任意变化
  • 过滤条件:brand = “如家”
  • 算分函数:可以简单粗暴,直接给固定的算分结果,weight
  • 运算模式:比如求和

因此最终的DSL语句如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
GET /hotel/_search
{
  "query": {
    "function_score": {
      "query": { .... }, // 原始查询,可以是任意条件
      "functions": [ // 算分函数
        {
          "filter": { // 满足的条件,品牌必须是如家
            "term": {
              "brand": "如家"
            }
          },
          "weight": 2 // 算分权重为2
        }
      ],
"boost_mode": "sum" // 加权模式,求和
    }
  }
}

测试,在未添加算分函数时,如家得分如下:

image-20210721193152520

添加了算分函数后,如家得分就提升了:

image-20210721193458182

3)小结

function score query定义的三要素是什么?

  • 过滤条件:哪些文档要加分
  • 算分函数:如何计算function score
  • 加权方式:function score 与 query score如何运算

1.5.3.布尔查询

布尔查询是一个或多个查询子句的组合,每一个子句就是一个子查询。子查询的组合方式有:

  • must:必须匹配每个子查询,类似“与”
  • should:选择性匹配子查询,类似“或”
  • must_not:必须不匹配,不参与算分,类似“非”
  • filter:必须匹配,不参与算分

比如在搜索酒店时,除了关键字搜索外,我们还可能根据品牌、价格、城市等字段做过滤:

image-20210721193822848

每一个不同的字段,其查询的条件、方式都不一样,必须是多个不同的查询,而要组合这些查询,就必须用bool查询了。

需要注意的是,搜索时,参与打分的字段越多,查询的性能也越差。因此这种多条件查询时,建议这样做:

  • 搜索框的关键字搜索,是全文检索查询,使用must查询,参与算分
  • 其它过滤条件,采用filter查询。不参与算分

1)语法示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
GET /hotel/_search
{
  "query": {
    "bool": {
      "must": [
        {"term": {"city": "上海" }}
      ],
      "should": [
        {"term": {"brand": "皇冠假日" }},
{"term": {"brand": "华美达" }}
      ],
      "must_not": [
        { "range": { "price": { "lte": 500 } }}
      ],
      "filter": [
        { "range": {"score": { "gte": 45 } }}
      ]
    }
  }
}

2)示例

需求:搜索名字包含“如家”,价格不高于400,在坐标31.21,121.5周围10km范围内的酒店。

分析:

  • 名称搜索,属于全文检索查询,应该参与算分。放到must中
  • 价格不高于400,用range查询,属于过滤条件,不参与算分。放到must_not中
  • 周围10km范围内,用geo_distance查询,属于过滤条件,不参与算分。放到filter中

image-20210721194744183

3)小结

bool查询有几种逻辑关系?

  • must:必须匹配的条件,可以理解为“与”
  • should:选择性匹配的条件,可以理解为“或”
  • must_not:必须不匹配的条件,不参与打分
  • filter:必须匹配的条件,不参与打分

2.搜索结果处理

搜索的结果可以按照用户指定的方式去处理或展示。

2.1.排序

elasticsearch默认是根据相关度算分(_score)来排序,但是也支持自定义方式对搜索结果排序。可以排序字段类型有:keyword类型、数值类型、地理坐标类型、日期类型等。

2.1.1.普通字段排序

keyword、数值、日期类型排序的语法基本一致。

语法

1
2
3
4
5
6
7
8
9
10
11
GET /indexName/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "FIELD": "desc"  // 排序字段、排序方式ASC、DESC
    }
  ]
}

排序条件是一个数组,也就是可以写多个排序条件。按照声明的顺序,当第一个条件相等时,再按照第二个条件排序,以此类推

示例

需求描述:酒店数据按照用户评价(score)降序排序,评价相同的按照价格(price)升序排序

image-20210721195728306

2.1.2.地理坐标排序

地理坐标排序略有不同。

语法说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
GET /indexName/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "_geo_distance" : {
          "FIELD" : "纬度,经度", // 文档中geo_point类型的字段名、目标坐标点
          "order" : "asc", // 排序方式
          "unit" : "km" // 排序的距离单位
      }
    }
  ]
}

这个查询的含义是:

  • 指定一个坐标,作为目标点
  • 计算每一个文档中,指定字段(必须是geo_point类型)的坐标 到目标点的距离是多少
  • 根据距离排序

示例:

需求描述:实现对酒店数据按照到你的位置坐标的距离升序排序

提示:获取你的位置的经纬度的方式:https://lbs.amap.com/demo/jsapi-v2/example/map/click-to-get-lnglat/

假设我的位置是:31.034661,121.612282,寻找我周围距离最近的酒店。

image-20210721200214690

2.2.分页

elasticsearch 默认情况下只返回top10的数据。而如果要查询更多数据就需要修改分页参数了。elasticsearch中通过修改from、size参数来控制要返回的分页结果:

  • from:从第几个文档开始
  • size:总共查询几个文档

类似于mysql中的limit ?, ?

2.2.1.基本的分页

分页的基本语法如下:

1
2
3
4
5
6
7
8
9
10
11
GET /hotel/_search
{
  "query": {
    "match_all": {}
  },
  "from": 0, // 分页开始的位置,默认为0
  "size": 10, // 期望获取的文档总数
  "sort": [
    {"price": "asc"}
  ]
}

2.2.2.深度分页问题

现在,我要查询990~1000的数据,查询逻辑要这么写:

1
2
3
4
5
6
7
8
9
10
11
GET /hotel/_search
{
  "query": {
    "match_all": {}
  },
  "from": 990, // 分页开始的位置,默认为0
  "size": 10, // 期望获取的文档总数
  "sort": [
    {"price": "asc"}
  ]
}

这里是查询990开始的数据,也就是 第990~第1000条 数据。

不过,elasticsearch内部分页时,必须先查询 0~1000条,然后截取其中的990 ~ 1000的这10条:

image-20210721200643029

查询TOP1000,如果es是单点模式,这并无太大影响。

但是elasticsearch将来一定是集群,例如我集群有5个节点,我要查询TOP1000的数据,并不是每个节点查询200条就可以了。

因为节点A的TOP200,在另一个节点可能排到10000名以外了。

因此要想获取整个集群的TOP1000,必须先查询出每个节点的TOP1000,汇总结果后,重新排名,重新截取TOP1000。

image-20210721201003229

那如果我要查询9900~10000的数据呢?是不是要先查询TOP10000呢?那每个节点都要查询10000条?汇总到内存中?

当查询分页深度较大时,汇总数据过多,对内存和CPU会产生非常大的压力,因此elasticsearch会禁止from+ size 超过10000的请求。

针对深度分页,ES提供了两种解决方案,官方文档

  • search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。官方推荐使用的方式。
  • scroll:原理将排序后的文档id形成快照,保存在内存。官方已经不推荐使用。

2.2.3.小结

分页查询的常见实现方案以及优缺点:

  • from + size

    • 优点:支持随机翻页
    • 缺点:深度分页问题,默认查询上限(from + size)是10000
    • 场景:百度、京东、谷歌、淘宝这样的随机翻页搜索
  • after search

    • 优点:没有查询上限(单次查询的size不超过10000)
    • 缺点:只能向后逐页查询,不支持随机翻页
    • 场景:没有随机翻页需求的搜索,例如手机向下滚动翻页
  • scroll

    • 优点:没有查询上限(单次查询的size不超过10000)
    • 缺点:会有额外内存消耗,并且搜索结果是非实时的
    • 场景:海量数据的获取和迁移。从ES7.1开始不推荐,建议用 after search方案。

2.3.高亮

2.3.1.高亮原理

什么是高亮显示呢?

我们在百度,京东搜索时,关键字会变成红色,比较醒目,这叫高亮显示:

image-20210721202705030

高亮显示的实现分为两步:

  • 1)给文档中的所有关键字都添加一个标签,例如<em>标签
  • 2)页面给<em>标签编写CSS样式

2.3.2.实现高亮

高亮的语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
GET /hotel/_search
{
  "query": {
    "match": {
      "FIELD": "TEXT" // 查询条件,高亮一定要使用全文检索查询
    }
  },
  "highlight": {
    "fields": { // 指定要高亮的字段
      "FIELD": {
        "pre_tags": "<em>",  // 用来标记高亮字段的前置标签
        "post_tags": "</em>" // 用来标记高亮字段的后置标签
      }
    }
  }
}

注意:

  • 高亮是对关键字高亮,因此搜索条件必须带有关键字,而不能是范围这样的查询。
  • 默认情况下,高亮的字段,必须与搜索指定的字段一致,否则无法高亮
  • 如果要对非搜索字段高亮,则需要添加一个属性:required_field_match=false

示例

image-20210721203349633

2.4.总结

查询的DSL是一个大的JSON对象,包含下列属性:

  • query:查询条件
  • from和size:分页条件
  • sort:排序条件
  • highlight:高亮条件

示例:

image-20210721203657850

3.RestClient查询文档

文档的查询同样适用昨天学习的 RestHighLevelClient对象,基本步骤包括:

  • 1)准备Request对象
  • 2)准备请求参数
  • 3)发起请求
  • 4)解析响应

3.1.快速入门

我们以match_all查询为例

3.1.1.发起查询请求

image-20210721203950559

代码解读:

  • 第一步,创建SearchRequest对象,指定索引库名

  • 第二步,利用request.source()构建DSL,DSL中可以包含查询、分页、排序、高亮等

    • query():代表查询条件,利用QueryBuilders.matchAllQuery()构建一个match_all查询的DSL
  • 第三步,利用client.search()发送请求,得到响应

这里关键的API有两个,一个是request.source(),其中包含了查询、排序、分页、高亮等所有功能:

image-20210721215640790

另一个是QueryBuilders,其中包含match、term、function_score、bool等各种查询:

image-20210721215729236

3.1.2.解析响应

响应结果的解析:

image-20210721214221057

elasticsearch返回的结果是一个JSON字符串,结构包含:

  • hits:命中的结果
    • total:总条数,其中的value是具体的总条数值
    • max_score:所有结果中得分最高的文档的相关性算分
    • hits:搜索结果的文档数组,其中的每个文档都是一个json对象
      • _source:文档中的原始数据,也是json对象

因此,我们解析响应结果,就是逐层解析JSON字符串,流程如下:

  • SearchHits:通过response.getHits()获取,就是JSON中的最外层的hits,代表命中的结果
    • SearchHits#getTotalHits().value:获取总条数信息
    • SearchHits#getHits():获取SearchHit数组,也就是文档数组
      • SearchHit#getSourceAsString():获取文档结果中的_source,也就是原始的json文档数据

3.1.3.完整代码

完整代码如下:

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
@Test
void testMatchAll() throws IOException {
// 1.准备Request
SearchRequest request = new SearchRequest("hotel");
// 2.准备DSL
request.source()
.query(QueryBuilders.matchAllQuery());
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);

// 4.解析响应
handleResponse(response);
}

private void handleResponse(SearchResponse response) {
// 4.解析响应
SearchHits searchHits = response.getHits();
// 4.1.获取总条数
long total = searchHits.getTotalHits().value;
System.out.println("共搜索到" + total + "条数据");
// 4.2.文档数组
SearchHit[] hits = searchHits.getHits();
// 4.3.遍历
for (SearchHit hit : hits) {
// 获取文档source
String json = hit.getSourceAsString();
// 反序列化
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
System.out.println("hotelDoc = " + hotelDoc);
}
}

3.1.4.小结

查询的基本步骤是:

  1. 创建SearchRequest对象

  2. 准备Request.source(),也就是DSL。

    ① QueryBuilders来构建查询条件

    ② 传入Request.source() 的 query() 方法

  3. 发送请求,得到结果

  4. 解析结果(参考JSON结果,从外到内,逐层解析)

3.2.match查询

全文检索的match和multi_match查询与match_all的API基本一致。差别是查询条件,也就是query的部分。

image-20210721215923060

因此,Java代码上的差异主要是request.source().query()中的参数了。同样是利用QueryBuilders提供的方法:

image-20210721215843099

而结果解析代码则完全一致,可以抽取并共享。

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
void testMatch() throws IOException {
// 1.准备Request
SearchRequest request = new SearchRequest("hotel");
// 2.准备DSL
request.source()
.query(QueryBuilders.matchQuery("all", "如家"));
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析响应
handleResponse(response);

}

3.3.精确查询

精确查询主要是两者:

  • term:词条精确匹配
  • range:范围查询

与之前的查询相比,差异同样在查询条件,其它都一样。

查询条件构造的API如下:

image-20210721220305140

3.4.布尔查询

布尔查询是用must、must_not、filter等方式组合其它查询,代码示例如下:

image-20210721220927286

可以看到,API与其它查询的差别同样是在查询条件的构建,QueryBuilders,结果解析等其他代码完全不变。

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
void testBool() throws IOException {
// 1.准备Request
SearchRequest request = new SearchRequest("hotel");
// 2.准备DSL
// 2.1.准备BooleanQuery
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// 2.2.添加term
boolQuery.must(QueryBuilders.termQuery("city", "杭州"));
// 2.3.添加range
boolQuery.filter(QueryBuilders.rangeQuery("price").lte(250));

request.source().query(boolQuery);
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析响应
handleResponse(response);

}

3.5.排序、分页

搜索结果的排序和分页是与query同级的参数,因此同样是使用request.source()来设置。

对应的API如下:

image-20210721221121266

完整代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
void testPageAndSort() throws IOException {
// 页码,每页大小
int page = 1, size = 5;

// 1.准备Request
SearchRequest request = new SearchRequest("hotel");
// 2.准备DSL
// 2.1.query
request.source().query(QueryBuilders.matchAllQuery());
// 2.2.排序 sort
request.source().sort("price", SortOrder.ASC);
// 2.3.分页 from、size
request.source().from((page - 1) * size).size(5);
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析响应
handleResponse(response);

}

3.6.高亮

高亮的代码与之前代码差异较大,有两点:

  • 查询的DSL:其中除了查询条件,还需要添加高亮条件,同样是与query同级。
  • 结果解析:结果除了要解析_source文档数据,还要解析高亮结果

3.6.1.高亮请求构建

高亮请求的构建API如下:

image-20210721221744883

上述代码省略了查询条件部分,但是大家不要忘了:高亮查询必须使用全文检索查询,并且要有搜索关键字,将来才可以对关键字高亮。

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
void testHighlight() throws IOException {
// 1.准备Request
SearchRequest request = new SearchRequest("hotel");
// 2.准备DSL
// 2.1.query
request.source().query(QueryBuilders.matchQuery("all", "如家"));
// 2.2.高亮
request.source().highlighter(new HighlightBuilder().field("name").requireFieldMatch(false));
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析响应
handleResponse(response);

}

3.6.2.高亮结果解析

高亮的结果与查询的文档结果默认是分离的,并不在一起。

因此解析高亮的代码需要额外处理:

image-20210721222057212

代码解读:

  • 第一步:从结果中获取source。hit.getSourceAsString(),这部分是非高亮结果,json字符串。还需要反序列为HotelDoc对象
  • 第二步:获取高亮结果。hit.getHighlightFields(),返回值是一个Map,key是高亮字段名称,值是HighlightField对象,代表高亮值
  • 第三步:从map中根据高亮字段名称,获取高亮字段值对象HighlightField
  • 第四步:从HighlightField中获取Fragments,并且转为字符串。这部分就是真正的高亮字符串了
  • 第五步:用高亮的结果替换HotelDoc中的非高亮结果

完整代码如下:

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
private void handleResponse(SearchResponse response) {
// 4.解析响应
SearchHits searchHits = response.getHits();
// 4.1.获取总条数
long total = searchHits.getTotalHits().value;
System.out.println("共搜索到" + total + "条数据");
// 4.2.文档数组
SearchHit[] hits = searchHits.getHits();
// 4.3.遍历
for (SearchHit hit : hits) {
// 获取文档source
String json = hit.getSourceAsString();
// 反序列化
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
// 获取高亮结果
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
if (!CollectionUtils.isEmpty(highlightFields)) {
// 根据字段名获取高亮结果
HighlightField highlightField = highlightFields.get("name");
if (highlightField != null) {
// 获取高亮值
String name = highlightField.getFragments()[0].string();
// 覆盖非高亮结果
hotelDoc.setName(name);
}
}
System.out.println("hotelDoc = " + hotelDoc);
}
}

算法题总结

数组

1、如何处理循环数组

使用2倍的数组长度来取%完成一次循环,可以查找到前半部分的数组

1
2
3
4
5
6
7
for (int i=1;i<2*nums.length;i++){
while(!st.isEmpty()&&nums[i]>nums[st.peek()]){
result[st.peek()] = nums[i % nums.length];//更新result
st.pop();//弹出栈顶
}
st.push(i);
}nums[i]=nums[i % nums.length];

2、二分查找

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
    public int search(int[] nums, int target) {
// 避免当 target 小于nums[0] nums[nums.length - 1]时多次循环运算
if (target < nums[0] || target > nums[nums.length - 1]) {
return -1;
}
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + ((right - left) >> 1);
if (nums[mid] == target)
return mid;
else if (nums[mid] < target)
left = mid + 1;
else if (nums[mid] > target)
right = mid - 1;
}

return -1;
}
//二分寻找插入位置 第一种左闭右闭
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + ((right - left) >> 1);
else if (nums[mid] < target)
left = mid + 1;[mid+1,right]
else
right = mid - 1;[left,mid-1]
}
return left;
//第二种左闭右开
int left = 0, right = nums.length;
while (left < right) {
int mid = left + ((right - left) >> 1);
else if (nums[mid] < target)
left = mid + 1;[mid+1,right)
else
right = mid ;[left,mid)
}
return left;//right;都可以
//第三种左开右开
int left = -1, right = nums.length; // 开区间 (left, right)
while (left + 1 < right) { // 区间不为空
int mid = left + (right - left) / 2;
if (nums[mid] < target)
left = mid; // (mid, right)
else
right = mid; // (left, mid)
}
return right;


剑指 Offer 53 - I. 在排序数组中查找数字 I 考察二分法

考察了对于二分查找后对数据左右边界判断的方法

1
2
3
4
5
6
7
8
9
10
11
else {
if (nums[begin] == nums[end]) {
return end - begin + 1;
}
if (nums[begin] < target) {
begin++;
}
if (nums[end] > target) {
end--;
}
}

剑指 Offer 53 - II. 0~n-1中缺失的数字 思想依然是二分查找的方法

考察如何对于low和high两个数值的修改判断出缺失数字

1
2
3
4
5
int mid=low+(high-low)/2;
if(nums[mid]!=mid)high=mid-1;
else low=mid+1;
//最后return
return low;

287. 寻找重复数

思路是数量既然是1-n之中存在,但是却有n+1个笼子那可以只用二分查找缩小查找范围,如果小于mid数字多那就是小的那半重复,如果大于mid多就是大于部分重复

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
while (l<=r){
int half=(r-l)/2+l;
int count=0;
for (int i = 0; i < nums.length; i++) {
if(nums[i]<=half){
count++;
}
}
if(count<=half){
l=half+1;
}
else {
r=half-1;
res=half;
}
}

153. 寻找旋转排序数组中的最小值

33. 搜索旋转排序数组

这两个题目的破题关键:

定理一:只有在顺序区间内才可以通过区间两端的数值判断target是否在其中。

定理二:判断顺序区间还是乱序区间,只需要对比 left 和 right 是否是顺序对即可,left <= right,顺序区间,否则乱序区间。

定理三:每次二分都会至少存在一个顺序区间。

通过不断的用Mid二分,根据定理二,将整个数组划分成顺序区间和乱序区间,然后利用定理一判断target是否在顺序区间,如果在顺序区间,下次循环就直接取顺序区间,如果不在,那么下次循环就取乱序区间。

将数组一分为二,其中一定有一个是有序的,另一个可能是有序,也能是部分有序。 此时有序部分用二分法查找。无序部分再一分为二,其中一个一定有序,另一个可能有序,可能无序。就这样循环.

4. 寻找两个正序数组的中位数

假设我们要找第 k 小数,我们可以每次循环排除掉 k/2 个数,所以我们采用递归的思路,为了防止数组长度小于 k/2,所以每次比较 min(k/2,len(数组) 对应的数字,把小的那个对应的数组的数字排除,将两个新数组进入递归,并且 k 要减去排除的数字的个数。递归出口就是当 k=1 或者其中一个数字长度是 0 了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int n = nums1.length;
int m = nums2.length;
int left = (n + m + 1) / 2;
int right = (n + m + 2) / 2;
//将偶数和奇数的情况合并,如果是奇数,会求两次同样的 k 。
return (getKth(nums1,0,nums2,0,left)+ getKth(nums1,0,nums2,0,right)) * 0.5;
}

private int getKth(int[] nums1, int i, int[] nums2, int j, int k) {
if( i >= nums1.length) return nums2[j + k - 1];//nums1为空数组
if( j >= nums2.length) return nums1[i + k - 1];//nums2为空数组
if(k == 1){
return Math.min(nums1[i], nums2[j]);
}
//数值情况如何你寻找第k大的数都需要排除前k个数 所以无需理睬数组长度小的数,如果最后长度和小长度的数组一致了,那意味着到达新的k分解一样正常分解
int midVal1 = (i + k / 2 - 1 < nums1.length) ? nums1[i + k / 2 - 1] : Integer.MAX_VALUE;
int midVal2 = (j + k / 2 - 1 < nums2.length) ? nums2[j + k / 2 - 1] : Integer.MAX_VALUE;
if(midVal1 < midVal2){
return getKth(nums1, i + k / 2, nums2, j , k - k / 2);
}else{
return getKth(nums1, i, nums2, j + k / 2 , k - k / 2);
}
}

378. 有序矩阵中第 K 小的元素 - 力扣(LeetCode)

此题二分是因为本身矩阵是有序的,我们找值时总最小的最左值到最大的右下值进行二分查找,小于此值的都在上板部分,统计这部分个数,然后就可以返回结果,对于数组我们选用梯形遍历从左下角一直遍历到右上角

fig3
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
public int kthSmallest(int[][] matrix, int k) {
int n = matrix.length;
int left = matrix[0][0];
int right = matrix[n - 1][n - 1];
while (left < right) {
int mid = left + ((right - left) >> 1);
if (check(matrix, mid, k, n)) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
}

public boolean check(int[][] matrix, int mid, int k, int n) {
int i = n - 1;
int j = 0;
int num = 0;
while (i >= 0 && j < n) {
if (matrix[i][j] <= mid) {
num += i + 1;
j++;
} else {
i--;
}
}
return num >= k;
}

34. 在排序数组中查找元素的第一个和最后一个位置

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
public int[] searchRange(int[] nums, int target) {
int start = lowerBound(nums, target); // 选择其中一种写法即可
if (start == nums.length || nums[start] != target) {
return new int[]{-1, -1}; // nums 中没有 target
}
// 如果 start 存在,那么 end 必定存在
int end = lowerBound(nums, target + 1) - 1;
return new int[]{start, end};
}

// lowerBound 返回最小的满足 nums[i] >= target 的 i
// 如果数组为空,或者所有数都 < target,则返回 nums.length
// 要求 nums 是非递减的,即 nums[i] <= nums[i + 1]

// 闭区间写法
private int lowerBound(int[] nums, int target) {
int left = 0, right = nums.length - 1; // 闭区间 [left, right]
while (left <= right) { // 区间不为空
// 循环不变量:
// nums[left-1] < target
// nums[right+1] >= target
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1; // 范围缩小到 [mid+1, right]
} else {
right = mid - 1; // 范围缩小到 [left, mid-1]
}
}
return left;
}

162. 寻找峰值

不难发现,如果 在确保有解的情况下,我们可以根据当前的分割点 mid 与左右元素的大小关系来指导 l 或者 r 的移动。

假设当前分割点 mid 满足关系 num[mid]>nums[mid+1] 的话,一个很简单的想法是 num[mid] 可能为峰值,而 nums[mid+1] 必然不为峰值,于是让 r=mid,从左半部分继续找峰值。

估计不少同学靠这个思路 AC 了,只能说做法对了,分析没对。

上述做法正确的前提有两个:

对于任意数组而言,一定存在峰值(一定有解);
二分不会错过峰值。

1
2
3
4
5
6
7
8
9
10
public int findPeakElement(int[] nums) {
int n = nums.length;
int l = 0, r = n - 1;
while (l < r) {
int mid = l + r >> 1;
if (nums[mid] > nums[mid + 1]) r = mid;
else l = mid + 1;
}
return r;
}

3、删除数组时使用双指针法

27.移除元素-双指针法

双指针在遇到不满足条件时一起增加但是在满足条件时快指针动这样可以将后面元素加上来

1
2
3
4
5
6
7
8
9
10
11
public:
int removeElement(vector<int>& nums, int val) {
int slowIndex = 0;
for (int fastIndex = 0; fastIndex < nums.size(); fastIndex++) {
if (val != nums[fastIndex]) {
nums[slowIndex++] = nums[fastIndex];
}
}
return slowIndex;
}
};

双指针还可以用于前后倒序排列

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public int[] sortedSquares(int[] nums) {
int l = 0;
int r = nums.length - 1;
int[] res = new int[nums.length];
int j = nums.length - 1;
while(l <= r){
if(nums[l] * nums[l] > nums[r] * nums[r]){
res[j--] = nums[l] * nums[l++];
}else{
res[j--] = nums[r] * nums[r--];
}
}
return res;
}

75. 颜色分类

image-20240403163417829

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void sortColors(int[] nums) {
int n = nums.length;
int p0 = 0, p1 = 0;
for (int i = 0; i < n; ++i) {
if (nums[i] == 1) {
int temp = nums[i];
nums[i] = nums[p1];
nums[p1] = temp;
++p1;
} else if (nums[i] == 0) {
int temp = nums[i];
nums[i] = nums[p0];
nums[p0] = temp;
if (p0 < p1) {
temp = nums[i];
nums[i] = nums[p1];
nums[p1] = temp;
}
++p0;
++p1;
}
}
}

581. 最短无序连续子数组

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
public int findUnsortedSubarray(int[] nums) {
int n = nums.length;

int l = 0; // l 标记从前往后找到第一个出现降序的下标,nums[l] > nums[l + 1]
while (l + 1 < n && nums[l] <= nums[l + 1]) {
l++;
}

// 若l == n - 1, 说明 nums 为升序序列
if (l == n - 1) {
return 0;
}

int r = n - 1; // r 标记从后往前找到第一个出现升序的下标,nums[r] < nums[r - 1]
while (r - 1 >= 0 && nums[r] >= nums[r - 1]) {
r--;
}


/* 在子区间 [l, r] 中找到最小值 min 和最大值 max*/
int min = nums[l];
int max = nums[r];
for (int i = l, j = r; i <= r && j >= l; i++, j--) {
min = min < nums[i] ? min : nums[i];
max = max > nums[j] ? max : nums[j];
}

/* 从 l 开始向前查找 min 在 nums 中的最终位置 l */
while (l - 1 >= 0 && nums[l - 1] > min) {
l--;
}

/* 从 r 开始向后查找 max 在 nums 中的最终位置 r*/
while (r + 1 < n && nums[r + 1] < max) {
r++;
}

/* 确定无序子数组的最小值和最大值的最终位置后,[l, r] 中的元素就是原数组 nums 待排序的子数组*/
return r - l + 1;
}

5、滑动窗口

209.长度最小的子数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int minSubArrayLen(int s, vector<int>& nums) {
int result = INT32_MAX;
int sum = 0; // 滑动窗口数值之和
int i = 0; // 滑动窗口起始位置
int subLength = 0; // 滑动窗口的长度
for (int j = 0; j < nums.size(); j++) {
sum += nums[j];
// 注意这里使用while,每次更新 i(起始位置),并不断比较子序列是否符合条件
while (sum >= s) {
subLength = (j - i + 1); // 取子序列的长度
result = result < subLength ? result : subLength;
sum -= nums[i++]; // 这里体现出滑动窗口的精髓之处,不断变更i(子序列的起始位置)
}
}
// 如果result没有被赋值的话,就返回0,说明没有符合条件的子序列
return result == INT32_MAX ? 0 : result;
}

fig1

注意需要统计次数时或者不求最大值时建议使用hashmap配合这样滑动窗口可以包含自身

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 //76. 最小覆盖子串  https://leetcode.cn/problems/minimum-window-substring/description/
//这里面i就是r j就是l通过i右移先包含整个t字符然后再j右移删除无效字符 使用length 判断是否已经将t的字符全部匹配
public String minWindow(String s, String t) {
char[] sc = s.toCharArray();char[] tc = t.toCharArray();
int [] hash=new int[128];
for (char ch : tc) hash[ch]--;
String res = "";
int length=0;
for (int i = 0,j=0; i < sc.length; i++) {
hash[sc[i]]++;
if(hash[sc[i]]<=0)length++;
while (length == tc.length && hash[sc[j]] > 0) hash[sc[j++]]--;
if(length==tc.length)
if(res.equals("")||res.length()>i-j+1)
res=s.substring(j,i+1);`
}
return res;
}

424. 替换后的最长重复字符

567. 字符串的排列

这两题拥有相同的思路将窗口开到指定大小在窗口大小内寻找是否符合要求,通过右指针到达具体窗口大小,左指针移动来整个平移滑动窗口,比较在规定窗口内的值是否符合答案.

img

1
2
#424
if (right - left + 1 > historyCharMax + k) {
1
2
#567
if(right-left+1==length1)return true;

239. 滑动窗口最大值

Picture1.png

只要元素a>元素b而且元素a还在元素b的右侧,那么当滑动窗口移动时,元素b在不在队列里最大值都是元素a所以就可以在add时去除b元素,在弹出时判断该元素是否存在再弹出

1
2
3
4
5
6
7
8
9
10
11
12
13
int res[]=new int[nums.length-k+1];
Deque<Integer>queue=new LinkedList<>();
for (int i = k,j=0; i <nums.length ; i++) {
res[j++]=queue.peek();
if(nums[i-k]==queue.peek())
queue.poll();
while(!queue.isEmpty()&&queue.getLast()<nums[i])
{
queue.removeLast();
}
queue.offer(nums[i]);
}
return res;

30. 串联所有单词的子串

我们每次移动一个单词的长度,也就是 3 个字符,这样所有的移动被分成了三类。总共需要移动对比的次数就是一个word的长度因为我们滑动窗口是以每个word长度进行的根据鸽笼原理大于word长度x开始的都是前面word长度以内开始的子情况,之后就是三种情况完全相同count == word_num 添加坐标,不同则清空left移动到不同的之后,对应word数量不同也清空left移动一位

image.png

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
public List<Integer> findSubstring(String s, String[] words) {
List<Integer> res = new ArrayList<>();
if (s == null || s.length() == 0 || words == null || words.length == 0) return res;
HashMap<String, Integer> map = new HashMap<>();
int one_word = words[0].length();
int word_num = words.length;
int all_len = one_word * word_num;
for (String word : words) {
map.put(word, map.getOrDefault(word, 0) + 1);
}
for (int i = 0; i < one_word; i++) {
int left = i, right = i, count = 0;
HashMap<String, Integer> tmp_map = new HashMap<>();
while (right + one_word <= s.length()) {
String w = s.substring(right, right + one_word);
right += one_word;
if (!map.containsKey(w)) {
count = 0;
left = right;
tmp_map.clear();
} else {
tmp_map.put(w, tmp_map.getOrDefault(w, 0) + 1);
count++;
while (tmp_map.getOrDefault(w, 0) > map.getOrDefault(w, 0)) {
String t_w = s.substring(left, left + one_word);
count--;
tmp_map.put(t_w, tmp_map.getOrDefault(t_w, 0) - 1);
left += one_word;
}
if (count == word_num) res.add(left);
}
}
}
return res;
}

排序

1、基本排序介绍

在这里插入图片描述

1.冒泡排序(Bubble Sort)

在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class BubbleSort1_01 {
public static void main(String[] args) {
int a[]={3,44,38,5,47,15,36,26,27,2,46,4,19,50,48};
int count=0;
for (int i = 0; i < a.length-1; i++) {
boolean flag=true;
for (int j = 0; j < a.length-1-i; j++) {
if (a[j]>a[j+1]) {
int temp=a[j];
a[j]=a[j+1];
a[j+1]=temp;
flag=false;
}
count++;
}
if (flag) {
break;
}
}
System.out.println(Arrays.toString(a));// [2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]
System.out.println("一共比较了:"+count+"次");//一共比较了:95次
}
}

2.选择排序(Select Sort)

在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
3.import java.util.Arrays;
//选择排序:先定义一个记录最小元素的下标,然后循环一次后面的,找到最小的元素,最后将他放到前面排序好的序列。
public class SelectSort_02 {
public static void main(String[] args) {
int a[]={3,44,38,5,47,15,36,26,27,2,46,4,19,50,48};
for (int i = 0; i < a.length-1; i++) {
int index=i;//标记第一个为待比较的数
for (int j = i+1; j < a.length; j++) { //然后从后面遍历与第一个数比较
if (a[j]<a[index]) { //如果小,就交换最小值
index=j;//保存最小元素的下标
}
}
//找到最小值后,将最小的值放到第一的位置,进行下一遍循环
int temp=a[index];
a[index]=a[i];
a[i]=temp;
}
System.out.println(Arrays.toString(a));//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]
}
}

3.插入排序(Insert Sort)

在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.Arrays;
//插入排序:定义一个待插入的数,再定义一个待插入数的前一个数的下标,然后拿待插入数与前面的数组一一比较,最后交换。
public class InsertSort_03 {
public static void main(String[] args) {
int a[]={3,44,38,5,47,15,36,26,27,2,46,4,19,50,48};
for (int i = 0; i < a.length; i++) { //长度不减1,是因为要留多一个位置方便插入数
//定义待插入的数
int insertValue=a[i];
//找到待插入数的前一个数的下标
int insertIndex=i-1;
while (insertIndex>=0 && insertValue <a[insertIndex]) {//拿a[i]与a[i-1]的前面数组比较
a[insertIndex+1]=a[insertIndex];
insertIndex--;
}
a[insertIndex+1]=insertValue;
}
System.out.println(Arrays.toString(a));//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]
}
}

4.希尔排序(Shell Sort)

在这里插入图片描述

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
import java.util.Arrays;
//希尔排序:插入排序的升级
public class ShellSort_04 {
public static void main(String[] args) {
int a[]={3,44,38,5,47,15,36,26,27,2,46,4,19,50,48};
int count=0;//比较次数
for (int gap=a.length / 2; gap > 0; gap = gap / 2) {
//将整个数组分为若干个子数组
for (int i = gap; i < a.length; i++) {
//遍历各组的元素
for (int j = i - gap; j>=0; j=j-gap) {
//交换元素
if (a[j]>a[j+gap]) {
int temp=a[j];
a[j]=a[j+gap];
a[j+gap]=temp;
count++;
}
}
}
}
System.out.println(count);//16
System.out.println(Arrays.toString(a));//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]
}
}

5.快速排序(Quick Sort)

image-20230422164916915

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
import java.util.Arrays;
//快速排序:冒泡排序的升华版
public class QuickSort_05 {
public static void main(String[] args) {
//int a[]={50,1,12,2};
int a[]={3,44,38,5,47,15,36,26,27,2,46,4,19,50,48};
quicksort(a,0,a.length-1);
System.out.println(Arrays.toString(a));
}
private static void quicksort(int[] a, int low, int high) {
int i,j;
if (low>high) {
return;
}
i=low;
j=high;
int temp=a[low];//基准位,low=length时,会报异常,java.lang.ArrayIndexOutOfBoundsException: 4 ,所以必须在if判断后面,就跳出方法。
while(i<j){
//先从右边开始往左递减,找到比temp小的值才停止
while ( temp<=a[j] && i<j) {
j--;
}
//再看左边开始往右递增,找到比temp大的值才停止
while ( temp>=a[i] && i<j) {
i++;
}
//满足 i<j 就交换,继续循环while(i<j)
if (i<j) {
int t=a[i];
a[i]=a[j];
a[j]=t;
}
}
//最后将基准位跟 a[i]与a[j]相等的位置,进行交换,此时i=j
a[low]=a[i];
a[i]=temp;
//左递归
quicksort(a, low, j-1);
//右递归
quicksort(a, j+1, high);
}
}

6.归并排序(Merge Sort)

在这里插入图片描述

在这里插入图片描述

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import java.util.Arrays;
//归并排序
public class MergeSort_06 {
public static void main(String[] args) {
int a[]={3,44,38,5,47,15,36,26,27,2,46,4,19,50,48};
//int a[]={5,2,4,7,1,3,2,2};
int temp[]=new int[a.length];
mergesort(a,0,a.length-1,temp);
System.out.println(Arrays.toString(a));
}
private static void mergesort(int[] a, int left, int right, int[] temp) {
//分解
if (left<right) {
int mid=(left+right)/2;
//向左递归进行分解
mergesort(a, left, mid, temp);
//向右递归进行分解
mergesort(a, mid+1, right, temp);
//每分解一次便合并一次
merge(a,left,right,mid,temp);
}
}
/**
*
* @param a 待排序的数组
* @param left 左边有序序列的初始索引
* @param right 右边有序序列的初始索引
* @param mid 中间索引
* @param temp 做中转的数组
*/
private static void merge(int[] a, int left, int right, int mid, int[] temp) {
int i=left; //初始i,左边有序序列的初始索引
int j=mid+1;//初始化j,右边有序序列的初始索引(右边有序序列的初始位置即中间位置的后一位置)
int t=0;//指向temp数组的当前索引,初始为0
//先把左右两边的数据(已经有序)按规则填充到temp数组
//直到左右两边的有序序列,有一边处理完成为止
while (i<=mid && j<=right) {
//如果左边有序序列的当前元素小于或等于右边的有序序列的当前元素,就将左边的元素填充到temp数组中
if (a[i]<=a[j]) {
temp[t]=a[i];
t++;//索引向后移
i++;//i后移
}else {
//反之,将右边有序序列的当前元素填充到temp数组中
temp[t]=a[j];
t++;//索引向后移
j++;//j后移
}
}
//把剩余数据的一边的元素填充到temp中
while (i<=mid) {
//此时说明左边序列还有剩余元素
//全部填充到temp数组
temp[t]=a[i];
t++;
i++;
}
while (j<=right) {
//此时说明左边序列还有剩余元素
//全部填充到temp数组
temp[t]=a[j];
t++;
j++;
}
//将temp数组的元素复制到原数组
t=0;
int tempLeft=left;
while (tempLeft<=right) {
a[tempLeft]=temp[t];
t++;
tempLeft++;
}
}

7.堆排序(Heap Sort)

第一步:构建初始堆buildHeap, 使用sink(arr,i, length)调整堆顶的值;
第二步:将堆顶元素下沉 目的是将最大的元素浮到堆顶来,然后使用sink(arr, 0,length)调整;

在这里插入图片描述

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
public class Heap_Sort_07 {
public static void main(String[] args) {
int a[]={3,44,38,5,47,15,36,26,27,2,46,4,19,50,48};
sort(a);
System.out.println(Arrays.toString(a));
}
public static void sort(int[] arr) {
int length = arr.length;
//构建堆
buildHeap(arr,length);
for ( int i = length - 1; i > 0; i-- ) {
//将堆顶元素与末位元素调换
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
//数组长度-1 隐藏堆尾元素
length--;
//将堆顶元素下沉 目的是将最大的元素浮到堆顶来
sink(arr, 0,length);
}
}
private static void buildHeap(int[] arr, int length) {
for (int i = length / 2; i >= 0; i--) {
sink(arr,i, length);
}
}
private static void sink(int[] arr, int index, int length) {
int leftChild = 2 * index + 1;//左子节点下标
int rightChild = 2 * index + 2;//右子节点下标
int present = index;//要调整的节点下标

//下沉左边
if (leftChild < length && arr[leftChild] > arr[present]) {
present = leftChild;
}

//下沉右边
if (rightChild < length && arr[rightChild] > arr[present]) {
present = rightChild;
}

//如果下标不相等 证明调换过了
if (present != index) {
//交换值
int temp = arr[index];
arr[index] = arr[present];
arr[present] = temp;

//继续下沉 在取出头元素后与尾元素交换继续下沉
sink(arr, present, length);
}
}
}

8.计数排序 (Count Sort)

9.桶排序(Bucket Sort)

在这里插入图片描述

思路:

  • 设置一个定量的数组当作空桶子。
  • 寻访序列,并且把项目一个一个放到对应的桶子去。
  • 对每个不是空的桶子进行排序。
  • 从不是空的桶子里把项目再放回原来的序列中。
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
public class BucketSort_09 {
public static void sort(int[] arr){
//最大最小值
int max = arr[0];
int min = arr[0];
int length = arr.length

for(int i=1; i<length; i++) {
if(arr[i] > max) {
max = arr[i];
} else if(arr[i] < min) {
min = arr[i];
}
}

//最大值和最小值的差
int diff = max - min;

//桶列表
​ ArrayList<ArrayList<Integer>> bucketList = new ArrayList<>();
for(int i = 0; i < length; i++){
​ bucketList.add(new ArrayList<>());
​ }

//每个桶的存数区间
float section = (float) diff / (float) (length - 1);

//数据入桶
for(int i = 0; i < length; i++){
//当前数除以区间得出存放桶的位置 减1后得出桶的下标
int num = (int) (arr[i] / section) - 1;
if(num < 0){
​ num = 0;
​ }
​ bucketList.get(num).add(arr[i]);
​ }

//桶内排序
for(int i = 0; i < bucketList.size(); i++){
//jdk的排序速度当然信得过
​ Collections.sort(bucketList.get(i));
​ }

//写入原数组
int index = 0;
for(ArrayList<Integer> arrayList : bucketList){
for(int value : arrayList){
​ arr[index] = value;
​ index++;
​ }
​ }
}

}

10.基数排序(Raix Sort)

2386. 找出数组的第 K 大和

将问题转换为image-20240308210525630

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
public long kSum(int[] nums, int k) {
int n = nums.length;
long total = 0;
for (int i = 0; i < n; i++) {
if (nums[i] >= 0) {
total += nums[i];
} else {
nums[i] = -nums[i];
}
}
Arrays.sort(nums);

long ret = 0;
PriorityQueue<long[]> pq = new PriorityQueue<long[]>((a, b) -> Long.compare(a[0], b[0]));
pq.offer(new long[]{nums[0], 0});
for (int j = 2; j <= k; j++) {
long[] arr = pq.poll();
long t = arr[0];
int i = (int) arr[1];
ret = t;
if (i == n - 1) {
continue;
}
pq.offer(new long[]{t + nums[i + 1], i + 1});
pq.offer(new long[]{t - nums[i] + nums[i + 1], i + 1});
}
return total - ret;
}

LCR 170. 交易逆序对的总数

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 count;
public int reversePairs(int[] nums) {
this.count = 0;
merge(nums, 0, nums.length - 1);
return count;
}

public void merge(int[] nums, int left, int right) {
int mid = left + ((right - left) >> 1);
if (left < right) {
merge(nums, left, mid);
merge(nums, mid + 1, right);
mergeSort(nums, left, mid, right);
}
}

public void mergeSort(int[] nums, int left, int mid, int right) {
int[] temparr = new int[right - left + 1];
int index = 0;
int temp1 = left, temp2 = mid + 1;

while (temp1 <= mid && temp2 <= right) {
if (nums[temp1] <= nums[temp2]) {
temparr[index++] = nums[temp1++];
} else {
//用来统计逆序对的个数
count += (mid - temp1 + 1);
temparr[index++] = nums[temp2++];
}
}
//把左边剩余的数移入数组
while (temp1 <= mid) {
temparr[index++] = nums[temp1++];
}
//把右边剩余的数移入数组
while (temp2 <= right) {
temparr[index++] = nums[temp2++];
}
//把新数组中的数覆盖nums数组
for (int k = 0; k < temparr.length; k++) {
nums[k + left] = temparr[k];
}
}
}

拓扑排序

  1. 选择图中一个入度为0的点,记录下来
  2. 在图中删除该点和所有以它为起点的边
  3. 重复1和2,直到图为空或没有入度为0的点。
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
public List<Integer> haveCircularDependency(int n, int[][] prerequisites) {
List<List<Integer>> g = new ArrayList<>(); // 邻接表存储图结构
int[] indeg = new int[n]; // 每个点的入度
List<Integer> res = new ArrayList<>(); // 存储结果序列

for (int i = 0; i < n; i++) {
g.add(new ArrayList<>());
}

for (int[] prerequisite : prerequisites) {
int a = prerequisite[0];
int b = prerequisite[1];
g.get(a).add(b);
indeg[b]++;
}

Queue<Integer> q = new LinkedList<>();
// 一次性将入度为0的点全部入队
for (int i = 0; i < n; i++) {
if (indeg[i] == 0) {
q.add(i);
}
}

while (!q.isEmpty()) {
int t = q.poll();
res.add(t);
// 删除边时,将终点的入度-1。若入度为0,果断入队
for (int j : g.get(t)) {
indeg[j]--;
if (indeg[j] == 0) {
q.add(j);
}
}
}

if (res.size() == n) return res; // 如果结果列表大小等于节点数,说明排序成功,没有环
else return new ArrayList<>(); // 否则返回空列表,表示有环
}

链表

ps:在链表操作中不会直接使用head头节点操作而是使用一个新头指针来操作链表

1、206.反转链表

img

标准双指针问题

1
2
3
4
5
6
7
8
9
10
11
12
13
ListNode* reverseList(ListNode* head) {
ListNode* temp; // 保存cur的下一个节点
ListNode* cur = head;
ListNode* pre = NULL;
while(cur) {
temp = cur->next; // 保存一下 cur的下一个节点,因为接下来要改变cur->next
cur->next = pre; // 翻转操作
// 更新pre 和 cur指针
pre = cur;
cur = temp;
}
return pre;
}

链表作题一定要画图,不画图,操作多个指针很容易乱,而且要操作的先后顺序

2、快慢指针法

3、环形链表判断

141.环形链表

如果有环,如何找到这个环的入口

此时已经可以判断链表是否有环了,那么接下来要找这个环的入口了。

假设从头结点到环形入口节点 的节点数为x。 环形入口节点到 fast指针与slow指针相遇节点 节点数为y。 从相遇节点 再到环形入口节点节点数为 z。 如图所示:

img

那么相遇时: slow指针走过的节点数为: x + y, fast指针走过的节点数:x + y + n (y + z),n为fast指针在环内走了n圈才遇到slow指针, (y+z)为 一圈内节点的个数A。

因为fast指针是一步走两个节点,slow指针一步走一个节点, 所以 fast指针走过的节点数 = slow指针走过的节点数 * 2:

1
(x + y) * 2 = x + y + n (y + z)

两边消掉一个(x+y): x + y = n (y + z)

因为要找环形的入口,那么要求的是x,因为x表示 头结点到 环形入口节点的的距离。

所以要求x ,将x单独放在左面:x = n (y + z) - y ,

再从n(y+z)中提出一个 (y+z)来,整理公式之后为如下公式:x = (n - 1) (y + z) + z 注意这里n一定是大于等于1的,因为 fast指针至少要多走一圈才能相遇slow指针。

这个公式说明什么呢?

先拿n为1的情况来举例,意味着fast指针在环形里转了一圈之后,就遇到了 slow指针了。

当 n为1的时候,公式就化解为 x = z

这就意味着,从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点

同类型题目287. 寻找重复数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {// 有环
ListNode index1 = fast;
ListNode index2 = head;
// 两个指针,从头结点和相遇结点,各走一步,直到相遇,相遇点即为环入口
while (index1 != index2) {
index1 = index1.next;
index2 = index2.next;
}
return index1;
}
}

19. 删除链表的倒数第 N 个结点

img
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummyNode = new ListNode(0);
dummyNode.next = head;
ListNode fast=dummyNode;
ListNode slow=dummyNode;
for (int i = 0; i < n ; i++){
fast = fast.next;
}
while (fast.next != null){
fast = fast.next;
slow = slow.next;
}
//此时 slowIndex 的位置就是待删除元素的前一个位置。
//具体情况可自己画一个链表长度为 3 的图来模拟代码来理解
slow.next = slow.next.next;
return dummyNode.next;
}

160. 相交链表

循环遍历直到相交

1
2
3
4
5
6
7
8
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode A = headA, B = headB;
while (A != B) {
A = A != null ? A.next : headB;
B = B != null ? B.next : headA;
}
return A;
}

4、分割链表反转

img

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
public class ReorderList {
public void reorderList(ListNode head) {
ListNode fast = head, slow = head;
//求出中点
while (fast.next != null && fast.next.next != null) {
slow = slow.next;
fast = fast.next.next;
}
//right就是右半部分 12345 就是45 1234 就是34
ListNode right = slow.next;
//断开左部分和右部分
slow.next = null;
//反转右部分 right就是反转后右部分的起点
right = reverseList(right);
//左部分的起点
ListNode left = head;
//进行左右部分来回连接
//这里左部分的节点个数一定大于等于右部分的节点个数 因此只判断right即可
while (right != null) {
ListNode curLeft = left.next;
left.next = right;
left = curLeft;

ListNode curRight = right.next;
right.next = left;
right = curRight;
}
}

public ListNode reverseList(ListNode head) {
ListNode headNode = new ListNode(0);
ListNode cur = head;
ListNode next = null;
while (cur != null) {
next = cur.next;
cur.next = headNode.next;
headNode.next = cur;
cur = next;
}
return headNode.next;
}
}

使用虚拟构造一个头节点pre来完成对于一个新链表的创建 例如合并两个排序链表时就可以使用这个方法来创建一个新链表

1
2
3
4
5
6
   ListNode pre = new ListNode(0);      
ListNode cur = pre;

//将值插入新链表中

return pre.next;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode cur=new ListNode(0);
ListNode re=cur;
while (l1!=null&&l2!=null){
if(l1.val<l2.val){
re.next=l1;
l1=l1.next;
}
else {
re.next=l2;
l2=l2.next;
}
re=re.next;
}
if(l1==null){
re.next=l2;
}else re.next=l1;
return cur.next;
}

147. 对链表进行插入排序

148. 排序链表

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
57
58
59
60
//插入排序 
public ListNode insertionSortList(ListNode head) {
if(head==null)return head;
ListNode temp=new ListNode(0);
temp.next=head;
ListNode sorted=head,cur=head.next;
while (cur!=null){
if(sorted.val<=cur.val){
sorted=sorted.next;
}
else {
ListNode pre=temp;
while (pre.next.val<=cur.val){
pre=pre.next;
}
sorted.next=cur.next;
cur.next=pre.next;
pre.next=cur;
}
cur=sorted.next;
}
return temp.next;
}

//归并排序
public ListNode sortList(ListNode head) {
if (head == null || head.next == null)
return head;
ListNode slow=head,fast=head.next;
while (fast!=null&&fast.next!=null){
slow=slow.next;
fast=fast.next.next;
}
ListNode temp=slow.next;
slow.next=null;
ListNode left=sortList(head);
ListNode right=sortList(temp);
ListNode node =merge(left,right);
return node;
}

public ListNode merge(ListNode left,ListNode right){
ListNode res=new ListNode(0);
ListNode temp=res,temp1=left,temp2=right;
while (temp1!=null&&temp2!=null){
if(temp1.val> temp2.val){
temp.next=temp2;
temp2=temp2.next;
temp=temp.next;
}
else {
temp.next=temp1;
temp1=temp1.next;
temp=temp.next;
}
}
if(temp1==null)temp.next=temp2;
else temp.next=temp1;
return res.next;
}

5、合并k个链表

这题就是更新于合并两个排序链表

将k个链表两两合并即可完成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public ListNode mergeKLists(ListNode[] lists) {
if (lists == null || lists.length == 0) return null;
return merge(lists, 0, lists.length - 1);
}

private ListNode merge(ListNode[] lists, int left, int right) {
if (left == right) return lists[left];
int mid = left + (right - left) / 2;
ListNode l1 = merge(lists, left, mid);
ListNode l2 = merge(lists, mid + 1, right);//防止重复何必之前一个链表
return mergeTwoLists(l1, l2);
}
//递归合并两个链表
private ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if (l1 == null) return l2;
if (l2 == null) return l1;
if (l1.val < l2.val) {
l1.next = mergeTwoLists(l1.next, l2);
return l1;
} else {
l2.next = mergeTwoLists(l1,l2.next);
return l2;
}
}

8、2. 两数相加

小tips:使用while 加||可以在内部在判断l1或者l2是否为空这样可以哪怕一边为空都可以继续循环

1
2
3
4
while (l1!=null||l2!=null){
int num1 = l1 == null ? 0 : l1.val;
int num2 = l2 == null ? 0 : l2.val;
//此处为错误示范 while (l1!=null&&l2!=null){
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode pre=new ListNode(0);
ListNode cur=pre;
int carry=0;
while (l1!=null||l2!=null){
int num1 = l1 == null ? 0 : l1.val;
int num2 = l2 == null ? 0 : l2.val;
int sum=num1+num2+carry;
carry=sum/10;
int val=sum%10;
cur.next = new ListNode(val);
cur=cur.next;
if(l1!=null)l1=l1.next;
if(l2!=null)l2=l2.next;
}
//最后再判断是否有进位来看是否增加前缀长度
if(carry == 1) {
cur.next = new ListNode(carry);
}
return pre.next;
}

7、25. K 个一组翻转链表

实现关键就在于将其分组拆分后先反转链表再拼接

k个一组翻转链表.png
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public ListNode reverseKGroup(ListNode head, int k) {
ListNode hair = new ListNode(0);
hair.next = head;
ListNode pre = hair;
ListNode end=hair;
while (end.next != null) {
for (int i = 0; i < k&& end != null; i++) {
end=end.next;
}
if(end==null)break;
ListNode next=end.next;
ListNode start=pre.next;
end.next=null;
pre.next=reverse(start);
start.next=next;
end=start;
pre=start;
}
return hair.next;
}

92. 反转链表 II

image.png

记录pre指针位置,最后链接时pre的next链接right pre.next.next链接succ就可以

image.png
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public ListNode reverseBetween(ListNode head, int left, int right) {
ListNode dummy = new ListNode(0, head), p0 = dummy;
for (int i = 0; i < left - 1; ++i)
p0 = p0.next;
ListNode pre = null, cur = p0.next;
for (int i = 0; i < right - left + 1; ++i) {
ListNode nxt = cur.next;
cur.next = pre; // 每次循环只修改一个 next,方便大家理解
pre = cur;
cur = nxt;
}
p0.next.next = cur;
p0.next = pre;
return dummy.next;
}

61. 旋转链表

关键点在与将链表链接成环,先链接再斩断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public ListNode rotateRight(ListNode head, int k) {
if (k == 0 || head == null || head.next == null) {
return head;
}
int n = 1;
ListNode iter = head;
while (iter.next != null) {
iter = iter.next;
n++;
}
int add = n - k % n;
if (add == n) {
return head;
}
iter.next = head;
while (add-- > 0) {
iter = iter.next;
}
ListNode ret = iter.next;
iter.next = null;
return ret;
}

给定一个奇数位升序,偶数位降序的链表,将其重新排序

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
57
58
59
60
61
62
63
64
65
66
67
68
69
class ListNode {
int val;
ListNode next;

ListNode(int x) {
val = x;
next = null;
}
}

class Solution {
public ListNode sortOddEvenList(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode[] partitions = partition(head);
ListNode oddList = partitions[0];
ListNode evenList = reverse(partitions[1]);
return merge(oddList, evenList);
}

private ListNode[] partition(ListNode head) {
ListNode evenHead = head.next;
ListNode odd = head, even = evenHead;
while (even != null && even.next != null) {
odd.next = even.next;
odd = odd.next;
even.next = odd.next;
even = even.next;
}
odd.next = null; // Disconnect the odd list from the even list
return new ListNode[]{head, evenHead};
}

private ListNode reverse(ListNode head) {
ListNode dummy = new ListNode(-1);
ListNode p = head;
while (p != null) {
ListNode temp = p.next;
p.next = dummy.next;
dummy.next = p;
p = temp;
}
return dummy.next;
}

private ListNode merge(ListNode p, ListNode q) {
ListNode dummy = new ListNode(-1);
ListNode r = dummy;
while (p != null && q != null) {
if (p.val <= q.val) {
r.next = p;
p = p.next;
} else {
r.next = q;
q = q.next;
}
r = r.next;
}
if (p != null) {
r.next = p;
}
if (q != null) {
r.next = q;
}
return dummy.next;
}
}

哈希

哈希map遍历

1、 通过ForEach循环进行遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test {
public static void main(String[] args) throws IOException {
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
map.put(1, 10);
map.put(2, 20);

// Iterating entries using a For Each loop
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());
}

}
}

2、 ForEach迭代键值对方式
如果你只想使用键或者值,推荐使用如下方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Test {
public static void main(String[] args) throws IOException {
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
map.put(1, 10);
map.put(2, 20);

// 迭代键
for (Integer key : map.keySet()) {
System.out.println("Key = " + key);
}

// 迭代值
for (Integer value : map.values()) {
System.out.println("Value = " + value);
}
}

}

3、使用带泛型的迭代器进行遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test {
public static void main(String[] args) throws IOException {
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
map.put(1, 10);
map.put(2, 20);
Iterator<Map.Entry<Integer, Integer>> entries = map.entrySet().iterator();
while (entries.hasNext()) {
Map.Entry<Integer, Integer> entry = entries.next();
System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());
}
}

}

4、使用不带泛型的迭代器进行遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test {

public static void main(String[] args) throws IOException {
Map map = new HashMap();
map.put(1, 10);
map.put(2, 20);
Iterator<Map.Entry> entries = map.entrySet().iterator();
while (entries.hasNext()) {
Map.Entry entry = (Map.Entry) entries.next();
Integer key = (Integer) entry.getKey();
Integer value = (Integer) entry.getValue();
System.out.println("Key = " + key + ", Value = " + value);
}
}

}

5、通过Java8 Lambda表达式遍历

1
2
3
4
5
6
7
8
public class Test {
public static void main(String[] args) throws IOException {
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
map.put(1, 10);
map.put(2, 20);
map.forEach((k, v) -> System.out.println("key: " + k + " value:" + v));
}
}

computeIfAbsent()方法

hashMap.computeIfAbsent(“china”, key -> getValues(key)).add(“liSi”);的意思表示key为“China”的建值对是否存在,返回的是value的值。

如果存在则获取china的值,并操作值的set添加数据“lisi”。

如果不存在,则调用方法,新创建set结构,将”lisi”添加到set中,再存入到hashMap中

2808. 使循环数组所有元素相等的最少秒数

破题关键在于第一破除环形数组,在正常数组背后加头元素成为链式数组 ,第二找到两个相同的元素离得最远的距离除以二就是就是传播所需要的长时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public int minimumSeconds(List<Integer> nums) {
int size = nums.size();
Map<Integer,List<Integer>>map =new HashMap<>();
for (int i = 0; i <size ; i++) {
map.computeIfAbsent(nums.get(i), k -> new ArrayList<>()).add(i);
}
int res=size;
for (List<Integer> a : map.values()) {
a.add(a.get(0) + size);
int mx = 0;
for (int i = 1; i < a.size(); ++i) {
mx = Math.max(mx, (a.get(i) - a.get(i - 1)) / 2);
}
res = Math.min(res, mx);
}
return res;
}

1、基本哈希映射

2、字母相同哈希思路

开一个int hash[26]进行哈希映射,之后选择取min来完成相同字母的统计

1002.查找常用字符

3、多数之和

过程一

过程二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public int[] twoSum(int[] nums, int target) {
int[] res = new int[2];
if(nums == null || nums.length == 0){
return res;
}
Map<Integer, Integer> map = new HashMap<>();
for(int i = 0; i < nums.length; i++){
int temp = target - nums[i]; // 遍历当前元素,并在map中寻找是否有匹配的key
if(map.containsKey(temp)){
res[1] = i;
res[0] = map.get(temp);
break;
}
map.put(nums[i], i); // 如果没找到匹配对,就把访问过的元素和下标加入到map中
}
return res;
}

四数相加同理将两个数分组然后使用哈希匹配

4、s三数之和以上不适合用哈希 使用双指针法完成

给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。

15.三数之和

对数组先进行升序排序

依然还是在数组中找到 abc 使得a + b +c =0,我们这里相当于 a = nums[i],b = nums[left],c = nums[right]。

接下来如何移动left 和right呢, 如果nums[i] + nums[left] + nums[right] > 0 就说明 此时三数之和大了,因为数组是排序后了,所以right下标就应该向左移动,这样才能让三数之和小一些。

如果 nums[i] + nums[left] + nums[right] < 0 说明 此时 三数之和小了,left 就向右移动,才能让三数之和大一些,直到left与right相遇为止。

5、hashset去重

128. 最长连续序列

从小的开始去找大的跳过大的数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
	public int longestConsecutive(int[] nums) {
Set<Integer> num_set = new HashSet<Integer>();
for (int num : nums) {
num_set.add(num);
}
int longestStreak = 0;
for (int num : num_set) {
if (!num_set.contains(num - 1)) {
int currentNum = num;
int currentStreak = 1;
while (num_set.contains(currentNum + 1)) {
currentNum += 1;
currentStreak += 1;
}
longestStreak = Math.max(longestStreak, currentStreak);
}
}
return longestStreak;
}

355. 设计推特

纯模拟题

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
/**
* 用户 id 和推文(单链表)的对应关系
*/
private Map<Integer, Tweet> twitter;
/**
* 用户 id 和他关注的用户列表的对应关系
*/
private Map<Integer, Set<Integer>> followings;
/**
* 全局使用的时间戳字段,用户每发布一条推文之前 + 1
*/
private static int timestamp = 0;
/**
* 合并 k 组推文使用的数据结构(可以在方法里创建使用),声明成全局变量非必需,视个人情况使用
*/
private static PriorityQueue<Tweet> maxHeap;
/**
* Initialize your data structure here.
*/
public Twitter() {
followings = new HashMap<>();
twitter = new HashMap<>();
maxHeap = new PriorityQueue<>((o1, o2) -> -o1.timestamp + o2.timestamp);
}


public void postTweet(int userId, int tweetId) {
timestamp++;
if (twitter.containsKey(userId)) {
Tweet oldHead = twitter.get(userId);
Tweet newHead = new Tweet(tweetId, timestamp);
newHead.next = oldHead;
twitter.put(userId, newHead);
} else {
twitter.put(userId, new Tweet(tweetId, timestamp));
}
}

public List<Integer> getNewsFeed(int userId) {
// 由于是全局使用的,使用之前需要清空
maxHeap.clear();
// 如果自己发了推文也要算上
if (twitter.containsKey(userId)) {
maxHeap.offer(twitter.get(userId));
}

Set<Integer> followingList = followings.get(userId);
if (followingList != null && followingList.size() > 0) {
for (Integer followingId : followingList) {
Tweet tweet = twitter.get(followingId);
if (tweet != null) {
maxHeap.offer(tweet);
}
}
}

List<Integer> res = new ArrayList<>(10);
int count = 0;
while (!maxHeap.isEmpty() && count < 10) {
Tweet head = maxHeap.poll();
res.add(head.id);

// 这里最好的操作应该是 replace,但是 Java 没有提供
if (head.next != null) {
maxHeap.offer(head.next);
}
count++;
}
return res;
}

public void follow(int followerId, int followeeId) {
if (followeeId == followerId) {
return;
}
// 获取我自己的关注列表
Set<Integer> followingList = followings.get(followerId);
if(followingList==null){
Set<Integer> init = new HashSet<>();
init.add(followeeId);
followings.put(followerId, init);
}
else {
if (followingList.contains(followeeId)) {
return;
}
// 这里删除之前无需做判断,因为查找是否存在以后,就可以删除,反正删除之前都要查找
followingList.add(followeeId);
}
}

public void unfollow(int followerId, int followeeId) {
if(followeeId==followerId)return;
Set<Integer>followinglist=followings.get(followerId);
if(followinglist==null)return;
followinglist.remove(followeeId);
}
private class Tweet {
/**
* 推文 id
*/
private int id;
/**
* 发推文的时间戳
*/
private int timestamp;
private Tweet next;

public Tweet(int id, int timestamp) {
this.id = id;
this.timestamp = timestamp;
}
}

两数异或

421. 数组中两个数的最大异或值

2935. 找出强数对的最大异或值 II

以上两题是类似于两数之和的两数异或判断

lc421-c.png
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
public int findMaximumXOR(int[] nums) {
int max = 0;
for (int x : nums) {
max = Math.max(max, x);
}
int highBit = 31 - Integer.numberOfLeadingZeros(max);

int ans = 0, mask = 0;
Set<Integer> seen = new HashSet<>();
for (int i = highBit; i >= 0; i--) { // 从最高位开始枚举
seen.clear();
mask |= 1 << i;
int newAns = ans | (1 << i); // 这个比特位可以是 1 吗?
for (int x : nums) {
x &= mask; // 低于 i 的比特位置为 0
if (seen.contains(newAns ^ x)) {
ans = newAns; // 这个比特位可以是 1
break;
}
seen.add(x);
}
}
return ans;
}

public int maximumStrongPairXor(int[] nums) {
Arrays.sort(nums);
int highBit = 31 - Integer.numberOfLeadingZeros(nums[nums.length - 1]);

int ans = 0, mask = 0;
Map<Integer, Integer> mp = new HashMap<>();
for (int i = highBit; i >= 0; i--) { // 从最高位开始枚举
mp.clear();
mask |= 1 << i;
int newAns = ans | (1 << i); // 这个比特位可以是 1 吗?
for (int y : nums) {
int maskY = y & mask; // 低于 i 的比特位置为 0
if (mp.containsKey(newAns ^ maskY) && mp.get(newAns ^ maskY) * 2 >= y) {
ans = newAns; // 这个比特位可以是 1
break;
}
mp.put(maskY, y);
}
}
return ans;
}

828. 统计子串中的唯一字符

1.png

这题需要找到这个计算规律将寻找字符频数转换为区间内次数计算

1.png
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
	public int uniqueLetterString(String s) {
Map<Character, List<Integer>> map = new HashMap();
char[] sc = s.toCharArray();
for (int i = 0; i < sc.length; i++) {
if (!map.containsKey(sc[i])) map.put(sc[i], new ArrayList());
map.get(sc[i]).add(i);
}
int result = 0;
for(Map.Entry<Character, List<Integer>> entry : map.entrySet()) {
int head = -1, tail = -1;
List<Integer> item = entry.getValue();
for (int i = 0; i < item.size(); i++) {
tail = (i < item.size() - 1) ? item.get(i + 1) : sc.length;
result += (item.get(i) - head) * (tail - item.get(i));
head = item.get(i);
}
}
return result;
}

442. 数组中重复的数据

原地hash 448 41 题也是一样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public List<Integer> findDuplicates(int[] nums) {
for(int i=0;i<nums.length;i++){
while(nums[i]!=nums[nums[i]-1]){
swap(nums,i,nums[i]-1);
}
}
List<Integer> ans = new ArrayList<Integer>();
for (int i = 0; i < nums.length; ++i) {
if (nums[i] - 1 != i) {
ans.add(nums[i]);
}
}
return ans;
}
public void swap(int[] nums, int index1, int index2) {
int temp = nums[index1];
nums[index1] = nums[index2];
nums[index2] = temp;
}

187. 重复的DNA序列

由字符串预处理得到这样的哈希数组和次方数组复杂度为 O(n)。当我们需要计算子串 s[i…j] 的哈希值,只需要利用前缀和思想 h[j]−h[i−1]∗p[j−i+1] 即可在 O(1) 时间内得出哈希值(与子串长度无关)。类似于前缀和你需要求i到j的哈希值时就用 h[j]−h[i−1]∗p[j−i+1] 得到然后每次比较两个串的值是否相同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
int N = (int)1e5+10, P = 131313;
int[] h = new int[N], p = new int[N];
public List<String> findRepeatedDnaSequences(String s) {
int n = s.length();
List<String> ans = new ArrayList<>();
p[0] = 1;
for (int i = 1; i <= n; i++) {
h[i] = h[i - 1] * P + s.charAt(i - 1);
p[i] = p[i - 1] * P;
}
Map<Integer, Integer> map = new HashMap<>();
for (int i = 1; i + 10 - 1 <= n; i++) {
int j = i + 10 - 1;
int hash = h[j] - h[i - 1] * p[j - i + 1];
int cnt = map.getOrDefault(hash, 0);
if (cnt == 1) ans.add(s.substring(i - 1, i + 10 - 1));
map.put(hash, cnt + 1);
}
return ans;
}
}

1044. 最长重复子串

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
class Solution {
long[] h, p;
public String longestDupSubstring(String s) {
int P = 1313131, n = s.length();
h = new long[n + 10]; p = new long[n + 10];
p[0] = 1;
for (int i = 0; i < n; i++) {
p[i + 1] = p[i] * P;
h[i + 1] = h[i] * P + s.charAt(i);
}
String ans = "";
int l = 0, r = n;
while (l < r) {
int mid = l + r + 1 >> 1;
String t = check(s, mid);
if (t.length() != 0) l = mid;
else r = mid - 1;
ans = t.length() > ans.length() ? t : ans;
}
return ans;
}
String check(String s, int len) {
int n = s.length();
Set<Long> set = new HashSet<>();
for (int i = 1; i + len - 1 <= n; i++) {
int j = i + len - 1;
long cur = h[j] - h[i - 1] * p[j - i + 1];
if (set.contains(cur)) return s.substring(i - 1, j);
set.add(cur);
}
return "";
}
}

字符串

1.方法1:char[]数组转成String,使用 String 类的 valueOf() 方法

我们可以使用 String 类的 String.valueOf(char) 方法和 Character 类的 Character.toString(char) 方法在 java 中将 char 转换为 String。

String.valueOf(char) 方法和 Character 类的 Character.toString(char)方法的区别:

1.String.valueOf(char) 方法可以将char[] 和char 变量名转成String类型

2.Character.toString(char)方法只能在char 变量名转成String类型

2、字符翻转

本质就是双指针对于前面字符交换

344.反转字符串

在遇到交换k位之后的字符时,思路为翻转后k个再翻转前k个最后翻转整个字符

img

796. 旋转字符串

ccw-01-09.005.png

3、kmp算法

(1)KMP的主要思想是当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。

(2)所以如何记录已经匹配的文本内容,是KMP的重点,也是next数组肩负的重任。

前缀表是用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配。

KMP详解1

(3)如何计算前缀表

接下来就要说一说怎么计算前缀表。

如图:

KMP精讲5

长度为前1个字符的子串a,最长相同前后缀的长度为0。(注意字符串的前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。)

KMP精讲6

长度为前2个字符的子串aa,最长相同前后缀的长度为1。

KMP精讲7

长度为前3个字符的子串aab,最长相同前后缀的长度为0。

以此类推: 长度为前4个字符的子串aaba,最长相同前后缀的长度为1。 长度为前5个字符的子串aabaa,最长相同前后缀的长度为2。 长度为前6个字符的子串aabaaf,最长相同前后缀的长度为0。

那么把求得的最长相同前后缀的长度就是对应前缀表的元素,如图: KMP精讲8

可以看出模式串与前缀表对应位置的数字表示的就是:下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。

再来看一下如何利用 前缀表找到 当字符不匹配的时候应该指针应该移动的位置。如动画所示:

KMP精讲2

找到的不匹配的位置, 那么此时我们要看它的前一个字符的前缀表的数值是多少。

为什么要前一个字符的前缀表的数值呢,因为要找前面字符串的最长相同的前缀和后缀。

所以要看前一位的 前缀表的数值。

前一个字符的前缀表的数值是2, 所以把下标移动到下标2的位置继续比配。 可以再反复看一下上面的动画。

最后就在文本串中找到了和模式串匹配的子串了。

其实这并不涉及到KMP的原理,而是具体实现,next数组既可以就是前缀表,也可以是前缀表统一减一(右移一位,初始位置为-1)。

KMP精讲4

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
public void getNext(int[] next, String s){
int j = -1;
next[0] = j;
for (int i = 1; i < s.length(); i++){
while(j >= 0 && s.charAt(i) != s.charAt(j+1)){
j=next[j];
}

if(s.charAt(i) == s.charAt(j+1)){
j++;
}
next[i] = j;
}
}
public int strStr(String haystack, String needle) {
if(needle.length()==0){
return 0;
}

int[] next = new int[needle.length()];
getNext(next, needle);
int j = -1;
for(int i = 0; i < haystack.length(); i++){
while(j>=0 && haystack.charAt(i) != needle.charAt(j+1)){
j = next[j];
}
if(haystack.charAt(i) == needle.charAt(j+1)){
j++;
}
if(j == needle.length()-1){
return (i-needle.length()+1);
}
}
return -1;
}

2851. 字符串转换

操作等价于把末尾字母一个一个地移到开头,比如字符串 abcd,「把 cd 移到开头」和「先把 d 移到开头,再把 c 移到开头」,都会得到字符串 cdab。

所以操作得到的是 sss 的循环同构字符串,这意味着,只要 s+s中包含 t,就可以从 sss 变成 t。比如示例 1 的 s+s=abcdabcd,其中就包含一个 cdab。

计算有多少个 sss 的循环同构字符串等于 t,记作 ccc。这可以用 KMP 等字符串匹配算法解决,即寻找 ttt 在 s+ss+ss+s(去掉最后一个字符)中的出现次数。例如示例 2 中 c=3。

image-20240219203048533

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
public int numberOfWays(String s, String t, long k) {
int n = s.length();
int c = kmpSearch(s + s.substring(0, n - 1), t);//避免统计最后一个字母
long[][] m = {
{c - 1, c},
{n - c, n - 1 - c},
};
m = pow(m, k);
return s.equals(t) ? (int) m[0][0] : (int) m[0][1];
}

// KMP 模板
private int[] calcMaxMatch(String s) {
int[] match = new int[s.length()];
int c = 0;
for (int i = 1; i < s.length(); i++) {
char v = s.charAt(i);
while (c > 0 && s.charAt(c) != v) {
c = match[c - 1];
}
if (s.charAt(c) == v) {
c++;
}
match[i] = c;
}
return match;
}

// KMP 模板
// 返回 text 中出现了多少次 pattern(允许 pattern 重叠)
private int kmpSearch(String text, String pattern) {
int[] match = calcMaxMatch(pattern);
int lenP = pattern.length();
int matchCnt = 0;
int c = 0;
for (int i = 0; i < text.length(); i++) {
char v = text.charAt(i);
while (c > 0 && pattern.charAt(c) != v) {
c = match[c - 1];
}
if (pattern.charAt(c) == v) {
c++;
}
if (c == lenP) {
matchCnt++;
c = match[c - 1];
}
}
return matchCnt;
}

private static final long MOD = (long) 1e9 + 7;

// 矩阵乘法
private long[][] multiply(long[][] a, long[][] b) {
long[][] c = new long[2][2];
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 2; j++) {
c[i][j] = (a[i][0] * b[0][j] + a[i][1] * b[1][j]) % MOD;
}
}
return c;
}

// 矩阵快速幂
private long[][] pow(long[][] a, long n) {
long[][] res = {{1, 0}, {0, 1}};
for (; n > 0; n /= 2) {//二分法求幂
if (n % 2 > 0) {
res = multiply(res, a);
}
a = multiply(a, a);
}
return res;
}

4.Z 函数(扩展 KMP)

对于个长度为 n 的字符串s。定义函数 **z[i]**表示 s 和 **s[i,n-1]**(即以 s[i]开头的后缀)的最长公共前缀(LCP)的长度。z被称为 sZ 函数。特别地,z[0] = 0

Z Algorithm (JavaScript Demo) (utdallas.edu)

![image-20240204141139159](./leecode-sum-up/Z Algorithm.png)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public int minimumTimeToInitialState(String S, int k) {
char[] s = S.toCharArray();
int n = s.length;
int[] z = new int[n];
int l = 0, r = 0;
for (int i = 1; i < n; i++) {
if (i <= r) {
z[i] = Math.min(z[i - l], r - i + 1);
}
while (i + z[i] < n && s[z[i]] == s[i + z[i]]) {
l = i;
r = i + z[i];
z[i]++;
}
if (i % k == 0 && z[i] >= n - i) {
return i / k;
}
}
return (n - 1) / k + 1;
}

5、状态机

Picture1.png

318. 最大单词长度乘积

此题有个记录字符串字符出现频率的方法

1
masks[i] |= 1 << (word.charAt(j) - 'a');
  • 对于字符 ‘a’:1 << (word.charAt(j) - 'a') 计算为 1 << 0,结果是 1(二进制:0000 0001)。

  • 对于字符 ‘b’:1 << (word.charAt(j) - 'a') 计算为 1 << 1,结果是 2(二进制:0000 0010)。

  • 对于字符 ‘c’:1 << (word.charAt(j) - 'a') 计算为 1 << 2,结果是 4(二进制:0000 0100)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public int maxProduct(String[] words) {
    int length = words.length;
    int[] masks = new int[length];
    for (int i = 0; i < length; i++) {
    String word = words[i];
    int wordLength = word.length();
    for (int j = 0; j < wordLength; j++) {
    masks[i] |= 1 << (word.charAt(j) - 'a');
    }
    }
    int maxProd = 0;
    for (int i = 0; i < length; i++) {
    for (int j = i + 1; j < length; j++) {
    if ((masks[i] & masks[j]) == 0) {
    maxProd = Math.max(maxProd, words[i].length() * words[j].length());
    }
    }
    }
    return maxProd;
    }

670. 最大交换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public int maximumSwap(int num) {
char[] chars = String.valueOf(num).toCharArray();
int maxIdx = chars.length - 1;
int low=-1,high=0;
for (int i = chars.length-2; i >=0 ;i--) {
if(chars[i]>chars[maxIdx])
{
maxIdx=i;
}
else if(chars[i] < chars[maxIdx]){ // s[i] 右边有比它大的
low=i;
high= maxIdx; // 更新 p 和 q
}
}
if (low == -1) { // 这意味着 s 是降序的
return num;
}
char s=chars[low];
chars[low]=chars[high];
chars[high]=s;
return Integer.parseInt(String.valueOf(chars));
}

栈与队列

Stack

add & push

共同点:

add,push都可以向stack中添加元素。

不同点:

add是继承自Vector的方法,且返回值类型是boolean。

push是Stack自身的方法,返回值类型是参数类类型。

具体的看源码:

1
2
3
4
5
6
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
1
2
3
4
5
public E push(E item) {
addElement(item);

return item;
}

peek & pop

共同点:

peek,pop都是返回栈顶元素。

不同点:

peek()函数返回栈顶的元素,但不弹出该栈顶元素。
pop()函数返回栈顶的元素,并且将该栈顶元素出栈。

150. 逆波兰表达式求值

此题关键在于栈存放数字在遍历到符号时取出数字运算即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Stack<Integer>caculate=new Stack<>();
for(int i =0;i<tokens.length;i++){
if ("+".equals(tokens[i])) { // leetcode 内置jdk的问题,不能使用==判断字符串是否相等
caculate.push(caculate.pop() + caculate.pop()); // 注意 - 和/ 需要特殊处理
} else if ("-".equals(tokens[i])) {
caculate.push(-caculate.pop() + caculate.pop());
} else if ("*".equals(tokens[i])) {
caculate.push(caculate.pop() * caculate.pop());
} else if ("/".equals(tokens[i])) {
int temp1 = caculate.pop();
int temp2 = caculate.pop();
caculate.push(temp2 / temp1);
}
else {
caculate.push(Integer.valueOf(tokens[i]));
}
}
return caculate.pop();

739. 每日温度

此题在于使用stack存储数组下标这样在栈中取出时自然可以定位第几天的前面的最高温是多少

1
2
3
4
while(!stack.isEmpty()&&temperatures[i]>temperatures[stack.peek()]){
res[stack.peek()]=i-stack.peek();
stack.pop();
}

853. 车队

抓住一个思想,如果你位置在我前面,最终到达时间比我慢我们两个之间就会相遇,即使两个车相遇但是以最慢的那个车速度来行驶,行驶时间都是最慢那个车的最长时间,所以继续用那个车的最长时间来进行比较看是否会有二次相遇.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//注意此处需要使用TreeMap才能实现对于hash表的顺序输出 还要注意抓换spd数据类型
public int carFleet(int target, int[] position, int[] speed) {
Map<Integer,Integer>map=new TreeMap<>();
for (int i = 0; i < position.length; i++) {
map.put(position[i],speed[i]);
}
Stack<Float>stack = new Stack<>();
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
int pos = entry.getKey();
int spd = entry.getValue();
float time=(float) (target-pos)/(float)spd;
while (!stack.isEmpty()&&time>=stack.peek()){
stack.pop();
System.out.println(stack.size());
}
stack.push(time);
}
return stack.size();
}

Queue

1、add()和offer()区别:

add()和offer()都是向队列中添加一个元素。一些队列有大小限制,因此如果想在一个满的队列中加入一个新项,调用 add() 方法就会抛出一个 unchecked 异常,而调用 offer() 方法会返回 false。因此就可以在程序中进行有效的判断!

2、poll()和remove()区别:

remove() 和 poll() 方法都是从队列中删除第一个元素。如果队列元素为空,调用remove() 的行为与 Collection 接口的版本相似会抛出异常,但是新的 poll() 方法在用空集合调用时只是返回 null。因此新的方法更适合容易出现异常条件的情况。

3、element() 和 peek() 区别:

element() 和 peek() 用于在队列的头部查询元素。与 remove() 方法类似,在队列为空时, element() 抛出一个异常,而 peek() 返回 null。
下面是Java中Queue的一些常用方法:
add 增加一个元索 如果队列已满,则抛出一个IIIegaISlabEepeplian异常
remove 移除并返回队列头部的元素 如果队列为空,则抛出一个NoSuchElementException异常
element 返回队列头部的元素 如果队列为空,则抛出一个NoSuchElementException异常
offer 添加一个元素并返回true 如果队列已满,则返回false
poll 移除并返问队列头部的元素 如果队列为空,则返回null
peek 返回队列头部的元素 如果队列为空,则返回null
put 添加一个元素 如果队列满,则阻塞
take 移除并返回队列头部的元素 如果队列为空,则阻塞

PriorityQueue

1
2
//出现次数按从队头到队尾的顺序是从大到小排,出现次数最多的在队头(相当于大顶堆)
PriorityQueue<int[]> pq = new PriorityQueue<>((pair1, pair2)->pair2[1]-pair1[1]);
1
2
//出现次数按从队头到队尾的顺序是从小到大排,出现次数最低的在队头(相当于小顶堆)
PriorityQueue<int[]> pq = new PriorityQueue<>((pair1,pair2)->pair1[1]-pair2[1]);

LCP 30. 魔塔游戏

贪心 + 优先队列,在遍历房间的过程中,如果 nums[i]为负数,我们将其放入一个小根堆(优先队列)中。当计算完第 i 个房间的生命值影响后,如果生命值小于等于 0,那么我们取出堆顶元素,表示将该房间调整至末尾,并将其补回生命值中。由于一定会从小根堆中取出一个小于等于 nums[i] 的值,因此调整完成后,生命值一定大于 0。

当所有房间遍历完成后,我们还需要将所有从堆中取出元素的和重新加入生命值,如果生命值小于等于 0,说明无解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public int magicTower(int[] nums) {
Queue<Integer>queue=new PriorityQueue<>();
int cur=1;
int back=0;
int res=0;
for(int num:nums){
if(num<0)queue.add(num);
cur+=num;
if(cur<=0){
res++;
int n=queue.poll();
cur-=n;
back+=n;
}
}
cur+=back;
return cur>0?1:0;
}

使用例子利用大小顶堆反复跳实现对于队列的排序

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
public class MedianFinder {
Queue<Integer>min=new PriorityQueue<>();
Queue<Integer>max=new PriorityQueue<>();

// 295. 数据流的中位数 https://leetcode.cn/problems/find-median-from-data-stream/?envType=study-plan-v2&envId=top-100-liked
public MedianFinder() {
min = new PriorityQueue<Integer>((a, b) -> (b - a));
max = new PriorityQueue<Integer>((a, b) -> (a - b));
}

public void addNum(int num) {
if(min.isEmpty()||num<=min.peek()){
min.offer(num);
if(max.size()+1<min.size()){
max.offer(min.poll());
}
}
else {
max.offer(num);
if(min.size()+1<max.size()){
min.offer(max.poll());
}
}
}

public double findMedian() {
if(max.size()<min.size()){
return min.peek();
}
return (max.peek()+min.peek())/2.0;
}
}

373. 查找和最小的 K 对数字

此题使用PriorityQueue来完成 对于插入数字的排序 为了方便操作并且字符是已经排好序的我们选用字符索引作为priorityqueue的存储数字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public List<List<Integer>> kSmallestPairs(int[] nums1, int[] nums2, int k) {
List<List<Integer>>res=new ArrayList<>();
int m=nums1.length;
int n=nums2.length;
Queue<int[]>queue=new PriorityQueue<>((a,b)->{
return nums1[a[0]] + nums2[a[1]] - nums1[b[0]] - nums2[b[1]];
});
for (int i = 0; i < Math.min(m, k); i++) {
queue.offer(new int[]{i,0});
}
while (k-->0&&!queue.isEmpty()){
int[] idxPair = queue.poll();
List<Integer>group=new ArrayList<>();
group.add(nums1[idxPair[0]]);
group.add(nums2[idxPair[1]]);
res.add(group);
if(idxPair[1]+1<n){
queue.offer(new int[]{idxPair[0],idxPair[1]+1});
}
}
return res;
}

辅助栈法

最小栈

  1. 按照上面的思路,我们只需要设计一个数据结构,使得每个元素 a 与其相应的最小值 m 时刻保持一一对应。因此我们可以使用一个辅助栈,与元素栈同步插入与删除,用于存储与每个元素对应的最小值。

  2. 当一个元素要入栈时,我们取当前辅助栈的栈顶存储的最小值,与当前元素比较得出最小值,将这个最小值插入辅助栈中;

  3. 当一个元素要出栈时,我们把辅助栈的栈顶元素也一并弹出;

  4. 在任意一个时刻,栈内元素的最小值就存储在辅助栈的栈顶元素中。

fig1

字符串解码

  1. 构建辅助栈 stack, 遍历字符串 s 中每个字符 c;
  2. 当 c 为数字时,将数字字符转化为数字 multi,用于后续倍数计算;
  3. 当 c 为字母时,在 res 尾部添加 c;
  4. 当 c 为 [ 时,将当前 multi 和 res 入栈,并分别置空置 000:
  5. 记录此 [ 前的临时结果 res 至栈,用于发现对应 ] 后的拼接操作;
  6. 记录此 [ 前的倍数 multi 至栈,用于发现对应 ] 后,获取 multi × […] 字符串。
  7. 进入到新 [ 后,res 和 multi 重新记录。
  8. 当 c 为 ] 时,stack 出栈,拼接字符串 res = last_res + cur_multi * res,其中:
  9. last_res是上个 [ 到当前 [ 的字符串,例如 “3[a2[c]]” 中的 a;
  10. cur_multi是当前 [ 到 ] 内字符串的重复倍数,例如 “3[a2[c]]” 中的 2。
  11. 返回字符串 res。

使用辅助的栈帮忙存储每一层的字符结果这样可以将其递归返回到上一层输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public String decodeString(String s) {
StringBuilder res = new StringBuilder();
int multi = 0;
LinkedList<Integer> stack_multi = new LinkedList<>();
LinkedList<String> stack_res = new LinkedList<>();
for(Character c : s.toCharArray()) {
if(c == '[') {
stack_multi.addLast(multi);
stack_res.addLast(res.toString());
multi = 0;
res = new StringBuilder();
}
else if(c == ']') {
StringBuilder tmp = new StringBuilder();
int cur_multi = stack_multi.removeLast();
for(int i = 0; i < cur_multi; i++) tmp.append(res);
res = new StringBuilder(stack_res.removeLast() + tmp);
}
else if(c >= '0' && c <= '9') multi = multi * 10 + Integer.parseInt(c + "");
else res.append(c);
}
return res.toString();
}

42. 接雨水

两种思路如果使用栈则是按照行求

image.png

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public int trap(int[] height) {
Stack<Integer> st = new Stack<>();
st.push(0);
int sum = 0;
for (int i = 1; i < height.length; i++) {
while (!st.empty() && height[i] > height[st.peek()]) {
int mid = st.peek();
st.pop();
if (!st.empty()) {
int h = Math.min(height[st.peek()], height[i]) - height[mid];
int w = i - st.peek() - 1;
sum += h * w;
}
}
st.push(i);
}
return sum;
}

如果使用双指针则是按照列来求, 每次计算相邻两个列的差值只要最右边有高于左边的水就一定留得住

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public int trap(int[] height) {
int left=0,right=height.length-1;
int leftheight=height[left],rightheight=height[right];
int leftmax=0,rightmax=0;
int sum=0;
while (left<=right){
leftmax=Math.max(leftmax,height[left]);
rightmax=Math.max(rightmax,height[right]);
if(leftmax<rightmax){
sum+=leftmax-height[left];
left++;
}
else {
sum+=rightmax-height[right];
right--;
}
}
return sum;
}

621. 任务调度器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public int leastInterval(char[] tasks, int n) {
//统计每个任务出现的次数,找到出现次数最多的任务
int[] hash = new int[26];
for(int i = 0; i < tasks.length; ++i) {
hash[tasks[i] - 'A'] += 1;
}
Arrays.sort(hash);
//因为相同元素必须有n个冷却时间,假设A出现3次,n = 2,任务要执行完,至少形成AXX AXX A序列(X看作预占位置)
//该序列长度为
int minLen = (n+1) * (hash[25] - 1) + 1;

//此时为了尽量利用X所预占的空间(贪心)使得整个执行序列长度尽量小,将剩余任务往X预占的空间插入
//剩余的任务次数有两种情况:
//1.与A出现次数相同,比如B任务最优插入结果是ABX ABX AB,中间还剩两个空位,当前序列长度+1
//2.比A出现次数少,若还有X,则按序插入X位置,比如C出现两次,形成ABC ABC AB的序列
//直到X预占位置还没插满,剩余元素逐个放入X位置就满足冷却时间至少为n
for(int i = 24; i >= 0; --i){
if(hash[i] == hash[25]) ++ minLen;
}
//当所有X预占的位置插满了怎么办?
//在任意插满区间(这里是ABC)后面按序插入剩余元素,比如ABCD ABCD发现D之间距离至少为n+1,肯定满足冷却条件
//因此,当X预占位置能插满时,最短序列长度就是task.length,不能插满则取最少预占序列长度
return Math.max(minLen, tasks.length);
}

502. IPO

此题思路为银行家算法先从小满足能够获取的资源,然后经可能在最小资源中选取最大的获利

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public int findMaximizedCapital(int k, int w, int[] profits, int[] capital) {
int n=profits.length;
int curr = 0;
int eff[][]=new int[n][2];
for (int i = 0; i <n; i++) {
eff[i][0]=capital[i];
eff[i][1]=profits[i];
}
Arrays.sort(eff,(a,b)->{return a[0]-b[0];});
PriorityQueue<Integer>queue=new PriorityQueue<>((x, y) -> y - x);
for (int i = 0; i < k; i++) {
while (curr<n&&eff[curr][0]<=w){
queue.add(eff[curr][1]);
curr++;
}
if (!queue.isEmpty()) {
w += queue.poll();
} else {
break;
}
}
return w;
}

224. 基本计算器

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
public int calculate(String s) {
Deque<Integer> queue = new LinkedList<Integer>();
queue.add(1);
int pre=1;
int res=0;
int i=0;
while (i<s.length()) {
if(s.charAt(i)==' ')i++;
else if(s.charAt(i)=='+'){
pre=queue.peek();
i++;
}
else if(s.charAt(i)=='-'){
pre=-queue.peek();
i++;
}
else if(s.charAt(i)=='('){
queue.push(pre);
i++;
}
else if(s.charAt(i)==')') {
queue.pop();
i++;
}
else {
long num=0;
while (i<s.length()&&Character.isDigit(s.charAt(i))){
num=num*10+s.charAt(i)-'0';
i++;
}
res+=num*pre;
}
}
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
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
class Solution {
// 使用 map 维护一个运算符优先级
// 这里的优先级划分按照「数学」进行划分即可
Map<Character, Integer> map = new HashMap<>(){{
put('-', 1);
put('+', 1);
put('*', 2);
put('/', 2);
put('%', 2);
put('^', 3);
}};
public int calculate(String s) {
// 将所有的空格去掉
s = s.replaceAll(" ", "");
char[] cs = s.toCharArray();
int n = s.length();
// 存放所有的数字
Deque<Integer> nums = new ArrayDeque<>();
// 为了防止第一个数为负数,先往 nums 加个 0
nums.addLast(0);
// 存放所有「非数字以外」的操作
Deque<Character> ops = new ArrayDeque<>();
for (int i = 0; i < n; i++) {
char c = cs[i];
if (c == '(') {
ops.addLast(c);
} else if (c == ')') {
// 计算到最近一个左括号为止
while (!ops.isEmpty()) {
if (ops.peekLast() != '(') {
calc(nums, ops);
} else {
ops.pollLast();
break;
}
}
} else {
if (isNumber(c)) {
int u = 0;
int j = i;
// 将从 i 位置开始后面的连续数字整体取出,加入 nums
while (j < n && isNumber(cs[j])) u = u * 10 + (cs[j++] - '0');
nums.addLast(u);
i = j - 1;
} else {
if (i > 0 && (cs[i - 1] == '(' || cs[i - 1] == '+' || cs[i - 1] == '-')) {
nums.addLast(0);
}
// 有一个新操作要入栈时,先把栈内可以算的都算了
// 只有满足「栈内运算符」比「当前运算符」优先级高/同等,才进行运算
while (!ops.isEmpty() && ops.peekLast() != '(') {
char prev = ops.peekLast();
if (map.get(prev) >= map.get(c)) {
calc(nums, ops);
} else {
break;
}
}
ops.addLast(c);
}
}
}
// 将剩余的计算完
while (!ops.isEmpty()) calc(nums, ops);
return nums.peekLast();
}
void calc(Deque<Integer> nums, Deque<Character> ops) {
if (nums.isEmpty() || nums.size() < 2) return;
if (ops.isEmpty()) return;
int b = nums.pollLast(), a = nums.pollLast();
char op = ops.pollLast();
int ans = 0;
if (op == '+') ans = a + b;
else if (op == '-') ans = a - b;
else if (op == '*') ans = a * b;
else if (op == '/') ans = a / b;
else if (op == '^') ans = (int)Math.pow(a, b);
else if (op == '%') ans = a % b;
nums.addLast(ans);
}
boolean isNumber(char c) {
return Character.isDigit(c);
}
}

二叉树

递归三要素

  1. 确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
  2. 确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
  3. 确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。

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
//后序遍历
void postorder(TreeNode root, List<Integer> list) {
if (root==null){
return;
}
postorder(root.left,list);
postorder(root.right,list);
list.add(root.val);
}
//先序遍历
void frontorder(TreeNode root, List<Integer> list){
if(root==null)
return;
list.add(root.val);
frontorder(root.left,list);
frontorder(root.right,list);
}
//中序遍历
void midorder(TreeNode root, List<Integer> list){
if(root==null)
return;
midorder(root.left,list);
list.add(root.val);
frontorder(root.right,list);
}

迭代实现

前序到后序

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
57
58
59
60
61
62
63
64
//先序遍历
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null){
return result;
}
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()){
TreeNode node = stack.pop();
result.add(node.val);
if (node.right != null){
stack.push(node.right);
}
if (node.left != null){
stack.push(node.left);
}
}
return result;
}
}
//中序遍历 注意二叉树中序遍历风格与前后序遍历不统一
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null){
return result;
}
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
while (cur != null || !stack.isEmpty()){
if (cur != null){
stack.push(cur);
cur = cur.left;
}else{
cur = stack.pop();
result.add(cur.val);
cur = cur.right;
}
}
return result;
}

//后序遍历
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null){
return result;
}
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()){
TreeNode node = stack.pop();
result.add(node.val);
if (node.left != null){
stack.push(node.left);
}
if (node.right != null){
stack.push(node.right);
}
}
Collections.reverse(result);//反转先序遍历
return result;
}

层序遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 //层序遍历 使用队列辅助遍历
public List<List<Integer>> findBottomLeftValue(TreeNode root) {
List<List<Integer>> resList = new ArrayList<List<Integer>>();
if (root == null) return null;
Queue<TreeNode> que = new LinkedList<TreeNode>();
que.offer(root);
while (!que.isEmpty()) {
List<Integer> itemList = new ArrayList<Integer>();
int len = que.size();
while (len > 0) {
TreeNode tmpNode = que.poll();
itemList.add(tmpNode.val);
if (tmpNode.left != null) que.offer(tmpNode.left);
if (tmpNode.right != null) que.offer(tmpNode.right);
len--;
}

resList.add(itemList);
}
return resList;
}

Ps:层序遍历变种

请实现一个函数按照之字形顺序打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右到左的顺序打印,第三行再按照从左到右的顺序打印,其他行以此类推。

使用LinkedList实现对于队列的反转插入

1
2
3
 if(!isEven) item.addLast(root.val);

else item.addFirst(root.val);

958. 二叉树的完全性检验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 层序遍历解决,关键思想是:如果遍历到了一个非空节点之前遍历过空节点,那么为false,否则遍历完毕返回true
public boolean isCompleteTree(TreeNode root) {
Deque<TreeNode> deque = new LinkedList<>();
deque.addLast(root);
boolean flag = true;
while(!deque.isEmpty()){
TreeNode node = deque.pollLast();
if(node == null){
flag = false;
}
else{
// 如果在非空节点前出现了空节点那么则为false
if(!flag){
return false;
}
deque.addFirst(node.left);
deque.addFirst(node.right);
}
}
return true;
}

105. 从前序与中序遍历序列构造二叉树

106. 从中序与后序遍历序列构造二叉树

两个类型一致的题目关键点在于先从先序遍历/后序遍历找到树的头结点 然后再去拆分其左右子树范围再递归创建左右子树

1
2
3
4
5
6
7
8
9
10
11
#后序 
if (inBegin >= inEnd || postBegin >= postEnd) { // 不满足左闭右开,说明没有元素,返回空树
return null;
}
int rootIndex = map.get(postorder[postEnd - 1]); // 找到后序遍历的最后一个元素在中序遍历中的位置
TreeNode root = new TreeNode(inorder[rootIndex]); // 构造结点
int lenOfLeft = rootIndex - inBegin; // 保存中序左子树个数,用来确定后序数列的个数
root.left = findNode(inorder, inBegin, rootIndex,
postorder, postBegin, postBegin + lenOfLeft);
root.right = findNode(inorder, rootIndex + 1, inEnd,
postorder, postBegin + lenOfLeft, postEnd - 1);
1
2
3
4
5
6
7
8
9
10
11
#先序
if (preBegin >= preEnd || inBegin >= inEnd) { // 不满足左闭右开,说明没有元素,返回空树
return null;
}
int rootIndex = map.get(preorder[preBegin]); // 找到前序遍历的第一个元素在中序遍历中的位置
TreeNode root = new TreeNode(inorder[rootIndex]); // 构造结点
int lenOfLeft = rootIndex - inBegin; // 保存中序左子树个数,用来确定前序数列的个数
root.left = findNode(preorder, preBegin + 1, preBegin + lenOfLeft + 1,
inorder, inBegin, rootIndex);
root.right = findNode(preorder, preBegin + lenOfLeft + 1, preEnd,
inorder, rootIndex + 1, inEnd);

889. 根据前序和后序遍历构造二叉树

其中 preorder 是一个具有 无重复 值的二叉树的前序遍历,postorder 是同一棵树的后序遍历

树可以构造,答案不唯一 也是找到后序遍历切入点找到左右子树长度创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public TreeNode constructFromPrePost(int[] preorder, int[] postorder) {
int n = preorder.length;
Map<Integer, Integer> postMap = new HashMap<Integer, Integer>();
for (int i = 0; i < n; i++) {
postMap.put(postorder[i], i);
}
return dfs(preorder, postorder, postMap, 0, n - 1, 0, n - 1);
}

public TreeNode dfs(int[] preorder, int[] postorder, Map<Integer, Integer> postMap, int preLeft, int preRight, int postLeft, int postRight) {
if (preLeft > preRight) {
return null;
}
int leftCount = 0;
if (preLeft < preRight) {
leftCount = postMap.get(preorder[preLeft + 1]) - postLeft + 1;
}
return new TreeNode(preorder[preLeft],
dfs(preorder, postorder, postMap, preLeft + 1, preLeft + leftCount, postLeft, postLeft + leftCount - 1),
dfs(preorder, postorder, postMap, preLeft + leftCount + 1, preRight, postLeft + leftCount, postRight - 1));
}

2641. 二叉树的堂兄弟节点 II

此题使用两遍dfs 第一遍算出每一层的节点值的和,第二次dfs时用每层和减去本节点的非兄弟节点的值的和就是替换的兄弟节点值的和

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
List<Integer>sum=new ArrayList<>();
public TreeNode replaceValueInTree(TreeNode root) {
dfs1(root,0);
root.val=0;
dfs2(root,0);
return root;
}
public void dfs1(TreeNode root,int depth){
if(root==null)return;
if(sum.size()<=depth)
{
sum.add(0);
}
sum.set(depth, sum.get(depth) + root.val);
dfs1(root.left, depth + 1);
dfs1(root.right, depth + 1);
}
public void dfs2(TreeNode node,int depth){
int l=node.left==null?0:node.left.val;
int r=node.right==null?0:node.right.val;
int add=l+r;
depth++;
if(node.left!=null){
node.left.val=sum.get(depth)-add;
dfs2(node.left,depth);
}
if(node.right!=null){
node.right.val=sum.get(depth)-add;
dfs2(node.right,depth);
}
}

236. 二叉树的最近公共祖先

此题思想是从下层的子节点的祖先找起一层层返回判断左右节点是否都存在公共祖先如果存在则返回当前节点,如果不存在返回存在节点的祖先

Picture2.png

1
2
3
4
5
6
7
8
9
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root==p||root==q||root==null)return root;
TreeNode left=lowestCommonAncestor(root.left,p,q);
TreeNode right=lowestCommonAncestor(root.right,p,q);
if(left!=null&&right!=null)return root;
else if(left==null&&right!=null)return right;
else if(left!=null&&right==null)return left;
else return null;
}

114. 二叉树展开为链表

具体做法是,对于当前节点,如果其左子节点不为空,则在其左子树中找到最右边的节点,作为前驱节点,将当前节点的右子节点赋给前驱节点的右子节点,然后将当前节点的左子节点赋给当前节点的右子节点,并将当前节点的左子节点设为空。对当前节点处理结束后,继续处理链表中的下一个节点,直到所有节点都处理结束.

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
    1
/ \
2 5
/ \ \
3 4 6

//将 1 的左子树插入到右子树的地方
1
\
2 5
/ \ \
3 4 6
//将原来的右子树接到左子树的最右边节点
1
\
2
/ \
3 4
\
5
\
6

//将 2 的左子树插入到右子树的地方
1
\
2
\
3 4
\
5
\
6

//将原来的右子树接到左子树的最右边节点
1
\
2
\
3
\
4
\
5
\
6
1
2
3
4
5
6
7
8
9
10
11
12
public void flatten(TreeNode root) {
while (root !=null){
if(root.left!=null){
TreeNode pre = root.left;
while (pre.right!=null)pre=pre.right;
pre.right=root.right;
root.right=root.left;
root.left=null;
}
root=root.right;
}
}

2、特殊二叉树

完全二叉树

在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2^(h-1) 个节点。

大家要自己看完全二叉树的定义,很多同学对完全二叉树其实不是真正的懂了。

我来举一个典型的例子如题:

img

完全二叉树只有两种情况,情况一:就是满二叉树,情况二:最后一层叶子节点没有满。

对于情况一,可以直接用 2^树深度 - 1 来计算,注意这里根节点深度为1。

对于情况二,分别递归左孩子,和右孩子,递归到某一深度一定会有左孩子或者右孩子为满二叉树,然后依然可以按照情况1来计算。

2673. 使二叉树所有路径值相等的最小代价

1
2
3
4
5
6
7
8
9
public int minIncrements(int n, int[] cost) {
int ans = 0;
for (int i = n - 2; i > 0; i -= 2) {
ans += Math.abs(cost[i] - cost[i + 1]);
// 叶节点 i 和 i+1 的双亲节点下标为 i/2(整数除法)
cost[i / 2] += Math.max(cost[i], cost[i + 1]);
}
return ans;
}

平衡二叉树

一棵度平衡二叉树定义为:一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过1。

二叉搜索树

二叉搜索树

前面介绍的树,都没有数值的,而二叉搜索树是有数值的了,二叉搜索树是一个有序树。意味着中序遍历是一个顺序递增数组

  • 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
  • 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
  • 它的左、右子树也分别为二叉排序树

下面这两棵树都是搜索树

img

数组构建二叉搜索树

1
2
3
4
5
6
7
8
9
10
11
public TreeNode constructMaximumBinaryTree2(int[] nums, int leftIndex, int rightIndex) {
if (rightIndex - leftIndex < 0) {// 没有元素了
return null;
}
int maxVal=(leftIndex+rightIndex)/2;
TreeNode root = new TreeNode(nums[maxVal]);
// 根据maxIndex划分左右子树
root.left = constructMaximumBinaryTree2(nums, leftIndex, (leftIndex+rightIndex)/2-1);
root.right = constructMaximumBinaryTree2(nums, (leftIndex+rightIndex)/2+1, rightIndex);
return root;
}

3、计算节点数/深度

递归的思想

1
2
3
int leftdepth = getdepth(node->left);       // 左
int rightdepth = getdepth(node->right); // 右
int depth = 1 + Math.max(leftdepth, rightdepth); // 中

Ps:在选择边界时尽量使用统一选择要么都左闭右开要么右闭左开

来看一下一共分几步:

  • 第一步:如果数组大小为零的话,说明是空节点了。
  • 第二步:如果不为空,那么取后序数组最后一个元素作为节点元素。
  • 第三步:找到后序数组最后一个元素在中序数组的位置,作为切割点
  • 第四步:切割中序数组,切成中序左数组和中序右数组 (顺序别搞反了,一定是先切中序数组)
  • 第五步:切割后序数组,切成后序左数组和后序右数组
  • 第六步:递归处理左区间和右区间
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
Map<Integer, Integer> map;  // 方便根据数值查找位置
public TreeNode buildTree(int[] inorder, int[] postorder) {
map = new HashMap<>();
for (int i = 0; i < inorder.length; i++) { // 用map保存中序序列的数值对应位置
map.put(inorder[i], i);
}

return findNode(inorder, 0, inorder.length, postorder,0, postorder.length); // 前闭后开
}
//从中序和后序构造二叉树
public TreeNode findNode(int[] inorder, int inBegin, int inEnd, int[] postorder, int postBegin, int postEnd) {
// 参数里的范围都是前闭后开
if (inBegin >= inEnd || postBegin >= postEnd) { // 不满足左闭右开,说明没有元素,返回空树
return null;
}
int rootIndex = map.get(postorder[postEnd - 1]); // 找到后序遍历的最后一个元素在中序遍历中的位置
TreeNode root = new TreeNode(inorder[rootIndex]); // 构造结点
int lenOfLeft = rootIndex - inBegin; // 保存中序左子树个数,用来确定后序数列的个数
root.left = findNode(inorder, inBegin, rootIndex,
postorder, postBegin, postBegin + lenOfLeft);
root.right = findNode(inorder, rootIndex + 1, inEnd,
postorder, postBegin + lenOfLeft, postEnd - 1);

return root;
}
//**从前序与中序遍历序列构造二叉树**
public TreeNode findNode(int[] preorder, int preBegin, int preEnd, int[] inorder, int inBegin, int inEnd) {
// 参数里的范围都是前闭后开
if (preBegin >= preEnd || inBegin >= inEnd) { // 不满足左闭右开,说明没有元素,返回空树
return null;
}
int rootIndex = map.get(preorder[preBegin]); // 找到前序遍历的第一个元素在中序遍历中的位置
TreeNode root = new TreeNode(inorder[rootIndex]); // 构造结点
int lenOfLeft = rootIndex - inBegin; // 保存中序左子树个数,用来确定前序数列的个数
root.left = findNode(preorder, preBegin + 1, preBegin + lenOfLeft + 1,
inorder, inBegin, rootIndex);
root.right = findNode(preorder, preBegin + lenOfLeft + 1, preEnd,
inorder, rootIndex + 1, inEnd);

return root;
}

450. 删除二叉搜索树中的节点

根据二叉搜索树的性质

如果目标节点大于当前节点值,则去右子树中删除;
如果目标节点小于当前节点值,则去左子树中删除;
如果目标节点就是当前节点,分为以下三种情况:
其无左子:其右子顶替其位置,删除了该节点;
其无右子:其左子顶替其位置,删除了该节点;
其左右子节点都有:其左子树转移到其右子树的最左节点的左子树上,然后右子树顶替其位置,由此删除了该节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (root == null) return null;
if (root.val > key) {
root.left = deleteNode(root.left,key);
} else if (root.val < key) {
root.right = deleteNode(root.right,key);
} else {
if(root.left==null)return root.right;
if(root.right==null)return root.left;
TreeNode rootleft=root.left;
TreeNode rootright=root.right;
while (rootright.left!=null)rootright=rootright.left;
rootright.left=rootleft;
root=root.right;
}
return root;

2476. 二叉搜索树最近节点查询

注意此处中序遍历后有个变式的二分查找,我们需要使用n和-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
32
33
34
35
36
37
38
39
40
41
42
public List<List<Integer>> closestNodes(TreeNode root, List<Integer> queries) {
List<Integer> arr = new ArrayList<>();
dfs(root, arr);
int n = arr.size();
int[] a = new int[n];
for (int i = 0; i < n; i++) {
a[i] = arr.get(i); // 转成数组,效率更高
}

List<List<Integer>> ans = new ArrayList<>(queries.size()); // 预分配空间
for (int q : queries) {
int j = binarysearch(a, q);
int mx = j == n ? -1 : a[j];
if (j == n || a[j] != q) { // a[j]>q, a[j-1]<q
j--;
}
int mn = j < 0 ? -1 : a[j];
ans.add(new ArrayList<Integer>(Arrays.asList(mn,mx)));

}
return ans;
}

private void dfs(TreeNode node, List<Integer> a) {
if (node == null) {
return;
}
dfs(node.left, a);
a.add(node.val);
dfs(node.right, a);
}
public int binarysearch(int a[],int target){
int left=-1,right=a.length;
while (left+1<right){
int mid=left+(right-left)/2;
if (a[mid]>=target)right=mid;
else {
left=mid;
}
}
return right;
}

117. 填充每个节点的下一个右侧节点指针 II

方法三:BFS+链表
思路
既然每一层都连接成一个链表了,那么知道链表头,就能访问这一层的所有节点。

所以在 BFS 的时候,可以一边遍历当前层的节点,一边把下一层的节点连接起来。这样就无需存储下一层的节点了,只需要拿到下一层链表的头节点。

算法
从第一层开始(第一层只有一个 root 节点),每次循环:
遍历当前层的链表节点,通过节点的 left 和 right 得到下一层的节点。
把下一层的节点从左到右连接成一个链表。
拿到下一层链表的头节点,进入下一轮循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public Node connect(Node root) {
Node dum=new Node();
Node cur=root;
while (cur!=null){
dum.next=null;
Node temp=dum;
while (cur!=null){
if(cur.left!=null){
temp.next=cur.left;
temp=temp.next;
}
if(cur.right!=null){
temp.next=cur.right;
temp=temp.next;
}
cur=cur.next;
}
cur=dum.next;
}
return root;
}

208. 实现 Trie (前缀树)

前缀树就是自己生成一个树使得这个数能够存储一段String,并且在之后输入时能够判断String的前缀是否吻合或者String是否相同

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
   //Tries的变量结构
private Trie []children;
private boolean isEnd;
public Trie() {
children=new Trie [26];
isEnd=false;
}
//插入时存储
public void insert(String word) {
Trie node=this;
for (int i = 0; i < word.length(); i++) {
char t=word.charAt(i);
if(node.children[t-'a']==null){
node.children[t-'a']=new Trie();
}
node=node.children[t-'a'];
}
node.isEnd=true;
}

//取出对比
public Trie presearch(String word){
Trie node=this;
for (int i = 0; i < word.length(); i++) {
char t=word.charAt(i);
if(node.children[t-'a']==null)
return null;
node=node.children[t-'a'];
}
return node;
}

变式 211. 添加与搜索单词 - 数据结构设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public boolean search(WordDictionary dic,String word,int length){
int n=word.length();
if(length==n)return dic.isEnd;
char t=word.charAt(length);
if(t!='.'){
WordDictionary item = dic.children[t-'a'];
return item!=null && search(item,word,length+1);
}
//在遇到.代替所有字母时使用dfs进行全部的遍历
for(int j = 0; j < 26; j++){
if(dic.children[j]!=null && search(dic.children[j],word,length+1))
return true;
}
return false;
}

212. 单词搜索 II

遍历二维网格中的所有单元格。

深度优先搜索所有从当前正在遍历的单元格出发的、由相邻且不重复的单元格组成的路径。因为题目要求同一个单元格内的字母在一个单词中不能被重复使用;所以我们在深度优先搜索的过程中,每经过一个单元格,都将该单元格的字母临时修改为特殊字符(例如 #),以避免再次经过该单元格。

如果当前路径是 words\textit{words}words 中的单词,则将其添加到结果集中。如果当前路径是 wordswordswords 中任意一个单词的前缀,则继续搜索;反之,如果当前路径不是 wordswordswords 中任意一个单词的前缀,则剪枝。我们可以将 words\textit{words}words 中的所有字符串先添加到前缀树中,而后用 O(∣S∣)O(|S|)O(∣S∣) 的时间复杂度查询当前路径是否为 words\textit{words}words 中任意一个单词的前缀。

之后再回溯,因为同一个单词可能在多个不同的路径中出现,所以我们需要使用哈希集合对结果集去重。

在回溯的过程中,我们不需要每一步都判断完整的当前路径是否是 words中任意一个单词的前缀;而是可以记录下路径中每个单元格所对应的前缀树结点,每次只需要判断新增单元格的字母是否是上一个单元格对应前缀树结点的子结点即可。

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
57
58
59
60
61
62
63
64
65
66
67
    int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
//循环测试每个字母开头判断是否存在单词
public List<String> findWords(char[][] board, String[] words) {
Trie2 trie = new Trie2();
for (String word : words) {
trie.insert(word);
}

Set<String> ans = new HashSet<String>();
for (int i = 0; i < board.length; ++i) {
for (int j = 0; j < board[0].length; ++j) {
dfs(board, trie, i, j, ans);
}
}

return new ArrayList<String>(ans);
}
//深度搜索寻找到单词
public void dfs(char[][] board, Trie2 now, int i1, int j1, Set<String> ans) {
if (!now.children.containsKey(board[i1][j1])) {
return;
}
char ch = board[i1][j1];
Trie2 nxt = now.children.get(ch);
if (!"".equals(nxt.word)) {
ans.add(nxt.word);
nxt.word = "";
}
//标记已经遍历的位置为# 然后回溯为了findwords的下一次循环
if (!nxt.children.isEmpty()) {
board[i1][j1] = '#';
for (int[] dir : dirs) {
int i2 = i1 + dir[0], j2 = j1 + dir[1];
if (i2 >= 0 && i2 < board.length && j2 >= 0 && j2 < board[0].length) {
dfs(board, nxt, i2, j2, ans);
}
}
board[i1][j1] = ch;
}

if (nxt.children.isEmpty()) {
now.children.remove(ch);
}
}
}
//构造字典树
class Trie2 {
String word;
Map<Character, Trie2> children;
boolean isWord;

public Trie2() {
this.word = "";
this.children = new HashMap<Character, Trie2>();
}

public void insert(String word) {
Trie2 cur = this;
for (int i = 0; i < word.length(); ++i) {
char c = word.charAt(i);
if (!cur.children.containsKey(c)) {
cur.children.put(c, new Trie2());
}
cur = cur.children.get(c);
}
cur.word = word;
}

二叉树的垂序遍历

我们需要按照优先级「“列号从小到大”,对于同列节点,“行号从小到大”,对于同列同行元素,“节点值从小到大”」进行答案构造。

因此我们可以对树进行遍历,遍历过程中记下这些信息 (col,row,val)(col, row, val)(col,row,val),然后根据规则进行排序,并构造答案。

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
Map<TreeNode ,int []> map= new HashMap<>();
public List<List<Integer>> verticalTraversal(TreeNode root) {
List<List<Integer>>res=new ArrayList<>();
map.put(root,new int[]{0,0,root.val});
dfs(root);
List<int[]> list = new ArrayList<>(map.values());
Collections.sort(list,(a,b)->{
if(a[0]!=b[0])return a[0]-b[0];
if(a[1]!=b[1])return a[1]-b[1];
return a[2]-b[2];
});
for (int i = 0; i < list.size();) {
int j=i;
List<Integer>cur=new ArrayList<>();
while (j<list.size()&&list.get(j)[0]==list.get(i)[0])cur.add(list.get(j++)[2]);
res.add(cur);
i=j;
}
return res;
}
public void dfs(TreeNode root){
if(root==null)return;
int []node=map.get(root);
int col=node[0],row=node[1],val=node[2];
if(root.left!=null){
map.put(root.left, new int[]{col - 1, row + 1, root.left.val});
dfs(root.left);
}
if (root.right != null) {
map.put(root.right, new int[]{col + 1, row + 1, root.right.val});
dfs(root.right);
}
}

回溯

回溯法模板

回溯函数伪代码如下:

1
void backtracking(参数)
  • 回溯函数终止条件

既然是树形结构,那么我们在讲解二叉树的递归 (opens new window)的时候,就知道遍历树形结构一定要有终止条件。

所以回溯也有要终止条件。

什么时候达到了终止条件,树中就可以看出,一般来说搜到叶子节点了,也就找到了满足条件的一条答案,把这个答案存放起来,并结束本层递归。

所以回溯函数终止条件伪代码如下:

1
2
3
4
if (终止条件) {
存放结果;
return;
}
  • 回溯搜索的遍历过程

在上面我们提到了,回溯法一般是在集合中递归搜索,集合的大小构成了树的宽度,递归的深度构成的树的深度。

回溯函数遍历过程伪代码如下:

1
2
3
4
5
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}

分析完过程,回溯算法模板框架如下:

1
2
3
4
5
6
7
8
9
10
11
12
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}

for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}

这份模板很重要,后面做回溯法的题目都靠它了!

回溯法解决的问题

回溯法,一般可以解决如下几种问题:

  • 组合问题:N个数里面按一定规则找出k个数的集合

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    private void combineHelper(int n, int k, int startIndex){
    //终止条件
    if (path.size() == k){
    result.add(new ArrayList<>(path));
    return;
    }
    for (int i = startIndex; i <= n - (k - path.size()) + 1; i++){
    path.add(i);
    combineHelper(n, k, i + 1);
    path.removeLast();
    }
    }
  • 切割问题:一个字符串按一定规则有几种切割方式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    for (int i = startIndex; i < s.length(); i++) {
    //如果是回文子串,则记录
    if (isPalindrome(s, startIndex, i)) {
    String str = s.substring(startIndex, i + 1);
    deque.addLast(str);
    } else {
    continue;
    }
    //起始位置后移,保证不重复
    backTracking(s, i + 1);
    deque.removeLast();
    }
  • 子集问题:一个N个数的集合里有多少符合条件的子集

    如果把 子集问题、组合问题、分割问题都抽象为一棵树的话,那么组合问题和分割问题都是收集树的叶子节点,而子集问题是找树的所有节点!

    其实子集也是一种组合问题,因为它的集合是无序的,子集{1,2} 和 子集{2,1}是一样的。

    那么既然是无序,取过的元素不会重复取,写回溯算法的时候,for就要从startIndex开始,而不是从0开始!

1
2
3
4
5
6
7
8
9
10
11
private void subsetsHelper(int[] nums, int startIndex){
result.add(new ArrayList<>(path));//「遍历这个树的时候,把所有节点都记录下来,就是要求的子集集合」。
if (startIndex >= nums.length){ //终止条件可不加
return;
}
for (int i = startIndex; i < nums.length; i++){
path.add(nums[i]);
subsetsHelper(nums, i + 1);
path.removeLast();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private void backTrack(int[] nums, boolean[] used) {
if (path.size() == nums.length) {
result.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++) {
// used[i - 1] == true,说明同⼀树⽀nums[i - 1]使⽤过
// used[i - 1] == false,说明同⼀树层nums[i - 1]使⽤过
// 如果同⼀树层nums[i - 1]使⽤过则直接跳过
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {
continue;
}
//如果同⼀树⽀nums[i]没使⽤过开始处理
if (used[i] == false) {
used[i] = true;//标记同⼀树⽀nums[i]使⽤过,防止同一树枝重复使用
path.add(nums[i]);
backTrack(nums, used);
path.remove(path.size() - 1);//回溯,说明同⼀树层nums[i]使⽤过,防止下一树层重复
used[i] = false;//回溯
}
}
}
  • 棋盘问题:N皇后,解数独等等

剑指 Offer 36. 二叉搜索树与双向链表

思路主要掌握二叉搜索树的中序遍历过程以及双向链表的创建方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Node pre,head;
public Node treeToDoublyList(Node root) {
if(root==null)return head;
dfs2(root);
pre.right=head;
head.left=pre;
return head;
}
public void dfs2(Node cur){
if(cur==null)return;
dfs2(cur.left);
if(pre != null) pre.right = cur;
else head = cur;
cur.left=pre;
pre=cur;
dfs2(cur.right);
}

二叉树的最近公共祖先

二叉树遍历从下到上依靠的就是回溯算法

236.二叉树的最近公共祖先2

1
2
3
4
5
6
7
8
9
public TreeNode lowestCommonAncestor2(TreeNode root, TreeNode p, TreeNode q) {
if(root==p||root==q||root==null)return root;
TreeNode left=lowestCommonAncestor2(root.left,p,q);
TreeNode right=lowestCommonAncestor2(root.right,p,q);
if(left!=null&&right!=null)return root;
else if(left==null&&right!=null)return right;
else if(left!=null&&right==null)return left;
else return null;
}

N皇后

这里我明确给出了棋盘的宽度就是for循环的长度,递归的深度就是棋盘的高度,这样就可以套进回溯法的模板里了

51.N皇后
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
List<List<String>> result4 = new ArrayList<>();
public List<List<String>> solveNQueens(int n) {
char[][] chessboard = new char[n][n];
for (char[] c : chessboard) {
Arrays.fill(c, '.');
}
queensHelper(chessboard, 0,n );
return result4;
}
public void queensHelper(char[][]chessboard,int row,int n){
if(row==n){
result4.add(Array2List(chessboard));
return;
}
for (int col = 0; col < n; col++) {
if(isValid(row,col,n,chessboard)){
chessboard[row][col]='Q';
queensHelper(chessboard,row+1,n);
chessboard[row][col]='.';
}
}

}
public List Array2List(char[][] chessboard) {
List<String> list = new ArrayList<>();

for (char[] c : chessboard) {
list.add(String.copyValueOf(c));
}
return list;
}
public boolean isValid(int row, int col, int n, char[][] chessboard) {
//row
for (int i = 0; i < row; i++) {
if(chessboard[i][col]=='Q')return false;
}
//45o
for (int i=row-1, j=col-1; i>=0 && j>=0; i--, j--) {
if (chessboard[i][j] == 'Q') {
return false;
}
}
//135o
for (int i=row-1, j=col+1; i>=0 && j<=n-1; i--, j++) {
if (chessboard[i][j] == 'Q') {
return false;
}
}
return true;
}

DFS与BFS

dfs是由底下遍历到上层,而bfs是由上层依次遍历到底层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//DFS
public int maxDepth(TreeNode root) {
if(root==null)return 0;
int nLeft = maxDepth(root.left);
int nRight = maxDepth(root.right);
return nLeft > nRight ? nLeft + 1 : nRight + 1;
}
//BFS
public int maxDepth(TreeNode root) {
if (root == null) return 0;
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
int res = 0;
while (!queue.isEmpty()) {
res++;
int n = queue.size();
for (int i = 0; i < n; i++) {
TreeNode node = queue.poll();
if (node.left != null) queue.add(node.left);
if (node.right != null) queue.add(node.right);
}
}
return res;
}

前缀和,递归,回溯

437. 路径总和 III

在同一个路径之下(可以理解成二叉树从root节点出发,到叶子节点的某一条路径),如果两个数的前缀总和是相同的,那么这些节点之间的元素总和为零。进一步扩展相同的想法,如果前缀总和currSum,在节点A和节点B处相差target,则位于节点A和节点B之间的元素之和是target。

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
public int pathSum(TreeNode root, int sum) {
// key是前缀和, value是大小为key的前缀和出现的次数
Map<Long, Integer> prefixSumCount = new HashMap<>();
// 前缀和为0的一条路径
prefixSumCount.put(0L, 1);
// 前缀和的递归回溯思路
return recursionPathSum(root, prefixSumCount, sum, 0L);
}

private int recursionPathSum(TreeNode node, Map<Long, Integer> prefixSumCount, int target, long currSum) {
// 1.递归终止条件
if (node == null) {
return 0;
}
// 2.本层要做的事情
int res = 0;
// 当前路径上的和
currSum += node.val;-

//---核心代码
// 看看root到当前节点这条路上是否存在节点前缀和加target为currSum的路径
// 当前节点->root节点反推,有且仅有一条路径,如果此前有和为currSum-target,而当前的和又为currSum,两者的差就肯定为target了
// currSum-target相当于找路径的起点,起点的sum+target=currSum,当前点到起点的距离就是target
res += prefixSumCount.getOrDefault(currSum - target, 0);
// 更新路径上当前节点前缀和的个数
prefixSumCount.put(currSum, prefixSumCount.getOrDefault(currSum, 0) + 1);
//---核心代码

// 3.进入下一层
res += recursionPathSum(node.left, prefixSumCount, target, currSum);
res += recursionPathSum(node.right, prefixSumCount, target, currSum);

// 4.回到本层,恢复状态,去除当前节点的前缀和数量
prefixSumCount.put(currSum, prefixSumCount.get(currSum) - 1);
return res;
}

124. 二叉树中的最大路径和

例如,考虑如下二叉树。

-10
/
9 20
/
15 7
叶节点 999、151515、777 的最大贡献值分别为 999、151515、777。

得到叶节点的最大贡献值之后,再计算非叶节点的最大贡献值。节点 202020 的最大贡献值等于 20+max⁡(15,7)=3520+\max(15,7)=3520+max(15,7)=35,节点 −10-10−10 的最大贡献值等于 −10+max⁡(9,35)=25-10+\max(9,35)=25−10+max(9,35)=25。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int maxsum=Integer.MIN_VALUE;;
public int maxPathSum(TreeNode root) {
dfs(root);
return maxsum;

}

public int dfs(TreeNode root){
if(root==null)
return 0;
int leftgain=Math.max(dfs(root.left),0);
int rightgain=Math.max(dfs(root.right),0);
int sum=root.val+leftgain+rightgain;
maxsum=Math.max(sum,maxsum);
return root.val + Math.max(leftgain, rightgain);
}

2602. 使数组元素全部相等的最少操作次数

此题关键点在于排序后如何找到分割点,这样的话对于小于的我们用long left=(long) q * j - sum[j];大于的我们用long right=sum[n]-sum[j]-(long) q * (n - 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
public List<Long> minOperations(int[] nums, int[] queries) {
Arrays.sort(nums);
int n = nums.length;
long[] sum = new long[n + 1]; // 前缀和
for (int i = 0; i < n; ++i)
sum[i + 1] = sum[i] + nums[i];
List<Long> ans = new ArrayList<Long>(queries.length);
for (int q:queries) {
int j=search(nums,q);
long left=(long) q * j - sum[j];
long right=sum[n]-sum[j]-(long) q * (n - j);
ans.add(left+right);
}
return ans;
}
public int search(int nums[],int tar){
int left=0,right=nums.length-1;
while (left<=right){
int mid=left+(right-left)/2;
if(nums[mid]<tar)left=mid+1;
else right=mid-1;
}
return left;
}

22. 括号生成301. 删除无效的括号

这两题思路相似都是处理括号有效的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
List<String>result=new ArrayList<>();
public List<String> generateParenthesis(int n) {
dfs("",n,n);
return result;
}
public void dfs(String cur,int left,int right){
if(left==0&&right==0){
result.add(cur);
return;
}
if (left > right) {
return;
}
if(left>0){
dfs(cur+"(",left-1,right);
}
if(right>0){
dfs(cur+")",left,right-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
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
57
58
59
60
61
62
63
64
65
66
class Solution {
private List<String> res = new ArrayList<String>();

public List<String> removeInvalidParentheses(String s) {
int lremove = 0;
int rremove = 0;

for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) == '(') {
lremove++;
} else if (s.charAt(i) == ')') {
if (lremove == 0) {
rremove++;
} else {
lremove--;
}
}
}
helper(s, 0, lremove, rremove);

return res;
}

private void helper(String str, int start, int lremove, int rremove) {
if (lremove == 0 && rremove == 0) {
if (isValid(str)) {
res.add(str);
}
return;
}

for (int i = start; i < str.length(); i++) {
if (i != start && str.charAt(i) == str.charAt(i - 1)) {
continue;
}
// 如果剩余的字符无法满足去掉的数量要求,直接返回
if (lremove + rremove > str.length() - i) {
return;
}
// 尝试去掉一个左括号
if (lremove > 0 && str.charAt(i) == '(') {
helper(str.substring(0, i) + str.substring(i + 1), i, lremove - 1, rremove);
}
// 尝试去掉一个右括号
if (rremove > 0 && str.charAt(i) == ')') {
helper(str.substring(0, i) + str.substring(i + 1), i, lremove, rremove - 1);
}
}
}

private boolean isValid(String str) {
int cnt = 0;
for (int i = 0; i < str.length(); i++) {
if (str.charAt(i) == '(') {
cnt++;
} else if (str.charAt(i) == ')') {
cnt--;
if (cnt < 0) {
return false;
}
}
}

return cnt == 0;
}
}

二维前缀和

304. 二维区域和检索 - 矩阵不可变

1.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int[][] sum;
public NumMatrix(int[][] matrix) {
int n = matrix.length, m = n == 0 ? 0 : matrix[0].length;
// 与「一维前缀和」一样,前缀和数组下标从 1 开始,因此设定矩阵形状为 [n + 1][m + 1](模板部分)
sum = new int[n + 1][m + 1];
// 预处理除前缀和数组(模板部分)
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + matrix[i - 1][j - 1];
}
}
}

public int sumRegion(int x1, int y1, int x2, int y2) {
// 求某一段区域和 [i, j] 的模板是 sum[x2][y2] - sum[x1 - 1][y2] - sum[x2][y1 - 1] + sum[x1 - 1][y1 - 1];(模板部分)
// 但由于我们源数组下标从 0 开始,因此要在模板的基础上进行 + 1
x1++; y1++; x2++; y2++;
return sum[x2][y2] - sum[x1 - 1][y2] - sum[x2][y1 - 1] + sum[x1 - 1][y1 - 1];
}

动态规划

动态规划的解题步骤

  1. 确定dp数组(dp table)以及下标的含义

  2. 确定递推公式

  3. dp数组如何初始化

  4. 确定遍历顺序

  5. 举例推导dp数组

32. 最长有效括号

此题思想在于不以(来计算有效个数而是以)来计算有效个数

1
2
3
4
5
6
7
8
9
if (s.charAt(i) == ')') {
if (s.charAt(i - 1) == '(') {
dp[i] = (i >= 2 ? dp[i - 2] : 0) + 2;
} else if (i - dp[i - 1] > 0 && s.charAt(i - dp[i - 1] - 1) == '(') {
dp[i] = dp[i - 1] + ((i - dp[i - 1]) >= 2 ? dp[i - dp[i - 1] - 2] : 0) + 2;
}
maxans = Math.max(maxans, dp[i]);

}

300. 最长递增子序列

1.当 nums[i]>nums[j] 时: nums[i] 可以接在nums[j] 之后(此题要求严格递增),此情况下最长上升子序列长度为dp[j]+1 ;
2.当 nums[i]<=nums[j] 时: nums[i] 无法接在 nums[j] 之后,此情况上升子序列不成立,跳过。
上述所有 1. 情况 下计算出的 dp[j]+1 的最大值,为直到 iii 的最长上升子序列长度(即 dp[i] )。实现方式为遍历 j 时,每轮执行 dp[i]=max(dp[i],dp[j]+1)。

转移方程为 dp[i] = max(dp[i], dp[j] + 1) for j in [0, i)

优化方式将长度变化g寻找递增元素值, 如果num[i]>g中的最后一个元素值时长度增加将其添加,如果小于则去更新g中第一个大于num[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
public int lengthOfLIS(int[] nums) {
if(nums.length == 0) return 0;
int[] dp = new int[nums.length];
int res = 0;
Arrays.fill(dp, 1);
for(int i = 0; i < nums.length; i++) {
for(int j = 0; j < i; j++) {
if(nums[j] < nums[i]) dp[i] = Math.max(dp[i], dp[j] + 1);
}
res = Math.max(res, dp[i]);
}
return res;
}

//二分查找优化
public int lengthOfLIS(int[] nums) {
int[] tails = new int[nums.length];
int res = 0;
for(int num : nums) {
int i = 0, j = res;
while(i < j) {
int m = (i + j) / 2;
if(tails[m] < num) i = m + 1;
else j = m;
}
tails[i] = num;
if(res == j) res++;
}
return res;
}

变种题1671. 得到山形数组的最少删除次数

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
class Solution {
public int minimumMountainRemovals(int[] nums) {
int n = nums.length;
int[] pre = getLISArray(nums);
int[] reversed = reverse(nums);
int[] suf = getLISArray(reversed);
suf = reverse(suf);

int ans = 0;
for (int i = 0; i < n; ++i) {
if (pre[i] > 1 && suf[i] > 1) {
ans = Math.max(ans, pre[i] + suf[i] - 1);
}
}

return n - ans;
}

public int[] getLISArray(int[] nums) {
int n = nums.length;
int[] dp = new int[n];
Arrays.fill(dp, 1);
for (int i = 0; i < n; ++i) {
for (int j = 0; j < i; ++j) {
if (nums[j] < nums[i]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
}
return dp;
}

public int[] reverse(int[] nums) {
int n = nums.length;
int[] reversed = new int[n];
for (int i = 0; i < n; i++) {
reversed[i] = nums[n - 1 - i];
}
return reversed;
}
}

918. 环形子数组的最大和

Picture1.png

第一种情况:这个子数组不是环状的,就是说首尾不相连。
第二种情况:这个子数组一部分在首部,一部分在尾部,我们可以将这第二种情况转换成第一种情况

image.png

1
2
3
4
5
6
7
8
9
10
11
12
public int maxSubarraySumCircular(int[] nums) {
int n=nums.length,total=0;
int max=0,maxsum=nums[0],min=0,minsum=nums[0];
for (int i = 0; i < n; i++) {
max=Math.max(max+nums[i],nums[i]);
maxsum=Math.max(max,maxsum);
min=Math.min(min+nums[i],nums[i]);
maxsum=Math.min(min,minsum);
total+=nums[i];
}
return maxsum > 0 ? Math.max(maxsum, total - minsum) : maxsum;
}

1696. 跳跃游戏 VI

动态规划加滑动窗口,使用一个queue来负责记录最大的取值的位置 dp数组为走到当前位置能获取的最大分数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public int maxResult(int[] nums, int k) {
int n = nums.length;
int[] dp = new int[n];
dp[0] = nums[0];
Deque<Integer> queue = new ArrayDeque<>();
queue.offerLast(0);
for (int i = 1; i < n; i++) {
while (queue.peekFirst() < i - k) {
queue.pollFirst();
}
dp[i] = dp[queue.peekFirst()] + nums[i];
while (!queue.isEmpty() && dp[queue.peekLast()] <= dp[i]) {
queue.pollLast();
}
queue.offerLast(i);
}
return dp[n - 1];
}
416.分割等和子集1

给定一个数组,数组中含有重复的元素,给定两个数字num1,num2,求这两个数字在数组中出现的位置的最小距离。

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
public static int minDistance(int[] arr,int num1,int num2)
{
if(arr==null||arr.length<=0)
{
System.out.println("参数不合格!");
return Integer.MAX_VALUE;
}
int lastPos1=-1; //上次遍历到num1的位置
int lastPos2=-1; //上次遍历到num1的位置
int minDis=Integer.MAX_VALUE; //num1,num2的最小距离
for(int i=0;i<arr.length;++i)
{
if(arr[i]==num1)
{
lastPos1=i;
if(lastPos2>=0)
minDis=Math.min(minDis, lastPos1-lastPos2);
}
if(arr[i]==num2)
{
lastPos2=i;
if(lastPos1>=0)
minDis=Math.min(minDis, lastPos2-lastPos1);
}
}
return minDis;
}

01背包理论基础

动态规划-背包问题2

对于背包问题,有一种写法, 是使用二维数组,即dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少

1
状态转移方程 dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 可以看出i 是由 i-1 推导出来,那么i为0的时候就一定要初始化。
1
2
3
4
5
6
for(int i = 1; i < weight.size(); i++) { // 遍历物品
for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
if (j < weight[i]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}

动态规划-背包问题5

1
2
3
4
5
6
7
// weight数组的大小 就是物品个数
for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
for(int i = 1; i < weight.size(); i++) { // 遍历物品
if (j < weight[i]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}

先遍历背包再遍历物品

动态规划-背包问题6

一维dp数组(滚动数组)

1
2
3
4
5
6
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}

这里大家发现和二维dp的写法中,遍历背包的顺序是不一样的!

二维dp遍历的时候,背包容量是从小到大,而一维dp遍历的时候,背包是从大到小。

为什么呢?

倒序遍历是为了保证物品i只被放入一次!。但如果一旦正序遍历了,那么物品0就会被重复加入多次!

再来看看两个嵌套for循环的顺序,代码中是先遍历物品嵌套遍历背包容量,那可不可以先遍历背包容量嵌套遍历物品呢?

不可以!

因为一维dp的写法,背包容量一定是要倒序遍历(原因上面已经讲了),如果遍历背包容量放在上一层,那么每个dp[j]就只会放入一个物品,即:背包里只放入了一个物品。

倒序遍历的原因是,本质上还是一个对二维数组的遍历,并且右下角的值依赖上一层左上角的值,因此需要保证左边的值仍然是上一层的,从右向左覆盖。

完全背包理论基础

每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。

完全背包和01背包问题唯一不同的地方就是,每种物品有无限件

而完全背包的物品是可以添加多次的,所以要从小到大去遍历,即:

1
2
3
4
5
6
7
// 先遍历物品,再遍历背包
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = weight[i]; j <= bagWeight ; j++) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

}
}

在完全背包中,对于一维dp数组来说,其实两个for循环嵌套顺序是无所谓的!

如果求组合数就是外层for循环遍历物品,内层for遍历背包

如果求排列数就是外层for遍历背包,内层for循环遍历物品

多重背包

很少出现

337. 打家劫舍 II

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public int rob(int[] nums) {
if (nums.length == 0) return 0;
if (nums.length == 1) return nums[0];
int result1 = robRange(nums, 0, nums.length- 2); // 情况二
int result2 = robRange(nums, 1, nums.length - 1); // 情况三
return Math.max(result1, result2);
}
public int robRange(int []nums,int begin, int end){
if (end == begin) return nums[begin];
int[] dp=new int [nums.length];
dp[begin] = nums[begin];
dp[begin + 1] = Math.max(nums[begin], nums[begin + 1]);
for(int i=begin+2;i<=end;i++){
dp[i]=Math.max(dp[i-2]+nums[i],dp[i-1]);
}
return dp[end];
}

152. 乘积最大子数组

此题破题在于对于负数的处理,选择mindp和maxdp两个来存储最大值、最小值,这样在遇到负数时交换两个的值就可以

139. 单词拆分

dp[i] : 字符串长度为i的话,dp[i]为true,表示可以拆分为一个或多个在字典中出现的单词

1
2
3
4
String word=s.substring(j,i);
if(set.contains(word)&&dp[j]){
dp[i]=true;
}

416. 分割等和子集

322. 零钱兑换

494. 目标和

此类型题目一般会给出target 值以及物品值,使用物品值与target匹配在计算dp数组时下标计算则是按照他们target与物品值的差值来计算下标

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
//[416. 分割等和子集](https://leetcode.cn/problems/partition-equal-subset-sum/)
int target = sum / 2;
int dp[]=new int[target+1];
for (int i = 0; i < n; i++) {
for (int j = target; j >=nums[i] ; j--) {
dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
}
if(dp[target] == target)
return true;
}
//494. 目标和 此题主要思路与分割等和子集相同先将(sum+target)/2得到我们要分割的子集 只是这个是求满足和的个数
for (int i = 0; i < nums.length; i++) sum += nums[i];
if ( target < 0 && sum < -target) return 0;
if ((target + sum) % 2 != 0) return 0;
int size = (target + sum) / 2;
if(size < 0) size = -size;
int[] dp = new int[size + 1];
dp[0] = 1;
for (int i = 0; i < nums.length; i++) {
for (int j = size; j >= nums[i]; j--) {
dp[j] += dp[j - nums[i]];
}
}
//[322. 零钱兑换](https://leetcode.cn/problems/coin-change/)
for (int i = 0; i < coins.length; i++) {
for (int j = coins[i]; j < dp.length; j++) {
if(dp[j - coins[i]]!=max){
dp[j]=Math.min(dp[j],dp[j-coins[i]]+1);
}
}
}

1143. 最长公共子序列,718. 最长重复子数组 115. 不同的子序列

2维的dp数组使用dp[i][j]分别代表第一个数组和第二个数组的长度 他们在创建时都创建为dp[i+1][j+1]这样才能读取前一个的字符位置计算下一个字符位置,使得字符串长度从1开始而不是从0开始。

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
//最长重复数组
for (int i = 1; i <= nums1.size(); i++) {
for (int j = 1; j <= nums2.size(); j++) {
if (nums1[i - 1] == nums2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
}
if (dp[i][j] > result) result = dp[i][j];
}
}
//最长公共子序列
for (int i = 1; i <=text1.length(); i++) {
for (int j = 1; j <=text2.length(); j++) {
if(text1.charAt(i-1)==text2.charAt(j-1)){
dp[i][j]=dp[i-1][j-1]+1;
}
else {
dp[i][j]=Math.max(dp[i-1][j],dp[i][j-1]);
}
}
// 115. 不同的子序列
int[][] dp = new int[m + 1][n + 1];
for (int i = 0; i <= m; i++) {
dp[i][n] = 1;
}
for (int i = m - 1; i >= 0; i--) {
char sChar = s.charAt(i);
for (int j = n - 1; j >= 0; j--) {
char tChar = t.charAt(j);
if (sChar == tChar) {
dp[i][j] = dp[i + 1][j + 1] + dp[i + 1][j];
} else {
dp[i][j] = dp[i + 1][j];
}
}
}
return dp[0][0];

518. 零钱兑换 II377. 组合总和 Ⅳ

这两个就是明显的组合数和排列数的计算,求装满背包有几种方法,递推公式一般都是dp[i] += dp[i - nums[j]];

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//求组合数先遍历物品容量再遍历背包重量 零钱兑换
for (int i = 0; i < coins.size(); i++) { // 遍历物品
for (int j = coins[i]; j <= amount; j++) { // 遍历背包容量
dp[j] += dp[j - coins[i]];
}
}
//求排列数先遍历背包重量在遍历物品重量
for (int i = 0; i <= target; i++) {
for (int j = 0; j < nums.length; j++) {
if (i >= nums[j]) {
dp[i] += dp[i - nums[j]];
}
}
}

买卖股票类型题目

121. 买卖股票的最佳时机122. 买卖股票的最佳时机 II123. 买卖股票的最佳时机 III188. 买卖股票的最佳时机 IV309. 买卖股票的最佳时机含冷冻期714. 买卖股票的最佳时机含手续费

对于第一二两个题递推公式为:

这里重申一下dp数组的含义:

  • - dp[i][0]表示第i天持有股票所得现金。
    - dp[i][1] 表示第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
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41

    第三题

    1. 确定dp数组以及下标的含义

    一天一共就有五个状态,

    1. 没有操作 (其实我们也可以不设置这个状态)
    2. 第一次持有股票
    3. 第一次不持有股票
    4. 第二次持有股票
    5. 第二次不持有股票

    dp[i][j]中 i表示第i天,j为 [0 - 4] 五个状态,dp[i][j]表示第i天状态j所剩最大现金。

    ```java
    确定递推公式
    达到dp[i][1]状态,有两个具体操作:

    操作一:第i天买入股票了,那么dp[i][1] = dp[i-1][0] - prices[i]
    操作二:第i天没有操作,而是沿用前一天买入的状态,即:dp[i][1] = dp[i - 1][1]
    那么dp[i][1]究竟选 dp[i-1][0] - prices[i],还是dp[i - 1][1]呢?

    一定是选最大的,所以 dp[i][1] = max(dp[i-1][0] - prices[i], dp[i - 1][1]);

    同理dp[i][2]也有两个操作:

    操作一:第i天卖出股票了,那么dp[i][2] = dp[i - 1][1] + prices[i]
    操作二:第i天没有操作,沿用前一天卖出股票的状态,即:dp[i][2] = dp[i - 1][2]
    所以dp[i][2] = max(dp[i - 1][1] + prices[i], dp[i - 1][2])

    同理可推出剩下状态部分:

    dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i]);

    dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]);

    dp[i][1] = Math.max(dp[i - 1][1], -prices[i]);
    dp[i][2] = Math.max(dp[i - 1][2], dp[i - 1][1] + prices[i]);
    dp[i][3] = Math.max(dp[i - 1][3], dp[i - 1][2] - prices[i]);
    dp[i][4] = Math.max(dp[i - 1][4], dp[i - 1][3] + prices[i]);
    第四题状态更多的第三题自然就是题目要求是至多有K笔交易,那么j的范围就定义为 2 * k + 1 就可以了。**偶数就是卖出,奇数就是买入**
    1
    2
    3
    4
    5
    6
    7
    8
    9
    for (int i = 1; i < k*2; i += 2) {
    dp[0][i] = -prices[0];
    }
    for (int i = 1; i < len; i++) {
    for (int j = 0; j < k*2 - 1; j += 2) {
    dp[i][j + 1] = Math.max(dp[i - 1][j + 1], dp[i - 1][j] - prices[i]);
    dp[i][j + 2] = Math.max(dp[i - 1][j + 2], dp[i - 1][j + 1] + prices[i]);
    }
    }

含冷冻期此题

  • 状态一:持有股票状态(今天买入股票,或者是之前就买入了股票然后没有操作,一直持有)

  • 不持有股票状态,这里就有两种卖出股票状态

    • 状态二:保持卖出股票的状态(两天前就卖出了股票,度过一天冷冻期。或者是前一天就是卖出股票状态,一直没操作)
    • 状态三:今天卖出股票
  • 状态四:今天为冷冻期状态,但冷冻期状态不可持续,只有一天!

  • for (int i = 1; i < n; i++) {
                dp[i][0] = max(dp[i - 1][0], max(dp[i - 1][3] - prices[i], dp[i - 1][1] - prices[i]));
                dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]);
                dp[i][2] = dp[i - 1][0] + prices[i];
                dp[i][3] = dp[i - 1][2];
            }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21

    手续费题思路和第二题相似只需要在执行时减去手续费

    ### 特殊思想dp转换

    #### [72. 编辑距离](https://leetcode.cn/problems/edit-distance/)

    关键点在于理解转换三个状态其中,`dp[i-1][j-1]` 表示替换操作,`dp[i-1][j]` 表示删除操作,`dp[i][j-1]` 表示插入操作。

    ```java
    // 第一行
    for (int j = 1; j <= n2; j++) dp[0][j] = dp[0][j - 1] + 1;
    // 第一列
    for (int i = 1; i <= n1; i++) dp[i][0] = dp[i - 1][0] + 1;

    for (int i = 1; i <= n1; i++) {
    for (int j = 1; j <= n2; j++) {
    if (word1.charAt(i - 1) == word2.charAt(j - 1)) dp[i][j] = dp[i - 1][j - 1];
    else dp[i][j] = Math.min(Math.min(dp[i - 1][j - 1], dp[i][j - 1]), dp[i - 1][j]) + 1;
    }
    }

337.打家劫舍 III 树形dp的应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public int rob3(TreeNode root) {
int[] res = robAction1(root);
return Math.max(res[0], res[1]);
}

int[] robAction1(TreeNode root) {
int res[] = new int[2];
if (root == null)
return res;

int[] left = robAction1(root.left);
int[] right = robAction1(root.right);

res[0] = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
res[1] = root.val + left[0] + right[0];
return res;
}

2008. 出租车的最大盈利

状态方程很明显

dpi+1=max(dpi,dpj+endi−stari+tipi)

基础思想在于及时更新前面地区所取得的费用也就是没此在计算前更新dp[rides[1]]之前的数据,二分优化:找到在第 i 个乘客上车地点之前,最后一个下车地点不大于 starti 的乘客,记为 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
29
30
31
32
33
34
35
36
37
38
  public long maxTaxiEarnings(int n, int[][] rides) {
long[] dp = new long[n+1];
Arrays.sort(rides, (a,b) -> a[1]-b[1]);
int end = 1;
for (int[] ride:rides) {
for (; end <= ride[1]; end++) {
dp[end] = Math.max(dp[end], dp[end-1]);
}
dp[ride[1]] = Math.max(dp[ride[1]], dp[ride[0]] + ride[2] + ride[1] - ride[0]);
}
long res = 0;
for (int i = 0; i <= n; i++) {
res = Math.max(res, dp[i]);
}
return res;
}
//二分法优化
public long maxTaxiEarnings(int n, int[][] rides) {
Arrays.sort(rides, (a, b) -> a[1] - b[1]);
int m = rides.length;
long[] dp = new long[m + 1];
for (int i = 0; i < m; i++) {
int j = binarySearch1(rides, i, rides[i][0]);
dp[i + 1] = Math.max(dp[i], dp[j] + rides[i][1] - rides[i][0] + rides[i][2]);
}
return dp[m];
}
private int binarySearch1(int[][] rides, int right, int target){
int low =0;
while (low<right){
int mid=low+(right-low)/2;
if(rides[mid][1]>target){
right=mid;
}
else low=mid+1;
}
return low;
}

1235. 规划兼职工作

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
public int jobScheduling(int[] startTime, int[] endTime, int[] profit) {
int n = startTime.length;
int[][] jobs = new int[n][];
for (int i = 0; i < n; ++i)
jobs[i] = new int[]{startTime[i], endTime[i], profit[i]};
Arrays.sort(jobs, (a, b) -> a[1] - b[1]); // 按照结束时间排序
int[] f = new int[n + 1];
for (int i = 0; i < n; ++i) {
int j = Search(jobs, i, jobs[i][0]);
f[i + 1] = Math.max(f[i], f[j + 1] + jobs[i][2]);
}
return f[n];
}
// 返回 endTime <= upper 的最大下标
public int Search(int[][] jobs, int right, int target) {
int left = 0;
while (left < right) {
int mid = left + (right - left) / 2;
if (jobs[mid][1] > target) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
}

2312. 卖木头块

此题意义就是将图块分割为不同大小的块,计算总价值最大的情况,找到dp公式分三种情况,一直直接卖,一种垂直切割后卖,一种水平切割后卖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public long sellingWood(int m, int n, int[][] prices) {
int[][] pr = new int[m + 1][n + 1];
for (int p[] : prices) pr[p[0]][p[1]] = p[2];

long[][] f = new long[m + 1][n + 1];
for (int i = 0; i <=m; i++) {
for (int j = 0; j <=n; j++) {
f[i][j]=pr[i][j];
for (int k = 1; k < j; k++) f[i][j] = Math.max(f[i][j], f[i][k] + f[i][j - k]); // 垂直切割
for (int k = 1; k < i; k++) f[i][j] = Math.max(f[i][j], f[k][j] + f[i - k][j]); // 水平切割
}
}
return f[m][n];
}

221. 最大正方形

此类面积dp都是以i,j代表矩形或者正方形的右下顶点,dp[i] [j]就可以靠左边上方左上方得到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public int maximalSquare(char[][] matrix) {
int maxSide = 0;
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
return maxSide;
}
int rows = matrix.length, columns = matrix[0].length;
int[][] dp = new int[rows][columns];
for (int i = 0; i < rows; i++) {
for (int j = 0; j < columns; j++) {
if (matrix[i][j] == '1') {
if (i == 0 || j == 0) {
dp[i][j] = 1;
} else {
dp[i][j] = Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]) + 1;
}
maxSide = Math.max(maxSide, dp[i][j]);
}
}
}
int maxSquare = maxSide * maxSide;
return maxSquare;
}

记忆化搜索

514. 自由之路

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
public int findRotateSteps(String ring, String key) {
int r=ring.length(),k=key.length();
List<Integer>pos[]=new List[26];
for (int i = 0; i < 26; ++i) {
pos[i] = new ArrayList<Integer>();
}
for (int i = 0; i < r; i++) {
pos[ring.charAt(i)-'a'].add(i);
}
int dp[][]=new int[k][r];
for (int i = 0; i <k ; i++) {
Arrays.fill(dp[i], 0x3f3f3f);
}
for (int i : pos[key.charAt(0) - 'a']) {
dp[0][i] = Math.min(i, r - i) + 1;
}

for (int i = 1; i < k; i++) {
for (int j :pos[key.charAt(i)-'a']) {
for (int l:pos[key.charAt(i-1)-'a']) {
dp[i][j]=Math.min(dp[i][j],dp[i-1][l]+ Math.min(Math.abs(j - l), r - Math.abs(j - l)) + 1);
}
}
}
return Arrays.stream(dp[k - 1]).min().getAsInt();
}

1690. 石子游戏 VII

image-20240203153920704

image-20240203153948200

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public int stoneGameVII(int[] stones) {
int n=stones.length;
int sum[]=new int[n+1];
int momo[][]=new int[n][n];
for (int i = 1; i < sum.length; i++) {
sum[i]+=sum[i-1]+stones[i-1];
}
int res=dfs(0,n-1,sum,momo);
return res;
}
public int dfs(int begin,int end , int sum[],int mono[][]){
if(begin==end)return 0;
if(mono[begin][end]>0)return mono[begin][end];
int res1=sum[end+1]-sum[begin+1]-dfs(begin+1,end,sum,mono);
int res2=sum[end]-sum[begin]-dfs(begin,end-1,sum,mono);
return mono[begin][end]=Math.max(res1,res2);
}

1312. 让字符串成为回文串的最少插入次数 516. 最长回文子序列

这两个题都是动态规划,使用dp[i] [j]二维数组储存代表字符串从i 到j 的的最少插入次数/最长子序列, 然后从数组尾部开始遍历,直到遍历完整个数组为止

51.svg

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
//1312. 让字符串成为回文串的最少插入次数https://leetcode.cn/problems/minimum-insertion-steps-to-make-a-string-palindrome/description/
public int minInsertions(String s) {
int n =s.length();
int [][]dp=new int [n][n];
for(int i=n-1;i>=0;i--){
for(int j=i+1;j<n;j++){
if(s.charAt(i)==s.charAt(j)){
dp[i][j]=dp[i+1][j-1];
}
else{
dp[i][j]=Math.min(dp[i+1][j],dp[i][j-1])+1;
}
}
}
return dp[0][n-1];
}
// 516. 最长回文子序列https://leetcode.cn/problems/longest-palindromic-subsequence/description/
public int longestPalindromeSubseq(String s) {
int [][]dp=new int[s.length()][s.length()];
for (int i = 0; i < s.length(); i++) dp[i][i] = 1;
for(int i=s.length()-1;i>=0;i--)
{
for (int j=i+1;j<s.length();j++){
if(s.charAt(i)==s.charAt(j)){
dp[i][j]=dp[i+1][j-1]+2;
}
else {
dp[i][j]=Math.max(dp[i+1][j],dp[i][j-1]);
}
}
}
return dp[0][s.length()-1];
}

单调栈

402. 移掉 K 位数字

如何移除最大的数字首先是贪心算法计算左边数字和右边数字的大小,如果左边小就不移除保留,如果右边小于左边就移除左边

img

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
public String removeKdigits(String num, int k) {
Deque<Character> deque = new LinkedList<Character>();
int length = num.length();
for (int i = 0; i < length; ++i) {
char digit = num.charAt(i);
while (!deque.isEmpty() && k > 0 && deque.peekLast() > digit) {
deque.pollLast();
k--;
}
deque.offerLast(digit);
}
//处理如果左边全部小于右边但是没有移除数字的情况
for (int i = 0; i < k; ++i) {
deque.pollLast();
}

StringBuilder ret = new StringBuilder();
boolean leadingZero = true;
while (!deque.isEmpty()) {
char digit = deque.pollFirst();
if (leadingZero && digit == '0') {
continue;
}
leadingZero = false;
ret.append(digit);
}
return ret.length() == 0 ? "0" : ret.toString();
}

316. 去除重复字母

这题基本思路也是使用比较将之前放入栈的元素给弹出,但是作为判断方法这个使用的是出现的总频率(int[] num = new int[26];)来选择再来根据大小判断是否弹出,

boolean[] vis = new boolean[26];则是保证每个字母都被用到,这样就可以得到最小的字母顺序组

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
public String removeDuplicateLetters(String s) {
boolean[] vis = new boolean[26];
int[] num = new int[26];
for (int i = 0; i < s.length(); i++) {
num[s.charAt(i) - 'a']++;
}

StringBuffer sb = new StringBuffer();
for (int i = 0; i < s.length(); i++) {
char ch = s.charAt(i);
if (!vis[ch - 'a']) {
while (sb.length() > 0 && sb.charAt(sb.length() - 1) > ch) {
if (num[sb.charAt(sb.length() - 1) - 'a'] > 0) {
vis[sb.charAt(sb.length() - 1) - 'a'] = false;
sb.deleteCharAt(sb.length() - 1);
} else {
break;
}
}
vis[ch - 'a'] = true;
sb.append(ch);
}
num[ch - 'a'] -= 1;
}
return sb.toString();
}

84. 柱状图中最大的矩形

  • 使用哨兵加单调栈的思想,分别先求每个元素左边最小元素的位置,然后再求右边的最小元素的位置

我们用一个具体的例子[6,7,5,2,4,5,9,3] 来帮助读者理解单调栈。我们需要求出每一根柱子的左侧且最近的小于其高度的柱子。初始时的栈为空。

我们枚举 6,因为栈为空,所以 6 左侧的柱子是「哨兵」,位置为 -1。随后我们将 6 入栈。

栈:[6(0)]。(这里括号内的数字表示柱子在原数组中的位置)
我们枚举 7,由于 6<7,因此不会移除栈顶元素,所以 777 左侧的柱子是 6,位置为 0。随后我们将 7 入栈。

栈:[6(0), 7(1)]
我们枚举 5,由于 7≥5,因此移除栈顶元素 7。同样地,6≥5,再移除栈顶元素 6。此时栈为空,所以 5 左侧的柱子是「哨兵」,位置为 −1。随后我们将 5 入栈。

栈:[5(2)]
接下来的枚举过程也大同小异。我们枚举 2,移除栈顶元素 5,得到 2 左侧的柱子是「哨兵」,位置为 −1-。将 2 入栈。

栈:[2(3)]
我们枚举 4,5 和 9,都不会移除任何栈顶元素,得到它们左侧的柱子分别是 2,4 和 5,位置分别为 3,4 和 5。将它们入栈。

栈:[2(3), 4(4), 5(5), 9(6)]
我们枚举 3,依次移除栈顶元素 9,5 和 4,得到 3 左侧的柱子是 2,位置为 3。将 3入栈。

栈:[2(3), 3(7)]
这样以来,我们得到它们左侧的柱子编号分别为 [−1,0,−1,−1,3,4,5,3]。用相同的方法,我们从右向左进行遍历,也可以得到它们右侧的柱子编号分别为 [2,2,3,8,7,7,7,8],这里我们将位置 8 看作「哨兵」。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public int largestRectangleArea(int[] heights) {
int n = heights.length;
int[] left = new int[n];
int[] right = new int[n];
Arrays.fill(right, n);
Deque<Integer> stack = new ArrayDeque<Integer>();
for (int i = 0; i < heights.length; i++) {
while (!stack.isEmpty()&&heights[stack.peek()]>=heights[i]){
right[stack.peek()] = i;
stack.pop();
}
left[i]=(stack.isEmpty() ? -1 : stack.peek());
stack.push(i);
}
int res=0;
for (int i = 0; i < heights.length; i++) {
res=Math.max(res,(right[i]-left[i]-1)*heights[i]);
}
return res;
}

85. 最大矩形

84题的变形应用,枚举矩阵高度从1开始一直到n这样就将此题转换为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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public int maximalRectangle(char[][] matrix) {
if (matrix.length == 0) {
return 0;
}
int[] heights = new int[matrix[0].length];
int maxArea = 0;
for (int row = 0; row < matrix.length; row++) {
//遍历每一列,更新高度
for (int col = 0; col < matrix[0].length; col++) {
if (matrix[row][col] == '1') {
heights[col] += 1;
} else {
heights[col] = 0;
}
}
//调用上一题的解法,更新函数
maxArea = Math.max(maxArea, largestRectangleArea(heights));
}
return maxArea;
}

public int largestRectangleArea(int[] heights) {
int maxArea = 0;
Stack<Integer> stack = new Stack<>();
int p = 0;
while (p < heights.length) {
//栈空入栈
if (stack.isEmpty()) {
stack.push(p);
p++;
} else {
int top = stack.peek();
//当前高度大于栈顶,入栈
if (heights[p] >= heights[top]) {
stack.push(p);
p++;
} else {
//保存栈顶高度
int height = heights[stack.pop()];
//左边第一个小于当前柱子的下标
int leftLessMin = stack.isEmpty() ? -1 : stack.peek();
//右边第一个小于当前柱子的下标
int RightLessMin = p;
//计算面积
int area = (RightLessMin - leftLessMin - 1) * height;
maxArea = Math.max(area, maxArea);
}
}
}
while (!stack.isEmpty()) {
//保存栈顶高度
int height = heights[stack.pop()];
//左边第一个小于当前柱子的下标
int leftLessMin = stack.isEmpty() ? -1 : stack.peek();
//右边没有小于当前高度的柱子,所以赋值为数组的长度便于计算
int RightLessMin = heights.length;
int area = (RightLessMin - leftLessMin - 1) * height;
maxArea = Math.max(area, maxArea);
}
return maxArea;
}

2454. 下一个更大元素 IV

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
public int[] secondGreaterElement(int[] nums) {
int n = nums.length;
int[] res = new int[n];
Arrays.fill(res, -1);
List<Integer> stack1 = new ArrayList<Integer>();
List<Integer> stack2 = new ArrayList<Integer>();
for (int i = 0; i < n; ++i) {
int v = nums[i];
while (!stack2.isEmpty() && nums[stack2.get(stack2.size() - 1)] < v) {
res[stack2.get(stack2.size() - 1)] = v;
stack2.remove(stack2.size() - 1);
}
int pos = stack1.size() - 1;
while (pos >= 0 && nums[stack1.get(pos)] < v) {
--pos;
}
for (int j = pos + 1; j < stack1.size(); j++) {
stack2.add(stack1.get(j));
}
for (int j = stack1.size() - 1; j >= pos + 1; j--) {
stack1.remove(j);
}
stack1.add(i);
}
return res;
}

2866. 美丽塔 II

寻找到左边非递增的数和右边非递增的数,我们使用前后两个方向数组去存储前后缀和

对于左侧的非递减:将 maxHeights 依次入栈,对于第 i 个元素来说,不断从栈顶弹出元素,直到栈顶元素小于等于 **maxHeights[i]**。假设此时栈顶元素为 **maxHeights[j]**,则区间[j+1,i−1] 中的元素最多只能取到 **maxHeights[i],则prefix[i]=prefix[j]+(i−j)×maxHeights[i]**;

对于右侧的非递减:将 maxHeights 依次入栈,对于第 i 个元素来说,不断从栈顶弹出元素,直到栈顶元素小于等于 **maxHeights[i]**。假设此时栈顶元素为 **maxHeights[j]**,则区间 [i+1,j−1] 中的元素最多只能取到 **maxHeights[i]**,则 suffix[i]=suffix[j]+(j−i)×maxHeights[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
31
32
33
 public long maximumSumOfHeights(List<Integer> maxHeights) {
int n = maxHeights.size();
long res = 0;
long[] prefix = new long[n];
long[] suffix = new long[n];
Deque<Integer> stack1 = new ArrayDeque<Integer>();
Deque<Integer> stack2 = new ArrayDeque<Integer>();

for (int i = 0; i < n; i++) {
while (!stack1.isEmpty() && maxHeights.get(i) < maxHeights.get(stack1.peek())) {
stack1.pop();
}
if (stack1.isEmpty()) {
prefix[i] = (long) (i + 1) * maxHeights.get(i);
} else {
prefix[i] = prefix[stack1.peek()] + (long) (i - stack1.peek()) * maxHeights.get(i);
}
stack1.push(i);
}
for (int i = n - 1; i >= 0; i--) {
while (!stack2.isEmpty() && maxHeights.get(i) < maxHeights.get(stack2.peek())) {
stack2.pop();
}
if (stack2.isEmpty()) {
suffix[i] = (long) (n - i) * maxHeights.get(i);
} else {
suffix[i] = suffix[stack2.peek()] + (long) (stack2.peek() - i) * maxHeights.get(i);
}
stack2.push(i);
res = Math.max(res, prefix[i] + suffix[i] - maxHeights.get(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
int n = 1005; // 节点数量3 到 1000
int father[1005];

// 并查集初始化
void init() {
for (int i = 0; i < n; ++i) {
father[i] = i;
}
}
// 并查集里寻根的过程
int find(int u) {
return u == father[u] ? u : father[u] = find(father[u]);
}
// 将v->u 这条边加入并查集
void join(int u, int v) {
u = find(u);
v = find(v);
if (u == v) return ;
father[v] = u;
}
// 判断 u 和 v是否找到同一个根
bool same(int u, int v) {
u = find(u);
v = find(v);
return u == v;
}
  1. 寻找根节点,函数:find(int u),也就是判断这个节点的祖先节点是哪个
  2. 将两个节点接入到同一个集合,函数:join(int u, int v),将两个节点连在同一个根节点上
  3. 判断两个节点是否在同一个集合,函数:same(int u, int v),就是判断两个节点是不是同一个根节点

存储数据结构

如何表示节点与节点之间的连通性关系呢??

  • 如果pq连通,则它们有相同的根节点

用数组parent[]来表示这种关系

  • 如果自己就是根节点,那么parent[i] = i,即自己指向自己
  • 如果自己不是根节点,则parent[i] = root id
1
2
3
4
5
6
7
8
9
10
11
private int count;
private int[] parent;
// 构造函数
public UF (int n) {
this.count = n;
parent = new int[n];
for (int i = 0; i < n; i++) {
// 最初,每个节点均是独立的
parent[i] = i;
}
}

优化角度 1:平衡性优化

思路:当我们每次连接两个节点的时候,不希望出现头重脚轻的情况,而希望到达一种平衡的状态

使用额外的一个数组size[]记录每个连通分量中的节点数,每次均把节点数少的分量接到节点数多的分量上,如图

25

注意:只有每个连通分量的根节点的 size[] 才可以代表该连通分量中的节点数

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
private int count;
private int[] parent;
private int[] size;
// 构造函数
public UF (int n) {
this.count = n;
parent = new int[n];
size = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
// 最初,每个连通分量均为 1
size[i] = 1;
}
}
public void union(int p, int q) {
int rootP = find(p);
int rootQ = find(q);
if (rootP == rootQ) return;
/******** 修改部分 ********/
if (size[rootP] < size[rootQ]) {
parent[rootP] = rootQ;
size[rootQ] += size[rootP]
} else {
parent[rootQ] = rootP;
size[rootP] += size[rootQ]
}
/********** end **********/
count--;
}

优化角度 2:路径压缩

思路:使树高始终保持为常数

1
2
3
4
5
6
7
8
private int find(int x) {
while (parent[x] != x) {
// 进行路径压缩
parent[x] = parent[parent[x]];
x = parent[x];
}
return x;
}

上面是用迭代实现的「路径压缩」,下面给出一种用递归实现的「路径压缩」,其效率更高!

1
2
3
4
5
6
private int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}

递归直接一次性把一棵树拉平了!!**(强力推荐使用这种方法!!!✨✨✨)**

注意:

  • 「路径压缩优化」比「平衡性优化」更为常用
  • 当使用了「路径压缩优化」后,「平衡性优化」可以不使用
  • 但是可以在某些题目中使用「平衡性优化」的思想,最长连续序列

完整模版

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
class UF {
private int count;
private int[] parent;
private int[] size;
public UF(int n) {
this.count = n;
parent = new int[n];
size = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
size[i] = 1;
}
}
public void union(int p, int q) {
int rootP = find(p);
int rootQ = find(q);
if (rootP == rootQ) return ;
// 平衡性优化
if (size[rootP] < size[rootQ]) {
parent[rootP] = rootQ;
size[rootQ] += size[rootP];
} else {
parent[rootQ] = rootP;
size[rootP] += size[rootQ];
}
this.count--;
}
public boolean connected(int p, int q) {
int rootP = find(p);
int rootQ = find(q);
return rootP == rootQ;
}
public int count() {
return this.count;
}
private int find(int x) {
// 路径压缩
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
}

684. 冗余连接

1.读取[1,2]:
读取顺序为[[1,2], [3,4], [3,2], [1,4], [1,5]] 当前vector[0, 1, 2, 3, 4, 5] 当前index [0, 1, 2, 3, 4, 5] 原本1->1,2->2, 由1节点出发,vector[1]=1, 找到1所在集合的代表节点1 由2节点出发,vector[2]=2, 找到2所在集合的代表节点2 于是,将1的代表置为2,vector[1]=2, vector[2]=2 对应的vector[0, 2, 2, 3, 4, 5] 对应的index [0, 1, 2, 3, 4, 5] 原集合变为下图:image.png

2.读取[3, 4]
读取顺序为[[1,2], [3,4], [3,2], [1,4], [1,5]] 当前vector[0, 2, 2, 3, 4, 5] 当前index [0, 1, 2, 3, 4, 5] 同理,将3所在集合的的代表节点3的代表节点置为4 对应的vector[0, 2, 2, 4, 4, 5] 对应的index [0, 1, 2, 3, 4, 5] 集合变化如下图:image.png

3.读取[3, 2]
读取顺序为[[1,2], [3,4], [3,2], [1,4], [1,5]] 当前vector[0, 2, 2, 4, 4, 5] 当前index [0, 1, 2, 3, 4, 5] 从节点3出发,vector[3]=4, vector[4]=4,于是找到节点3所在集合的代表节点为4 从节点2出发,vector[2]=2, 找到节点2所在集合的代表节点为2 于是,将4的代表置为2,vector[4]=2, vector[2]=2 对应的vector[0, 2, 2, 4, 2, 5] 对应的index [0, 1, 2, 3, 4, 5] 集合变化如下图:image.png

4.最后读取[1,4].判断重复

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
public int[] findRedundantConnection(int[][] edges) {
int n = edges.length;
int[] parent = new int[n + 1];
for (int i = 1; i <= n; i++) {
parent[i] = i;
}
for (int i = 0; i < n; i++) {
int edge[]=edges[i];
int node1 = edge[0], node2 = edge[1];
if(find(parent,node1)!=find(parent,node2)){
union(parent,node1,node2);
}else {
return edge;
}
}
return new int[0];
}
public void union (int[] parent, int index1, int index2){
parent[find(parent,index1)]=find(parent,index2);
}
public int find(int[] parent, int index){
if (parent[index] != index) {
parent[index] = find(parent, parent[index]);
}
return parent[index];
}

127. 单词接龙

看到最短首先想到的就是广度优先搜索

990. 等式方程的可满足性

标准转化为并查集题目 将两个节点的相等和不等性转换为并查集

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
public boolean equationsPossible(String[] equations) {
int parent[]=new int[26];
for (int i = 0; i < parent.length; i++) {
parent[i]=i;
}
for (String str : equations) {
if (str.charAt(1) == '=') {
int index1 = str.charAt(0) - 'a';
int index2 = str.charAt(3) - 'a';
union(parent, index1, index2);
}
}
for (String str : equations) {
if (str.charAt(1) == '!') {
int index1 = str.charAt(0) - 'a';
int index2 = str.charAt(3) - 'a';
if (find(parent, index1) == find(parent, index2)) {
return false;
}
}
}
return true;
}
public int find(int parent[],int x){
if(parent[x]!=x){
parent[x]=find(parent,parent[x]);
}
return parent[x];
}
public void union(int parent[],int x,int y){
int rootX=find(parent,x);
int rootY=find(parent,y);
parent[rootX]=rootY;
}

399. 除法求值

image.png

根据 a 经过 b 可以到达 c,a 经过 d 也可以到达 c,因此 两条路径上的有向边的权值的乘积是一定相等的。设 b 到 c 的权值为 xxx,那么 3.0⋅x=6.0⋅4.0,得 x=8.0。

image.png

一边查询一边修改结点指向是并查集的特色。

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
public double[] calcEquation(List<List<String>> equations, double[] values, List<List<String>> queries) {
int equationsSize = equations.size();
UnionFind unionFind = new UnionFind(2 * equationsSize);
Map<String, Integer> hashMap = new HashMap<>(2 * equationsSize);
int id = 0;
for (int i = 0; i < equationsSize; i++) {
List<String> equation = equations.get(i);
String var1 = equation.get(0);
String var2 = equation.get(1);

if (!hashMap.containsKey(var1)) {
hashMap.put(var1, id);
id++;
}
if (!hashMap.containsKey(var2)) {
hashMap.put(var2, id);
id++;
}
unionFind.union(hashMap.get(var1), hashMap.get(var2), values[i]);
}
int queriesSize = queries.size();
double[] res = new double[queriesSize];
for (int i = 0; i < queriesSize; i++) {
String var1 = queries.get(i).get(0);
String var2 = queries.get(i).get(1);

Integer id1 = hashMap.get(var1);
Integer id2 = hashMap.get(var2);

if (id1 == null || id2 == null) {
res[i] = -1.0d;
} else {
res[i] = unionFind.isConnected(id1, id2);
}
}
return res;

}
private class UnionFind {
private int parent[];
private double weight[];
public UnionFind(int n){
this.parent=new int[n];
this.weight=new double[n];
for (int i = 0; i < n; i++) {
parent[i]=i;
weight[i]=1.0d;
}
}
public void union(int x, int y, double value){
int rootX = find(x);
int rootY = find(y);
if(rootY==rootX)return;
parent[rootX]=rootY;
weight[rootX]=weight[y]*value/weight[x];
}
public int find(int x){
if(parent[x]!=x){
int origin = parent[x];
parent[x] = find(parent[x]);
weight[x] *= weight[origin];
}
return parent[x];
}
public double isConnected(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX == rootY) {
return weight[x] / weight[y];
} else {
return -1.0d;
}
}
}

位运算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//1356. 根据数字二进制下 1 的数目排序  https://leetcode.cn/problems/sort-integers-by-the-number-of-1-bits/
private int cntInt(int val){
int count = 0;
while(val > 0) {
val = val & (val - 1);
count ++;
}

return count;
}

public int[] sortByBits(int[] arr) {
return Arrays.stream(arr).boxed()
.sorted(new Comparator<Integer>(){
@Override
public int compare(Integer o1, Integer o2) {
int cnt1 = cntInt(o1);
int cnt2 = cntInt(o2);
return (cnt1 == cnt2) ? Integer.compare(o1, o2) : Integer.compare(cnt1, cnt2);
}
})
.mapToInt(Integer::intValue)
.toArray();
}

这段代码的作用是对一个 int 数组进行排序,排序规则是首先按照每个数的数字个数从小到大排序,如果数字个数相同,则按照数值大小从小到大排序。

具体实现是通过使用 Java 8 中的流式 API,将 int 数组转换为一个 IntStream 流,并对流中的元素进行操作。代码首先使用 boxed() 方法将流中的元素转换为 Integer 对象,然后使用 sorted() 方法对这些 Integer 对象进行排序。

sorted() 方法接受一个 Comparator 对象作为参数,这里创建了一个匿名的 Comparator 对象来实现自定义的排序规则。在 compare() 方法中,首先分别计算了两个数字的数字个数,然后根据这些数字个数进行比较。如果数字个数相同,则按照数值大小进行比较,使用 Integer.compare() 方法进行比较。最后,使用 mapToInt() 方法将排序后的 Integer 对象转换为 int 类型的流,并使用 toArray() 方法将其转换为一个 int 数组,即返回排序后的结果。

需要注意的是,该实现对每个数都会计算一遍数字个数,因此在对大量数据进行排序时可能会影响性能。如果要优化性能,可以考虑使用缓存来避免重复计算数字个数。

201. 数字范围按位与

官解写的太复杂了,其实就是我们只看第一个二进制位,只存在0,1两种情况,所以如果left<right,区间中必然存在left+1,那么最低位&一下一定等于0了,然后不停的右移,一直移到两个相等为止,就这么简单

1
2
3
4
5
6
7
8
9
10
public int rangeBitwiseAnd(int m, int n) {
int shift = 0;
// 找到公共前缀
while (m < n) {
m >>= 1;
n >>= 1;
++shift;
}
return m << shift;
}

贪心算法

贪心顾名思义,就是满足每步解的最优解,这样最终解就是最优解

55. 跳跃游戏45. 跳跃游戏 II

主要思想为计算每一步能够跳到的最大值,然后一直到最后返回true或者返回计算步数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 //跳跃游戏      
for (int i=0;i<=cover;i++){
cover = Math.max(i + nums[i], cover);
if(cover>=nums.length-1)
return true;
}
return false;

//跳跃游戏 II
for (int i = 0; i < nums.length-1; i++) {
max=Math.max(max,i+nums[i]);
if(i==end) {
end = max;
res++;
}
}

134. 加油站

关键思路:要能够正常启动首先gas[i]-cost[i]>=0,其次就是能否走完最终路程需要看gas和与cost和大小如果大于那么就可以走完

1
2
3
4
5
6
7
8
for(int i=0;i<gas.length;i++){
curSum+=gas[i]-cost[i];
TotalSum+=gas[i]-cost[i];
if(curSum<0){
start=i+1;
curSum=0;
}
}

846. 一手顺子

注意下标和存储的结合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
for (int i : hand) {
map.put(i, map.getOrDefault(i, 0) + 1);
q.add(i);
}
while (!q.isEmpty()) {
int t = q.poll();
if (map.get(t) == 0) continue;
for (int i = 0; i < groupSize; i++) {
int cnt = map.getOrDefault(t + i, 0);
if (cnt == 0) return false;
map.put(t + i, cnt - 1);
}
}
return true;

1899. 合并若干三元组以形成目标三元组

关键在于满足每个三元组的要求 ,也就是每个三元组的每个位置数值满足小于等于,指定位数值相等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
for (int i = 0; i < triplets.length; i++) {
if(triplets[i][0]==target[0] && triplets[i][1]<=target[1] && triplets[i][2]<=target[2]){
one=true;
}
if(triplets[i][0]<=target[0] && triplets[i][1]==target[1] && triplets[i][2]<=target[2]){
two=true;
}
if(triplets[i][0]<=target[0] && triplets[i][1]<=target[1] && triplets[i][2]==target[2]){
three=true;
}
if(one && three && two){
return true;
}
}
return false;

763. 划分字母区间

使用元素最后一次出现的位置作为最大值,找到最大的切割位置,在这个位置内的元素最大出现的位置都不会大于最大位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int last[]=new int[26];
int length = s.length();
for (int i = 0; i < length; i++) {
last[s.charAt(i) - 'a'] = i;
}
List<Integer> partition = new ArrayList<Integer>();
int start = 0, end = 0;
for (int i = 0; i < length; i++) {
end = Math.max(end, last[s.charAt(i) - 'a']);
if (i == end) {
partition.add(end - start + 1);
start = end + 1;
}
}

2789. 合并后数组中的最大元素

我们从后往前倒序遍历一次数组,依次比较两个相邻的元素,如果两个相邻的元素能够合并,就将其合并。如果不能合并,就继续往前判断。因为这样的操作流程,在比较过程中,靠后的数是所有操作流程可能性中能产生的最大值,而靠前的数,是所有操作流程可能性中能产生的最小值。如果在遍历过程中,比较的结果是不能合并,那么其他任何操作流程都无法合并这两个数。如果可以合并,那我们就贪心地合并,因为这样能使接下来的比较中,靠后的数字尽可能大。

1
2
3
4
5
6
7
public long maxArrayValue(int[] nums) {
long sum = nums[nums.length - 1];
for (int i = nums.length - 2; i >= 0; i--) {
sum = nums[i] <= sum ? nums[i] + sum : nums[i];
}
return sum;
}

621. 任务调度器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public int leastInterval(char[] tasks, int n) {
//统计每个任务出现的次数,找到出现次数最多的任务
int[] hash = new int[26];
for(int i = 0; i < tasks.length; ++i) {
hash[tasks[i] - 'A'] += 1;
}
Arrays.sort(hash);
//因为相同元素必须有n个冷却时间,假设A出现3次,n = 2,任务要执行完,至少形成AXX AXX A序列(X看作预占位置)
//该序列长度为
int minLen = (n+1) * (hash[25] - 1) + 1;

//此时为了尽量利用X所预占的空间(贪心)使得整个执行序列长度尽量小,将剩余任务往X预占的空间插入
//剩余的任务次数有两种情况:
//1.与A出现次数相同,比如B任务最优插入结果是ABX ABX AB,中间还剩两个空位,当前序列长度+1
//2.比A出现次数少,若还有X,则按序插入X位置,比如C出现两次,形成ABC ABC AB的序列
//直到X预占位置还没插满,剩余元素逐个放入X位置就满足冷却时间至少为n
for(int i = 24; i >= 0; --i){
if(hash[i] == hash[25]) ++ minLen;
}
//当所有X预占的位置插满了怎么办?
//在任意插满区间(这里是ABC)后面按序插入剩余元素,比如ABCD ABCD发现D之间距离至少为n+1,肯定满足冷却条件
//因此,当X预占位置能插满时,最短序列长度就是task.length,不能插满则取最少预占序列长度
return Math.max(minLen, tasks.length);
}

Intervals 插空类题目

678. 有效的括号字符串

栈的思想使用左栈和*号栈存储

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
Deque<Integer> leftStack = new LinkedList<Integer>();
Deque<Integer> asteriskStack = new LinkedList<Integer>();
int n = s.length();
for (int i = 0; i < n; i++) {
char c = s.charAt(i);
if (c == '(') {
leftStack.push(i);
} else if (c == '*') {
asteriskStack.push(i);
} else {
if (!leftStack.isEmpty()) {
leftStack.pop();
} else if (!asteriskStack.isEmpty()) {
asteriskStack.pop();
} else {
return false;
}
}
}
while (!leftStack.isEmpty() && !asteriskStack.isEmpty()) {
int leftIndex = leftStack.pop();
int asteriskIndex = asteriskStack.pop();
if (leftIndex > asteriskIndex) {
return false;
}
}
return leftStack.isEmpty();

56. 合并区间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public int[][] merge(int[][] intervals) {
List<int[]> res = new LinkedList<>();
Arrays.sort(intervals,(a,b)->Integer.compare(a[0],b[0]));
int left=intervals[0][0];
int right=intervals[0][1];
for (int i=1;i<intervals.length;i++){
if(intervals[i][0]>right){
res.add(new int[]{left, right});
left = intervals[i][0];
right = intervals[i][1];
}
else {
right=Math.max(intervals[i][1],right);
}
}
res.add(new int[]{left, right});
return res.toArray(new int[res.size()][]);
}

435. 无重叠区间

贪心思想,先将数组按照结尾排序,然后每次找到相交数组的最小结束长度,分割,这样最后总数组数减去分割出的不相交的数组个数就得到了需要删除的数组个数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public int eraseOverlapIntervals(int[][] intervals) {
if (intervals.length == 0) {
return 0;
}
Arrays.sort(intervals,new Comparator<int[]>(){
@Override
public int compare(int[] o1, int[] o2) {
return o1[1]-o2[1];
}
});
int n = intervals.length;
int right=intervals[0][1];
int ans=1;
for (int i = 1; i < n; i++) {
if(intervals[i][0]>=right){
ans++;
right=intervals[i][1];
}
}
return n-ans;
}

扫描线

391. 完美矩形

将每个矩形 rectangles[i] 看做两条竖直方向的边,使用 (x,y1,y2) 的形式进行存储(其中 y1 代表该竖边的下端点,y2 代表竖边的上端点),同时为了区分是矩形的左边还是右边,再引入一个标识位,即以四元组 (x,y1,y2,flag) 的形式进行存储。

一个完美矩形的充要条件为:对于完美矩形的每一条非边缘的竖边,都「成对」出现(存在两条完全相同的左边和右边重叠在一起);对于完美矩形的两条边缘竖边,均独立为一条连续的(不重叠)的竖边。

如图(红色框的为「完美矩形的边缘竖边」,绿框的为「完美矩形的非边缘竖边」):

image.png

1851. 包含每个查询的最小区间

此题第一眼思路是暴力求解,但是细想后发现可以先排序intervals和queries 这样遍历时就不需要每次把intervals全遍历只要遍历到intervals[i][1]<queries就可以(离线算法是指在开始时就需要知道问题的所有输入数据,而且在解决一个问题后就要立即输出结果)

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
public int[] minInterval(int[][] intervals, int[] queries) {
Integer[] qindex = new Integer[queries.length];
for (int i = 0; i < queries.length; i++) {
qindex[i] = i;
}
Arrays.sort(qindex, (i, j) -> queries[i] - queries[j]);
Arrays.sort(intervals, (i, j) -> i[0] - j[0]);
PriorityQueue<int[]> pq = new PriorityQueue<int[]>((a, b) -> a[0] - b[0]);
int[] res = new int[queries.length];
Arrays.fill(res, -1);
int i = 0;
for (int qi : qindex) {
while (i < intervals.length && intervals[i][0] <= queries[qi]) {
pq.offer(new int[]{intervals[i][1] - intervals[i][0] + 1, intervals[i][0], intervals[i][1]});
i++;
}
//排序后去重虽然大于queries>interval[i][0]但是也大于了interval[i][1]不在范围里面
while (!pq.isEmpty() && pq.peek()[2] < queries[qi]) {
pq.poll();
}
if (!pq.isEmpty()) {
res[qi] = pq.peek()[0];
}
}
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
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
57
58
59
60
//判断有无会议冲突
public boolean canAttendMeetings(List<Interval> intervals) {
// Write your code here
Collections.sort(intervals, (a, b) -> a.start - b.start);
for (int i = 1; i < intervals.size(); i++) {
if (intervals.get(i - 1).end > intervals.get(i).start) {
return false;
}
}
return true;
}

//计算需要的最少的会议房间,此题是一个贪心的思想,通过将开始时间与结束时间分别排序,将开始时间大于结束时间的就可以放一组,如果不是则新开一个房间,这样最终结果就是最小值
public int minMeetingRooms(List<Interval> intervals) {
// Check for the base case. If there are no intervals, return 0
if (intervals.size() == 0) {
return 0;
}

Integer[] start = new Integer[intervals.size()];
Integer[] end = new Integer[intervals.size()];

for (int i = 0; i < intervals.size(); i++) {
start[i] = intervals.get(i).start;
end[i] = intervals.get(i).end;
}

// Sort the intervals by end time
Arrays.sort(
end,
new Comparator<Integer>() {
public int compare(Integer a, Integer b) {
return a - b;
}
});
// Sort the intervals by start time
Arrays.sort(
start,
new Comparator<Integer>() {
public int compare(Integer a, Integer b) {
return a - b;
}
});

int startPointer = 0, endPointer = 0;

int usedRooms = 0;
while (startPointer < intervals.size()) {

if (start[startPointer] >= end[endPointer]) {
usedRooms -= 1;
endPointer += 1;
}
usedRooms += 1;
startPointer += 1;

}

return usedRooms;
}

数学

快速幂

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
//取模性质
(a + b) % p = (a % p + b % p) % p (1
(a - b) % p = (a % p - b % p ) % p (2
(a * b) % p = (a % p * b % p) % p (3
a ^ b % p = ((a % p)^b) % p (4
//递归
long c=10000007;
public long divide(long a, long b) {
if (b == 0)
return 1;
else if (b % 2 == 0) //偶数情况
return divide((a % c) * (a % c), b / 2) % c;
else//奇数情况
return a % c * divide((a % c) * (a % c), (b - 1) / 2) % c;
}
//非递归
long c = 1000000007;
public long divide(long a, long b) {
a %= c;
long res = 1;
for (; b != 0; b /= 2) {
if (b % 2 == 1)
res = (res * a) % c;
a = (a * a) % c;
}
return res;
}
long long int quik_power(int base, int power)
{
long long int result = 1;
while (power > 0) //指数大于0进行指数折半,底数变其平方的操作
{
if (power & 1) //指数为奇数,power & 1这相当于power % 2 == 1
result *= base; //分离出当前项并累乘后保存
power >>= 1; //指数折半,power >>= 1这相当于power /= 2;
base *= base; //底数变其平方
}
return result; //返回最终结果
}
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
//快速幂 二进制
long long int quik_power(int base, int power)
{
long long int result = 1; //用于存储项累乘与返回最终结果,由于要存储累乘所以要初始化为1
while (power > 0) //指数大于0说明指数的二进制位并没有被左移舍弃完毕
{
if (power & 1) //指数的当前计算二进制位也就是最末尾的位是非零位也就是1的时候
//例如1001的当前计算位就是1, 100*1* 星号中的1就是当前计算使用的位
result *= base; //累乘当前项并存储
base *= base; //计算下一个项,例如当前是n^2的话计算下一项n^2的值
//n^4 = n^2 * n^2;
power >>= 1; //指数位右移,为下一次运算做准备
//一次的右移将舍弃一个位例如1011(2)一次左移后变成101(2)
}
return result; //返回最终结果
}
//快速幂 指数折半
long long int quik_power(int base, int power)
{
long long int result = 1;
while (power > 0) //指数大于0进行指数折半,底数变其平方的操作
{
if (power % 2 == 1) //指数为奇数
result *= base; //分离出当前项并累乘后保存
power /= 2; //指数折半
base *= base; //底数变其平方
}
return result; //返回最终结果
}
//优化
long long int quik_power(int base, int power)
{
long long int result = 1;
while (power > 0) //指数大于0进行指数折半,底数变其平方的操作
{
if (power & 1) //指数为奇数,power & 1这相当于power % 2 == 1
result *= base; //分离出当前项并累乘后保存
power >>= 1; //指数折半,power >>= 1这相当于power /= 2;
base *= base; //底数变其平方
}
return result; //返回最终结果
}

矩阵快速幂

image-20240524001117376

1137. 第 N 个泰波那契数

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
class Solution {
int N = 3;
int[][] mul(int[][] a, int[][] b) {
int[][] c = new int[N][N];
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
c[i][j] = a[i][0] * b[0][j] + a[i][1] * b[1][j] + a[i][2] * b[2][j];
}
}
return c;
}
public int tribonacci(int n) {
if (n == 0) return 0;
if (n == 1 || n == 2) return 1;
int[][] ans = new int[][]{
{1,0,0},
{0,1,0},
{0,0,1}
};
int[][] mat = new int[][]{
{1,1,1},
{1,0,0},
{0,1,0}
};
int k = n - 2;
while (k != 0) {
if ((k & 1) != 0) ans = mul(ans, mat);
mat = mul(mat, mat);
k >>= 1;
}
return ans[0][0] + ans[0][1];
}
}
摩尔投票法:求的是绝对众数(数组总出现频率超过1/2的数)

image-20230430151444159

Picture1.png

1
2
3
4
5
6
7
8
public int majorityElement(int[] nums) {
int x=0,vote=0;
for (int i = 0; i < nums.length; i++) {
if(vote==0)x=nums[i];
vote+=nums[i]==x?1:-1;
}
return x;
}

数组中出现次数超过一半的数字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public int MoreThanHalfNum_Solution(int[] nums) {
int majority = nums[0];
for (int i = 1, cnt = 1; i < nums.length; i++) {
cnt = nums[i] == majority ? cnt + 1 : cnt - 1;
if (cnt == 0) {
majority = nums[i];
cnt = 1;
}
}
int cnt = 0;
for (int val : nums)
if (val == majority)
cnt++;
return cnt > nums.length / 2 ? majority : 0;
}

13. 罗马数字转整数

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
//破题点当前位置的元素比下个位置的元素小,就减去当前值,否则加上当前值

public int romanToInt(String s) {
int sum = 0;
int preNum = getValue(s.charAt(0));
for(int i = 1;i < s.length(); i ++) {
int num = getValue(s.charAt(i));
if(preNum < num) {
sum -= preNum;
} else {
sum += preNum;
}
preNum = num;
}
sum += preNum;
return sum;
}

private int getValue(char ch) {
switch(ch) {
case 'I': return 1;
case 'V': return 5;
case 'X': return 10;
case 'L': return 50;
case 'C': return 100;
case 'D': return 500;
case 'M': return 1000;
default: return 0;
}
}

365. 水壶问题

贝祖定理寻找最大公约数如果target能整除则可以

2575. 找出字符串的可整除数组

image-20240306191903172

这样可以化解爆int的情况 提前取模这样余数就不会超int

1
2
3
4
5
6
7
8
9
10
11
12
public int[] divisibilityArray(String word, int m) {
int res[]=new int[word.length()];
long cur=0;
for (int i = 0; i < word.length(); i++) {
char c=word.charAt(i);
cur=cur*10+Integer.valueOf(i);
System.out.println(cur);
if(cur%m==0)res[i]=1;
else res[i]=0;
}
return res;
}

96. 不同的二叉搜索树

公式!!! 卡塔兰数

1
2
3
4
5
6
7
8
9
10
11
public int numTrees(int n) {
int[] dp = new int[n + 1];
dp[0] = 1;
dp[1] = 1;
for (int i = 2; i <= n; i++) {
for (int j = 1; j <= i; j++) {
dp[i] += dp[j - 1] * dp[i - j];
}
}
return dp[n];
}

随机数生成

470. 用 Rand7() 实现 Rand10()

概念一

已知 rand_N() 可以等概率的生成[1, N]范围的随机数
那么:
(rand_X() - 1) × Y + rand_Y() ==> 可以等概率的生成[1, X * Y]范围的随机数
即实现了 rand_XY()

概念二

只要rand_N()中N是2的倍数,就都可以用来实现rand2(),反之,若N不是2的倍数,则产生的结果不是等概率的

ok,现在回到本题中。已知rand7(),要求通过rand7()来实现rand10()。

有了前面的分析,要实现rand10(),就需要先实现rand_N(),并且保证N大于10且是10的倍数。这样再通过rand_N() % 10 + 1 就可以得到[1,10]范围的随机数了。

]但是这样实现的N不是10的倍数啊!这该怎么处理?这里就涉及到了“拒绝采样”的知识了,也就是说,如果某个采样结果不在要求的范围内,则丢弃它。基于上面的这些分析,再回头看下面的代码,想必是不难理解了。

1
2
3
4
5
6
7
8
class Solution extends SolBase {
public int rand10() {
while(true) {
int num = (rand7() - 1) * 7 + rand7(); // 等概率生成[1,49]范围的随机数
if(num <= 40) return num % 10 + 1; // 拒绝采样,并返回[1,10]范围的随机数
}
}
}

根据part 1的分析,我们已经知道(rand7() - 1) * 7 + rand7() 等概率生成[1,49]范围的随机数。而由于我们需要的是10的倍数,因此,不得不舍弃掉[41, 49]这9个数。优化的点就始于——我们能否利用这些范围外的数字,以减少丢弃的值,提高命中率总而提高随机数生成效率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution extends SolBase {
public int rand10() {
while(true) {
int a = rand7();
int b = rand7();
int num = (a-1)*7 + b; // rand 49
if(num <= 40) return num % 10 + 1; // 拒绝采样

a = num - 40; // rand 9
b = rand7();
num = (a-1)*7 + b; // rand 63
if(num <= 60) return num % 10 + 1;

a = num - 60; // rand 3
b = rand7();
num = (a-1)*7 + b; // rand 21
if(num <= 20) return num % 10 + 1;
}
}
}

440. 字典序的第K小数字

image-20240729084058459
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public int findKthNumber(int n, int k) {
int ans = 1;
while (k > 1) {
int cnt = getCnt(ans, n);
if (cnt < k) {
k -= cnt; ans++;
} else {
k--; ans *= 10;
}
}
return ans;
}
int getCnt(int x, int limit) {
String a = String.valueOf(x), b = String.valueOf(limit);
int n = a.length(), m = b.length(), k = m - n;
int ans = 0, u = Integer.parseInt(b.substring(0, n));
for (int i = 0; i < k; i++) ans += Math.pow(10, i);
if (u > x) ans += Math.pow(10, k);
else if (u == x) ans += limit - x * Math.pow(10, k) + 1;
return ans;
}

8. 字符串转换整数 (atoi) 7. 整数反转

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
//7
public int reverse(int x) {
int res = 0;
while(x!=0) {
//每次取末尾数字
int tmp = x%10;
//判断是否 大于 最大32位整数
if (res>214748364 || (res==214748364 && tmp>7)) {
return 0;
}
//判断是否 小于 最小32位整数
if (res<-214748364 || (res==-214748364 && tmp<-8)) {
return 0;
}
res = res*10 + tmp;
x /= 10;
}
return res;
}
//8
public int myAtoi(String s) {
int res=0, up=Integer.MAX_VALUE/10;
int i=0,sign =1,len=s.length();
if(len==0)return 0;
while(s.charAt(i)==' '){
if(++i==len)return 0;
}
if(s.charAt(i)=='-')sign=-1;
if(s.charAt(i)=='-'||s.charAt(i)=='+')i++;
for(int j=i;j<len;j++){
if(s.charAt(j) < '0' || s.charAt(j) > '9') break;
if(res > up || res == up && s.charAt(j) > '7')
return sign == 1 ? Integer.MAX_VALUE : Integer.MIN_VALUE;
res = res * 10 + (s.charAt(j) - '0');
}
return sign*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
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
57
public static string NumberToChinese(uint num)
{
string result = "";
if (num == 0)
{
return "零";
}
uint _num = num;
//string[] chn_str = new string[] { "零","壹","贰","叁","肆","伍","陆","柒","捌","玖"};
//string[] unit_value = new string[] { "", "拾", "佰", "仟" };
string[] chn_str = new string[] { "零","一", "二", "三", "四", "五", "六", "七", "八", "九" };
string[] section_value = new string[] { "","万","亿","万亿"};
string[] unit_value = new string[] { "", "十", "百", "千" };
uint section = _num % 10000;
for (int i = 0; _num != 0 && i < 4; i++)
{
if (section == 0)
{
//0不需要考虑节权值,不能出现连续的“零”
if (result.Length > 0 && result.Substring(0, 1) != "零")
{
result = "零" + result;
}
_num = _num / 10000;
section = _num % 10000;
continue;
}
result = section_value[i]+result;
uint unit = section % 10;
for (int j = 0; j<4 ; j++)
{
if (unit == 0)
{
//0不需要考虑位权值,不能出现联系的“零”,每节最后的0不需要
if (result.Length > 0 && result.Substring(0, 1) != "零" && result.Substring(0, 1) != section_value[i])
{
result = "零" + result;
}
}
else
{
result = chn_str[unit] + unit_value[j] + result;
}
section = section / 10;
unit = section % 10;
}
_num = _num / 10000;
section = _num % 10000;
}
if (result.Length > 0 && result.Substring(0, 1) == "零")
{
//清理最前面的"零"
result = result.Substring(1);
}
return result;
}

https://leetcode.cn/problems/perfect-rectangle/)

图论

DFS

200、岛屿数量 本质是图的连通性的问题 寻找有多少个连通图

首先,网格结构中的格子有多少相邻结点?答案是上下左右四个。对于格子 (r, c) 来说(r 和 c 分别代表行坐标和列坐标),四个相邻的格子分别是 (r-1, c)、(r+1, c)、(r, c-1)、(r, c+1)。换句话说,网格结构是「四叉」的。

网格结构中四个相邻的格子

其次,网格 DFS 中的 base case 是什么?从二叉树的 base case 对应过来,应该是网格中不需要继续遍历、grid[r][c] 会出现数组下标越界异常的格子,也就是那些超出网格范围的格子。

网格 DFS 的 base case

这一点稍微有些反直觉,坐标竟然可以临时超出网格的范围?这种方法我称为「先污染后治理」—— 甭管当前是在哪个格子,先往四个方向走一步再说,如果发现走出了网格范围再赶紧返回。这跟二叉树的遍历方法是一样的,先递归调用,发现 root == null 再返回。

这样,我们得到了网格 DFS 遍历的框架代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void dfs(int[][] grid, int r, int c) {
// 判断 base case
// 如果坐标 (r, c) 超出了网格范围,直接返回
if (!inArea(grid, r, c)) {
return;
}
// 访问上、下、左、右四个相邻结点
dfs(grid, r - 1, c);
dfs(grid, r + 1, c);
dfs(grid, r, c - 1);
dfs(grid, r, c + 1);
}

// 判断坐标 (r, c) 是否在网格中
boolean inArea(int[][] grid, int r, int c) {
return 0 <= r && r < grid.length
&& 0 <= c && c < grid[0].length;
}

417. 太平洋大西洋水流问题

破题点在于沿边遍历这样可以分开寻找到能流进Atlantic 和Pacific的岛屿然后求交集就可以知道位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
for (int i = 0; i < m; i++) {
dfs3(pacific,i, 0, heights);
}
for (int j = 1; j < n; j++) {
dfs3(pacific,0, j, heights);
}
for (int i = 0; i < m; i++) {
dfs3(atlantic,i, n - 1,heights );
}
for (int j = 0; j < n - 1; j++) {
dfs3(atlantic,m - 1, j, heights);
}


public void dfs3(boolean [][]ocean,int i,int j,int [][]height){
if(ocean[i][j])return;
ocean[i][j]=true;
for (int[] dir : dirs) {
int newi=i+dir[0],newj=j+dir[1];
if(newi>=0 && newi<height.length && newj>=0 && newj<height[0].length && height[newi][newj]>=height[i][j])
dfs3(ocean,newi,newj,height);
}
}

130. 被围绕的区域

此题同上一题类似寻找四周岛屿影响的内部岛屿

778. 水位上升的泳池中游泳 1631. 最小体力消耗路径

此题融合了二分查找与dfs并存,使用二分查找来寻找到从左到右的最小路径。这是本问题具有的单调性。因此可以使用二分查找定位到最短等待时间。具体来说:在区间 [0, N * N - 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
32
33
34
35
private  int N;
public static final int[][] DIRECTIONS = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
public int swimInWater(int[][] grid) {
this.N=grid.length;
int left = 0;
int right = N * N - 1;
while (left < right) {
int mid=(left+right)/2;
boolean used [][]=new boolean[N][N];
if(grid[0][0]<=mid&&dfs(grid,0,0,used,mid)){
right=mid;
}
else left=mid+1;
}
return right;
}
private boolean dfs(int[][] grid, int x, int y, boolean[][] used, int mid) {
used[x][y] = true;
for(int dic[]:DIRECTIONS){
int newx=x+dic[0];
int newy=y+dic[1];
if(inArea(newx,newy)&&!used[newx][newy]&&grid[newx][newy]<=mid){
if (newx == N - 1 && newy == N - 1) {
return true;
}
if (dfs(grid, newx, newy, used, mid)) {
return true;
}
}
}
return false;
}
private boolean inArea(int x, int y) {
return x >= 0 && x < N && y >= 0 && y < N;
}

332. 重新安排行程

按照树的遍历来计算所存在的边数方法一:Hierholzer 算法
思路及算法

Hierholzer 算法用于在连通图中寻找欧拉路径,其流程如下:

从起点出发,进行深度优先搜索。

每次沿着某条边从某个顶点移动到另外一个顶点的时候,都需要删除这条边。

如果没有可移动的路径,则将所在节点加入到栈中,并返回。

当我们顺序地考虑该问题时,我们也许很难解决该问题,因为我们无法判断当前节点的哪一个分支是「死胡同」分支。

不妨倒过来思考。我们注意到只有那个入度与出度差为 111 的节点会导致死胡同。而该节点必然是最后一个遍历到的节点。我们可以改变入栈的规则,当我们遍历完一个节点所连的所有节点后,我们才将该节点入栈(即逆序入栈)。

对于当前节点而言,从它的每一个非「死胡同」分支出发进行深度优先搜索,都将会搜回到当前节点。而从它的「死胡同」分支出发进行深度优先搜索将不会搜回到当前节点。也就是说当前节点的死胡同分支将会优先于其他非「死胡同」分支入栈。

这样就能保证我们可以「一笔画」地走完所有边,最终的栈中逆序地保存了「一笔画」的结果。我们只要将栈中的内容反转,即可得到答案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Map<String, PriorityQueue<String>> map=new HashMap<>();
List<String> res = new LinkedList<>();
public List<String> findItinerary(List<List<String>> tickets) {

for (List<String> list:tickets){
String src =list.get(0);String des=list.get(1);
if(!map.containsKey(src)){
map.put(src,new PriorityQueue<>());
}
map.get(src).add(des);
}
dfs("JHK");
Collections.reverse(res); // 反转链表,最先找到的是最深的不能再走的目的地,所以要反转过来
return res;
}
public void dfs(String src) {
// 当传入的参数是始发地而且还有边的时候,取边出队删除并且继续递归深搜这条边的点,一直到不能再走再返回
while (map.containsKey(src) && map.get(src).size() > 0) {
dfs(map.get(src).poll());
}
// 没有子递归可以调用时,递归函数开始返回,把搜过的点一次加到结果集的路线里
res.add(src);
}

2368. 受限条件下可到达节点的数目

img

按照题目要求构建出树,然后dfs遍历遇到阻塞就跳过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int res=1;
public int reachableNodes(int n, int[][] edges, int[] restricted) {
ArrayList <Integer>[]list=new ArrayList [n];
for (int i = 0; i < n; i++) {
list[i] = new ArrayList<Integer>();
}
for (int []edge:edges){
list[edge[0]].add(edge[1]);
list[edge[1]].add(edge[0]);
}
boolean[] vis = new boolean[n];
for (int r : restricted) vis[r] = true;
dfs(list,vis,0);
return res;
}
public void dfs(List<Integer>[] g, boolean[] vis, int fa){
vis[fa]=true;
for(int child:g[fa]){
if(vis[child])continue;
res++;
dfs(g,vis,child);
}
}

换根dp

834. 树中距离之和 2581. 统计可能的树根数目

lc834.png

思路都为优先算出一次dfs从0开始的树初始情况,然后因为换节点只会影响该节点与其子节点关系所以使用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
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
//2581. 统计可能的树根数目  https://leetcode.cn/problems/count-number-of-possible-root-nodes/description/?envType=daily-question&envId=2024-02-29
private List<Integer>[] times;
private Set<Long> s = new HashSet<>();
private int k, res, cnt0;

public int rootCount(int[][] edges, int[][] guesses, int k) {
this.k = k;
times = new ArrayList[edges.length + 1];
Arrays.setAll(times, i -> new ArrayList<>());
for (int[] e : edges) {
int x = e[0];
int y = e[1];
times[x].add(y);
times[y].add(x); // 建图
}

for (int[] e : guesses) { // guesses 转成哈希表
s.add((long) e[0] << 32 | e[1]); // 两个 4 字节 int 压缩成一个 8 字节 long
}

dfs(0, -1);
reroot(0, -1, cnt0);
return res;
}

private void dfs(int x, int fa) {
for (int y : times[x]) {
if (y != fa) {
if (s.contains((long) x << 32 | y)) { // 以 0 为根时,猜对了
cnt0++;
}
dfs(y, x);
}
}
}

private void reroot(int x, int fa, int cnt) {
if (cnt >= k) { // 此时 cnt 就是以 x 为根时的猜对次数
res++;
}
for (int y : times[x]) {
if (y != fa) {
int c = cnt;
if (s.contains((long) x << 32 | y)) c--; // 原来是对的,现在错了
if (s.contains((long) y << 32 | x)) c++; // 原来是错的,现在对了
reroot(y, x, c);
}
}
}

//834. 树中距离之和 https://leetcode.cn/problems/sum-of-distances-in-tree/description/
private List<Integer>[] g;
private int[] ans, size;
public int[] sumOfDistancesInTree(int n, int[][] edges) {
g = new ArrayList[n]; // g[x] 表示 x 的所有邻居
Arrays.setAll(g, e -> new ArrayList<>());
for (int [] e : edges) {
int x = e[0], y = e[1];
g[x].add(y);
g[y].add(x);
}
ans = new int[n];
size = new int[n];
dfs(0, -1, 0); // 0 没有父节点
reroot(0, -1); // 0 没有父节点
return ans;
}

private void dfs(int x, int fa, int depth) {
ans[0] += depth; // depth 为 0 到 x 的距离
size[x] = 1;
for (int y : g[x]) { // 遍历 x 的邻居 y
if (y != fa) { // 避免访问父节点
dfs(y, x, depth + 1); // x 是 y 的父节点
size[x] += size[y]; // 累加 x 的儿子 y 的子树大小
}
}
}

private void reroot(int x, int fa) {
for (int y : g[x]) { // 遍历 x 的邻居 y
if (y != fa) { // 避免访问父节点
ans[y] = ans[x] + g.length - 2 * size[y];
reroot(y, x); // x 是 y 的父节点
}
}
}

BFS

994. 腐烂的橘子

BFS 可以看成是层序遍历。从某个结点出发,BFS 首先遍历到距离为 1 的结点,然后是距离为 2、3、4…… 的结点。因此,BFS 可以用来求最短路径问题。BFS 先搜索到的结点,一定是距离最近的结点。

再看看这道题的题目要求:返回直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。翻译一下,实际上就是求腐烂橘子到所有新鲜橘子的最短路径。那么这道题使用 BFS,应该是毫无疑问的了。

如何写(最短路径的) BFS 代码
我们都知道 BFS 需要使用队列,代码框架是这样子的(伪代码):

1
2
3
4
5
while queue 非空:
node = queue.pop()
for node 的所有相邻结点 m:
if m 未访问过:
queue.push(m)

但是用 BFS 来求最短路径的话,这个队列中第 1 层和第 2 层的结点会紧挨在一起,无法区分。因此,我们需要稍微修改一下代码,在每一层遍历开始前,记录队列中的结点数量 nnn ,然后一口气处理完这一层的 nnn 个结点。代码框架是这样的:

1
2
3
4
5
6
7
8
9
depth = 0 # 记录遍历到第几层
while queue 非空:
depth++
n = queue 中的元素个数
循环 n 次:
node = queue.pop()
for node 的所有相邻结点 m:
if m 未访问过:
queue.push(m)
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
public int orangesRotting(int[][] grid) {
int l=grid.length;
int c=grid[0].length;
Queue<int[]> queue=new LinkedList<>();
int count=0;
for (int i = 0; i < l; i++) {
for (int j = 0; j < c; j++) {
if(grid[i][j]==1){
count++;
} else if (grid[i][j]==2) {
queue.add(new int[]{i,j});
}
}
}
int res=0;
while(!queue.isEmpty()&&count>0){
res++;
int n = queue.size();
for (int i = 0; i < n; i++) {
int []bad=queue.poll();
int j=bad[0];
int k=bad[1];
if (j-1 >= 0 && grid[j-1][k] == 1) {
grid[j-1][k]=2;
count--;
queue.add(new int[]{j-1,k});
}
if (j+1 <l && grid[j+1][k] == 1) {
grid[j+1][k]=2;
count--;
queue.add(new int[]{j+1,k});
}
if (k-1 >= 0 && grid[j][k-1] == 1) {
grid[j][k-1]=2;
count--;
queue.add(new int[]{j,k-1});
}
if (k+1 <c && grid[j][k+1] == 1) {
grid[j][k+1]=2;
count--;
queue.add(new int[]{j,k+1});
}
}
}
if(count>0)return -1;
else
return res;
}

2684. 矩阵中移动的最大次数

bfs 按照每一列满足条件的加入 遍历了无元素加入为止

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public int maxMoves(int[][] grid) {
int m = grid.length, n = grid[0].length;
Set<Integer> q = new HashSet<>();
for (int i = 0; i < m; i++) {
q.add(i);
}
for (int j = 1; j < n; j++) {
Set<Integer> q2 = new HashSet<>();
for (int i : q) {
for (int i2 = i - 1; i2 <= i + 1; i2++) {
if (0 <= i2 && i2 < m && grid[i][j - 1] < grid[i2][j]) {
q2.add(i2);
}
}
}
q = q2;
if (q.isEmpty()) {
return j - 1;
}
}
return n - 1;
}

拓扑结构(拓扑排序)

207. 课程表

这是一个包含 n个节点的有向图 G我们需要判断此有向图是否存在环路

我们将每一门课看成一个节点;

如果想要学习课程 A之前必须完成课程 B,那么我们从 B 到 A连接一条有向边。这样以来,在拓扑排序中,B 一定出现在 A 的前面。

DFS思路

直接遍历,如果入度为0那么进行对其下一门课程遍历,如果一直到最后末尾都不会回到之前的课程那么则无环

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
List<List<Integer>> edges;
int[] visited;
boolean valid = true;
public boolean canFinish(int numCourses, int[][] prerequisites) {
edges = new ArrayList<List<Integer>>();
for (int i = 0; i < numCourses; ++i) {
edges.add(new ArrayList<Integer>());
}
visited = new int[numCourses];
for (int[] info : prerequisites) {
edges.get(info[1]).add(info[0]);
}
for (int i = 0; i < numCourses && valid; ++i) {
if (visited[i] == 0) {
dfs(i);
}
}
return valid;
}

public void dfs(int u) {
visited[u] = 1;
for (int v: edges.get(u)) {
if (visited[v] == 0) {
dfs(v);
if (!valid) {
return;
}
} else if (visited[v] == 1) {
valid = false;
return;
}
}
visited[u] = 2;
}

BFS思路

我们先统计出入度为0 的项目的个数,遍历入度为0的节点,然后减少入度如果减少到也变成入度为0时加入,如果所有节点被加入那么就不存在环

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
class Solution {
List<List<Integer>> edges;
int[] indeg;

public boolean canFinish(int numCourses, int[][] prerequisites) {
edges = new ArrayList<List<Integer>>();
for (int i = 0; i < numCourses; ++i) {
edges.add(new ArrayList<Integer>());
}
indeg = new int[numCourses];
for (int[] info : prerequisites) {
edges.get(info[1]).add(info[0]);
++indeg[info[0]];
}

Queue<Integer> queue = new LinkedList<Integer>();
for (int i = 0; i < numCourses; ++i) {
if (indeg[i] == 0) {
queue.offer(i);
}
}

int visited = 0;
while (!queue.isEmpty()) {
++visited;
int u = queue.poll();
for (int v: edges.get(u)) {
--indeg[v];
if (indeg[v] == 0) {
queue.offer(v);
}
}
}

return visited == numCourses;
}
}

210. 课程表 II

BFS

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
class Solution {
// 存储有向图
List<List<Integer>> edges;
// 存储每个节点的入度
int[] indeg;
// 存储答案
int[] result;
// 答案下标
int index;
public int[] findOrder(int numCourses, int[][] prerequisites) {
edges = new ArrayList<List<Integer>>();
for (int i = 0; i < numCourses; ++i) {
edges.add(new ArrayList<Integer>());
}
indeg = new int[numCourses];
result = new int[numCourses];
index = 0;
for (int[] info : prerequisites) {
edges.get(info[1]).add(info[0]);
++indeg[info[0]];
}

Queue<Integer> queue = new LinkedList<Integer>();
// 将所有入度为 0 的节点放入队列中
for (int i = 0; i < numCourses; ++i) {
if (indeg[i] == 0) {
queue.offer(i);
}
}

while (!queue.isEmpty()) {
// 从队首取出一个节点
int u = queue.poll();
// 放入答案中
result[index++] = u;
for (int v: edges.get(u)) {
--indeg[v];
// 如果相邻节点 v 的入度为 0,就可以选 v 对应的课程了
if (indeg[v] == 0) {
queue.offer(v);
}
}
}

if (index != numCourses) {
return new int[0];
}
return result;
}
}

909. 蛇梯棋

蛇形棋因为行列关系在奇偶数行时不同使用一个n*n的数组在不同奇偶数情况时去处理行列 之后遍历每次6个点

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
public int snakesAndLadders(int[][] board) {
int n = board.length;
boolean[] vis = new boolean[n * n + 1];
Queue<int[]> queue = new LinkedList<int[]>();
queue.offer(new int[]{1,0});
while (!queue.isEmpty()){
int p[]=queue.poll();
for (int i = 1; i <=6; i++) {
int next=p[0]+i;
if(next>n*n)break;
int []post=id2rc(next,n);
if(board[post[0]][post[1]]>0)next=board[post[0]][post[1]];
if(next==n*n)return p[1]+1;
if(!vis[next]){
vis[next]=true;
queue.offer(new int[]{next,p[1]+1});
}
}
}
return -1;
}
public int[] id2rc(int id, int n) {
int r = (id - 1) / n, c = (id - 1) % n;
if (r % 2 == 1) {
c = n - 1 - c;
}
return new int[]{n - 1 - r, c};
}

433. 最小基因变化

每层搜索将字母依次替换,然后用Hashmap记录

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
static char[] items = new char[]{'A', 'C', 'G', 'T'};
public int minMutation(String startGene, String endGene, String[] bank) {
Set<String> set = new HashSet<>();
for (String s : bank) set.add(s);
Deque<String> d = new ArrayDeque<>();
Map<String, Integer> map = new HashMap<>();
d.addLast(startGene);
map.put(startGene, 0);
while (!d.isEmpty()){
int size=d.size();
while (size-->0){
String s=d.pollFirst();
char[] cs = s.toCharArray();
int step = map.get(s);
for (int i=0;i<8;i++){
for (char c : items) {
if (cs[i] == c) continue;
char[] clone = cs.clone();
clone[i] = c;
String sub = String.valueOf(clone);
if (!set.contains(sub)) continue;
if (map.containsKey(sub)) continue;
if (sub.equals(endGene)) return step + 1;
map.put(sub, step + 1);
d.addLast(sub);
}
}
}
}
return -1;
}

OVERRIDE VS OVERLOAD

override eg: when the interface contain this method ,you implements it and try to override the method for a new purpose

1
2
3
4
@Override
public void addFirst(Item x) {
insert(x, 0);
}

overload eg: the same named method but contains different parameter

1
2
3
4
5
6
7
8
public static void peek(List61B<String> list) {
System.out.println(list.getLast());
}
public static void peek(SLList<String> list) {
System.out.println(list.getFirst());
}


dynamic method selection

1
2
3
4
5
6
LivingThing lt1;

lt1= new Fox();

Animal a1=lt1;

image-20230419133109776

A Trap!!!!

This is a overload not the different class override

1
2
3
4
5
6
7
SLList<String> SP = new SLList<String>();
List61B<String> LP = SP;
SP.addLast("elk");
SP.addLast("are");
SP.addLast("cool");
peek(SP);
peek(LP);
  • Interface inheritance (what): Simply tells what the subclasses should be able to do.
    • EX) all lists should be able to print themselves, how they do it is up to them.
  • Implementation inheritance (how): Tells the subclasses how they should behave.
    • EX) Lists should print themselves exactly this way: by getting each element in order and then printing them.

Implementation inheritance is a relationship where a child class inherits behaviour implementation from a base class.

Interface inheritance is when a child class only inherits the description of behaviour from the base class and provides the implementation itself.

Implementation inheritance may sound nice and all but there are some drawbacks:

  • We are fallible humans, and we can’t keep track of everything, so it’s possible that you overrode a method but forgot you did.
  • It may be hard to resolve conflicts in case two interfaces give conflicting default methods.
  • It encourages overly complex code

extends and implements

the extends keywords defines an “is-a” relationship between a subclass and a parent class

1
2
3
4
public VengefulSLList() {
//super();
deletedItems = new SLList<Item>();
}
1
2
3
4
public VengefulSLList(Item x) {
super(x);
deletedItems = new SLList<Item>();
}

notice: you can omit the super when you call the no parameter method but if you call the method with parameter you must call the right super(x)with the right parameter.

image-20230420133808622

Casting

1
2
3
4
Poodle frank = new Poodle("Frank", 5);
Malamute frankSr = new Malamute("Frank Sr.", 100);

Poodle largerPoodle = (Poodle) maxDog(frank, frankSr); // runtime exception!

Interface and abstract class

We’ve seen interfaces that can do a lot of cool things! They allow you to take advantage of interface inheritance and implementation inheritance. As a refresher, these are the qualities of interfaces:

  • All methods must be public.
  • All variables must be public static final.
  • Cannot be instantiated
  • All methods are by default abstract unless specified to be default
  • Can implement more than one interface per class

Below are the characteristics of abstract classes:

  • Methods can be public or private
  • Can have any types of variables
  • Cannot be instantiated
  • Methods are by default concrete unless specified to be abstract
  • Can only implement one per class

这是第一个测试博客

图片测试:

引用测试:

这是一条引用

代码测试:

1
2
3
4
5
6
7
8
9
10
public int removeElement(int[] nums, int val) {
int slowIndex=0;
for (int fastIndex=0;fastIndex<nums.length;fastIndex++){
if(nums[fastIndex]!=val){
nums[slowIndex]=nums[fastIndex];
slowIndex++;
}
}
return slowIndex;
}

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment