原因

在写md文档的时候,不免插入一些图片,借助typora可以轻松的实现图片的插入,还可以设定文件夹,不过在hexo下只能存放在与md文档同名的文件夹下,特别混乱,尤其是借助obsidan浏览时。

所有我就在文件夹下新增了一个assets文件夹,图片分类存放了进去,但是hexo无法处理,经常2天的研究终于搞定了。特记录如下:

方法

确定版本

1
2
3
4
5
6
7
8
9
10
11
12
13
"hexo": "^7.3.0",
"hexo-deployer-git": "^4.0.0",
"hexo-generator-archive": "^2.0.0",
"hexo-generator-category": "^2.0.0",
"hexo-generator-index": "^4.0.0",
"hexo-generator-searchdb": "^1.4.1",
"hexo-generator-tag": "^2.0.0",
"hexo-renderer-ejs": "^2.0.0",
"hexo-renderer-marked": "^6.3.0",
"hexo-renderer-stylus": "^3.0.1",
"hexo-server": "^3.0.0",
"hexo-theme-landscape": "^1.0.0",
"hexo-theme-next": "^8.20.0"

_config.yml

需要开启下面的配置

1
2
3
4
5
6
7
8
9
post_asset_folder: true
marked:
prependRoot: true
postAsset: true

# 默认不会处理这类文件 需要配置下
include:
- '**/assets/**'

修改源码

