语法基础

CURD

1
2
3
4
INSERT INTO TABLE  (XXX)VALUE( XX,XX)
SELECT XX FROM TABLE
UPDATE TABLE SET 'XX' = XX WHERE
DELETE FROM TABLE WHERE

查询

DISTINCT(去重)

1
2
SELECT DISTINCT name, city FROM users;
对(name,city) 所有列去重,即sql语句里写的

LIMIT()

限制返回的行数。第一个参数为起始行,从 0 开始;第二个参数为返回的总行数

1
2
3
4
5
6
//返回前5
SELECT city,name FROM TABLE LIMIT 5;
SELECT city,name FROM TABLE LIMIT 0,5;
//返回2-5
SELECT city,name FROM TABLE LIMIT 2,3;

排序

  • ASC:升序
  • DESC:降序

可以按多个列进行排序,并且为每个列指定不同的排序方式:

1
2
3
SELECT *
FROM mytable
ORDER BY col1 DESC, col2 ASC;

过滤

操作符 示例 SQL 说明
= SELECT * FROM students WHERE city = '北京'; 等于
< SELECT * FROM students WHERE age < 20; 小于
> SELECT * FROM students WHERE score > 90; 大于
<>!= SELECT * FROM students WHERE city <> '北京'; 不等于
<= / !> SELECT * FROM students WHERE age <= 18; 小于等于
>= / !< SELECT * FROM students WHERE score >= 90; 大于等于
BETWEEN SELECT * FROM students WHERE score BETWEEN 80 AND 90; 在两值之间(包含边界)
IS NULL SELECT * FROM students WHERE remark IS NULL; 是 NULL
IS NOT NULL SELECT * FROM students WHERE remark IS NOT NULL; 不是 NULL
AND SELECT * FROM students WHERE age > 18 AND score > 80; 同时满足两个条件
OR SELECT * FROM students WHERE city = '北京' OR city = '上海'; 满足任一条件
() 优先级 SELECT * FROM students WHERE city='北京' OR (age < 20 AND score > 80); 加括号调整逻辑
IN SELECT * FROM students WHERE city IN ('北京', '上海'); 等价于多个 OR
IN (SELECT ...) SELECT * FROM students WHERE id IN (SELECT id FROM scores WHERE score>90); 从子查询匹配
NOT SELECT * FROM students WHERE NOT city = '北京'; 否定条件

通配符

通配符 示例 SQL 说明
%(>=0 个字符) SELECT * FROM students WHERE name LIKE '张%'; 匹配所有以“张”开头的名字,例如“张三”、“张小明”
%abc% SELECT * FROM students WHERE city LIKE '%京%'; 匹配包含“京” 的城市,例如“北京”、“南京”
_(=1 个字符) SELECT * FROM students WHERE name LIKE '_三'; 匹配第二个字是“三”的两个字名字,如“张三”,但不匹配“王小三”
[ab] SELECT * FROM students WHERE name LIKE '[李王]%'; (部分数据库支持,如 SQL Server) 匹配姓李或姓王的学生
[^ab] SELECT * FROM students WHERE name LIKE '[^李王]%'; 匹配不姓李也不姓王的学生

计算字段

函数

汇总(重要)

汇总函数 作用 示例 SQL 示例结果
COUNT(*) 计算总行数(包含 NULL) SELECT COUNT(*) FROM students; 5
COUNT(col) 统计非 NULL 的个数 SELECT COUNT(score) FROM students; 4(NULL 被忽略)
AVG(col) 计算平均值(忽略 NULL) SELECT AVG(score) FROM students; (85+90+75+95) / 4 = 86.25
SUM(col) 求和(忽略 NULL) SELECT SUM(score) FROM students; 85+90+75+95 = 345
MAX(col) 最大值 SELECT MAX(score) FROM students; 95
MIN(col) 最小值 SELECT MIN(score) FROM students; 75
1
2
3
4
5
6
7
AVG
SUM
MIN
MAX
COUNT(*) //统计全部
COUNT(col) //统计非空

1
ACG( DISTINCT col) 只会对不同的col做汇总

日期&文本

数组处理

分组!(重要)

分组 其实就是把相同的数据值的行放到同一组中

通过我们会要求 返回的是每一组的一个汇总情况

指定的分组字段除了能按该字段进行分组,也会自动按该字段进行排序。即group by会自动排序

WHERE 过滤行,HAVING 过滤分组,行过滤应当先于分组过滤。

1
2
3
4
5
SELECT cal,SUM(age) AS cnt FROM TABLE 
WHERE name LIKE 'XX%'
GROUP BY cal
ORDER BY xxx
HAVING cnt > 2

规则

  1. 除了那个汇总字段外,select里出现的字段都应该在 group by中给出

  1. order by 应该在 group by之后

  1. NULL 的行会单独分为一组;
  2. MySQL 的group by不支持blog text,其实大多数 SQL 实现不支持 GROUP BY 列具有可变长度的数据类型。PS:char()为固定,varchar()为可变

子查询

!!!子查询只能返回一个字段的数据

可以将子查询的结果作为 WHRER 语句的过滤条件:

1
2
3
4
SELECT *
FROM mytable1
WHERE col1 IN (SELECT col2
FROM mytable2);

下面的语句可以检索出客户的订单数量,子查询语句会对第一个查询检索出的每个客户执行一次:

1
2
3
4
5
6
SELECT cust_name, (SELECT COUNT(*)
FROM Orders
WHERE Orders.cust_id = Customers.cust_id)
AS orders_num
FROM Customers
ORDER BY cust_name;

标量子查询

1️⃣ 结构分析
  1. 外层 SELECT
1
SELECT (子查询) AS SecondHighestSalary
  • 外层 SELECT 里只有一个表达式:一个 标量子查询
  • 标量子查询(scalar subquery)返回 单个值
  • 即使没有匹配的行,MySQL 也会返回一行,值为 NULL
  1. 子查询
1
SELECT salary FROM Rank_S WHERE sRank = 2 LIMIT 1
  • 查找 sRank = 2 的工资
  • 如果存在多行(同一工资多个员工),LIMIT 1 保证只取 一行
  • 如果不存在第二高工资(比如表里只有一条记录),子查询 不返回任何行 → 标量子查询自动返回 NULL

2️⃣ 为什么保证一行输出
  • 外层 SELECT 不依赖表,只是用标量子查询生成一列
  • MySQL 规则:

标量子查询如果没有返回值 → 结果为 NULL

  • 外层 SELECT 至少会输出一行(列名 SecondHighestSalary,值为 NULL 或工资值)

