Liangziye

深入浅出不如不进不出

0%

环境

根目录.mocharc.json

1
2
3
4
5
6
7
8
9
10
{
"require": "ts-node/register",
"spec": "src/test/**/*.ts",
"extension": ["ts"],
"timeout": 5000,
"reporter": "spec",
"slow": 300,
"ui": "bdd",
"recursive": true
}

根目录package.json部分

1
2
3
4
5
6
7
8
9
10
11
12
{
"type": "commonjs",
"scripts": {
"test": "mocha"
},
"dependencies": {
"@types/chai": "^4.3.16",
"@types/mocha": "^10.0.6",
"chai": "^4.4.1",
"mocha": "^10.4.0"
}
}

复现

运行npm run test报错

1
Exception during run: TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for ......................

解决

chai当前5.x版本仅支持纯ESM,而当前项目type为commonjs,安装char的4.x版本即可

1
2
3
npm uninstall @types/chai chai
npm cache clean --force
npm install chai@4 @types/chai@4

相关链接

v5 - Unknown file extension ".ts" when running with mocha · Issue #1568 · chaijs/chai (github.com)

当前环境

  • win10
  • npm v10.7.0
  • node v18.20.3

安装chocolatey

powershell管理员安装:

1
Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))

耐心等待,过程涉及到下载,需要一定耗时。

详见官网

安装环境相关

1
choco install python visualstudio2022-workload-vctools -y

详见node-gpy README

npm安装node-gyp

1
npm install -g node-gyp

场景

在win10 VS2022上试了一下调试ProwlarrFlareSolverr,想搞懂为什么docker上跑着的这两容器在某一两个pt站上不能成功过CF的5秒盾,需要抓下包确认下请求头是否设置无误。

尝试

想从VS里面直接抓包,一些旧版VS在性能查看器那里是有关于网络的,从某个版本开始也移除了这个功能,没有网络了。

打开了wireshark从网卡当然可以抓到所有包,但请求都是https,用改环境变量让chrome记录ssllog接着wireshark去读它然后解密的方法,只适用于浏览器,对IDE调试没用。想走一样的思路让IDE去记录调试中网络连接的私钥,也没能搜到相关的内容。

于是我打开了fiddler,fiddler是代理中间人的模式,肯定能解https,可是调试时却发现没能抓到任何一个VS的包,浏览器的包倒是能抓到,原因是vs发出的网络请求没从fiddler代理的8888端口经过。

接着便是修改VS的设置让VS从8888走代理,搜了一下发现2022版本好像是没有代理服务器设置的,旧版是有的,某个版本开始也移除了。

最后用netsh修改WinHTTP的代理,然后重启VS,问题得到了解决。

解决方案

设置代理:

1
netsh winhttp set proxy 127.0.0.1:8888

重置:

1
netsh winhttp reset proxy

引用

wireshark https抓包

Introducing Visual Studio’s Network tool

Configure .NET Core Applications

开了dnsmasq的日志之后,发现一次query下面却跟了两个forwarded

1
2
3
4
Jul  6 13:03:12 dnsmasq[14543]: 1 192.168.x.xxx/60201 query[A] clientservices.googleapis.com from 192.168.x.xxx
Jul 6 13:03:12 dnsmasq[14543]: 1 192.168.x.xxx/60201 forwarded clientservices.googleapis.com to 223.5.5.5
Jul 6 13:03:12 dnsmasq[14543]: 1 192.168.x.xxx/60201 forwarded clientservices.googleapis.com to 223.5.5.5
Jul 6 13:03:12 dnsmasq[14543]: 1 192.168.x.xxx/60201 reply clientservices.googleapis.com is 120.253.255.98

我第一反应跟我用两条宽带有关,于是把其中一条宽带的DNS改成114.114.114以区分,果然,两次forwarded,一次223.5.5.5一次114.114.114

1
2
3
4
Jul  6 13:29:45 dnsmasq[780]: 1 192.168.x.xxx/58192 query[A] www.google.com from 192.168.x.xxx
Jul 6 13:29:45 dnsmasq[780]: 1 192.168.x.xxx/58192 forwarded www.google.com to 223.5.5.5
Jul 6 13:29:45 dnsmasq[780]: 1 192.168.x.xxx/58192 forwarded www.google.com to 114.114.114.114
Jul 6 13:29:45 dnsmasq[780]: 1 192.168.2.135/58192 reply www.google.com is 142.251.43.4