需要修改一个包的内容 hexo-renderer-marked,把image部分的处理修改一下

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
image(href, title, text) {
const { hexo, options } = this;
const { relative_link } = hexo.config;
const { lazyload, figcaption, prependRoot, postPath } = options;

// ============================== 以下代码有改动 ==============================
if (!/^(#|\/\/|http(s)?:)/.test(href) && !relative_link && prependRoot) {
if (!href.startsWith('/') && !href.startsWith('\\') && postPath) {
let destPath = join(postPath, '../');
// findById requires forward slash
destPath = destPath.replace('source/_posts', '/md-images')
href = join(destPath, href.replace(/\\/g, '/'))
}
}
let out = `<img src="${encodeURL(href)}"`;
// ============================== 以上代码有改动 ==============================

if (text) out += ` alt="${text}"`;
if (title) out += ` title="${title}"`;
if (lazyload) out += ' loading="lazy"';

out += '>';
if (figcaption) {
if (text) out += `<figcaption aria-hidden="true">${text}</figcaption>`;
}
return out;
}

md文档写法

文件组织形式

1
2
3
4
5
6
7
8
9
摄像技术/
摄影技术.md
拍照技术.md
assets/
摄影技术/
1.png
2.png
拍照技术/
1.png

md文档的写法

!()[assets/拍照技术/1.png]

部署流程

todo 多了一步流程,以后优化掉吧

hexo build之后,在public文件夹内找到_post文件夹(全是图片),重命名为md-images(与代码中的一致即可以)

执行 hexo deploy 就可以了

为什么要多此一举重命名呢,因为_post文件夹推送上去,访问不到,我猜是github忽略了 _开头的文件

优化部署流程

项目根目录新建一个rename.js帮助我们自动重命名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const fs = require('fs');
const path = require('path');

const publicDir = path.join(__dirname, 'public');
const oldFolder = path.join(publicDir, '_posts');
const newFolder = path.join(publicDir, 'md-images');

// 检查 _posts 文件夹是否存在
fs.access(oldFolder, fs.constants.F_OK, (err) => {
if (err) {
console.error('Error: _posts folder does not exist.');
process.exit(1);
} else {
// 重命名文件夹
fs.rename(oldFolder, newFolder, (err) => {
if (err) {
console.error('Error renaming folder:', err);
process.exit(1);
} else {
console.log('Folder renamed successfully.');
}
});
}
});

修改package.json

1
2
3
4
5
6
7
8
9
10
{
"scripts": {
"hexo:generate": "hexo generate",
"rename": "node rename.js",
"build": "npm run hexo:generate && npm run rename",
"clean": "hexo clean",
"deploy": "node deploy.js",
"server": "hexo server"
},
}

这样就可以在 npm run build 后,自动重命名了

完成

上去看一下,已经解决了问题,而且支持文章嵌套 ,让文章关系更加清晰,例如:vue分类下还有一个源码分析的分类。

image-20240719164530109

常见网页协议

  • TCP/IP 是互联网相关的各类协议族的总称
  • IP(Internet Protocol)网际协议(网络层协议)IP 协议的作用是把各种数据包传送给对方。而要保证确实传送到对方那里,则需要满足各类条件。其中两个重要的条件是 IP 地址和 MAC 地址(Media Access Control Address)
  • HTTP 超文本传输协议是一个用于传输超媒体文档(例如 HTML)的应用层协议。它是为 Web 浏览器与 Web 服务器之间的通信而设计的,但也可以用于其他目的
  • TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议
  • UDP(User Data Protocol,用户数据报协议)一个非连接的协议,传输数据之前源端和终端不建立连接, 当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上

TCP/IP 的分层管理

利用 TCP/IP 协议族进行网络通信时,会通过分层顺序与对方进行通信

image-20210714152647756

这些层基本上被分为4层:

  • 应用层

    • 1、超文本传输协议(HTTP):万维网的基本协议
    • 2、文件传输(FTP文件传输协议);
    • 3、远程登录(Telnet),提供远程访问其它主机功能, 它允许用户登录internet主机,并在这台主机上执行命令
    • 4、网络管理(SNMP简单网络管理协议),该协议提供了监控网络设备的方法, 以及配置管理,统计信息收集,性能管理及安全管理等
    • 5、域名系统(DNS),该系统用于在internet中将域名及其公共广播的网络节点转换成IP地址
  • 传输层

    • 1、TCP
    • 2、UDP
  • 网络层

    • 1、Internet协议(IP)
    • 2、Internet控制信息协议(ICMP)
    • 3、地址解析协议(ARP)ARP 是一种用以解析地址的协议,根据通信方的 IP 地址就可以反查出对应的 MAC 地址。
    • 4、反向地址解析协议(RARP)
  • 链路层

    用来处理连接网络的硬件部分。包括控制操作系统、硬件的设备驱动、NIC(Network Interface Card,网络适配器,即网卡),及光纤等物理可见部分(还包括连接器等一切传输媒介)。硬件上的范畴均在链路层的作用范围之内。

    OSI 七层模型

    img

    附图 tcp段,IP分段 HTTP权威指南80页

    HTTP权威指南第六章IP分组

TCP/IP 通信传输流

利用 TCP/IP 协议族进行网络通信时,会通过分层顺序与对方进行通信。发送端从应用层往下走,接收端则往应用层往上走。 发送端在层与层之间传输数据时,每经过一层时必定会被打上一个该层所属的首部信息。反之,接收端在层与层传输数据时,每经过一层时会把对应的首部消去。

这种把数据信息包装起来的做法称为封装(encapsulate),如下图所示

image-20210714153451934

TCP

img

头部至少20个字节

待整理

TCP协议的特点

面向连接

面向连接,是指发送数据之前必须在两端建立连接。建立连接的方法是“三次握手”,这样能建立可靠的连接。建立连接,是为数据的可靠传输打下了基础。

仅支持单播传输

每条TCP传输连接只能有两个端点,只能进行点对点的数据传输,不支持多播和广播传输方式。

面向字节流

TCP不像UDP一样那样一个个报文独立地传输,而是在不保留报文边界的情况下以字节流方式进行传输。

可靠传输

对于可靠传输,判断丢包,误码靠的是TCP的段编号以及确认号。TCP为了保证报文传输的可靠,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的字节发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据(假设丢失了)将会被重传。

提供拥塞控制

当网络出现拥塞的时候,TCP能够减小向网络注入数据的速率和数量,缓解拥塞

TCP提供全双工通信

TCP允许通信双方的应用程序在任何时候都能发送数据,因为TCP连接的两端都设有缓存,用来临时存放双向通信的数据。当然,TCP可以立即发送一个数据段,也可以缓存一段时间以便一次发送更多的数据段(最大的数据段大小取决于MSS)

UDP

UDP协议全称是用户数据报协议,在网络中它与TCP协议一样用于处理数据包,是一种无连接的协议。在OSI模型中,在第四层——传输层,处于IP协议的上一层。UDP有不提供数据包分组、组装和不能对数据包进行排序的缺点,也就是说,当报文发送之后,是无法得知其是否安全完整到达的。

它有以下几个特点:

1. 面向无连接

首先 UDP 是不需要和 TCP一样在发送数据前进行三次握手建立连接的,想发数据就可以开始发送了。并且也只是数据报文的搬运工,不会对数据报文进行任何拆分和拼接操作

具体来说就是:

  • 在发送端,应用层将数据传递给传输层的 UDP 协议,UDP 只会给数据增加一个 UDP 头标识下是 UDP 协议,然后就传递给网络层了
  • 在接收端,网络层将数据传递给传输层,UDP 只去除 IP 报文头就传递给应用层,不会任何拼接操作

2. 有单播,多播,广播的功能

UDP 不止支持一对一的传输方式,同样支持一对多,多对多,多对一的方式,也就是说 UDP 提供了单播,多播,广播的功能。

3. UDP是面向报文的

发送方的UDP对应用程序交下来的报文,在添加首部后就向下交付IP层。UDP对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。因此,应用程序必须选择合适大小的报文

4. 不可靠性

首先不可靠性体现在无连接上,通信都不需要建立连接,想发就发,这样的情况肯定不可靠。

并且收到什么数据就传递什么数据,并且也不会备份数据,发送数据也不会关心对方是否已经正确接收到数据了。

再者网络环境时好时坏,但是 UDP 因为没有拥塞控制,一直会以恒定的速度发送数据。即使网络条件不好,也不会对发送速率进行调整。这样实现的弊端就是在网络条件不好的情况下可能会导致丢包,但是优点也很明显,在某些实时性要求高的场景(比如电话会议)就需要使用 UDP 而不是 TCP。

img

从上面的动态图可以得知,UDP只会把想发的数据报文一股脑的丢给对方,并不在意数据有无安全完整到达。

5. 头部开销小,传输数据报文时是很高效的

img

UDP 头部包含了以下几个数据:

  • 两个十六位的端口号,分别为源端口(可选字段)和目标端口 (4字节?)
  • 整个数据报文的长度
  • 整个数据报文的检验和(IPv4 可选 字段),该字段用于发现头部信息和数据中的错误

因此 UDP 的头部开销小,只有八字节,相比 TCP 的至少二十字节要少得多,在传输数据报文时是很高效的

TCP与UDP的区别

UDP TCP
是否连接 无连接 面向连接
是否可靠 不可靠传输,不使用流量控制和拥塞控制 可靠传输,使用流量控制和拥塞控制
连接对象个数 支持一对一,一对多,多对一和多对多交互通信 只能是一对一通信
传输方式 面向报文 面向字节流
首部开销 首部开销小,仅8字节 首部最小20字节,最大60字节
适用场景 适用于实时应用(IP电话、视频会议、直播等) 适用于要求可靠传输的应用,例如文件传输

TCP 拥塞控制

拥塞处理和流量控制不同,后者是作用于接收方,保证接收方来得及接受数据。而前者是作用于网络,防止过多的数据拥塞网络,避免出现网络负载过大的情况。

拥塞处理包括了四个算法,分别为:慢开始,拥塞避免,快速重传,快速恢复。

慢开始算法

慢开始算法,顾名思义,就是在传输开始时将发送窗口慢慢指数级扩大,从而避免一开始就传输大量数据导致网络拥塞。

慢开始算法步骤具体如下

  1. 连接初始设置拥塞窗口(Congestion Window) 为 1 MSS(一个分段的最大数据量)
  2. 每过一个 RTT 就将窗口大小乘二
  3. 指数级增长肯定不能没有限制的,所以有一个阈值限制,当窗口大小大于阈值时就会启动拥塞避免算法。

拥塞避免算法

拥塞避免算法相比简单点,每过一个 RTT 窗口大小只加一,这样能够避免指数级增长导致网络拥塞,慢慢将大小调整到最佳值。

在传输过程中可能定时器超时的情况,这时候 TCP 会认为网络拥塞了,会马上进行以下步骤:

  • 将阈值设为当前拥塞窗口的一半
  • 将拥塞窗口设为 1 MSS
  • 启动拥塞避免算法

快速重传

快速重传一般和快恢复一起出现。一旦接收端收到的报文出现失序的情况,接收端只会回复最后一个顺序正确的报文序号(没有 Sack 的情况下)。如果收到三个重复的 ACK,无需等待定时器超时再重发而是启动快速重传。具体算法分为两种:

TCP Taho 实现如下

  • 将阈值设为当前拥塞窗口的一半
  • 将拥塞窗口设为 1 MSS
  • 重新开始慢开始算法

TCP Reno 实现如下

  • 拥塞窗口减半
  • 将阈值设为当前拥塞窗口
  • 进入快恢复阶段(重发对端需要的包,一旦收到一个新的 ACK 答复就退出该阶段)
  • 使用拥塞避免算法

TCP New Ren 改进后的快恢复

TCP New Reno 算法改进了之前 TCP Reno 算法的缺陷。在之前,快恢复中只要收到一个新的 ACK 包,就会退出快恢复。

TCP New Reno 中,TCP 发送方先记下三个重复 ACK 的分段的最大序号。

假如我有一个分段数据是 1 ~ 10 这十个序号的报文,其中丢失了序号为 3 和 7 的报文,那么该分段的最大序号就是 10。发送端只会收到 ACK 序号为 3 的应答。这时候重发序号为 3 的报文,接收方顺利接收并会发送 ACK 序号为 7 的应答。这时候 TCP 知道对端是有多个包未收到,会继续发送序号为 7 的报文,接收方顺利接收并会发送 ACK 序号为 11 的应答,这时发送端认为这个分段接收端已经顺利接收,接下来会退出快恢复阶段。

http 0.9

HTTP/0.9 是于 1991 年提出的,主要用于学术交流,需求很简单——用来在网络之间传递 HTML 超文本的内容,所以被称为超文本传输协议

总的来说,当时的需求很简单,就是用来传输体积很小的 HTML 文件,所以 HTTP/0.9 的实现有以下三个特点。

  • 第一个是只有一个请求行,并没有HTTP 请求头和请求体,因为只需要一个请求行就可以完整表达客户端的需求了。

  • 第二个是服务器也没有返回头信息,这是因为服务器端并不需要告诉客户端太多信息,只需要返回数据就可以了。

  • 第三个是返回的文件内容是以 ASCII 字符流来传输的,因为都是 HTML 格式的文件,所以使用 ASCII 字节码来传输是最合适的。

http 1.0

万维网的高速发展带来了很多新的需求,而 HTTP/0.9 已经不能适用新兴网络的发展,所以这时就需要一个新的协议来支撑新兴网络,这就是 HTTP/1.0 诞生的原因。

  • 引入了请求头和响应头
    • 多类型数据
    • 压缩方式
    • 用户代理 user-agent
  • 引入了状态码
  • 引入了cache机制

http 1.1

不过随着技术的继续发展,需求也在不断迭代更新,很快 HTTP/1.0 也不能满足需求了,所以 HTTP/1.1 又在 HTTP/1.0 的基础之上做了大量的更新

  • 改进持久连接(默认开启),它的特点是在一个 TCP 连接上可以传输多个 HTTP 请求,只要浏览器或者服务器没有明确断开连接,那么该 TCP 连接会一直保持

    每个域名最多同时维护 6 个 TCP 持久连接

  • 不成熟的 HTTP 管线化,如果 TCP 通道中的某个请求因为某些原因没有及时返回,那么就会阻塞后面的所有请求,这就是著名的队头阻塞的问题

    • HTTP/1.1 中试图通过管线化的技术来解决队头阻塞的问题。HTTP/1.1 中的管线化是指将多个 HTTP 请求整批提交给服务器的技术,虽然可以整批发送请求,不过服务器依然需要根据请求顺序来回复浏览器的请求。

      FireFox、Chrome 都做过管线化的试验,但是由于各种原因,它们最终都放弃了管线化技术。

  • 提供虚拟主机的支持

  • 对动态生成的内容提供了完美支持,HTTP/1.1 通过引入Chunk transfer 机制来解决这个问题,最后使用一个零长度的块作为发送数据完成的标志

  • 客户端 Cookie、安全机制

http 2.0

多路复用

前面我们分析了 HTTP/1.1 所存在的一些主要问题:慢启动和 TCP 连接之间相互竞争带宽是由于 TCP 本身的机制导致的,而队头阻塞是由于 HTTP/1.1 的机制导致的。

HTTP/2 的思路就是一个域名只使用一个 TCP 长连接来传输数据,这样整个页面资源的下载过程只需要一次慢启动,同时也避免了多个 TCP 连接竞争带宽所带来的问题

另一个问题就是队头阻塞的问题,等待请求完成后才能去请求下一个资源,这种方式无疑是最慢的,所以 HTTP/2 需要实现资源的并行请求,解决了应用层面的队头阻塞,也就是任何时候都可以将请求发送给服务器,而并不需要等待其他请求的完成,然后服务器也可以随时返回处理好的请求资源给浏览器

image-20210803172922718

image-20210803173309562

http2分帧

帧是数据传输的最小单位,以二进制传输代替原本的明文传输,原本的报文消息被划分为更小的数据帧

帧结构
1
2
3
4
5
6
7
8
9
10
 +-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+=+=============================================================+
| Frame Payload (0...) ...
+---------------------------------------------------------------+
复制代码
名称 长度 描述
Length 3 字节 表示帧负载的长度,取值范围为 (2 的 14 次方)至 (2 的 24 次方 - 1)。(2 的 14 次方) 16384 字节是默认的最大帧大小,如果需要更大的帧,必须在 SETTINGS 帧中设置
Type 1 字节 当前帧类型(见下表)
Flags 1 字节 具体帧类型的标识
R 1 位 保留位,不要设置,否则可能会带来严重的后果
Stream Identifier 31 位 每个流的唯一 ID
Frame Payload 长度可变 真实的帧内容,长度是在 Length 字段中设置的

由于 HTTP/2 是分帧的,请求和响应都可以多路复用,有助于解决类似类似队头阻塞的问题。

帧类型
名称 ID 描述
DATA 0x0 传输流的核心内容
HEADERS 0x1 包含 HTTP 首部,和可选的优先级参数
PRIORITY 0x2 指示或更改流的优先级和依赖
RST_STREAM 0x3 允许一端停止流(通常由于错误导致的)
SETTINGS 0x4 协商连接级参数
PUSH_PROMISE 0x5 提示客户端,服务器要推送些东西
PING 0x6 测试连接可用性和往返时延(RTT)
GOAWAY 0x7 告诉另一端,当前的端已结束
WINDOW_UPDATE 0x8 协商一端将要接收多少字节(用于流量控制)
CONTINUATION 0x9 用以扩展 HEADERS 模块

HTTP/2 其他特性

  • 可以设置请求优先级
  • 服务器推送
  • 头部压缩

队头阻塞

应用层面

HTTP/1.1 中使用持久连接时,虽然能公用一个 TCP 管道,但是在一个管道中同一时刻只能处理一个请求,在当前的请求没有结束之前,其他的请求只能处于阻塞状态。

http 2通过对请求和响应进行编号的方式,实现资源并行请求解决了这个问题

TCP层面

http 1.1

image-20210805145222668

通过上图你会发现,从一端发送给另外一端的数据会被拆分为一个个按照顺序排列的数据包,这些数据包通过网络传输到了接收端,接收端再按照顺序将这些数据包组合成原始数据,这样就完成了数据传输。

不过,如果在数据传输的过程中,有一个数据因为网络故障或者其他原因而丢包了,那么整个 TCP 的连接就会处于暂停状态,需要等待丢失的数据包被重新传输过来。你可以把 TCP连接看成是一个按照顺序传输数据的管道,管道中的任意一个数据丢失了,那之后的数据都需要等待该数据的重新传输。为了直观理解,你可以参考下图:

image-20210805145256682

我们就把在 TCP 传输过程中,由于单个数据包的丢失而造成的阻塞称为 TCP 上的队头阻塞

http 2.0

那队头阻塞是怎么影响 HTTP/2 传输的呢?首先我们来看正常情况下 HTTP/2 是怎么传输多路请求的,为了直观理解,你可以参考下图:

image-20230216173807167

通过该图,我们知道在 HTTP/2 中,多个请求是跑在一个 TCP 管道中的,如果其中任意一路数据流中出现了丢包的情况,那么就会阻塞该 TCP 连接中的所有请求。这不同于HTTP/1.1,使用 HTTP/1.1 时,浏览器为每个域名开启了 6 个 TCP 连接,如果其中的 1个 TCP 连接发生了队头阻塞,那么其他的 5 个连接依然可以继续传输数据。

所以随着丢包率的增加,HTTP/2 的传输效率也会越来越差。有测试数据表明,当系统达到了 2% 的丢包率时,HTTP/1.1 的传输效率反而比 HTTP/2 表现得更好。

HTTP/2 是通过分帧并且给每个帧打上流的 ID 去避免依次响应的问题,对方接收到帧之后根据 ID 拼接出流,这样就可以做到乱序响应从而避免请求时的队首阻塞问题。但是 TCP 层面的队首阻塞是 HTTP/2 无法解决的(HTTP 只是应用层协议,TCP 是传输层协议),TCP 的阻塞问题是因为传输阶段可能会丢包,一旦丢包就会等待重新发包,阻塞后续传输,这个问题虽然有滑动窗口(Sliding Window)这个方案,但是只能增强抗干扰,并没有彻底解决。

chrome架构

打开一个chrome网页,如图所示会出现很多线程

image-20210712221958756

首先需要讲解一下 进程和线程

进程和线程

  • 一个进程就是一个程序的运行实例
  • 线程是不能单独存在的,它是由进程来启动和管理的
  • 线程是依附于进程的,而进程中使用多线程并行处理能提升运算效率

image-20210712222146731

总结来说,进程和线程之间的关系有以下 4 个特点。

  • 1. 进程中的任意一线程执行出错,都会导致整个进程的崩溃。
  • 2. 线程之间共享进程中的数据。
  • 3. 当一个进程关闭之后,操作系统会回收进程所占用的内存(包括泄漏的内存)。
  • 4. 进程之间的内容相互隔离。(通信依靠IPC进程间通信)

浏览器架构发展史

单进程浏览器

单进程浏览器是指浏览器的所有功能模块都是运行在同一个进程里

image-20210712222743496

缺点很明显

  • 不稳定

    一个插件的意外崩溃会引起整个浏览器的崩溃,除了插件之外,渲染引擎模块(复杂代码)也是不稳定的

  • 不流畅

    所有页面的渲染模块、JavaScript 执行环境以及插件都是运行在同一个线程中的,这就意味着同一时刻只能有一个模块可以执行

  • 不安全

    插件可以使用c++编写,可以轻易访问操作系统

多进程浏览器

多进程浏览器解决了单进程浏览器的几个问题

  • 不稳定:由于进程是相互隔离的,所以当一个页面或者插件崩溃时,影响到的仅仅是当前的页面进程或者插件进程,并不会影响到浏览器和其他页面
  • 不流畅:每个tab页均有一个渲染进程,JavaScript 也是运行在渲染进程中的,所以即使 JavaScript 阻塞了渲染进程,影响到的也只是当前的渲染页面
  • 不安全:安全沙箱,Chrome 把插件进程和渲染进程锁在沙箱里面

多进程浏览器也会有一些问题

  • 更高的资源占用:因为每个进程都会包含公共基础结构的副本(如 JavaScript 运行环

    境),这就意味着浏览器会消耗更多的内存资源。

  • 更复杂的体系架构:浏览器各模块之间耦合性高、扩展性差等问题,会导致现在的架构已

    经很难适应新的需求了。

多进程浏览器包括(浏览器架构持续优化中,仅供参考):
image-20210712223130924

  • 浏览器进程。主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。

  • 渲染进程。核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中。默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。

    • 主线程(也就是我们常说的js单线程
      • 渲染事件(如解析 DOM、计算布局、绘制)
      • 用户交互事件(如鼠标点击、滚动页面、放大缩小等)
      • JavaScript 脚本执行事件
      • 网络请求完成、文件读写完成事件
      • 垃圾回收
      • 等等
    • 合成线程(动画优化)
    • 光栅化线程池
    • IO 线程
  • GPU 进程

  • 网络进程

  • 插件进程

从输入URL到页面展示,这中间发生了什么

image-20210714180345180

从图中可以看出,整个过程需要各个进程之间的配合,首先介绍一下各进程的主要职责

  • 浏览器进程主要负责用户交互、子进程管理和文件储存等功能。

  • 网络进程是面向渲染进程和浏览器进程等提供网络下载功能。

  • 渲染进程的主要职责是把从网络下载的 HTML、JavaScript、CSS、图片等资源解析为可以显示和交互的页面。

渲染流程之前的流程

这个过程可以大致描述为如下。

  • 首先,浏览器进程接收到用户输入的 URL 请求,浏览器进程便将该 URL 转发给网络进程。然后,在网络进程中发起真正的 URL 请求。

  • 接着网络进程接收到了响应头数据,便解析响应头数据,并将数据转发给浏览器进程。

  • 浏览器进程接收到网络进程的响应头数据之后,发送“提交导航 (CommitNavigation)”消息到渲染进程;

  • 渲染进程接收到“提交导航”的消息之后,便开始准备接收 HTML 数据,接收数据的方式是直接和网络进程建立数据管道;

  • 最后渲染进程会向浏览器进程“确认提交”,这是告诉浏览器进程:“已经准备好接受和解析页面数据了”。

  • 浏览器进程接收到渲染进程“提交文档”的消息之后,便开始移除之前旧的文档,然后更新浏览器进程中的页面状态。

渲染流程

美团文章的图

img

按照渲染的时间顺序,流水线可分为如下几个子阶段:

构建 DOM 树

image-20210715143236150

构建 DOM 树的输入内容是一个非常简单的 HTML 文件,然后经由HTML 解析器解析,最终输出树状结构的 DOM

样式计算

样式计算的目的是为了计算出 DOM 节点中每个元素的具体样式,这个阶段大体可分为三步来完成:

  1. 把CSS转换为浏览器能够理解的结构

    和 HTML 文件一样,浏览器也是无法直接理解这些纯文本的 CSS 样式,所以当渲染引擎接

    收到 CSS 文本时,会执行一个转换操作,将 CSS 文本转换为浏览器可以理解的结构——

    styleSheets

  2. 转换样式表中的属性值,使其标准化

    image-20210715143850698

    需要将所有值转换为渲染引擎容易理解的、标准化的计算值

  3. 计算出 DOM 树中每个节点的具体样式

布局阶段

我们有 DOM 树和 DOM 树中元素的样式,但这还不足以显示页面,因为我们还不知道 DOM 元素的几何位置信息。那么接下来就需要计算出 DOM 树中可见元素的几何位置,我们把这个计算过程叫做布局

​ Chrome 在布局阶段需要完成两个任务:创建布局树和布局计算。

创建布局树

在显示之前,我们还要额外地构建一棵只包含可见元素布局树。

image-20210715165934972

布局计算

在执行布局操作的时候,会把布局运算的结果重新写回布局树中,所以布局树既是输入内容也是输出内容,这是布局阶段一个不合理的地方

所以在调试台中 经常看到layout之后 在再次update layer tree

image-20211223095237296

分层

渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)

image-20210715170447195

那么需要满足什么条件,渲染引擎才会为特定的节点创建新的层呢?

  • 拥有层叠上下文属性的元素会被提升为单独的一层

  • 需要剪裁(clip)的地方也会被创建为图层

    层叠上下文在css中也是一个很重要的概念,在blog中也有一些相关文章

图层绘制

分析出绘制列表

在完成图层树的构建之后,渲染引擎会对图层树中的每个图层进行绘制。渲染引擎会把一个图层的绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表,如下图所示:

image-20210715172527TCP协议的特点999

绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的合成线程来完成的。当图层的绘制列表准备好之后,主线程会把该绘制列表提交(commit)给合成线程

分块

通常一个页面可能很大,但是用户只能看到其中的一部分,我们把用户可以看到的这个部分叫做视口(viewport)。通过视口,用户只能看到页面的很小一部分,所以在这种情况下,要绘制出所有图层内容的话,就会产生太大的开销,而且也没有必要。

基于这个原因,合成线程会将图层划分为图块(tile),这些图块的大小通常是 256x256 或者 512x512

光栅化raster

合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图

而图块是栅格化执行的最小单位。渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的

image-20210715173725392

合成和显示

一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。

浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。

渲染流水线大总结

image-20210715173820255

结合上图,一个完整的渲染流程大致可总结为如下:

  • 渲染进程将 HTML 内容转换为能够读懂的DOM 树结构。
  • 渲染引擎将 CSS 样式表转化为浏览器可以理解的styleSheets,计算出 DOM 节点的样式。
  • 创建布局树,并计算元素的布局信息。
  • 计算结果重新写入布局树(我觉得这一步挺重要)
  • 对布局树进行分层,并生成分层树
  • 为每个图层生成绘制列表,并将其提交到合成线程。
  • 合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图。
  • 合成线程发送绘制图块命令DrawQuad给浏览器进程。
  • 浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上。

css和js对dom解析渲染的影响

直接说结论

  • css不会阻塞DOM的解析(document中已存在,只是没有渲染在屏幕上),但会阻塞其渲染

    样式树 和 dom树 共同决定渲染树,两者可以并行解析,但是dom树必须等样式树解析后才可以合成

  • css会阻塞后面JS的的执行

    因为js有修改cssom的能力

  • JS会阻塞DOM解析和渲染

    js有修改dom的能力

减少白屏时间

如果白屏时间过久,就会影响到用户体验。

为了缩短白屏时间,我们来挨个分析这个阶段的主要任务,包括了解析 HTML、下载 CSS、下载 JavaScript、生成 CSSOM、执行 JavaScript、生成布局树、绘制页面一系列操作。

通常情况下的瓶颈主要体现在下载 CSS 文件、下载 JavaScript 文件和执行 JavaScript

所以要想缩短白屏时长,可以有以下策略:

  • 通过内联 JavaScript、内联 CSS 来移除这两种类型的文件下载,这样获取到 HTML 文件之后就可以直接开始渲染流程了。
  • 但并不是所有的场合都适合内联,那么还可以尽量减少文件大小,比如通过 webpack 等工具移除一些不必要的注释,并压缩 JavaScript 文件。
  • 还可以将一些不需要在解析 HTML 阶段使用的 JavaScript 标记上 async 或者 defer。
  • 对于大的 CSS 文件,可以通过媒体查询属性,将其拆分为多个不同用途的 CSS 文件,这样只有在特定的场景下才会加载特定的 CSS 文件。
  • 骨架屏和loading css

回流、重绘、合成

需要补充

回流重绘

浏览器安全

浏览器安全可以分为三大块——Web 页面安全、浏览器网络安全浏览器系统安全

web页面安全

在没有安全保障的 Web 世界中,我们是没有隐私的,因此需要安全策略来保障我们的隐私和数据的安全。

同源策略

在页面中最基础、最核心的安全策略:**同源策略(Same-origin policy),如果两个 URL 的协议、域名和端口都相同,我们就称这两个 URL 同源,两个不同的源之间若想要相互访问资源或者操作 DOM**,那么会有一套基础的安全策略的制约,我们把这称为同源策略。

下表给出了与 URL http://store.company.com/dir/page.html 的源进行对比的示例:

image-20210805172753684

具体来讲,同源策略主要表现在 DOM、Web 数据和网络这三个层面:

  • DOM 层面。同源策略限制了来自不同源的 JavaScript 脚本对当前 DOM 对象读和写的操作。

  • 数据层面。同源策略限制了不同源的站点读取当前站点的 Cookie、IndexDB、LocalStorage 等数据

  • 网络层面。同源策略限制了通过 XMLHttpRequest 等方式将站点的数据发送给不同源的站点

安全和便利性的权衡

安全性和便利性是相互对立的,让不同的源之间绝对隔离,无疑是最安全的措施,但这也会使得 Web 项目难以开发和使用,因此浏览器就要在这之间做出权衡,出让一些安全性来满足灵活性:

  • 页面中可以嵌入第三方资源
  • 跨域资源共享和跨文档消息机制
    • 跨域资源共享(CORS)
    • 跨文档消息机制 postMessage

xss csrf

在前端安全中有详细介绍

前端安全

call函数

1
2
3
4
5
6
7
8
9
10
11
12
Function.prototype.selfCall = function(context) {
const ctx = context || window;
// 去除第一个参数
const arg = [...arguments].slice(1);
// const arg = Array.slice.call(arguments, 1);
// 将函数赋值给ctx.fnc
ctx.fnc = this;
// 执行函数
const res = ctx.fnc(...arg);
Reflect.deleteProperty(ctx, "fnc")
return res
}

上面的方法借用了很多ES6的方法,让我们看一下比较原始的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Function.prototype.call2 = function (context) {
var context = context || window;
context.fn = this;

var args = [];
for(var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}

var result = eval('context.fn(' + args +')');

delete context.fn
return result;
}

apply函数

1
2
3
4
5
6
7
8
9
Function.prototype.selfApply = function (context) {
const ctx = context || window;
ctx.fnc = this;
const arg = [...arguments].slice(1)[0];
// const arg = Array.slice.call(arguments, 1)[0];
const res = ctx.fnc(...arg)
Reflect.deleteProperty(ctx, "fnc")
return res
}

不借用ES6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Function.prototype.apply = function (context, arr) {
var context = Object(context) || window;
context.fn = this;

var result;
if (!arr) {
result = context.fn();
} else {
var args = [];
for (var i = 0, len = arr.length; i < len; i++) {
args.push('arr[' + i + ']');
}
result = eval('context.fn(' + args + ')')
}

delete context.fn
return result;
}

关于 Object(context) 原文是这么说的:

非严格模式下,指定为 null 或 undefined 时会自动指向全局对象,郑航写的是严格模式下的,我写的是非严格模式下的,实际上现在的模拟代码有一点没有覆盖,就是当值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的自动包装对象。

bind函数

  • 首先书写了一个简单的bind函数,并不包括 new 操作
1
2
3
4
5
6
7
Function.prototype.myBind = function (ctx) {
const args = [...arguments].slice(1)
const self = this
return function () {
return self.apply(ctx, [...args, ...arguments])
}
}

构造函数效果的模拟实现 // TODO

new 操作符

首先分析一下 new 操作符

1
2
3
4
5
6
7
8
function Student(name, age) {
this.name = name
this.age = age
}
Student.prototype.say = function () {
console.log(this.name);
}
var jojo = new Student("jsd", 23);

从结果分析

  • 返回了一个对象,其实例属性是通过构造函数(Student)生成的
  • 对象的__proto__指向Student.prototype
  • 创建一个空对象obj({})
  • 将obj的[[prototype]]属性指向构造函数constrc的原型(即obj.[[prototype]] = constrc.prototype)。
  • 将构造函数constrc内部的this绑定到新建的对象obj,执行constrc(也就是跟调用普通函数一样,只是此时函数的this为新创建的对象obj而已,就好像执行obj.constrc()一样);
  • 若构造函数没有返回非原始值(即不是引用类型的值),则返回该新建的对象obj(默认会添加return this)。否则,返回引用类型的值。

官方解释

  • 创建一个空的简单JavaScript对象(即{});
  • 链接该对象(设置该对象的constructor)到另一个对象 ;
  • 将步骤1新创建的对象作为this的上下文 ;
  • 如果该函数没有返回对象,则返回this。
    实现方式如下:
1
2
3
4
5
6
function createClass(_Class, ...rest) {
const obj = {}
const res = _Class.apply(obj, rest)
obj.__proto__ = _Class.prototype
return typeof res === "object" ? res : obj
}

利用Object.create稍微简化一下

1
2
3
4
5
6
7
function newFactorSimple(ctor) {
const arg = [...arguments].slice(1);
// 生成一个__proto__指向ctor.prototype的对象
var obj = Object.create(ctor.prototype);
ctor.call(obj, ...arg);
return obj;
}

其实构造函数内含有return语句时,结果会出现差异

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 例子4
function Student(name){
this.name = name;
// Null(空) null
// Undefined(未定义) undefined
// Number(数字) 1
// String(字符串)'1'
// Boolean(布尔) true
// Symbol(符号)(第六版新增) symbol

// Object(对象) {}
// Function(函数) function(){}
// Array(数组) []
// Date(日期) new Date()
// RegExp(正则表达式)/a/
// Error (错误) new Error()
// return /a/;
}
var student = new Student('若川');
console.log(student); {name: '若川'}

前面六种基本类型都会正常返回{name: ‘若川’},后面的Object(包含Functoin, Array, Date, RegExg, Error)都会直接返回这些值
下面是考虑到各种情况后的new实现

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
/**
* 模拟实现 new 操作符
* @param {Function} ctor [构造函数]
* @return {Object|Function|Regex|Date|Error} [返回结果]
*/
function newOperator(ctor){
if(typeof ctor !== 'function'){
throw 'newOperator function the first param must be a function';
}
// ES6 new.target 是指向构造函数
newOperator.target = ctor;
var newObj = Object.create(ctor.prototype);
// ES5 arguments转成数组 当然也可以用ES6 [...arguments], Aarry.from(arguments);
// 除去ctor构造函数的其余参数
var argsArr = [].slice.call(arguments, 1);
// 3.生成的新对象会绑定到函数调用的`this`。
// 获取到ctor函数返回结果
var ctorReturnResult = ctor.apply(newObj, argsArr);
// 小结4 中这些类型中合并起来只有Object和Function两种类型 typeof null 也是'object'所以要不等于null,排除null
var isObject = typeof ctorReturnResult === 'object' && ctorReturnResult !== null;
var isFunction = typeof ctorReturnResult === 'function';
if(isObject || isFunction){
return ctorReturnResult;
}
// 5.如果函数没有返回对象类型`Object`(包含`Functoin`, `Array`, `Date`, `RegExg`, `Error`),那么`new`表达式中的函数调用会自动返回这个新的对象。
return newObj;
}

create函数

很多框架源码作者使用它来初始化一个新的对象,难道是最佳实践?
原因有二

  • 通过Object.create(null)创建出来的对象,没有任何属性,显示No properties。我们可以将其当成一个干净的 map 来使用,自主定义 toString,hasOwnProperty等方法,并且不必担心将原型链上的同名方法被覆盖。
  • {…}创建的对象,使用for in遍历对象的时候,会遍历原型链上的属性,带来性能上的损耗。使用Object.create(null)则不必再对其进行遍历了。
    两种方式的比较

手写Object.create

1
2
3
4
5
6
7
8
9
10
function ObjCreate(proto, properties) {
// 判断类型,第一个参数传入的必须是 object, function
if (typeof proto !== "object" && typeof proto !== "function") {
throw new TypeError("Object prototype may only be an Object: " + proto);
}
// 简单的实现的过程,忽略了properties
var func = function() {};
func.prototype = proto; // 将fn的原型指向传入的proto
return new func(); // 返回创建的新对象,这里思考下,new func() 又做了什么事情呢?且往下看!
};

new func()的作用是创建一个新的对象,其中func是一个构造函数,在这个过程中,主要包含了如下步骤:

  • 创建空对象obj;
  • 将obj的原型设置为构造函数的原型,obj.proto= func.prototype;
  • 以obj为上下文执行构造函数,func.call(obj);
  • 返回obj对象。

手写promise

所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

  • 对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。
  • 一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

摘自阮一峰ES6深入理解

引入三个问题

这三个问题可以帮助理解

1、Promise 中为什么要引入微任务?

由于promise采用.then延时绑定回调机制,而new Promise时又需要直接执行promise中的方法,即发生了先执行方法后添加回调的过程,此时需等待then方法绑定两个回调后才能继续执行方法回调,便可将回调添加到当前js调用栈中执行结束后的任务队列中,由于宏任务较多容易堵塞,则采用了微任务

2、Promise 中是如何实现回调函数返回值穿透的?

首先Promise的执行结果保存在promise的data变量中,然后是.then方法返回值为使用resolved或rejected回调方法新建的一个promise对象,即例如成功则返回new Promise(resolved),将前一个promise的data值赋给新建的promise

3、Promise 出错后,是怎么通过“冒泡”传递给最后那个捕获

promise内部有resolved_和rejected_变量保存成功和失败的回调,进入.then(resolved,rejected)时会判断rejected参数是否为函数,若是函数,错误时使用rejected处理错误;若不是,则错误时直接throw错误,一直传递到最后的捕获,若最后没有被捕获,则会报错。可通过监听unhandledrejection事件捕获未处理的promise错误

简单的promise

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
function MyPromise(executor) {
var _this = this;
this._status = "pending";
this._successCallBack = null;
this._errorCallBack = null;
var resolve = function (res) {
if (_this._status === "pending") {
_this._status = "fulfilled";
// 运行then函数传递过来的成功函数
// 并将结果作为参数回传
_this._successCallBack(res);
}
}
var reject = function (res) {
if (_this._status === "pending") {
_this._status = "rejected";
// 运行then函数传递过来的错误函数
// 并将结果作为参数回传
_this._errorCallBack(res);
}
}
// 把内部函数resolve, reject作为参数,把传进来的函数执行一遍
setTimeout(() => { // 使用setTimeout 是让resolve晚于then方法赋值
executor(resolve, reject)
}, 0)
}
MyPromise.prototype.then = function (sucess, error) {
this._successCallBack = sucess;
this._errorCallBack = error;
}

进阶的promise

实现then的链式调用

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
class MyPromise {
PromiseResult = null; // 终值
PromiseState = "pending"; // 状态
toDoFulFilled = null;
toDoRejected = null;
// 构造方法
constructor(executor) {
const resolve = (value) => {
setTimeout(() => {
console.log("resolve");
if (this.PromiseState !== "pending") return;
this.PromiseState = "fulfilled";
this.PromiseResult = value;

if (this.toDoFulFilled) {
this.toDoFulFilled(this.PromiseResult);
}
}, 0);
};
const reject = (reason) => {
setTimeout(() => {
if (this.PromiseState !== "pending") return;
this.PromiseState = "rejected";
this.PromiseResult = reason;

if (this.toDoRejected) {
this.toDoRejected(this.PromiseResult);
}
}, 0);
};
// 执行传进来的函数
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}

then(onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
const resolvePromise = (cb) => {
try {
// 这个prevRes 就是本次then的返回值
// cb 就是 onFulfilled onRejected
const prevRes = cb(this.PromiseResult);
// TODO
if (prevRes instanceof MyPromise) {
prevRes.then(resolve, reject);
} else {
resolve(prevRes);
}
} catch (err) {
reject(err);
throw new Error(err);
}
};

this.toDoFulFilled = resolvePromise.bind(this, onFulfilled);
this.toDoRejected = resolvePromise.bind(this, onRejected);
});
}
}