3️⃣ 对比普通子查询
1
SELECT salary FROM Rank_S WHERE sRank = 2;
  • 直接查询,没找到行 → 返回 0 行
  • 不是标量子查询,外层没有包裹 → 可能没有任何结果行

4️⃣ 总结
  • 标量子查询 + 外层 SELECT → 总是返回一行
  • LIMIT 1 保证即使有多行,也只取一行
  • 没有匹配值 → 返回 NULL
  • ✅ 这就是为什么你写法“即使没有第二名,也会返回一行结果”

连接

连接用于连接多个表,使用 JOIN 关键字,并且条件语句使用 ON 而不是 WHERE。

连接可以替换子查询,并且比子查询的效率一般会更快。

可以用 AS 给列名、计算字段和表名取别名,给表名取别名是为了简化 SQL 语句以及连接相同表

其实就是根据某个条件把两张表的数据给拼接到一起

内连接

eg:

1
2
3
4
SELECT A.value AS A_value, B.value AS B_value
FROM tablea AS A
INNER JOIN tableb AS B
ON A.key = B.key;

可以不明确使用 INNER JOIN,而使用普通查询并在 WHERE 中将两个表中要连接的列用等值方法连接起来。

1
2
3
SELECT A.value, B.value
FROM tablea AS A, tableb AS B
WHERE A.key = B.key;

在没有条件语句的情况下返回笛卡尔积

PS:内连接;在没有条件语句的情况下返回笛卡尔积。

自连接

自连接可以看成内连接的一种,只是连接的表是自身而已。

一张员工表,包含员工姓名和员工所属部门,要找出与 Jim 处在同一部门的所有员工姓名。

子查询版本

1
2
3
4
5
6
SELECT name
FROM employee
WHERE department = (
SELECT department
FROM employee
WHERE name = "Jim");

自连接版本

1
2
3
4
SELECT e1.name
FROM employee AS e1 INNER JOIN employee AS e2
ON e1.department = e2.department
AND e2.name = "Jim";
1
2
ON e1.department = e2.department
AND e2.name = "Jim"

解释:

e1.department = e2.department:找和某人同部门的员工

e2.name = “Jim”:指定 e2 这一行是 “Jim”

换句话说,就是:查找和 Jim 在同一个部门的所有员工。

外连接

外连接保留了没有关联的那些行。分为左外连接,右外连接以及全外连接,左外连接就是保留左表没有关联的行。

检索所有顾客的订单信息,包括还没有订单信息的顾客。

1
2
3
SELECT Customers.cust_id, Orders.order_num
FROM Customers LEFT OUTER JOIN Orders
ON Customers.cust_id = Orders.cust_id;

customers 表:

cust_id cust_name
1 a
2 b
3 c

orders 表:

order_id cust_id
1 1
2 1
3 3
4 3

结果:

cust_id cust_name order_id
1 a 1
1 a 2
3 c 3
3 c 4
2 b Null

组合查询

使用 UNION 来组合两个查询,如果第一个查询返回 M 行,第二个查询返回 N 行,那么组合查询的结果一般为 M+N 行。

每个查询必须包含相同的列、表达式和聚集函数。

默认会去除相同行,如果需要保留相同行,使用 UNION ALL。

只能包含一个 ORDER BY 子句,并且必须位于语句的最后。

总结:

  • 合并两个或多个 SELECT 查询的结果
  • 默认会 去掉重复行
  • 如果想保留重复行,用 UNION ALL
  • 要求
    1. 每个查询的列数必须相同
    2. 对应列的数据类型要兼容
  • ORDER BY 只能在最后使用一次

可以理解为把查询结果 “上下拼接在一起”

1
2
3
4
5
6
7
SELECT col
FROM mytable
WHERE col = 1
UNION
SELECT col
FROM mytable
WHERE col =2;

MYSQL 8 窗口函数(重要)

MySQL六种窗口函数用法案例 - 白露~ - 博客园

窗口函数(Window Function)是 SQL 中用于在不分组的前提下对行进行计算的强大功能,尤其适用于排名、累计求和、移动平均等场景。

常用窗口函数

分类 函数 常见用途
累计/聚合 SUM(), AVG(), COUNT(), MAX(), MIN() 累计和 / 移动平均
排序 / 排名 ROW_NUMBER(), RANK(), DENSE_RANK() Top N、去重排序
偏移比较 LAG(), LEAD() 比较当前行与上一/下一行
统计总数 NTILE() 分组分位(如四分位)

窗口函数细则

分类 函数 典型语法 说明
累计 / 聚合 SUM(expr) SUM(weight) OVER (ORDER BY turn) 累积求和
AVG(expr) AVG(score) OVER (PARTITION BY class) 组内平均
COUNT(expr) COUNT(*) OVER (ORDER BY date) 累计计数
MAX(expr) MAX(salary) OVER (PARTITION BY dept) 每组最大值
MIN(expr) MIN(salary) OVER () 全局最小值
排序 / 排名 ROW_NUMBER() ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) 连续排名(无并列)
RANK() RANK() OVER (ORDER BY score DESC) 有并列,跳号
DENSE_RANK() DENSE_RANK() OVER (ORDER BY score DESC) 有并列,不跳号
偏移比较 LAG(expr, offset, default) LAG(salary, 1, 0) OVER (ORDER BY id) 取上一行数据
LEAD(expr, offset, default) LEAD(salary) OVER (ORDER BY id) 取下一行数据
分布统计 NTILE(n) NTILE(4) OVER (ORDER BY score DESC) 分成 4 份(四分位)
PERCENT_RANK() PERCENT_RANK() OVER (ORDER BY score) 百分比排名
CUME_DIST() CUME_DIST() OVER (ORDER BY score) 累计分布比例
位置 / 首尾 FIRST_VALUE(expr) FIRST_VALUE(salary) OVER (PARTITION BY dept ORDER BY salary) 组内第一个
LAST_VALUE(expr) LAST_VALUE(salary) OVER (...) 默认要配 frame 子句
NTH_VALUE(expr, n) NTH_VALUE(score, 2) OVER (...) 取第 n 个

OVER语法

OVER,OVER 里只能出现 PARTITION BYORDER BY

写法 含义
OVER() 不分组也不排序,整个表算
OVER(PARTITION BY ...) 在每个组内计算
OVER(ORDER BY ...) 按顺序逐行累积
OVER(PARTITION BY ... ORDER BY ...) 分组后再排序计算

大厂真题

百度

2021年11月每天的人均浏览文章时长_牛客题霸_牛客网