不知道为什么dnsmasq要每一条宽带都请求一次,可能是在负载均衡的规则中进了balanced。

于是去负载均衡里面加上了两条DNS的规则,让tcp/udp协议的目标端口为53的流量都优先走其中一条宽带,发现并不管用。

去看看dnsmasq的解析文件/tmp/resolv.conf.d/resolv.conf.auto

1
2
3
4
# Interface wan
nameserver 223.5.5.5
# Interface wan1
nameserver 114.114.114.114

突然明白了,我之前勾选了“使用 all-servers 并发查询”,这样每个网络接口里面自定义的DNS服务器都会查一次,但只用最快得到的结果。我取消了并发查询之后,一次query只有一个forwarded。

其实问题出在resolv.conf,两个网络接口用相同的nameserver,resolv.conf会加上两条相同的nameserver,而不是相同的nameserver会自动去重。

在openwrt的luci可以直接开启dnsmasq的日志:

网络->DHCP/DNS->基本请求->记录查询日志 勾上

下面小字写着“将收到的 DNS 请求写入系统日志”,可是我没发现系统日志有dns查询记录

去看了一下配置文件/etc/dnsmasq.conf,除了一行log-facility=/dev/null就全是注释,很明显主要配置不在这里

又去这个/etc/config/dhcp配置文件看了一下,看config dnsmasq的部分,有关日志的也只有 option logqueries '1',这样看来,日志确实是开了,但是却不知道在哪

搜了一下dnsmasq的运行配置在/var/etc/dnsmasq.conf.cfgxxxxxx,也没有看到相关的日志位置

经过一番搜索,发现上面/etc/dnsmasq.conflog-facility就是配置日志的目录,改了之后/etc/init.d/dnsmasq restart重启dnsmasq,搞定。

要开关的话还是在luci开关,只把日志位置写在/etc/dnsmasq.conf就好了。也可以直接在/etc/config/dhcp加上 option logqueries '1',这里就是luci的配置文件,加上之后luci的记录查询日志的框框会自己勾上。

当然在/etc/dnsmasq.conf加上log-queries也可以直接开启日志,但这样的坏处是跟luci不同步了,加上之后,虽然luci的记录查询日志的框框没勾上,但却开启了日志。

最后,不要去改运行配置。

不想写await/sync,又不想嵌套括号,于是偷懒用了sync-request,两行就完成了GET请求,但是返回的结果对象,内部有三个字段,headers是对象,url是字符串,唯独body是一串二进制不能直接阅读,需要处理的数据。主要很奇怪的是在编辑器上console出来,控制台显示这是一个uint8Array,调试显示的也是一个uint8Array,但在终端补全时候提示却是一个Buffer。instanceof这两个都是true,unit8Array和Buffer是一个东西吗?

1
2
3
4
5
6
7
8
const request = require('sync-request');
const url = '';
const body = request('GET', url).body;

console.log(body instanceof Buffer);
//true
console.log(body instanceof Uint8Array);
//true

Buffer和Uint8Array的继承关系:

1
2
3
4
const buffer = new Buffer(3);

console.log(buffer instanceof Uint8Array);
//true

buffer继承于Uint8Array

Buffer Uint8Array与ArrayBuffer的关系

ArrayBuffer是一段字节,一段二进制数据,可以看做是内存上的一片格子,实质上是内存上一片连续空间的引用,它就代表了这一片连续的空间。ArrayBuffer不能更改长度容量,也不能直接操作它读取上面的数据。

为了操作它,读写上面的数据,我们需要Buffer Uint8Array这些东西,我们称它们为视图(view) ,可以想象Uint8Array覆盖在ArrayBuffer这一片连续内存之上,我们透过这一层视图来操作内存上的数据。

除了Uint8Array以外,ArrayBuffer的视图还有Uint16ArrayUint32Array诸如此类,看命名就可以得知,它们的区别只有bit大小不同,Uint8Array是8bit,1个byte,后两者分别是2个byte和4个byte,这个byte的区别是它们分别用多大的“尺寸”去“解释”ArrayBuffer,它们视图就像翻译机,我们偷着覆盖在上面这层翻译机翻译出下面的数据,Uint8Array是以每1个byte为单位作为一个元素,而Uint16Array是以每2个byte为单位作为一个元素,Uint32Array则是4个byte作为一个单位。他们统称为TypedArray

1111 1111 1111 1111 1111 1111 1111 1111这段数据,

Uint8Array解释为[255,255,255,255](十进制)