/* const test1 = new MyPromise((resolve, reject) => {
resolve("成功");
}).then(
(res) => {
console.log(res);
},
(error) => {
console.log(error);
}
); */

const test2 = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve("定时器");
}, 2000);
})
.then(
(res) => {
console.log(res);
return res + "链式调用";
},
(error) => {
console.log(error);
}
)
.then(
(res) => {
console.log(res);
},
(error) => {
console.log(error);
}
);

限制promise并发

主要的点

  • 在then方法里启动下一次任务
  • while为了塞满队列
  • task是一个待启动的promise
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
class MaxDuty{
used = 0
tasks = []
constructor(max) {
this.max = max
}
addTask(task) {
this.tasks.push(task)
}
walk() {
if (this.tasks.length && this.used < this.max) {
this.used++
const task = this.tasks.shift()
task().then(res => {
console.log(res);
this.used--
this.walk()
})
}
}
start() {
// 需要塞满
while (this.used < this.max) {
this.walk()
}
}
}
// 返回一个待启动的promise
function createTask(time, order) {
return () => {
return new Promise(resolve => {
setTimeout(() => {
resolve(order)
}, time);
})
}
}

const maxDuty = new MaxDuty(2)

maxDuty.addTask(createTask(1000, 1))
maxDuty.addTask(createTask(500, 2))
maxDuty.addTask(createTask(300, 3))
maxDuty.addTask(createTask(400, 4))