统计2021年11月每天的人均浏览文章时长(秒数),结果保留1位小数,并按时长由短到长排序

分析:

:::info

sum(out_time - in_time) / count(distinct uid)   as avg_time where 月份 group by day order by avg_time

函数使用:

人均:sum(out_time - in_time) / count(distinct uid)

每天的:group by day

2021年11月:where date_time() = “”

保留1位小数:Round( value,1)

:::

1

SQL 50

在前面的内容里,我们讨论了一些让大模型高质量输出内容的方法。其中让大模型输出 JSON 格式的数据,是一个非常有效且方便的方法。但是,当我们要进一步改善用户体验,希望通过流式传输减少等待时间时,就会发现 JSON 数据格式本身存在一个问题。对于从事前端行业的你来说,JSON 应该并不陌生,它是一种封闭的数据结构,通常以左花括号“{”开头,右花括号“}”结尾。封闭的数据结构,意味着一般情况下,前端对 JSON 的解析必须等待 JSON 数据全部传输完成,否则会因为 JSON 数据不完整而导致解析报错。这就导致一个问题,即使我们在前端用流式获取 JSON 数据,我们也得等待 JSON 完成后才能解析数据并更新 UI,这就让原本流式数据快速响应的特性失效了。那么有没有办法解决这个问题呢?JSON 的流式解析办法是有的。为了解决这个问题,有些人主张规范大模型的输出,比如采取 NDJSON(Newline-Delimited JSON)的方式,要求大模型输出的内容分为多行,每一行是一个独立的 JSON。但是这么做对大模型的输出进行了限制,不够灵活,而且很可能会影响大模型推理的准确性,有点得不偿失。另外一些人则使用 JSONStream 库,根据大模型输出的 JSON 配合 JSONStream 使用,这样能一定程度上解决问题,但是也不够通用,必须要事先针对大模型输出的特定结构进行处理,而且只能在 Server 端进行处理,没法直接在前端使用。

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
@RestController
@RequestMapping("/api/travel")
public class TravelController {

private final ChatClient chatClient;

public TravelController(ChatClient chatClient) {
this.chatClient = chatClient;
}

@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<Map<String, Object>> streamScenicSpots() {
String prompt = """
请依次介绍中国的5个著名景点。
每个景点单独输出一个JSON对象,不要合并。
格式如下:
{"name": "景点名", "desc": "简介"}
每个景点之间用字符串 <END> 分隔。
""";

StringBuilder buffer = new StringBuilder();

return chatClient.prompt()
.user(prompt)
.stream()
.flatMap(chunk -> {
String text = chunk.getOutput().getContent();
buffer.append(text);

// 按 <END> 分割
List<Map<String, Object>> ready = new ArrayList<>();
int idx;
while ((idx = buffer.indexOf("<END>")) != -1) {
String jsonChunk = buffer.substring(0, idx).trim();
buffer.delete(0, idx + "<END>".length());
try {
Map<String, Object> json = new ObjectMapper().readValue(jsonChunk, Map.class);
ready.add(json);
} catch (Exception e) {
// 不完整的JSON先跳过
}
}

// 返回所有完整JSON块
return Flux.fromIterable(ready);
});
}
}

计网

小红书日常二面

HTTP传输最小的一个字节,是多大的数据包?

:::info
“在 HTTP 层,理论上可以只发送 1 个字节的数据,例如一个非常小的响应体。
但是 HTTP 是基于 TCP 的,而 TCP 又基于 IP,所以在网络上传输时,会被封装成 TCP/IP 数据包。

  • TCP 报文段最小有 20 字节 TCP 头 + 20 字节 IP 头,总共至少 40 字节。
  • 如果是在以太网环境下,以太网帧最小长度是 64 字节,低于这个长度会自动填充。

所以即便 HTTP 只发送 1 个字节,网络层实际传输的最小数据包大约是 64 字节
总结来说,HTTP 层最小可以 1 字节,但网络上实际传输受到协议栈和以太网最小帧限制。”

假设你在浏览器访问网站,发送了一个 HTTP GET 请求,只有 1 个字节的响应体(比如返回 “A”):

  1. HTTP 层:响应内容是 “A”,只有 1 字节
  2. TCP/IP 层:加上 TCP 头 20B + IP 头 20B → 40B
  3. 以太网层:以太网帧最小 64B,不够 64B → 自动填充
  4. 结果:即使 HTTP 只传 1 字节,实际在网线上传输的也是 64 字节的帧

所以“以太网帧”就是你电脑和路由器之间实际在网线里流动的数据单元。

:::

HTTPS的原理?和HTTP区别?

:::info
HTTPS 的原理,其实就是在应用层和传输层之间加了一层 TLS/SSL,用于对 HTTP 传输的数据进行加密。

它涉及几个关键点:

  1. 握手过程
    • 首先客户端发起请求,同时发送一个随机数、支持的密码套件,以及椭圆曲线相关参数(基点等)。
    • 客户端本地生成一个临时私钥,并计算出对应的公钥发送给服务端。
  2. 服务端响应
    • 服务端收到请求后,生成一个随机数,选择密码套件,也生成一个临时私钥。
    • 每次请求都会生成新的临时私钥,以保证前向安全性(即每次会话的密钥都是唯一的,不使用固定私钥)。
  3. 密钥协商
    • 双方通过椭圆曲线的基点、各自的私钥以及对方发送的公钥,计算出本次会话的对称加密密钥。
    • 这个密钥之后用于加密 HTTP 传输的数据。
  4. 握手确认
    • 第一次握手:客户端发起请求
    • 第二次握手:服务端返回公钥和选择的密码套件
    • 第三次握手:客户端确认并生成会话密钥
    • 第四次握手(可选):服务端确认信息完整
    • 通过这几次握手,双方最终确认一个安全的会话密钥,用于本次 HTTPS 会话

HTTP 与 HTTPS 的区别

  • HTTP:TCP 三次握手后即可直接发送请求,传输的是明文数据。
  • HTTPS:除了 TCP 三次握手,还要经过 TLS 握手(大约 3~4 次),建立安全的加密会话,所以传输速度略慢,但安全性高。

:::

传输是对称加密还是非对称加密?

:::info

生成密钥后的通信就是 对称加密了

生成密钥前就是 使用的是非对称加密。对称用的密钥就是

:::

淘天暑期

网络里面交换机和路由器区别?

常见网络协议?

介绍下HTTP?

操作系统

美团暑期二面

操作系统里的LRU算法?

:::info
面试官你是想问我们在那个页面置换里面的那个吗。