Uint16Array解释为[65535,65535](十进制)

Uint32Array解释为[33554431](十进制)

而Buffer继承自Uint8Array,自然也是1个字节为一个单位

这类视图有一个统称TypedArray,它们的方法都相同也比较简单,类似于数组的处理。

ArrayBuffer的实例化

1
new ArrayBuffer(length);

ArrayBuffer的构造函数只接受一个length参数,实例化出一个有着byte长度为length的内存块的ArrayBuffer实例,当实例化后,这片内存全为0。

注意,没有用数组或者其它buffer作为参数的构造方法,ArrayBuffer本身意义上就是一块连续内存上二进制数据的引用,而现成的数组或者其它buffer内部本身也指向了一块二进制数据。

TypedArray的实例化

1
new TypedArray(buffer [, byteOffset [, length]]);

如果说TypedArray这种视图只是一层在ArrayBuffer之上的操作工具,那严格来说它本身是没有数据的,先从ArrayBuffer的实例开始, buffer可以是一个ArrayBuffer的实例,这样实例化出来的TypedArray实例将指向这个ArrayBuffer实例

1
2
3
4
const arrayBuffer = new ArrayBuffer(4);
const view = new Uint8Array(arrayBuffer);
console.log(arrayBuffer === view.buffer);
//true

bufferTypedArray的一个属性,它是视图对象所对应的内存区域,也就是此实例所指向的ArrayBuffer实例。

再来看看可选参数,byteoffset代表的是TypedArray实例指向ArrayBuffer实例的偏移量,单位是byte,默认为0,length代表的是TypedArray实例覆盖ArrayBuffer实例的长度,这个默认不是ArrayBuffer实例的长度,默认是ArrayBuffer实例的长度减去byteoffset,如果byteoffset为ArrayBuffer实例的长度,那length就为0。

1
2
3
4
5
6
7
8
9
const arrayBuffer = new ArrayBuffer(4);
const view = new Uint8Array(arrayBuffer,1,2);
view[0]=0xff;
view[1]=0xff;
view[3]=0xff;
console.log(view);
//[ 255, 255 ]
console.log(arrayBuffer);
//<00 ff ff 00>

可见,实例view操作的是arrayBuffer的第一第二个字节。view[3]如果打印出来是undefined

arrayBufferArrayBuffer对象的实例,它是内存上真实存在的一段空间的引用,一串二进制数据看,这样uint8Array就成为了这一段内存空间引用的视图、操作工具。

现在来试一试,如果接受的参数不是ArrayBuffer实例,而是一个可迭代的数组对象,那实例化出来的TypedArray所指的ArrayBuffer实例是什么,跟传入的数组这个参数有什么关系

1
2
3
4
5
6
7
const array = [0,0,0,0];
const view = new Uint8Array(array);
view[0]=0xff;
console.log(view.buffer);
//<ff 00 00 00>
console.log(array);
//[ 0, 0, 0, 0 ]

很明显,它复制了一份数据,新建了一个ArrayBuffer实例,而view对象指向的就是这个新建的ArrayBuffer实例,视图对象跟数组已经没有关系了。也就是说除了直接给ArrayBuffer实例作为参数是直接指向,不然都是复制一份新的数据(因为数组内部没有指向的ArrayBuffer)。

包括参数为其他TypedArray也是如此,也是复制,而且这个构造函数没有可选偏移量跟长度。

1
new TypedArray(typedArray);	

多个视图可以同时指向的ArrayBuffer相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const arrayBuffer = new ArrayBuffer(4);
const uint8Array1 = new Uint8Array(arrayBuffer);
const uint8Array2 = new Uint8Array(arrayBuffer);
console.log(uint8Array1 === uint8Array2);
//false
console.log(uint8Array1[0]);
//0
console.log(uint8Array2[0]);
//0
uint8Array1[0]++;
console.log(uint8Array1[0]);
//1
console.log(uint8Array2[0]);
//1

那问题来了,我有一个旧的视图,我现在希望实例化一个新的视图,我希望这两个视图都指向同一片内存数据,又改怎么做呢?直接用旧视图作为构造参数去实例化当然是不行的,那样只会在内存上复制一份新的数据。由于直接使用ArrayBuffer的对象,即用这一块内存数据的引用去实例化视图,才使得视图直接指向这块内存,不会复制新的数据,那我们取出旧视图的代表了内存引用的ArrayBuffer对象,用它去实例化新视图不就可以了吗?