maxDuty.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
var curry = function (fn) {
// 取到fn之后的参数
var args = [].slice.call(arguments, 1);
// 返回一个待使用的新函数
return function () {
// 将新函数的参数 和 之前生成珂里化函数的参数合并
var newArgs = args.concat([].slice.call(arguments));
// 不改变this,使用所有参数进行调用
return fn.apply(this, newArgs);
};
};

function add(a, b) {
return a + b;
}

var addCurry = curry(add, 1, 2);
addCurry() // 3
//或者
var addCurry = curry(add, 1);
addCurry(2) // 3
//或者
var addCurry = curry(add);
addCurry(1, 2) // 3

固定参数

第二版 稍微复杂一些,先看实现的效果会比较好理解一点

1
2
3
4
5
6
7
8
var fn = curry(function (a, b, c) {
console.log([a, b, c]);
});

fn("a", "b", "c"); // ["a", "b", "c"]
fn("a", "b")("c"); // ["a", "b", "c"]
fn("a")("b")("c"); // ["a", "b", "c"]
fn("a")("b", "c"); // ["a", "b", "c"]

首先需要明确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
function curry(fn) {
// 形参的个数
const length = fn.length
let args = [].slice.call(arguments, 1)
return function () {
const argChild = [...args,...arguments]
if (argChild.length === length) {
return fn.apply(this, argChild)
} else {
// console.log(argChild);
return curry.call(this, fn, ...argChild)
// return curry.apply(this, fn, argChild)
}
}
}


var fn1 = curry(function(a, b, c) {
console.log([a, b, c]);
});

fn1("a", "b", "c") // ["a", "b", "c"]
fn1("a", "b")("c") // ["a", "b", "c"]
fn1("a")("b")("c") // ["a", "b", "c"]
fn1("a")("b", "c") // ["a", "b", "c"]

var fn2 = curry(function(a, b, c) {
console.log([a, b, c]);
}, "a");

fn2("b", "c") // ["a", "b", "c"]
fn2("b")("c") // ["a", "b", "c"]

不固定参数

这种方法太刻意了,建议闲下来的时候看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// fn(a,b,c) = fn(a)(b)(c)
function curry(fn) {
return function core(...args) {
let params = []
params = params.concat(args)
let inner = function(...args2) {
params = params.concat(args2)
return core.apply(this, params)
}
inner.toString = ()=>{
return fn.apply(null, params)
}
return inner
}
}