其实是说,在我们因为我们的操作系统它的内存是有限的么,它就是说如果我们内存到了一定的紧张程度的话,它就是需要我们通过一个算法把这个用不到的页面置换出去,操作系统底层用的是LRU算法,但它可以选择不同算法,但是不同的内核版本,它应该有不同的一个实现然后它就是说把最久没使用的页面给淘汰出去。

这个的话,它其实是可能会有一个问题,就是说我们如果一次性加载了多个页面进来后,它可能会把以前的一个老页面给顶出去,它可能会有就是其实我们所谓的一个缓存污染这么一个问题,所以说,操作系统它底层对我们这个LRU是进行了一个改进的就是说,它通过我们一个其实跟mysql的Bufferpool算法有点类似,就是分old 和 yong区分,然后他们大概是3,7分。然后它淘汰的时候,它也是优先投入小的区域,然后直到你这个小数据被访问,它才会放到我们的一个old区,就是避免一次性读取大量数据,然后把一些老的反而真正常用数据给顶掉。所以说,我觉得这是他对LRU改进最大的一个地方,也是比较核心的一个点么

:::

a.LRU的思想是什么?

:::info
它就是出于一种局部性原理,就操作系统他其实有这么一个概念,一种是时间局部性,比如说你在一定时间内访问了某个数据,未来的一段时间内,你对这个数据的访问频率可能会更大,然后还有一个就是空间局部性,就是说你访问了所谓的一个数据,然后接下来,你访问这个数据的概率也是比较大的。所以说,它就是说优先淘汰掉我们以前比较久远的一个数据吧,我觉得是这样子淘汰的一个思想嗯。

:::

用Java常见数据结构如何设计一个LRU?操作时间复杂度?线程安全性?

:::info
额如果用现成的话,我就会直接用jdk collection里自带的一个LInkedHashMap,其实说白了底层就是用HashMap+双向链表这样子。要详细说一下,然后Map就是作为我们的一个Key查询,因为我们缓存的根本目的还是希望通过一个key来实现O1复杂的查询么。所以说,我们肯定是得通过一个Map来进行一个

存储我们的缓存数据,然后LRU,比较好的数据结构就是双向链表么,它的目的就是说把访问的数据放到我们的一个头部么,然后把尾部的数据,就是我们不经常访问的数据,然后一旦我们的数据size大于我们的limit,就把这个尾部的数据delete掉么

Map<String,Node>

Node1 <=> Node2

插入的话是Map插一次,然后插到头结点,都是O1。

查找的话一个就是从Map里面查,容纳后就是从双向链表里找到节点,然后把它,因为它只涉及到指针的移动,所以也是O1

线程安全这一块的话,我觉得得看你怎么实现吧,如果你要简单粗暴的话,就是一把synchronized或者对象锁直接对增删改查进行加锁,这时候就是只允许一个线程进行增删改查。它这样就肯定是线程安全的么,但就是锁的粒度太大了,我们可以尝试把它放小,比如读写锁,只有写操作才加锁,读的话就没报要加锁了,但就是后面的链表指针的移动这些我觉得还是得加了。就具体看我们要怎么实现

:::

线程池

美团暑期二面

h.IO耗时长,吞吐上不来,cpu又吃不满,如何优化吞吐量?

7.Go和Java最大的区别?

a.协程和线程区别?

b.JDK也有类似协程的东西,你知道吗?8.如何理解“全异步链路”的?(网关介绍里的)a.做过这个事,那为什么不用异步解决刚刚说的优化吞吐的事?

场景

字节暑期

ZSet做排行榜,需要展示前1000人的榜单,1001-1500展示实际排名,1500以后展示1000+,如何处理 Bigkey?

mysql

13.自己举例一个SQL,说说MySQL是如何查数据的?

14.表中有status和isDelete字段,where status=’已完成and isDelete =’N’超时,但数据量不大,只有一两百万条,可能是什么原因?如何解决?

:::info
举个例子,在数据库索引信息里有一个字段,比如叫 Cardinality,意思就是这个索引列里面有多少个不同的值,也就是索引的区分度

一般来说,这个值越接近表里的总行数,说明每一行的数据都不一样,区分度就越高,索引效果就越好。

但如果某个字段是状态字段,比如只有 successfail 两种值,各占一半,那不管你怎么建索引,这个索引的区分度都很低,因为查询 status='success' 仍然会匹配表里 50% 的数据。

这种情况下数据库优化器可能会觉得“既然要扫一半,我干脆直接全表扫描就好了”,所以这个索引就不一定会被真正用上。

解决方案:

“虽然表只有一两百万条数据,但 statusisDelete 是低区分度字段,优化器认为即使用索引过滤也要扫描大部分行,所以退化为全表扫描导致超时。解决方式是建立联合索引提升选择性或者使用覆盖索引,必要时强制优化器使用索引。”

解决方案:分页出来的数据order by limit + 内存里根据主键查询 数据 过滤出想要的数据

:::

二叉树问题思考角度就是站到ROOT节点,思考我要让左右节点返回给我什么信息。然后思考我有了这个信息后要干什么

路径总和 III[12]

数组情况:和为k的子数组

思路:

  1. 暴力,以每个节点为起点出发,然后直接遍历到终点。两层for循环
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
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
//暴力
int res = 0;
long targetSum = 0;
public int pathSum(TreeNode root, int _targetSum) {
//暴力搜索,类似于我们 普通前缀和的 两层for循环
targetSum = (long)_targetSum;
t1(root);
return res;
}
public void t1(TreeNode root){
if(root == null) return;
//起点
traverse(root, 0);
t1(root.left);
t1(root.right);
}
public void traverse(TreeNode root,long curSum){
if(root == null){
return;
}
curSum+=root.val;
if(curSum == targetSum) res++;
traverse(root.left, curSum);
traverse(root.right, curSum);
}

}
  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
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
//暴力
int res = 0;
long targetSum = 0;
//只针对单一个数
//k:前缀和 v:相同前缀和的个数
Map<Long, Integer> preSumCounter = new HashMap<>();

public int pathSum(TreeNode root, int _targetSum) {
//前缀和 + 两数之和
targetSum = (long) _targetSum;
preSumCounter.put(0L, 1); // 前缀和为0的次数为1
traverse(root, 0);
return res;
}