1
2
3
4
5
6
7
8
const view1 = new Uint8Array([0,0,0]);
const arrayBuffer = view1.buffer;
const view2 = new Uint8Array(arrayBuffer);
view1[0]=0xff;
console.log(view1);
//[ 255, 0, 0 ]
console.log(view2);
//[ 255, 0, 0 ]

除了传入ArrayBuffer实例和TypedArray实例,还可以传入一个number值作为构造参数。

1
new TypedArray(length);

也可以使用默认构造方法,内部同样会创建一个ArrayBuffer实例并指向它,但其长度为0。

1
2
3
const view = new Uint8Array();
console.log(view.buffer.byteLength);
//0

from

TypedArray.from()可以获得一个新实例

1
TypedArray.from(source[, mapFn[, thisArg]])

但问题是这个source只能是其他的TypedArray或者可迭代的对象比如数组,也就是不能是ArrayBuffer实例。如果硬要传递进去,只会创建一个新的长度为0ArrayBuffer实例

1
2
3
4
5
6
const arrayBuffer = new ArrayBuffer(4);
const view = Uint8Array.from(arrayBuffer);
console.log(view.buffer.byteLength);
//0
console.log(view.buffer==arrayBuffer);
//false

当参数是数组或者其他TypedArray时,是跟构造方法是一样的,复制一份内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const array = [0,0,0,0]
const view = Uint8Array.from(array);
view[0]=0xff;
console.log(view);
//[ 255, 0, 0, 0 ]
console.log(array);
//[ 0, 0, 0, 0 ]

const array1 = [0,0,0,0]
const view1 = Uint8Array.from(array1);
const view2 = Uint8Array.from(view1);
view1[0]=0xff;
console.log(view1);
//[ 255, 0, 0, 0 ]
console.log(view2);
//[ 0, 0, 0, 0 ]

由此可见from方法只能复制,不能共用一段内存数据

可选参数mapFn是个函数,可以类比于数组的map,可以处理每一个元素;thisArg是mapFn的this参数

Buffer

from

那Node的Buffer继承自Uint8Array,大部分跟Unit8Array一致,其中有一些不同的点,比如构造函数Buffer()已经废弃了,要获得一个Buffer实例需要用到Buffer的静态方法from

from可以传入数组和Buffer实例,没有可选参数,跟TypedArray的构造方法new TypedArray(array)new TypedArray(buffer)也是一致的,会复制一份数据。

它接受参数为ArrayBuffer实例时,有两个可选参数,byteoffset和length。这里的参数跟TypedArray的构造方法new TypedArray(buffer [, byteOffset [, length]])也是一致的,实例化的行为上也一样,都是直接使用传入ArrayBuffer实例,不会再复制一份数据。

1
2
3
Buffer.from(array)
Buffer.from(buffer)
Buffer.from(arrayBuffer[, byteOffset[, length]])
1
2
3
4
5
6
7
const array = [0,0,0,0];
const buffer = Buffer.from(array);
buffer[0]=0xff;
console.log(buffer);
//<Buffer ff 00 00 00>
console.log(array);
//[ 0, 0, 0, 0 ]
1
2
3
4
5
6
7
8
const array = [0,0,0,0];
const buffer1 = Buffer.from(array);
const buffer2 = Buffer.from(buffer1);
buffer1[0]=0xff;
console.log(buffer1);
//<Buffer ff 00 00 00>
console.log(buffer2);
//<Buffer 00 00 00 00>
1
2
3
4
5
6
7
const arrayBuffer = new ArrayBuffer(4);
const buffer = Buffer.from(arrayBuffer);
buffer[0]=0xff;
console.log(buffer);
//<Buffer ff 00 00 00>
console.log(arrayBuffer);
//<Buffer ff 00 00 00>

Buffer内存池

BufferBuffer.from()这个静态方法来获取实例化,跟TypedArray直接用构造方法相比,看起来确实一样,但其实会遇到一点问题:

1
2
3
4
5
6
7
8
9
10
const array = [0,0,0,0];
const view1 = new Uint8Array(array);
const view2 = Buffer.from(view1);
view1[0]=0xff;
console.log(view1);
//[ 255, 0, 0, 0 ]
console.log(view2);
//<Buffer 00 00 00 00>
console.log(view1.buffer===view2.buffer);
//false
1
2
3
4
5
6
7
8
9
10
const array = [0,0,0,0];
const buffer1 = Buffer.from(array);
const buffer2 = Buffer.from(buffer1);
buffer1[0]=0xff;
console.log(buffer1);
//<Buffer ff 00 00 00>
console.log(buffer2);
//<Buffer 00 00 00 00>
console.log(buffer1.buffer===buffer2.buffer);
//true 注意看这里打印出来是true