function add(...args) {
return args.reduce((prev,curr)=>prev + curr, 0)
}
let curriedAdd = curry(add)
alert(curriedAdd(1,2,3))

alert(curriedAdd(1, 2)(1, 2, 3))
curriedAdd(1)(2)(3)(4)(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
28
29
30
31
32
33
34
35
36
37
38
39
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
html, body {
height: 100%;
width: 100%;
}
</style>
</head>
<body>
<script>
var count = 1
function handle(event) {
console.log(this);
}
document.onmousemove = debounce(handle, 400)

function debounce(fn, wait) {
var timer = null
return function () {
clearTimeout(timer)
timer = setTimeout(
// 这么写 实际上没有效果
fn.apply(this, arguments),
// 下面的写法均有效果
fn.bind(this, arguments),
fn,
wait
)
}
}
</script>
</body>
</html>

需要注意的是,setTimeout第一个参数需要是一个函数,而fn.apply(this, arguments)不能算一个函数

下面写一个比较标准的debounce,可以正确响应参数和this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 function debounce(fn, wait) {
var timer = null
return function () {
clearTimeout(timer)
timer = setTimeout(
fn.bind(this, ...arguments),
wait
)
}
}
// 不使用es6
function debounce(func, wait) {
var timeout;

return function () {
var context = this;
var args = arguments;

clearTimeout(timeout)
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);
}
}

立刻执行

这个时候,代码已经很是完善了,但是为了让这个函数更加完善,我们接下来思考一个新的需求。

这个需求就是:

我不希望非要等到事件停止触发后才执行,我希望立刻执行函数,然后等到停止触发 n 秒后,才可以重新触发执行

其实这个实现也算简单,主要是理解需求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function immedia(fn, wait) {
var flag = true
var timer = null
return function () {
clearTimeout(timer)
// 保证不在触发后wait时间后,可以再次被触发
timer = setTimeout(
function () {
flag = true
},
wait
)
if(flag){
// 触发后 开启保护
flag = false
fn.apply(this, arguments)
}
}
}

写在一个函数里,也很简单

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
function debounce(fn, wait, immediate) {
var timer = null
if(immediate){
var flag = true
return function () {
clearTimeout(timer)
timer = setTimeout(
function () {
flag = true
},
wait
)
if(flag){
flag = false
fn.apply(this, arguments)
}
}
}else{
return function () {
clearTimeout(timer)
timer = setTimeout(
fn.bind(this, ...arguments),
wait
)
}
}
}

节流

节流的原理很简单:

如果你持续触发事件,每隔一段时间,只执行一次事件。

关于节流的实现,有两种主流的实现方式,一种是使用时间戳,一种是设置定时器。

时间戳

1
2
3
4
5
6
7
8
9
10
function throttle(fn, wait) {
const previous = 0;
return function () {
const now = new Date().getTime()
if (now - previous > wait) {
previous = now
}
fn.apply(this, arguments)
}
}

定时器

1
2
3
4
5
6
7
8
9
10
11
12
function throttle(fn, wait) {
let flag = true
return function () {
if (flag) {
flag = false
fn.apply(this, arguments)
setTimeouto=(function () {
flag = true
},wait)
}
}
}

deepclone

先看一个足够用的版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function deepClone(obj) {
if (obj === 'null') return null
if (typeof obj !== 'object') return obj
if (obj instanceof RegExp) return new RegExp(obj)
if (obj instanceof Date) return new Date(obj)
let result = new obj.constructor
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
result[key] = deepClone(obj[key])
}
}
// 上面的for循环可以改成
// Reflect.ownKeys(key => result[key] = deepClone(obj[key])
return result
}

首先实现一个简单clone,包括对象 数组和基本类型的clone

1
2
3
4
5
6
7
8
9
10
11
function deepClone(target) {
if (typeof target !== "object") {
return target
} else {
const copy = Array.isArray(target) ? [] : {}
for (const key in target) {
copy[key] = deepClone(target[key])
}
return copy
}
}

如果出现循环引用,就会栈溢出,如何解决这个问题呢,需要借助map数据结构

解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。

这个存储空间,需要可以存储key-value形式的数据,且key可以是一个引用类型,我们可以选择Map这种数据结构:

  • 检查map中有无克隆过的对象
  • 有 - 直接返回
  • 没有 - 将当前对象作为key,克隆对象作为value进行存储
  • 继续克隆
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
function deepClone(target) {
const map = new Map();
return (function _clone(target) {
if (typeof target !== "object") {
return target
} else {
if (map.has(target)) {
return target
}
map.set(target, null)
const copy = Array.isArray(target) ? [] : {}
for (const key in target) {
copy[key] = _clone(target[key])
}
return copy
}
})(target)
}

const target = {
field1: 1,
field2: undefined,
field3: {
child: 'child'
},
field4: [2, 4, 8]
};
target.target = target
console.log(deepClone(target));

控制台结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
{
field1: 1,
field2: undefined,
field3: { child: 'child' },
field4: [ 2, 4, 8 ],
target: <ref *1> {
field1: 1,
field2: undefined,
field3: { child: 'child' },
field4: [ 2, 4, 8 ],
target: [Circular *1]
}
}

circular表示循环引用的意思,把Map换成weakMap,Map有强引用,不利用垃圾收集

常规遍历

优秀题解

递归

递归的代码很简单

前序 中左右

这种写法其实不好,容易让人误解,return res 会被执行好多次

1
2
3
4
5
6
7
8
9
var preorderTraversal = function(root, res = []) {
if(!root){
return res
}
res.push(root.val)
preorderTraversal(root.left, res)
preorderTraversal(root.right, res)
return res
};

下面这种容易理解

1
2
3
4
5
6
7
8
9
10
11
12
13
var preorderTraversal = function(root) {
var res = []
if(!root){
return res
}
res.push(root.val)
preorderTraversal(root.left, res)
preorderTraversal(root.right, res)
return res
};
function handle(root) {

}

中序 左中右

1
2
3
4
5
6
7
8
9
var preorderTraversal = function(root, res = []) {
if(!root){
return res
}
preorderTraversal(root.left, res)
res.push(root.val)
preorderTraversal(root.right, res)
return res
};

后序 左右中

1
2
3
4
5
6
7
8
9
var preorderTraversal = function(root, res = []) {
if(!root){
return res
}
preorderTraversal(root.left, res)
preorderTraversal(root.right, res)
res.push(root.val)
return res
};

迭代

迭代的很不容易理解

前序 中左右

普通写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var preorderTraversal = function(root) {
if(!root){
return []
}
const stack = [];
const res = []
stack.push(root)
while(stack.length){
const popNode = stack.pop()
res.push(popNode.val)
if(popNode.right){
stack.push(popNode.right)
}
if(popNode.left){
stack.push(popNode.left)
}
}
return res
};

统一写法 前序和中序 结果采集的地方不同,一个是出栈 一个是入栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var preorderTraversal = function(root) {
const res = []
let cur = root
const stack = []
while (stack.length || cur) {
while (cur) {
res.push(cur.val)
stack.push(cur)
cur = cur.left
}
const popNode = stack.pop()
if(popNode.right){
cur = popNode.right
}
}
return res
};

中序 左中右

统一写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var inorderTraversal = (root) => {
const res = []
let cur = root
const stack = []
while (stack.length || cur) {
// 找到最左侧的节点,并把沿路的节点全部推入栈中
while(cur){
stack.push(cur)
cur = cur.left
}
// 取出栈顶元素
const popNode = stack.pop()
// 记录出栈元素
res.push(popNode.val)
// 存在右节点 即为父节点 而且左节点已经处理过了
if(popNode.right){
cur = popNode.right
}
}
return res
};

后序 左右中

可以把前序的普通写法改一下,变成中右左,然后倒着输出

普通写法倒着输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const postorderTraversal = function(root) {
const res = []
if(!root){
return res
}
const stack = []
stack.push(root)
while(stack.length){
const popRoot = stack.pop()
res.push(popRoot.val)
// 左右换一下顺序
if(popRoot.left){
stack.push(popRoot.left)
}
if(popRoot.right){
stack.push(popRoot.right)
}
}
// 倒着输出
return res.reverse()
};

统一写法倒着输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const postorderTraversal = function(root) {
const res = []
let cur = root
const stack = []
while (stack.length || cur) {
while (cur) {
res.push(cur.val)
stack.push(cur)
cur = cur.right
}
const popNode = stack.pop()
if(popNode.left){
cur = popNode.left
}
}
// 把上边的push 换成unshift 这边就不用倒着了 但是时间复杂度更高了,不好
return res.reverse()
};

利用标识符进行迭代,普通写法

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
const postorderTraversal = function(root) {
if (!root) {
return []
}
const stack = []
const res = []
stack.push({
node: root,
flag: 0
})
while (stack.length) {
const {node, flag} = stack.pop()
if (!node) {
continue
}
if (flag === 1) {
res.push(node.val)
} else {
stack.push({
node: node,
flag: 1
})
stack.push({
node: node.right,
flag: 0
})
stack.push({
node: node.left,
flag: 0
})
}
}
return res
};

优秀统一迭代法 直接记这个就好

前序遍历统一迭代法

// 前序遍历:中左右
// 压栈顺序:右左中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var preorderTraversal = function(root, res = []) {
const stack = [];
if (root) stack.push(root);
while(stack.length) {
const node = stack.pop();
if(!node) {
res.push(stack.pop().val);
continue;
}
if (node.right) stack.push(node.right); // 右
if (node.left) stack.push(node.left); // 左
stack.push(node); // 中
stack.push(null);
};
return res;
};

中序遍历统一迭代法

// 中序遍历:左中右
// 压栈顺序:右中左

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var inorderTraversal = function(root, res = []) {
const stack = [];
if (root) stack.push(root);
while(stack.length) {
const node = stack.pop();
if(!node) {
res.push(stack.pop().val);
continue;
}
if (node.right) stack.push(node.right); // 右
stack.push(node); // 中
stack.push(null);
if (node.left) stack.push(node.left); // 左
};
return res;
};

后序遍历统一迭代法

// 后续遍历:左右中
// 压栈顺序:中右左

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var postorderTraversal = function(root, res = []) {
const stack = [];
if (root) stack.push(root);
while(stack.length) {
const node = stack.pop();
if(!node) {
res.push(stack.pop().val);
continue;
}
stack.push(node); // 中
stack.push(null);
if (node.right) stack.push(node.right); // 右
if (node.left) stack.push(node.left); // 左
};
return res;
};

作者:carlsun-2
链接:https://leetcode-cn.com/problems/binary-tree-preorder-traversal/solution/dai-ma-sui-xiang-lu-chi-tou-qian-zhong-hou-xu-de-d/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

层序遍历

leetcode一道基础的题

102. 二叉树的层序遍历

难度中等1081收藏分享切换为英文接收动态反馈

给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)。

示例:
二叉树:[3,9,20,null,null,15,7],

1
2
3
4
5
  3
/ \
9 20
/ \
15 7

返回其层序遍历结果:

1
2
3
4
5
[
[3],
[9,20],
[15,7]
]

递归解法

利用层级和数组解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var levelOrder = function(root) {
const levelArr = []
handle(root,0,levelArr)
return levelArr
};
function handle(root, level, levelArr){
if(!root){
return
}
Array.isArray(levelArr[level]) ? levelArr[level].push(root.val) : levelArr[level] = [root.val]
level++
handle(root.left, level, levelArr)
handle(root.right, level, levelArr)
}

迭代解法