public void traverse(TreeNode root, long curSum) {
if (root == null) {
return;
}
curSum += root.val;
// preSum - xx = targetSum
// preSum - targetSum = xx
//即在map里找是否有 树为 presum + (-targetSum)
res += preSumCounter.getOrDefault(curSum + (-targetSum), 0);
//更新前缀和
preSumCounter.put(curSum, preSumCounter.getOrDefault(curSum, 0) + 1);

traverse(root.left, curSum);
traverse(root.right, curSum);

//回退
preSumCounter.put(curSum, preSumCounter.get(curSum) - 1);
}

}

二叉树中的最大路径和[173]

思路

  1. 站在ROOT节点思考左右节点要返回给我什么信息?我希望左子节点返回给我左子树的最大路径和。右节点类似
  2. 思考有了这个信息要干什么。无非就是求出最大路径和
    四种情况
    leftSum + cur.val

rightSum + cur.val

cur.val

cur.val + leftSum + rightSum

画板

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
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
int res = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
dfs(root);
return res;
}
public int dfs(TreeNode cur){
if(cur == null) return 0;
int leftSum = dfs(cur.left);
int rightSum = dfs(cur.right);
//子树的返回
int ret = Math.max(cur.val, Math.max(leftSum,rightSum) + cur.val);
//最终结果
res = Math.max(res, Math.max(ret, cur.val + leftSum + rightSum));
return ret;
}
}

二叉树的最近公共祖先[251]

思路:

法一: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
32
33
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
//法一 dfs
return dfs(root, p, q);
}

public TreeNode dfs(TreeNode root, TreeNode p, TreeNode q) {
if (root == null) {
return root;
}
if (root == p || root == q)
return root;
TreeNode left = dfs(root.left, p, q);
TreeNode right = dfs(root.right, p, q);
if (left == null)
return right;
if (right == null)
return left;
if (left == right)
return root;
//p,q都没找到
return root;
}
}

法二:Map存子节点->父节点,然后转换成相交链表问题

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
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
//k:子节点 v:父节点
Map<TreeNode, TreeNode> map = new HashMap<>();

public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
dfs(root, null);
//q节点的祖先 集合
Set<TreeNode> qZX = new HashSet<>();
while (q != null) {
qZX.add(q);
q = map.get(q);
}

while (p != null) {
//相交节点
if (qZX.contains(p)) {
return p;
}
p = map.get(p);
}
return null;
}

//
public void dfs(TreeNode cur, TreeNode par) {
if (cur == null) {
return;
}
map.put(cur, par);
dfs(cur.left, cur);
dfs(cur.right, cur);
}
}

时间复杂度都是O( N )

对称二叉树[91]

思路:

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
class Solution {
public boolean isSymmetric(TreeNode root) {
if(root==null) {
return true;
}
//调用递归函数,比较左节点,右节点
return dfs(root.left,root.right);
}

boolean dfs(TreeNode left, TreeNode right) {
//递归的终止条件是两个节点都为空
//或者两个节点中有一个为空
//或者两个节点的值不相等
if(left==null && right==null) {
return true;
}
if(left==null || right==null) {
return false;
}
if(left.val!=right.val) {
return false;
}
//再递归的比较 左节点的左孩子 和 右节点的右孩子
//以及比较 左节点的右孩子 和 右节点的左孩子
return dfs(left.left,right.right) && dfs(left.right,right.left);
}
}

作者:王尼玛
链接:https://leetcode.cn/problems/symmetric-tree/solutions/46560/dong-hua-yan-shi-101-dui-cheng-er-cha-shu-by-user7/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

翻转二叉树[67]

思路:

  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
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public TreeNode invertTree(TreeNode root) {
return dfs(root);
}

public TreeNode dfs(TreeNode root) {
if (root == null)
return null;
TreeNode left = dfs(root.left);
TreeNode right = dfs(root.right);
//在根节点 交换左右子节点即可
root.left = right;
root.right = left;

return root;
}
}

二叉树的最大深度[87]

思路(DFS):

  1. if(root.left == null && root.right == null) 结果更新时

思路(BFS):

  1. 层序遍历,每过一层 depth++
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
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
//int cnt = 0;
int res = Integer.MIN_VALUE;
public int maxDepth(TreeNode root) {
if(root != null){
dfs(root,1);
return res;
}
return 0;
}
public void dfs(TreeNode root,int cnt){
if(root == null) {
return ;
}
if(root.left == null && root.right == null){
res = Math.max(res,cnt);
return;
}
dfs(root.left,cnt + 1);
dfs(root.right,cnt + 1);
}
}

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
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public int maxDepth(TreeNode root) {
ArrayDeque<TreeNode> q = new ArrayDeque();
if (root != null)
q.add(root);
int depth = 0;
while (!q.isEmpty()) {
int size = q.size();
while (size > 0) {
TreeNode cur = q.remove();
if (cur.left != null) {
q.add(cur.left);
}
if (cur.right != null) {
q.add(cur.right);
}
size--;
}
depth++;
}
return depth;
}
}

二叉树的直径[79]

思路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
int res = 0;

public int diameterOfBinaryTree(TreeNode root) {
dfs(root);
return res;
}
//也是站在当前根节点 思考 左右子节点要给我返回什么
public int dfs(TreeNode root) {
if (root == null) {
return 0;
}
int lM = dfs(root.left);
int rM = dfs(root.right);
res = Math.max(lM + rM, res);
// 这个 1 是指Cur节点返回给上一级的时候,要把cur节点 加上去
return Math.max(lM, rM) + 1;
}
}

验证二叉搜索树[79]

思路:

  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
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
List<Integer> list = new ArrayList();

public boolean isValidBST(TreeNode root) {
f1(root);
for (int i = 1; i < list.size(); i++) {
if (list.get(i) <= list.get(i - 1)) {
return false;
}
}
return true;
}
//二叉搜索树 中序遍历 为 单调递增数组
public void f1(TreeNode root) {
if (root == null) {
return;
}
f1(root.left);
list.add(root.val);
f1(root.right);
}
}

二叉树的序列化与反序列化[57]

思路:

  1. 序列化容易,就是前序遍历,转成字符串,或者层序遍历
  2. 反序列化比较难,一种是bfs,一种是dfs
  3. 就是你序列化以BFS,你反序列化就是要BFS。DFS同样
  4. 题目其实就是给你root,序列化成字符串,然后再根据字符串反序列为树

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
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
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
public class Codec {
StringBuilder serRes = new StringBuilder();
// Encodes a tree to a single string.
public String serialize(TreeNode root) {
f1(root);
return serRes.toString();
}
public void f1(TreeNode root){
if(root == null){
serRes.append("null,");
return;
}
else serRes.append(root.val + ",");
f1(root.left);
f1(root.right);
}

// Decodes your encoded data to tree.
public TreeNode deserialize(String data) {
String []datas = data.split(",");
return builder(datas);
}
//构建树
int index = 0;

public TreeNode builder(String[] data){
//看示例1 即 从上到下,从右倒左遍历
String curV = data[index];
index++;
//当前节点
TreeNode curNode = null;
if(curV.equals("null")){
return curNode;
}
else {
int curVN = Integer.parseInt(curV);
curNode = new TreeNode(curVN);
//左右子节点 交给子树去构造,相信递归
curNode.left = builder(data);
curNode.right = builder(data);
}

return curNode;

}
}