buffer1buffer2竟然是同一个ArrayBuffer实例。可是buffer1修改了数据在buffer2却是没有变化的。

证明了buffer1buffer2指向的是同一个ArrayBuffer实例,却是不同的位置,也就是说它们是有偏移量的。

1
2
3
4
5
6
7
8
9
//接着上面的代码
console.log(view1.byteOffset);
//0
console.log(view1.byteLength);
//4
console.log(view2.byteOffset);
//616
console.log(view2.byteLength);
//4

view2的起始位置是从ArrayBuffer实例的第616位开始的。

这里插一下队看看Buffer的另两个方法

1
2
Buffer.alloc(size[, fill[, encoding]])
Buffer.allocUnsafe(size)

这两个静态方法实例化出size大小的Buffer实例,它们的一个重要区别是allocUnsafe会在利用Buffer模块的内存池。模块预先分配了一个内存池,大小可以通过属性Buffer.poolsize得到。整个池有一个ArrayBuffer引用,当调用allocUnsafe调用时,检查size大小,如果size小于Buffer.poolsize的一半,直接将现成的内存池,也就是现成的ArrayBuffer引用分配给即将实例化的Buffer,否则,会新开一个内存池,当然,内存池不够大了也会开一个新的,并且下一次allocUnsafe的时候检查的就是这个新的池了。

除了allocUnsafe(size),还有from(array)``from(string)``from(buffer)都是会用到这个机制,所以上面buffer11buffer2经过检查后size都小于池子大小的一半,分配到的内存的引用都是同一个,只不过他们指向的都不是同一片区域,因为他们的偏移量不同,但是它们确实是在同一个ArrayBuffer里面。

所以利用这个机制我们也很容易可以将两个Buffer实例完全操作一样的内存区域。只要偏移量也相同长度也相同即可。

1
2
3
4
5
6
7
8
9
10
const array = [0,0,0,0];
const buffer1 = Buffer.from(array);
const buffer2 = Buffer.from(buffer1.buffer,buffer1.byteOffset,buffer1.byteLength);
buffer1[0]=0xff;
console.log(buffer1);
//<Buffer ff 00 00 00>
console.log(buffer2);
//<Buffer ff 00 00 00>
console.log(buffer1.buffer===buffer2.buffer);
//true

总结

  • ArrayBuffer是内存上一片连续数据的引用,它不能操作数据。

  • BufferTypedArray有继承关系,TypedArrayArrayBuffer的视图,用于操作二进制数据

  • 实例化TypedArray靠它的构造函数,参数为可迭代的对象、其他TypedArray实例时,会复制一份新的数据,当参数为ArrayBuffer实例时,直接指向这个实例,同时还可以通过两个可选参数控制对于ArrayBuffer实例的长度和偏移。获取实例还可以使用静态方法from,只能传可迭代的对象作为参数,但两个可选参数,用来处理每一个元素。

  • 实例化Buffer的构造函数已经废弃,实例可以靠静态方法from获得,与上面一样,当参数是可迭代的对象或者其他Buffer实例时,会复制一份数据,当参数是ArrayBuffer的实例时,直接指向它,同时可以控制长度和偏移。

  • Buffer有内存池机制,当实例化Buffer时如果参数不是ArrayBuffer时,会检查Buffer实例的大小,如果小于内存池的一半则将现有的内存池分配给它,否则新建一个内存池。

起因是抓IPTV,移动光猫 接 交换机8口,本机电脑 接 交换机7口,本机开wireshark,捕获过滤器用mac过滤掉移动光猫、本机和交换机的所有报文:

1
!ether host 00:D8:61:xx:xx:xx && !ether host CC:ED:21:xx:xx:xx && !ether host 58:41:20:xx:xx:xx

发现怎么还有东西一直在广播,mac是00:00:00:00:00:12。wireshark每隔约5s抓到一个broadcast:

No. Source Destination Protocal Length Info
1 00:00:00_00:00:12 Broadcast 0xfffa 72 Ethernet II
2 00:00:00_00:00:12 Broadcast 0xfffa 72 Ethernet II
3 00:00:00_00:00:12 Broadcast 0xfffa 72 Ethernet II
…… …… …… …… …… ……