方法看一下 很容易就能理解

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
var levelOrder = function(root) {
const res = []
if(!root){
return res
}
const queue = [root]
let index = 0
while (queue.length) { // 关键点
// 每一层的新开始
// 记录当前层的节点个数,防止shift多了
const l = queue.length // 关键点
res.push([])
// 把该层推入结果,顺便把该层的下一层推入q中
for(var i=0;i<l;i++){ // 关键点
const head = queue.shift()
res[index].push(head.val)
if(head.left){
queue.push(head.left)
}
if(head.right){
queue.push(head.right)
}
}
index++
}
return res
};

加深对二叉树递归的理解

230. 二叉搜索树中第K小的元素 labuladong 题解 思路

给定一个二叉搜索树的根节点 root ,和一个整数 k ,请你设计一个算法查找其中第 k 个最小元素(从 1 开始计数)。

示例 1:

img

1
2
输入:root = [3,1,4,null,2], k = 1
输出:1

示例 2:

img

1
2
输入:root = [5,3,6,2,4,null,null,1], k = 3
输出:3

对递归理解不深的解法,没错 我写的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var kthSmallest = function(root, k) {
let res
function handle(root, count, k){
if(!root){
return
}
handle(root.left, count, k)
if(count === k){
res = root.val
return
}
count++
handle(root.right, count, k)
}
handle(root, 1, k)
return res
};

实际上的正确解法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var kthSmallest = function(root, k) {
let count = 1
function handle(root, k){
if(!root){
return
}
handle(root.left, k)
if(count === k){
res = root.val
// 如果没有这个count++ 会一直指向下一个节点
count++
return
}
count++
handle(root.right, k)
}
handle(root, k)
return res
};

搜索二叉树插入数据

重点理解两种模式的不同

我的解法 简单直接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var insertIntoBST = function(root, val) {
if(!root){
return new TreeNode(val)
}
if(val > root.val){
if(!root.right){
root.right = new TreeNode(val)
}else{
insertIntoBST(root.right, val)
}
}else{
if(!root.left){
root.left = new TreeNode(val)
}else{
insertIntoBST(root.left, val)
}
}
return root
};

另一种解法 不太容易理解

1
2
3
4
5
6
7
8
9
10
11
12
13
var insertIntoBST = function(root, val) {
if(!root){
return new TreeNode(val)
}
if(val > root.val){
root.right = insertIntoBST(root.right, val)
}else{
root.left = insertIntoBST(root.left, val)
}
// 对return之外情况的兜底 找不到原样返回
// root.right = root.right
return root
};

搜索二叉树的删除操作

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
var deleteNode = function(root, key) {
if(!root){
return root
}
if(root.val === key){
if(!root.left){
// 如果root.right 也不存在 就是null
return root.right
}
if(!root.right){
return root.left
}
const minVal = getRightMin(root.right)
root.val = minVal.val
root.right = deleteNode(root.right, minVal.val)
}else if(key < root.val){
root.left = deleteNode(root.left, key)
}else{
root.right = deleteNode(root.right, key)
}
return root
};
function getRightMin(root){
while(root.left){
root = root.left
}
return root
}

95. 不同的二叉搜索树 II

给你一个整数 n ,请你生成并返回所有由 n 个节点组成且节点值从 1 到 n 互不相同的不同 二叉搜索树 。可以按 任意顺序 返回答案。

img

示例 1:

1
2
3
输入:n = 3
输出:[[1,null,2,null,3],[1,null,3,2],[2,1,3],[3,1,null,null,2],[3,2,null,1]]
示例 2:
1
2
输入:n = 1
输出:[[1]]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var generateTrees = function(n) {
if(n===0){
return []
}
return build(1, n)
};
function build(l, r){
const res = []
if(l>r){
res.push(null)
return res
}
for(var i=l;i<=r;i++){
const leftTree = build(l, i-1)
const rightTree = build(i+1, r)
for(var left of leftTree){
for(var right of rightTree){
const root = new TreeNode(i, left, right)
res.push(root)
}
}
}
return res
}

146.LRU缓存算法

运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制 。

1
2
3
4
5
6
7
8
9
10
11
实现 LRUCache 类:

LRUCache(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存
int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
void put(int key, int value) 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字-值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。

进阶:你是否可以在 O(1) 时间复杂度内完成这两种操作?

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/lru-cache
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

利用链表保持时序,利用hash存储节点

调试了好久,终于搞定了,需要仔细

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
class ListNode {
constructor(key, val, pre=null, next=null){
this.key = key;
this.val = val;
this.pre = pre;
this.next = next;
}
}

var LRUCache = function (capacity) {
// 最大长度
this.capacity = capacity
this.hash = new Map()
// 计数器
this.count = 0
// 虚拟头部
this.dummyHead = new ListNode(-1, -1)
// 虚拟尾部
this.dummyTail = new ListNode(-1, -1)
this.dummyHead.next = this.dummyTail
this.dummyTail.pre = this.dummyHead
// 头部
this.listHead = this.dummyHead
// 尾部
this.listTail = this.dummyTail
};

LRUCache.prototype.get = function(key) {
if (this.hash.has(key)) {
const existNode = this.hash.get(key)
// 从原位置删除
existNode.next.pre = existNode.pre
existNode.pre.next = existNode.next
// 把节点插入虚拟尾部之前
this.insertBeforeTail(existNode)
return this.hash.get(key).val
}else{
return -1
}
};

LRUCache.prototype.put = function(key, value) {
if (this.hash.has(key)) { // 已存在
const existNode = this.hash.get(key)
// 从原位置删除
existNode.next.pre = existNode.pre
existNode.pre.next = existNode.next
// 题目有个变更要求
existNode.val = value
// 把节点插入虚拟尾部之前
this.insertBeforeTail(existNode)
} else { // 未存在
const newNode = new ListNode(key, value)
if (this.count < this.capacity) { // 容量没满
// 把新节点插入虚拟尾部之前
this.insertBeforeTail(newNode)
// 用key记录节点
this.hash.set(key, newNode)
// 计数
this.count++
} else { // 容量满了
// 删除节点记录
this.hash.delete(this.listHead.next.key)
// 把虚拟头部后边节点删除
this.listHead.next = this.listHead.next.next
this.listHead.next.pre = this.listHead
// 把新节点插入虚拟尾部之前
this.insertBeforeTail(newNode)
// 用key记录节点
this.hash.set(key, newNode)
}
}
};
LRUCache.prototype.insertBeforeTail = function (newNode) {
// 把节点插入虚拟尾部之前
this.listTail.pre.next = newNode
newNode.pre = this.listTail.pre
newNode.next = this.listTail
this.listTail.pre = newNode
}

利用map数据结构

利用map特别简单

map数据结构兼顾有序性和hash

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var LRUCache = function(capacity) {
this.capacity = capacity;
this.map = new Map();
};

LRUCache.prototype.get = function(key) {
if(this.map.has(key)){
let temp=this.map.get(key)
this.map.delete(key);
this.map.set(key, temp);
return temp
}else{
return -1
}
};

LRUCache.prototype.put = function(key, value) {
if(this.map.has(key)){
this.map.delete(key);
}
this.map.set(key,value);
if(this.map.size > this.capacity){

this.map.delete(this.map.keys().next().value);
}
};

380.O(1) 时间插入、删除和获取随机元素

实现RandomizedSet 类:

RandomizedSet() 初始化 RandomizedSet 对象
bool insert(int val) 当元素 val 不存在时,向集合中插入该项,并返回 true ;否则,返回 false 。
bool remove(int val) 当元素 val 存在时,从集合中移除该项,并返回 true ;否则,返回 false 。
int getRandom() 随机返回现有集合中的一项(测试用例保证调用此方法时集合中至少存在一个元素)。每个元素应该有 相同的概率 被返回。
你必须实现类的所有函数,并满足每个函数的 平均 时间复杂度为 O(1) 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
示例:

输入
["RandomizedSet", "insert", "remove", "insert", "getRandom", "remove", "insert", "getRandom"]
[[], [1], [2], [2], [], [1], [2], []]
输出
[null, true, false, true, 2, true, false, 2]

解释
RandomizedSet randomizedSet = new RandomizedSet();
randomizedSet.insert(1); // 向集合中插入 1 。返回 true 表示 1 被成功地插入。
randomizedSet.remove(2); // 返回 false ,表示集合中不存在 2 。
randomizedSet.insert(2); // 向集合中插入 2 。返回 true 。集合现在包含 [1,2] 。
randomizedSet.getRandom(); // getRandom 应随机返回 1 或 2 。
randomizedSet.remove(1); // 从集合中移除 1 ,返回 true 。集合现在包含 [2] 。
randomizedSet.insert(2); // 2 已在集合中,所以返回 false 。
randomizedSet.getRandom(); // 由于 2 是集合中唯一的数字,getRandom 总是返回 2 。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/insert-delete-getrandom-o1
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

利用map进行设计,利用水塘抽样算法实现等概率

但是随机获取 时间复杂度不是O1,而是O(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
var RandomizedSet = function() {
this.hash = new Map()
};

RandomizedSet.prototype.insert = function(val) {
if (this.hash.has(val)) {
return false
} else {
this.hash.set(val, true)
return true
}
};

RandomizedSet.prototype.remove = function(val) {
if (this.hash.has(val)) {
this.hash.delete(val)
return true
} else {
return false
}
};

RandomizedSet.prototype.getRandom = function() {
let count = 1;
let iterator = this.hash.keys();
let result = null;
while (true) {
const { value, done } = iterator.next()
if (done) {
break;
}
if (Math.floor(Math.random() * count) === 0) {
result = value
}
count++
}
return result
};

利用hash实现O1存储删除,数组实现O1等概率访问

难点还是在于理解和设计,熟悉数据结构的基础方法时间复杂度

删除的前提是访问

数组某个元素和尾部交换后删除,O1

数组push 01

代码重点在于交换时,hash对应的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
var RandomizedSet = function() {
this.hash = Object.create(null, {})
this.array = []
};

RandomizedSet.prototype.insert = function(val) {
if (typeof this.hash[val] !== "undefined") {
return false
} else {
this.array.push(val)
// 存储位置
this.hash[val] = this.array.length - 1
return true
}
};

RandomizedSet.prototype.remove = function(val) {
if (typeof this.hash[val] !== "undefined") {
// 待删除元素的索引
const index = this.hash[val]
// 对末尾元素进行操作
const tailValue = this.array[this.array.length - 1]
this.hash[tailValue] = index
this.array[index] = tailValue
// 删除
this.array.pop()
delete this.hash[val]
return true
} else {
return false
}
};

RandomizedSet.prototype.getRandom = function() {
return this.array[Math.floor(Math.random()*this.array.length)]
};

232.用栈实现队列

题解简单易懂,主要是需要搞懂push pop穿插如果保证有序性就可以了,pop必须一次性从inStack全部转移过来才能保持顺序

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
var MyQueue = function() {
this.inStack = []
this.outStack = []
};

MyQueue.prototype.push = function(x) {
this.inStack.push(x)
};

MyQueue.prototype.pop = function() {
if(!this.outStack.length){
while(this.inStack.length){
this.outStack.push(
this.inStack.pop()
)
}
}
return this.outStack.pop()
};

MyQueue.prototype.peek = function() {
if(!this.outStack.length){
while(this.inStack.length){
this.outStack.push(
this.inStack.pop()
)
}
}
return this.outStack[this.outStack.length-1]
};

MyQueue.prototype.empty = function() {
return !this.inStack.length && !this.outStack.length
};

225. 用队列实现栈

请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty)。

实现 MyStack 类:

void push(int x) 将元素 x 压入栈顶。
int pop() 移除并返回栈顶元素。
int top() 返回栈顶元素。
boolean empty() 如果栈是空的,返回 true ;否则,返回 false 。

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
var MyStack = function() {
this.list = []
this.tempList = []
};

MyStack.prototype.push = function(x) {
if(this.list.length){
while(this.list.length){
this.tempList.push(this.list.shift())
}
this.list.push(x)
while(this.tempList.length){
this.list.push(this.tempList.shift())
}
}else{
this.list.push(x)
}
};

MyStack.prototype.pop = function() {
return this.list.shift()
};

MyStack.prototype.top = function() {
return this.list[0]
};

MyStack.prototype.empty = function() {
return !this.list.length
};

206.反转链表 -简单

迭代方法

使用迭代方法,我可以做出来,但是不太完美,代码不够精简

自己的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var reverseList = function(head) {
if(!head){ // 这个判断多余了
return head
}
let prev = null // 这个利用了哨兵,简化了判断
while(head){ // 判断条件很重要
const next = head.next
head.next = prev
if(!next){
return head
}
prev = head
head = next
}
return head // return pre 就不用判断next了
};
优秀代码
1
2
3
4
5
6
7
8
9
10
11
var reverseList = function (head) {
var pre = null, cur = head, next;
while (cur) { // 用cur取代head 迭代过程更加清晰
next = cur.next;
cur.next = pre;

pre = cur;
cur = next;
}
return pre;
};

少了两步判断,注意细节

递归方法

自己的代码

条件重复了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var reverseList = function(head) {
if(!head){ // 这个其实和下边的条件重复了
return head
}
return df(null, head)
function df(prev, cur){
if(!cur){
return prev
}else{
const next = cur.next
cur.next = prev
return df(cur, next)
}
}
};
优秀代码
1
2
3
4
5
6
7
8
9
10
11
12
var reverseList = function (head) {
function df(pre, cur) {
if (cur === null) {
return pre;
} else {
var temp = cur.next;
cur.next = pre;
return df(cur, temp);
}
}
return df(null, head);
};
单参数递归-==需要重点理解==

可以被称为前置递归,而上边那种可以叫做后置递归

这种递归处理 从尾部开始处理,一步步向头部靠近,不太容易理解,需要重点关注

1
2
3
4
5
6
7
8
9
10
11
12
var reverseList = function (head) {
function df(head) {
if (!head || head.next === null) {
return head;
}
const last = df(head.next)
head.next.next = head
head.next = null
return last
}
return df(head);
};

这个算法常常拿来显示递归的巧妙和优美

重点理解

这个方法很难理解,一篇文章写的很好labuladong,在这里摘抄一部分重要的内容

原文如下

这个算法可能很多读者都听说过,这里详细介绍一下,先直接看实现代码:

1
2
3
4
5
6
7
ListNode reverse(ListNode head) {
if (head.next == null) return head;
ListNode last = reverse(head.next);
head.next.next = head;
head.next = null;
return last;
}

看起来是不是感觉不知所云,完全不能理解这样为什么能够反转链表?这就对了,这个算法常常拿来显示递归的巧妙和优美,我们下面来详细解释一下这段代码。

**==对于递归算法,最重要的就是明确递归函数的定义==**。

除原文之外,还有几个容易遗漏而且比较重要的点

  • last的指向一直未变
  • 函数的截止条件和只有一个listNode的情况一样(也许这也是递归的魅力吧)
  • head的指向由于是栈结构,所以前置运行的时候head是倒着的
  • 前置递归能不能看成是一种倒叙迭代呢?,利用栈的特效进行倒叙操作

具体来说,我们的reverse函数定义是这样的:

输入一个节点head,将「以head为起点」的链表反转,并返回反转之后的头结点

明白了函数的定义,再来看这个问题。比如说我们想反转这个链表:

图片

那么输入reverse(head)后,会在这里进行递归:

1
ListNode last = reverse(head.next);

不要跳进递归(你的脑袋能压几个栈呀?),而是要根据刚才的函数定义,来弄清楚这段代码会产生什么结果:

图片

按照定义,这个reverse(head.next)执行完成后,整个链表应该变成了这样:

图片

并且根据函数定义,reverse函数会返回反转之后的头结点,我们用变量last接收了。

现在再来看下面的代码:

1
head.next.next = head;

图片

接下来进行的操作:

1
2
head.next = null;
return last;

图片

神不神奇,这样整个链表就反转过来了!递归代码就是这么简洁优雅,不过其中有两个地方需要注意:

1、递归函数要有 base case,也就是这句:

1
if (head.next == null) return head;

意思是如果链表只有一个节点的时候反转也是它自己,直接返回即可。

2、当链表递归反转之后,新的头节点是last,而之前的head变成了最后一个节点,别忘了链表的末尾要指向 null:

1
head.next = null;
总结一下

虽然按照他的说法,代码理解起来确实简单了一些,但是怎么把代码写出来是个问题。

针对过程做一个代码示意

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
// 1->2->3->4->5->null
var reverseList = function (head) {
function df(head) {
if (!head || head.next === null) {
return head;
}
const last = df(head.next)
head.next.next = head
head.next = null
return last
}
return df(head);
};
// 模拟代码运行步骤
df(1)
df(2)
df(3)
df(4)
df(5) // 满足条件 开始return 开始运行df函数后面的代码
// 第一次执行df后代码
// last 5
// head 4->5->null
head.next.next = head // 5->4
head.next = null // 4->null
// 执行完成后
// last 5->4->null
return last

// 第二次执行
// last 5->4->null
// head 3->4
head.next.next = head // 4->3
head.next = null // 3->null
// 执行完成后
// last 5->4->3->null
return last

// 第三次执行
// last 5->4->3->null
// head 2->3
head.next.next = head // 3->2
head.next = null // 2->null
// 执行完成后
// last 5->4->3->2->null
return last

// 第四次执行
// last 5->4->3->2->null
// head 1->2
head.next.next = head // 2->1
head.next = null // 1->null
// 执行完成后
// last 5->4->3->2->1->null
return last
// 整体结束

92.反转链表2 困难

官方头穿法容易理解,labuladong 公众号文章 较难理解

自己的代码

虽然头穿法理解起来容易,但是写起来需要仔细和不断试错,还需要照顾边界情况

代码中需要注意一下几点

  • 使用了哨兵,方便left为1的情况,使用了哨兵,返回的时候需要注意处理
  • cur情况不同 赋值的方式也不同
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
var reverseBetween = function(head, left, right) {
const dmNode = new ListNode(-1, head)
let cur = dmNode
let pre = null
let leftNode = null
let index = 0
while(cur){
if(index === right+1){
break;
}
if(index===left-1){
leftNode = cur
pre = cur.next

cur = cur.next
}else if(index > left){
const leftNodeNext = leftNode.next
pre.next = cur.next
leftNode.next = cur
cur.next = leftNodeNext
cur = pre.next
}else{
cur = cur.next
}
index++

}
return dmNode.next
};

优秀代码

官方代码 采取的分断式处理-for循环分段,逻辑较为清晰,ye

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var reverseBetween = function(head, left, right) {
// 设置 dummyNode 是这一类问题的一般做法
const dummy_node = new ListNode(-1);
dummy_node.next = head;
let pre = dummy_node;
for (let i = 0; i < left - 1; ++i) {
pre = pre.next;
}

let cur = pre.next;
for (let i = 0; i < right - left; ++i) {
const next = cur.next;
cur.next = next.next;
next.next = pre.next;
pre.next = next;
}
return dummy_node.next;
};

作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/reverse-linked-list-ii/solution/fan-zhuan-lian-biao-ii-by-leetcode-solut-teyq/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

234.如何判断回文链表

自己的代码

缺点无法提前结束

时间 空间都是O n

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var isPalindrome = function (head) {
let left = right = head;
let flag = true
function df(head){
if(!head){
return head
}
df(head.next)
if(head.val !== left.val){
flag = false
return
}
left = left.next
}
df(right)
return flag
};

双指针

通过双指针判断中间节点,翻转后半段并进行比较

时间 空间都是O 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
var isPalindrome = function (head) {
// 特殊情况
if(!head || !head.next){
return true
}
let slow = fast = head;
while (fast && fast.next) {
slow = slow.next
fast = fast.next.next
}
// 区别单双链表
if (fast !== null) {
slow = slow.next
}
let right = reserve(slow)
// 注意结束条件 right比较短
while (right) {
if (right.val !== head.val) {
return false
}
right = right.next
head = head.next
}
return true
};

function reserve(head) {
if (!head.next) {
return head
}
const newHead = reserve(head.next)
head.next.next = head
head.next = null
return newHead
}

142.环形链表 2

利用hash可以很方便的解决

1
2
3
4
5
6
7
8
var detectCycle = function(head) {
const set = new Set();
while(head && !set.has(head)){
set.add(head)
head = head.next
}
return head
};

这里和回文链表不一样,没有比较listNode.val

利用快慢指针

条件放的位置需要格外注意

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var detectCycle = function(head) {
let slow = fast = head;
while(fast && fast.next){
slow = slow.next
fast = fast.next.next
// 刚开始位置放错了
if(slow === fast){
break;
}
}
if(!fast || !fast.next){
return null
}
slow = head
while(slow !== fast){
slow = slow.next
fast = fast.next
}
return slow
};

快慢指针可行性推导

https://leetcode-cn.com/problems/linked-list-cycle-ii/solution/huan-xing-lian-biao-ii-by-leetcode-solution/

1
2
3
4
5
6
7
解释:为何慢指针第一圈走不完一定会和快指针相遇, 很精彩的推论
首先,第一步,快指针先进入环
第二步:当慢指针刚到达环的入口时,快指针此时在环中的某个位置(也可能此时相遇)
第三步:设此时快指针和慢指针距离为x,若在第二步相遇,则x = 0;
第四步:设环的周长为n,那么看成快指针追赶慢指针,需要追赶n-x;
第五步:快指针每次都追赶慢指针1个单位,设慢指针速度1/s,快指针2/s,那么追赶需要(n-x)s
第六步:在n-x秒内,慢指针走了n-x单位,因为x>=0,则慢指针走的路程小于等于n,即走不完一圈就和快指针相遇

剑指offer II 023

自己的代码,暴力法 Om*n,这里需要注意一个细节,内层while需要重置条件,可能是for循环用多了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var getIntersectionNode = function(headA, headB) {
let pA = headA,pB;
while(pA){
pB = headB
while(pB){
if(pB === pA){
console.log(pA)
return pA;
}
pB = pB.next
}
pA = pA.next
}
return null
}

官方解法,建议看题解

1
2
3
4
5
6
7
8
9
const getIntersectionNode = (A, B) => {
let pA = A,
pB = B;
while (pA !== pB) {
pA = pA === null ? B : pA.next;
pB = pB === null ? A : pB.next;
}
return pA;
};

关于二分查找,可以看labuladong,或者是极客王铮的算法课15 16

王铮总结的左右边界 要比公众号好记的多

704.二分查找 基础题

最基础的一个二分查找

给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。

示例 1:

1
2
3
输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4

示例 2:

1
2
3
输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1

提示:

你可以假设 nums 中的所有元素是不重复的。
n 将在 [1, 10000]之间。
nums 的每个元素都将在 [-9999, 9999]之间。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/binary-search
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

自己的代码,含有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
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
// 正常二分法
var search = function(nums, target) {
let left = 0, right = nums.length - 1;
while (left <= right) {
const mid = left + ((right - left) >> 1);
if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] < target) {
left = mid + 1
} else {
return mid
}
}
return -1
};
// 含有重复元素 找到第一个等于target的数
var search = function(nums, target) {
let left = 0, right = nums.length - 1;
while (left <= right) {
const mid = left + ((right - left) >> 1);
if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] < target) {
left = mid + 1
} else {
if (mid === 0 || nums[mid - 1] !== target) {
return mid
}
right = mid - 1
}
}
return -1
};
// 含有重复元素 找到最后一个等于target的数
var search = function(nums, target) {
let left = 0, right = nums.length - 1;
while (left <= right) {
const mid = left + ((right - left) >> 1);
if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] < target) {
left = mid + 1
} else {
if (mid === nums.length - 1 || nums[mid + 1] !== target) {
return mid
}
left = mid + 1
}
}
return -1
};
// 含有重复元素 查找第一个大于等于给定值的元素
var search = function(nums, target) {
let left = 0, right = nums.length - 1;
while (left <= right) {
const mid = left + ((right - left) >> 1);
if (nums[mid] >= target) {
if (mid === 0 || nums[mid - 1] < target) {
return mid
}
right = mid - 1
} else if (nums[mid] < target) {
left = mid + 1
}
}
return -1
};
// 含有重复元素 查找最后一个小于等于给定值的元素
var search = function(nums, target) {
let left = 0, right = nums.length - 1;
while (left <= right) {
const mid = left + ((right - left) >> 1);
if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] <= target) {
if (mid === nums.length - 1 || nums[mid + 1] > target) {
return mid
}
left = mid + 1
}
}
return -1
};