// Your Codec object will be instantiated and called as such:
// Codec ser = new Codec();
// Codec deser = new Codec();
// TreeNode ans = deser.deserialize(ser.serialize(root));

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
public class Codec {
StringBuilder serRes = new StringBuilder();

// Encodes a tree to a single string.
public String serialize(TreeNode root) {
Queue<TreeNode> q = new LinkedList<>();
q.add(root);
while (!q.isEmpty()) {
TreeNode cur = q.poll();
if (cur != null) {
serRes.append(cur.val).append(",");
q.add(cur.left);
q.add(cur.right);
} else {
serRes.append("null,");
}
}
return serRes.toString();

}

// Decodes your encoded data to tree.
public TreeNode deserialize(String data) {

String[] datas = data.split(",");

return builder(datas);
}

//构建树
public TreeNode builder(String[] data) {

if (data.length == 0 || data[0].equals("null")) {
return null;
}
TreeNode res = new TreeNode(Integer.parseInt(data[0]));
Queue<TreeNode> q = new LinkedList<>();
q.add(res);
int index = 1;
while (!q.isEmpty()) {
TreeNode curNode = q.poll();
if (!data[index].equals("null")) {
curNode.left = new TreeNode(Integer.parseInt(data[index]));
q.add(curNode.left);
}
index++;
if (!data[index].equals("null")) {
curNode.right = new TreeNode(Integer.parseInt(data[index]));
q.add(curNode.right);
}
index++;
}
return res;
}
}

// Your Codec object will be instantiated and called as such:
// Codec ser = new Codec();
// Codec deser = new Codec();
// TreeNode ans = deser.deserialize(ser.serialize(root));

二叉树展开为链表[43]

思路:

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
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
TreeNode pre=null;
public void flatten(TreeNode root) {
if(root==null) return;

if(pre!=null){
pre.right=root;
}
//提前保存一下即可,防止丢失子树
TreeNode tmp=root.right;
pre=root;
flatten(root.left);
flatten(tmp);
root.left=null;
}
}

不同的二叉搜索树[31]

背诵题

思路:

  1. 找规律,先找出 比如 1个节点(n=1),2,….的时候,这时候有多少种二叉搜索树
  2. 然后再找 n和n-1的关系,即递归公式

明确它的一个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
class Solution {
public int numTrees(int n) {
//1: 1
//2: 2
//3: 5
//dp数组含义:n = i时有多少种二叉搜索树

////如果整数1 ~ n中的 k 作为根节点值,则 1 ~ k-1 会去构建左子树,k+1 ~ n 会去构建右子树。!!!
if( n <= 2) return n;
int []dp = new int[n+1];
dp[0] = 1;
dp[1] = 1;
for(int i = 2;i <= n;i++){
//在 i 范围内,当前节点val = k
//如果整数1 ~ i中的 k 作为根节点值,则 1 ~ k-1 会去构建左子树,k+1 ~ i 会去构建右子树。!!!
//此时
for(int k = 0;k <= i - 1;k++){
//左 和 右 组合
//为什么组合这里是乘法?
dp[i] += dp[k] * dp[i - k - 1];
}
}
return dp[n];
}
}

1️⃣ 为什么是 乘法

我们考虑:

如果整数 1 ~ i 中选 k 作为根节点,
左子树可以用 1 ~ k-1 构建,右子树可以用 k+1 ~ i 构建。

假设:

  • 左子树可以构建 L = dp[k] 种不同的 BST
  • 右子树可以构建 R = dp[i - k - 1] 种不同的 BST

总的 BST 数量 = 左子树方案数 × 右子树方案数

为什么是乘法?
因为每一种左子树都可以和每一种右子树组合成一个新的 BST。
这是经典的排列组合思想:左 * 右

比如小明有4个苹果,小红有5个香蕉。他们组合是多少,那肯定是4*5

举个小例子:

  • k=2,左子树用 1 构建 → 1 种方案
  • 右子树用 3,4 构建 → 2 种方案

总的 BST 数 = 1 * 2 = 2 种。


2️⃣ dp[k] 和 dp[i - k - 1] 分别是左子树和右子树

  • dp[k]左子树的方案数
    因为左子树包含 k 左边的 k 个节点(1 ~ k),所以 dp[k]
  • dp[i - k - 1]右子树的方案数
    因为右子树包含剩下的 i - k - 1 个节点(k+1 ~ i),所以 dp[i - k - 1]

⚠️ 注意下标关系:

  • i = 当前节点总数
  • k = 左子树节点数(或根节点下标?在你的循环中,k是左子树节点数)
  • 右子树节点数 = i - k - 1

打家劫舍 III[19]

路径总和 III[]

实现 Trie (前缀树)[34]

思路:

  1. 构造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
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

class Trie {
private static class Node {
Node[] son = new Node[26];
boolean end = false;
}

private final Node root = new Node();

public Trie() {

}

public void insert(String word) {
Node cur = root;
for (char c : word.toCharArray()) {
int index = c - 'a';
//无路可走? new 出来
if (cur.son[index] == null) {
cur.son[index] = new Node();
}
cur = cur.son[index];
}
cur.end = true;
}

public boolean search(String word) {
return find(word) == 2;
}

public boolean startsWith(String prefix) {
return find(prefix) != 0;
}

public int find(String word) {
Node cur = root;
for (char c : word.toCharArray()) {
int index = c - 'a';
if (cur.son[index] == null)
return 0;
cur = cur.son[index];
}
//2为找到了 1为找到前缀
return cur.end ? 2 : 1;
}
}

/**
* Your Trie object will be instantiated and called as such:
* Trie obj = new Trie();
* obj.insert(word);
* boolean param_2 = obj.search(word);
* boolean param_3 = obj.startsWith(prefix);
*/

把二叉搜索树转换为累加树[4]