看一个完整报文:

1
2
3
4
5
0000   ff ff ff ff ff ff 00 00 00 00 00 12 ff fa 00 00   ................
0010 00 03 cc ed 21 c0 dc e8 49 6e 20 74 68 65 20 67 ....!...In the g
0020 61 70 20 61 20 6e 65 77 20 74 6f 6f 74 68 20 67 ap a new tooth g
0030 72 65 77 2c 00 00 00 00 00 00 00 00 00 00 00 00 rew,............
0040 00 00 00 00 00 00 00 00 ........

其中广播带有58个bytes的data:

1
......!...In the gap a new tooth grew,....................

我抓了100个之后在wireshark存为unknown_broadcast.txt,用sed提取出data的文本内容并拼接起来:

1
sed -n '/^00[012]0[ ]*/p' unknown_broadcast.txt | sed -n 's/^.*\(.\{17\}$\)/\1/p'| sed 's/^\(\.*!\.*\)*\([^.]*\)\(\.*\)*/\2/'| sed ':a;N;s/\r\n//;t a'

(ps:sed的正则怎么用不了非贪婪)

(ps:sed的正则怎么匹配不了行尾$)

1
In the gap a new tooth grew,And now it makes me wonder,If people are like teeth tooThe day I lost my very first tooth,Was halfway through grade four,I'd run my tongue along the gap,Where my tooth had been before,I remember I went home crying,And showed it to my mum,She told me that a brand new tooth,Would grow up in my gum,In a while the gap would stop feeling I wouldn't notice the tooth was gone,The only reason I missed it now,Was because it was there for so long,Then slowly but surely over the weeks,In the gap a new tooth grew,And now it makes me wonder,If people are like teeth tooThe day I lost my very first tooth,Was halfway through grade four,I'd run my tongue along the gap,Where my tooth had been before,I remember I went home crying,And showed it to my mum,She told me that a brand new tooth,Would grow up in my gum,In a while the gap would stop feeling I wouldn't notice the tooth was gone,The only reason I missed it now,Was because it was there for so long,Then slowly but surely over the weeks,In the gap a new tooth grew,And now it makes me wonder,If people are like teeth tooThe day I lost my very first tooth,Was halfway through grade four,I'd run my tongue along the gap,Where my tooth had been before,I remember I went home crying,And showed it to my mum,She told me that a brand new tooth,Would grow up in my gum,In a while the gap would stop feeling I wouldn't notice the tooth was gone,The only reason I missed it now,Was because it was there for so long,Then slowly but surely over the weeks,In the gap a new tooth grew,And now it makes me wonder,If people are like teeth tooThe day I lost my very first tooth,Was halfway through grade four,I'd run my tongue along the gap,Where my tooth had been before,I remember I went home crying,And showed it to my mum,She told me that a brand new tooth,Would grow up in my gum,In a while the gap would stop feeling I wouldn't notice the tooth was gone,The only reason I missed it now,Was because it was there for so long,Then slowly but surely over the weeks,In the gap a new tooth grew,And now it makes me wonder,If people are like teeth tooThe day I lost my very first tooth,Was halfway through grade four,I'd run my tongue along the gap,Where my tooth had been before,I remember I went home crying,And showed it to my mum,She told me that a brand new tooth,Would grow up in my gum,In a while the gap would stop feeling I wouldn't notice the tooth was gone,The only reason I missed it now,Was because it was there for so long,Then slowly but surely over the weeks,In the gap a new tooth grew,And now it makes me wonder,If people are like teeth tooThe day I lost my very first tooth,Was halfway through grade four,I'd run my tongue along the gap,Where my tooth had been before,I remember I went home crying,And showed it to my mum,She told me that a brand new tooth,Would grow up in my gum,In a while the gap would stop feeling I wouldn't notice the tooth was gone,The only reason I missed it now,Was because it was there for so long,Then slowly but surely over the weeks,In the gap a new tooth grew,And now it makes me wonder,If people are like teeth tooThe day I lost my very first tooth,

手动去掉重复片段整理:

1
The day I lost my very first tooth,Was halfway through grade four,I'd run my tongue along the gap,Where my tooth had been before,I remember I went home crying,And showed it to my mum,She told me that a brand new tooth,Would grow up in my gum,In a while the gap would stop feeling I wouldn't notice the tooth was gone,The only reason I missed it now,Was because it was there for so long,Then slowly but surely over the weeks,In the gap a new tooth grew,And now it makes me wonder,If people are like teeth too. 