33.搜索旋转排序数组

整数数组 nums 按升序排列,数组中的值 互不相同 。

在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], …, nums[n-1], nums[0], nums[1], …, nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。

给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -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
var search = function(nums, target) {
let left = 0,right = nums.length - 1, n = nums.length;
if (!n) {
return -1;
}
if (n == 1) {
return nums[0] == target ? 0 : -1;
}
while(left <= right){
// 两种写法都能过
const mid = (right + left) >> 1;
const mid = left + ((right - left) >> 1);
if(nums[mid] === target){
return mid
}else if(nums[left] <= nums[mid]){
// 细节一,必须保证target 在左右区间
// 普通二分法 只需要一个就够了,那是因为target 必然在有序的left-right之间
// 细节二 小于等于 等于不能忘
if(nums[mid] > target && nums[left] <= target){
right = mid - 1
}else{
left = mid + 1
}
}else{
if(nums[mid] < target && target <= nums[right]){
left = mid + 1
}else{
right = mid - 1
}
}

}
return -1
};

167.两数之和 II - 输入有序数组

给定一个已按照 非递减顺序排列 的整数数组 numbers ,请你从数组中找出两个数满足相加之和等于目标数 target 。

函数应该以长度为 2 的整数数组的形式返回这两个数的下标值。numbers 的下标 从 1 开始计数 ,所以答案数组应当满足 1 <= answer[0] < answer[1] <= numbers.length 。

你可以假设每个输入 只对应唯一的答案 ,而且你 不可以 重复使用相同的元素。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/two-sum-ii-input-array-is-sorted
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

自己的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var twoSum = function(numbers, target) {
const l = numbers.length;
for(let i = 0; i<l-1;i++){
const result = getIndex(i+1, target - numbers[i]);
if(result){
return result
}
}
function getIndex(i, target){
let left = i, right = l - 1;
while(left <= right){
const mid = left + ((right - left) >> 1);
if(numbers[mid] === target) {
return [i, mid+1]
}else if(numbers[mid]>target){
right = mid - 1
}else{
left = mid +1
}
}
}
};

稍微优化了一下,把函数体放入循环里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var twoSum = function(numbers, target) {
const l = numbers.length;
for(let i = 0; i<l-1;i++){
const result = target - numbers[i];
let left = i+1, right = l - 1;
while(left <= right){
const mid = left + ((right - left) >> 1);
if(numbers[mid] === result) {
return [i+1, mid+1]
}else if(numbers[mid]>result){
right = mid - 1
}else{
left = mid +1
}
}
}
};

单调栈实际上就是栈,只是利用了一些巧妙的逻辑,使得每次新元素入栈后,栈内的元素都保持有序(单调递增或单调递减)。

听起来有点像堆(heap)?不是的,单调栈用途不太广泛,只处理一种典型的问题,叫做 Next Greater Element。

先来一道基础题

1
2
3
4
给一个数组,返回一个大小相同的数组。返回的数组的第i个位置的值应当是,对于原数组中的第i个元素,至少往右走多少步,才能遇到一个比自己大的元素(如果之后没有比自己大的元素,或者已经是最后一个元素,则在返回数组的对应位置放上-1)。

例如给定数组为:[2,1,5,6,2,3]
返回数组应该为:[2,1,1,-1,1,-1]

正序

建议自己再画一遍示意图,根据代码画,还是比较容易理解的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const arr1 = [2, 1, 5, 6, 2, 3];
function countSteps(arr) {
// 用栈存储下标
const stack = [];
const result = Array(arr.length).fill(-1);
for (let i = 0; i < arr.length; i++) {
while (stack.length && arr[i] > arr[stack[stack.length - 1]]) {
result[stack.pop()] = arr[i];
}
stack.push(i);
}
return result;
}

console.log(countSteps(arr1));
[ 5, 5, 6, -1, 3, -1 ]

151635833917_.pic_副本

倒叙

这个讲的比较好,labuladong

倒叙好像是不用记录下标,因为倒叙处理,每到一个元素,它的结果都会在此轮遍历中确定,和正序不同,再多理解理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const arr2 = [2, 1, 2, 4, 3];
function nextBigElement(arr) {
const stack = []
const result = []
for (let i = arr.length-1; i >=0; i--) {
while (stack.length && arr[i] >= stack[stack.length-1]) {
stack.pop()
}
result[i] = stack.length ? stack[stack.length - 1] : -1
stack.push(arr[i])
}
return result
}
console.log(nextBigElement(arr2));

图像

正序和倒序的一些区别

  • 确定值的时机不同
    • 倒序在每一轮循环中都可以确认
    • 正序只会在pop时确认
    • 由此造成书写结构和逻辑有些许差异

496.下一个更大元素I

给你两个 没有重复元素 的数组 nums1 和 nums2 ,其中nums1 是 nums2 的子集。

请你找出 nums1 中每个元素在 nums2 中的下一个比其大的值。

nums1 中数字 x 的下一个更大元素是指 x 在 nums2 中对应位置的右边的第一个比 x 大的元素。如果不存在,对应位置输出 -1 。

示例 1

1
2
3
4
5
6
输入: nums1 = [4,1,2], nums2 = [1,3,4,2].
输出: [-1,3,-1]
解释:
对于 num1 中的数字 4 ,你无法在第二个数组中找到下一个更大的数字,因此输出 -1 。
对于 num1 中的数字 1 ,第二个数组中数字1右边的下一个较大数字是 3 。
对于 num1 中的数字 2 ,第二个数组中没有下一个更大的数字,因此输出 -1 。

示例 2:

1
2
3
4
5
输入: nums1 = [2,4], nums2 = [1,2,3,4].
输出: [3,-1]
解释:
对于 num1 中的数字 2 ,第二个数组中的下一个较大数字是 3 。
对于 num1 中的数字 4 ,第二个数组中没有下一个更大的数字,因此输出 -1 。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/next-greater-element-i
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

我的解法

这道题和基础题基本上一致,多了一个映射的过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var nextGreaterElement = function(nums1, nums2) {
const map = new Map();
// 存储数值 而不是位置 因为没有重复元素
const stack = [];
for(let i=0;i<nums2.length;i++){
// 出栈条件
while(stack.length && nums2[i] > stack[stack.length-1]) {
map.set(stack.pop(), nums2[i])
}
stack.push(nums2[i])
}
return nums1.map(item => {
const r = map.get(item);
return r?r:-1
})
};

739.每日温度

1
2
3
4
5
6
请根据每日 气温 列表 temperatures ,请计算在每一天需要等几天才会有更高的温度。如果气温在这之后都不会升高,请在该位置用 0 来代替。

示例 1:
输入: temperatures = [73,74,75,71,69,72,76,73]
输出: [1,1,4,2,1,1,0,0]

这里以正序和倒序各写一次,方便理解记忆

正序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var dailyTemperatures = function(temperatures) {
// 需要预先设置查找不到的情况
const r = Array(temperatures.length).fill(0)
// 栈底到栈顶单调递减 **可以等于**
const stack = []
// 正序
for(let i=0;i<temperatures.length;i++){
// 当栈不为空 且 当前温度大于栈顶温度
while(stack.length && temperatures[i] > temperatures[stack[stack.length-1]]){
// 当前温度大于栈顶温度,即找到了栈顶元素 对应的 符合条件的元素
// 弹出栈顶 并记录下标
const index = stack.pop()
// 记录栈顶元素的对应的天数
r[index] = i - index
}
// 当栈为空 或者 当前元素**小于等于** 栈顶元素时 入栈
stack.push(i)
}
return r
};

倒序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var dailyTemperatures = function(temperatures) {
const r = []
const s = []
// 倒序
for(let i=temperatures.length-1;i>=0;i--){
// 当栈不为空 且 当前元素**大于等于**栈顶元素
while(s.length && temperatures[i] >= temperatures[s[s.length-1]]){
s.pop()
}
r[i] = s.length ? s[s.length-1] - i : 0
s.push(i)
}
return r
};

差异性

  • 赋值
    • 倒序遍历在while外,每轮遍历赋值一次
    • 正序每次pop赋值,需要提前准备默认值
  • 大小比较
    • 倒序大于等于
    • 正序大于

503.下一个更大元素 II 循环数组

给定一个循环数组(最后一个元素的下一个元素是数组的第一个元素),输出每个元素的下一个更大元素。数字 x 的下一个更大的元素是按数组遍历顺序,这个数字之后的第一个比它更大的数,这意味着你应该循环地搜索它的下一个更大的数。如果不存在,则输出 -1。

1
2
3
4
5
6
7
8
示例 1:

输入: [1,2,1]
输出: [2,-1,2]
解释: 第一个 1 的下一个更大的数是 2;
数字 2 找不到下一个更大的数;
第二个 1 的下一个最大的数需要循环搜索,结果也是 2。

最简单的方式,拼接一个数组在后边

1
2
3
4
5
6
7
8
9
10
11
12
13
var nextGreaterElements = function(nums) {
const arr = nums.concat(nums)
const res = []
const stack = []
for(let i=arr.length-1;i>=0;i--){
while(stack.length && arr[i] >= stack[stack.length-1]){
stack.pop()
}
res[i] = stack.length ? stack[stack.length-1] : -1
stack.push(arr[i])
}
return res.slice(0, nums.length)
};

这种方式,时间复杂度和空间复杂度都比较高

利用取余 %

减少了空间复杂度,利用 % 模拟循环数组

1
2
3
4
5
6
7
8
9
10
11
12
13
var nextGreaterElements = function(nums) {
const result = []
const stack = []
const n = nums.length
for(let i=2*n-1;i>=0;i--){
while(stack.length && nums[i%n] >= stack[stack.length-1]){
stack.pop()
}
result[i%n] = stack.length ? stack[stack.length-1] : -1
stack.push(nums[i%n])
}
return result
};

316.去除重复字符串

建议参考 labuladong

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
// 困难题
var s = "bcabc"
function handle(string) {
const stack = []
const map = new Map()
const countMap = Object.create(null, {})
for (let i = 0; i < string.length; i++) {
const s = string[i];
if (countMap[s]) {
countMap[s]++
} else {
countMap[s] = 1
}
}
for (let i = 0; i < string.length; i++) {
const s = string[i]
// 何时减法 很重要 重点理解
countMap[s]--
// 后边可以删除的
if (map.has(s)) {
continue
}
// 如果charcode码较小 会把栈顶元素(后面存在重复的) pop掉
// 既保证了顺序 也不会删除只有一个的元素
while (stack.length && stack[stack.length - 1].charCodeAt(0) > string.charCodeAt(i)) {
if (countMap[stack[stack.length - 1]] === 0) {
break;
}
map.delete(stack.pop())
}
map.set(s, true)
stack.push(s)
}
return stack
}
console.log(handle(s));
0%