思路:

  1. 利用二叉搜索树的特性,中序遍历为 有序数组
  2. 然后算出中序遍历后的数组的每个的元素的后缀和
  3. 然后再次中序遍历更改 树节点的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
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
List<Integer> l = new ArrayList();
int index = 0;
public TreeNode convertBST(TreeNode root) {
traverse(root);
//System.out.println(l);
//每个节点 node 的新值 等于 原树中大于或等于 node.val 的值之和
int sum = 0;
for(int i = 0;i < l.size(); i++){
sum += l.get(i);
}
int preSum = 0;
for(int i = 0; i < l.size(); i++){
int temp = l.get(i);
l.set(i, sum - preSum);
preSum += temp;
}
generate(root);
//System.out.println(l);
return root;
}
public void traverse(TreeNode root){
if(root == null) return;
traverse(root.left);
l.add(root.val);
traverse(root.right);
}
public void generate(TreeNode root){
if(root == null) return;
generate(root.left);
root.val = l.get(index);
index++;
generate(root.right);
}
}

合并二叉树[]

思路

  1. 前序遍历,但这样是错误,在merge时,root1 = root2,改变的不是引用!
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
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
dfs(root1, root2);
return root1;
}
public void dfs(TreeNode root1,TreeNode root2){
if(root1 == null && root2 == null){
return;
}
else if(root1 == null && root2 != null){
root1 = root2;
return;
}
else if(root1 != null && root2 == null){

return;
}
else if(root1 != null && root2 != null){
root1.val += root2.val;
}
dfs(root1.left,root2.left);
dfs(root1.right,root2.right);
}
}

正确的做法:

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
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
return build(root1, root2);
}
//返回节点
public TreeNode build(TreeNode root1, TreeNode root2){
if(root1 == null && root2 == null){
return null;
}
if(root1 != null && root2 == null){
return root1;
}
if(root1 == null && root2 != null){
return root2;
}

TreeNode res = new TreeNode();
if(root1 != null && root2 != null) res.val = root1.val + root2.val;
res.left = build(root1.left, root2.left);
res.right = build(root1.right, root2.right);

return res;
}
}

无重复字符的最长子串[1005]

滑动窗口最大值[134]

思路:

  1. 固定窗口
  2. 单调递减队列(队列头到队列尾)来维护区间最大值

这是一个降本增笑的故事:

如果新员工比老员工强(或者一样强),把老员工裁掉。(元素进入窗口)

如果老员工 35 岁了,也裁掉。(元素离开窗口)

裁员后,资历最老(最左边)(队列头)的人就是最强的员工了。

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 Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
int l = 0, r = 0;
List<Integer> res = new ArrayList<>();
Deque<Integer> dq = new ArrayDeque<>(); // 单调队列,存下标,保持递减

for (; r < nums.length; r++) {
// 移除队尾比当前元素小的下标
while (!dq.isEmpty() && nums[dq.peekLast()] < nums[r]) {
dq.pollLast();
}
dq.offerLast(r);

// 当窗口长度 >= k
if (r - l + 1 < k) {
continue;
}
// 队头就是最大值
res.add(nums[dq.peekFirst()]);

// 移除队头已经滑出窗口的下标
if (!dq.isEmpty() && dq.peekFirst() == l) {
dq.pollFirst();
}

l++;
}

return res.stream().mapToInt(Integer::intValue).toArray();
}

}

最小覆盖子串[114]

排序数组[321]

思路

快排,堆,计数,归并

  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
class Solution {
public static Random random = new Random();
public int[] sortArray(int[] nums) {
sort(nums, 0, nums.length - 1);
return nums;
}
public void sort(int []nums, int l, int r){
if( l >= r) return;

int indexX = partition(nums, l, r);
//l....indexX
sort(nums, l ,indexX - 1);
//indeX...r
sort(nums, indexX + 1, r);
}
public int partition(int []nums, int l, int r){
int i = l + random.nextInt( r - l + 1);
int x = nums[i];
swap(nums, l, i);
int curL = l + 1;
int curR = r;
while(true){
while(curL <= curR && nums[curL] < x){
curL++;
}
while(curL <= curR && nums[curR] > x){
curR--;
}
if(curL >= curR) break;

swap(nums, curL, curR);
curL++;
curR--;
}
swap(nums, l, curR);

return curR;
}
public void swap(int []nums, int i, int j){
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
  1. 题目特殊性,时间复杂度为O(N)的解法,采用计数排序

https://leetcode.cn/problems/sort-an-array/solutions/179210/dang-wo-tan-pai-xu-shi-wo-zai-tan-xie-shi-yao-by-s/

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

class Solution {
public int[] sortArray(int[] nums) {
int max = 50001;
int min = -50001;

int[] counter = new int[max - min + 1];
for (int num : nums) {
counter[num - min]++;
}
int index = 0;
//index 为 数组值 value为 数组值的个数
for (int i = min; i <= max; i++) {
int cnt = counter[i - min];
// cnt > 0说明存在
while (cnt > 0) {
nums[index] = i;
cnt--;
index++;
}
}
return nums;
}
}

  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
class Solution {
public int[] sortArray(int[] nums) {
sort(nums, 0, nums.length - 1);
return nums;
}
//归并排序
public void sort(int []nums, int l, int r){
if(l >= r) return;
int mid = l + (r - l) / 2;
sort(nums, l, mid);
sort(nums, mid + 1, r);
//合并 [l...mid] [mid + 1....r]
merge(nums, l, mid, mid + 1, r);
}
public void merge(int []nums, int s1, int e1, int s2, int e2){
// [s1...e1] [s1 ... e2]
int i = s1;
int j = s2;
// [2,4] [1,5]
// [1,2,4,]
int []temp = new int[e2 - s1 + 1];
int n = temp.length;
int k = 0;
while(i <= e1 || j <= e2){
while( i > e1 && j <= e2){
temp[k] = nums[j];
k++;
j++;
}
while(j > e2 && i <= e1){
temp[k] = nums[i];
k++;
i++;
}
if(k >= n) break;
//谁小谁先移动
if(nums[i] > nums[j]){
temp[k] = nums[j];
k++;
j++;
}
else {
temp[k] = nums[i];
k++;
i++;
}
}
System.arraycopy(temp, 0, nums, s1, temp.length);

}
public void swap(int[] nums, int i, int j){
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}

字典序排数[13]

思路:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public List<Integer> lexicalOrder(int n) {
//256
// 1 10 100 101...109 11
List<Integer> res = new ArrayList();
for (int i = 0, j = 1; i < n; i++) {
res.add(j);
if (j * 10 <= n) {
j = j * 10;
} else {
// 1 10 100 101...109 11 or 1,2
// 109 -> 11 ,2 -> 3
while (j % 10 == 9 || j + 1 > n) {
j = j / 10;
}
j++;
}
}
return res;
}
}

排序链表[133]

思路:

  1. 归并排序
    head….tail-> head….mid mid…tail

不断分,然后并起来

归:对链表进行分割,直到每个链表只有一个 元素

并:对链表进行升序合并

链表取中点(快慢指针),并切割

  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
67
68
69
70
71
72
73
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode sortList(ListNode head) {
return sort(head);
}

//从中间分开
public ListNode divide(ListNode head) {
ListNode fast = head;
ListNode slow = head;
ListNode pre = head;
while (fast != null && fast.next != null) {
pre = slow;
fast = fast.next.next;
slow = slow.next;
}
pre.next = null;
return slow;
}

//归并排序
public ListNode sort(ListNode head1) {
if (head1 == null || head1.next == null) {
return head1;
}
//拆成两部分,并返回后部门的头结点
ListNode head2 = divide(head1);
//mid(4→2→1→3) ---> 分成两部分
//head1: 4 → 2 → null
//head2: 1 → 3 → null
head1 = sort(head1);

head2 = sort(head2);

return merge(head1, head2);
}

//两链表合并,要有序
// head1 ....
// head2 ....
public ListNode merge(ListNode head1, ListNode head2) {
ListNode res = new ListNode();
ListNode cur = res;
while (head1 != null || head2 != null) {
if (head2 == null) {
cur.next = head1;
break;
}
if (head1 == null) {
cur.next = head2;
break;
}
if (head1.val > head2.val) {
cur.next = head2;
head2 = head2.next;
} else {
cur.next = head1;
head1 = head1.next;
}
cur = cur.next;
}
return res.next;
}
}

时间复杂度:NlogN 空间复杂度:logN

合并K个排序链表[221]

思路:

  1. 归并排序
  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
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
if(lists.length == 0) return null;
//最小堆,堆顶为最小值
PriorityQueue<ListNode> pq = new PriorityQueue<>((a, b) -> a.val - b.val);
//先将最小的放进来
for(ListNode head: lists){
if(head == null) continue;
pq.add(head);
}
ListNode dummyHead = new ListNode();
ListNode res = dummyHead;

while(!pq.isEmpty()){
ListNode minNode = pq.poll();
res.next = minNode;
res = res.next;
if(minNode.next != null){
pq.add(minNode.next);
}
}
return dummyHead.next;
}
}
  • 时间复杂度:O(Llogm),其中 m lists 的长度,L 为所有链表的长度之和。

数组中的第K个最大元素[550]

题目:

  1. 要求时间复杂度为O(N)

思路:

  1. 维护size == k的最小堆(但不符合题意)时间复杂度:nums.length log(k)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public int findKthLargest(int[] nums, int k) {
if(nums.length == 1) return nums[0];
PriorityQueue<Integer> pq = new PriorityQueue();
for(int i: nums){
pq.add(i);
if(pq.size() > k){
pq.poll();
}
}
//System.out.println(pq);
return pq.isEmpty()?-1:pq.poll();
}
}
  1. 快速选择

思路:

  1. 构建partition函数,里面干的就是 数组随机取一个 x,然后划分 x
  2. 如何划分!?
    1、小技巧,先将 l 和 x交换,方便后面处理

2、curL–> …. <—curR,while把curL和curR推进到需要交换的位置,然后swap,交换后,重复这个过程
外层while(true) 内层while 推进curL,curR。若curR<=curL退出全部循环

3、将x放回正确位置,—> swap(l,curR)

  1. 经过划分我们可以得到x。那么这个时候判断x的index 和 targetIndex( n - k)的位置
  2. 若 index = targertIdex 就是找到答案了,直接返回。若index > targetIndex 则说明targetIndex在右边,那么这个时候让r = index缩小范围,反之,让l = index
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
class Solution {
private static final Random rand = new Random();

public int findKthLargest(int[] nums, int k) {
//选一个基准 元素 x
// <x.... x... >x
//如果 i = n - k ,那么 res = i
//如果 i > n - k, 那么res在左侧,我们在其中寻找,重复第一步
//如果 i < n - k, 那么res在右侧,我们在其中寻找,重复第一步
if(nums.length < k) return -1;
int n = nums.length;
int l = 0;
int r = n - 1;
int targetIndex = n - k;
while(true){
int resIndex = partition(nums, l, r);
if(resIndex == targetIndex){
return nums[resIndex];
}
//redIndex 在 targetIndex右边 说明 targetIndex的数在 l...resIndex 中
else if(resIndex > targetIndex){
r = resIndex - 1;
}
else {
l = resIndex + 1;
}
}
}
public int partition(int []nums, int l, int r){
int i = l + rand.nextInt(r - l + 1);
int x = nums[i];
//System.out.println(x);
//交换左边界元素 和 x,方便后面实现
swap(nums, l, i);
//System.out.println(Arrays.toString(nums));
int curL = l + 1;
int curR = r;
//处理示意图: (< x) curL-> (没处理的元素) <-curR ( > x)
while(true){
while(curL <= curR && nums[curL] < x){
curL++;
}
//经过上面的while循环此时 nums[curL] >= x
while(curL <= curR && nums[curR] > x){
curR--;
}
//经过上面的while循环此时 nums[curL] <= x
if(curL >= curR) break;
swap(nums, curL, curR);
curR--;
curL++;
}

//System.out.println(Arrays.toString(nums));
swap(nums, l, curR);
//System.out.println(Arrays.toString(nums));
return curR;
}
// 交换 nums[i] 与 nums[j]
private void swap(int[] nums, int i, int j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
}

颜色分类[41]

思路:

  1. 三路快排(三指针法)。0…zero i two…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


class Solution {
public void sortColors(int[] nums) {
int n = nums.length;


int i = 0;
int zero = 0;
int two = n - 1;
//[0 zero] 全为 0
//[zero i] 全为 1
//[two, n-1] 全为 2
// 0 1 2
while(i <= two){
if(nums[i] == 0){
swap(nums, i, zero);
zero++;
i++;
}
else if(nums[i] == 1){
i++;
}
else {
swap(nums, i, two);
two--;
}
}
}
public void swap(int[] nums, int i, int j){
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}

或者计数排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public void sortColors(int[] nums) {
int[] counter = new int[3];
for(int i: nums){
counter[i]++;
}
int index = 0;
for(int i = 0; i < 3; i++){
int cnt = counter[i];
while(cnt > 0) {
nums[index] = i;
index++;
cnt--;
}
}
}
}