拿上面这小作文一搜,发现是首诗。。。。。换下行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
The day I lost my very first tooth,
Was halfway through grade four,
I'd run my tongue along the gap,
Where my tooth had been before,
I remember I went home crying,
And showed it to my mum,
She told me that a brand new tooth,
Would grow up in my gum,
In a while the gap would stop feeling
I wouldn't notice the tooth was gone,
The only reason I missed it now,
Was because it was there for so long,
Then slowly but surely over the weeks,
In the gap a new tooth grew,
And now it makes me wonder,
If people are like teeth too

怎么我抓出来的跟搜出来的差一个词,In a while the gap would stop feeling strange这句,我的怎么没有strange,手动去翻了一下unknown_broadcast.txt,发现是有strange的,检查发现是sed写错了,改为:

1
sed -n 's/^00[0123]0[ ]\{2,\}.*[ ]\{2,\}\([\.]*\)/\1/p' unknown_broadcast.txt | sed 's/^\(\.*!\.*\)*\([^.]*\)\(\.*\)*/\2/' | sed '/^[ ]\{3,\}/d' | sed ':a;N;s/\r\n//;t a'

谷歌翻译下这首诗:

在我失去第一颗牙齿的那天,
四年级上到一半,
我会沿着缝隙伸出舌头,
我以前的牙齿在哪里,
我记得我哭着回家,
并把它展示给我妈妈,
她告诉我,一颗崭新的牙齿,
会在我的口香糖里长大,
过一会儿,差距就不再奇怪了,
我不会注意到牙齿掉了,
我现在错过的唯一原因,
是因为它存在了那么久,
然后在几周内缓慢但肯定地,
缝隙里长出了一颗新牙,
现在它让我想知道,
如果人也像牙齿

现在我们开始解决AVL树中提到最小不平衡子树,使AVL树重新恢复平衡状态。

下图先举例一个最简单的平衡的AVL树在经过插入操作后变得不平衡的例子

如图所示插入结点5后,父结点7的平衡因子变为1,再往上的父节点10变为2,这个结点10就是我们要找的最小不平衡子树的根结点。我们把最小不平衡子树拿出来单独讨论。

这是最简单的情况,最小不平衡子树的根结点为结点10,新插入结点5是根结点10的左子结点的左子结点,这种情况我们成为LL型(左左型)。在LL型中,根结点10比它的左子结点大,它的左子结点也比新插入的结点大,我们可以把这三个结点的位置转一转,把结点10转到结点7的右下方,也就是从结点7的父节点变成了结点7的右子结点,而结点7自己成了根结点

现在以结点7为根结点的子树平衡了,整个树也就重新达到AVL的平衡条件了。我们旋转的对象是根结点10,根据它的旋转方向我们称这种旋转为右旋。所以LL型我们只需要一次右旋就能重新回归平衡。因为结点10和结点7之间的大小关系,所以结点10既能做结点7的父结点,也能做结点7的右子结点,右旋操作就是将操作结点从父结点变为右子结点。

这个LL型过于简单,再举个LL型的最小子树例子,下面这个例子的子树高度比上面那个例子要高,但同样只需要一次右旋就能回到平衡状态。

完全对称的,我们也有RR型(右右型),所以对称地我们需要一次左旋来让RR型回归平衡,我现在把LL型的例子变成RR型,再看看示意图,完全是对称的操作。右旋操作就是将操作结点从父结点变为左子结点。

例子举完了,现在该分析一下所有可能存在的情况了。如下图,设想现在有一个子树的根结点t,子树处于平衡状态,它的左子树我们先记做ltree,右子树我们记做rtree。左子树和右子树中的结点数未知,但是它们的高度差因为平衡所以一定不大于1。现在我们插入一个新的结点n,假定我们插入新的结点之后,树将不再平衡,并且这个以结点t为根的子树是整个树的最小不平衡子树。那只有两种插入情况,一种是插入在左子树上,左子树比右子树高2,一种是插在右子树上,右子树比左子树高2,我们把比较矮的一边子树的高度记做h,则另一边比较高的子树的高度为h+2。当然h必须h≥0。可以想象到这两种情况是对称的,那他们解决平衡的操作也是对称的。

我们现在拿情况1出来分析,把ltree又分为它的根结点tl和tl的左子树lltree和右子树lrtree,同样有两种情况,结点n要么是接在lltree上要么是接在lrtree上,根据上面的讨论,高度如下图所示,同时我也标注好了平衡因子。

注意!以上的讨论都是建立在结点t构成的子树为最小不平衡子树下。因为这个子树为最小不平衡子树,所以必然的,情况1-1中lltree和lrtree还有rtree三个子树的高度都相等都是h,情况1-2中rltree和rrtree还有ltree三个子树的高度也相等也都是h。h要满足h≥0。反过来说lltree和lrtree还有rtree(或是rltree和rrtree还有ltree)它们的高度不可能不相等,如果不相等那结点t构成的子树就不是最小不平衡子树,记住我们所讨论的情况都是建立在结点t是最小不平衡子树的根结点上的。

对于情况1-1我们可以继续分类讨论出情况1-1-1和1-1-2……但这样套娃已经完全没有必要了,因为我们现在就能针对情况1-1做出普适性的回归平衡方法。

现在我们针对情况1-1来看,发现上文中右旋的两个例子就是情况1-1,情况1-1即是LL型。当lltree和lrtree还有rtree的高度为0时,就是上文右旋的第一个例子,而这三个子树高度为1时,就是上文右旋中第二个例子。可以继续推导的是,lltree和lrtree还有rtree的高度h只要满足h≥0,这种最小不平衡子树都是LL型,能都通过对根结点t一次右旋重新回归平衡,如下图。

接下来讨论情况1-2,我们发现这不再是LL型了,我们无法通过一次右旋来解决问题,如下图:

我们需要继续分析讨论情况1-2,分类讨论结点n插入的更准确的位置。我们把lrtree这个子树细分成结点tlr和其左右子树lrltree和lrrtree。我们对情况1-2分别得到以下两种情况1-2-a和1-2-b。1-2-a是结点n插在lrltree上,1-2-b是结点n插在lrrtree上。

经过分析我们发现,虽然我们不能通过一次右旋来回归平衡,但是我们发现如果对情况1-2-a和1-2-b中的结点tl进行一次左旋,我们将把情况1-2-a和1-2-b转变了LL型,如下图

情况1-2-a和1-2-b进行了左旋之后,得到的LL型是有区别的,情况1-2-a左旋结果的结点tlr的平衡因子为2,最小不平衡子树变成了以结点tlr为根的最小不平衡子树,不是结点t。而情况1-2-b左旋结果的结点tlr的平衡因子为1,所以依然是以结点t为根结点的最小不平衡子树。对于情况1-2-a左旋结果,它的最小不平衡子树变了,虽然也是LL型,但是我们能对结点tlr做一次右旋让它回归平衡吗?答案是不能,这样又变回情况情况1-2-a了,左右旋本身就是互为反操作。那这样的话,我们依旧是把情况1-2-a左旋结果的最小不平衡子树当做没有变动,根结点依旧是结点t,所以对于情况1-2-a左旋结果和情况1-2-b左旋结果,这两个都是根为结点t的LL型,我们对结点t做一次右旋即可回归平衡。如下图。

情况1-2细分的两种情况现在就已经解决了不平衡问题,方法都是先对结点tl做一次左旋,再对结点t做一次右旋,我们把情况1-2称作LR型。所以LR型的最小不平衡子树,我们先对根结点t的左子节点tl做左旋转换成LL型后,再对结点t做右旋,即可平衡。但LR型里面也是有区别的,虽然都是对结点一样的操作,可重新平衡之后结点t和结点tl的平衡因子是不一样的,在实现上要注意处理这个问题。

至此,其实我们已经解决掉AVL树的一半旋转方法了。LL型对根结点t做一次右旋,LR型对根结点t的左子节点tl做一次左旋再对结点t做一次右旋。对称地,我们可以得知,把LL型和LR型镜像翻转过来我们得到RR型,和RL型,重新平衡的操作也是对称的,对于RR型应当对根结点t做一次左旋,对于RL型应当对根结点t的右子结点tr做一次右旋,再对结点t做一次左旋。于是我们对于所有的不平衡情况都给出了平衡的旋转方法。下面示意图表示一下RR和RL的过程,就不从头分析一遍了。

RR型:

RL型

RL型对结点tr右旋转换成RR型:

最后再对结点t左旋:

前情提要:

插入

插入和二叉搜索树不一样的是,需要再数据被放入后回溯结点,向上更新每一个结点的平衡因子,判断是否有子树不平衡,找到最小不平衡子树,用上篇中讨论的旋转方式去让树回归平衡。