modify some mistakes in TranportLayer.md, update a brief conclusion on BST and BBSTS.

This commit is contained in:
Shine wOng
2019-11-07 16:46:13 +08:00
parent 1956b5dbe0
commit 63d3496fea
2 changed files with 52 additions and 28 deletions

View File

@@ -9,13 +9,13 @@
这样的话发送数据的一方就不只是需要指定接收方的IP地址还需要指定对方主机是由哪个进程来接收这些数据。
一个想法是每一台计算机中每一个进程都被分配了一个进程号,可以通过这个进程号来指定通信的双方。但是,很明显的是,这里存在一些问题首先是不同计算机分配的进程号格式不一致,在报文的首部并不容易用固定的字长编码。还有就是,这些进程号显然是不断变化的,每次通信的进程号不同会给数据传输带来很大的不方便。
一个想法是每一台计算机中每一个进程都被分配了一个进程号,可以通过这个进程号来指定通信的双方。但是,很明显这里存在一些问题——首先是不同计算机分配的进程号格式不一致,在报文的首部并不容易用固定的字长编码;另外,这些进程号显然是不断变化的,每次通信的进程号不同会给数据传输带来很大的不方便。
为了解决上面的问题,一个自然的想法是给不同计算机的所有进程一个统一的进程编码。但是进程往往是不确定,并且常常会产生新的进程。这样的话,很少会有主机会知道某台主机上出现了新的进程并与之通信。
真正的解决方案是采用_协议端口号_,这是基于缓存的思想:与当前主机通信的其他主机上的进程将数据传输到当前主机后,就将数据放在某个本地仓库中,之后由本地的进程前去读取。因此发送方只需要知道目的主机的某个应用进程的仓库号是多少,就可以把数据发送到目的主机进程的定仓库,从而实现进程之间的通信。
真正的解决方案是采用`协议端口号`,这是基于缓存的思想:与当前主机通信的其他主机上的进程将数据传输到当前主机后,就将数据放在某个本地仓库中,之后由本地的进程前去读取。因此发送方只需要知道目的主机的某个应用进程的仓库号是多少,就可以把数据发送到目的主机进程的定仓库,从而实现进程之间的通信。
存在一些常见的应用进程,他们是互联网中最重要的一些应用程序。因此给这类进程都制定了一个_熟知端口号_(或系统端口号)数值为0 ~ 1023。这些进程有
存在一些常见的应用进程,他们是互联网中最重要的一些应用程序。因此给这类进程都制定了一个`熟知端口号`(或系统端口号)数值为0 ~ 1023。这些进程有
+ 21: ftp控制信息端口
+ 22: ssh
@@ -39,7 +39,7 @@
### 实现可靠传输
前面说过IP层的传输是不可靠传输。这是因为一开始它就把锅丢给了传输层(误 IP层认为计算机的差错处理能力很强所以它只负责简单灵活地交付数据然后依靠传输层来保证数据的可靠传输。
前面说过IP层的传输是不可靠传输。这是因为一开始它就把锅丢给了传输层(误)。IP层认为计算机的差错处理能力很强所以它只负责简单灵活地交付数据然后依靠传输层来保证数据的可靠传输。
除此以外,传输层还会实现一些更高级的功能,如流量控制,拥塞控制这样。
@@ -68,7 +68,7 @@ TCP就比较强了提供的是可靠的传输。除此以外还提供了
+ 只能一对一。因为面向连接啊,你只能传输给连接的另一方。
+ 提供可靠交付的服务。通过TCP传输的数据无差错不丢失不重复并且按序到达(你听听,这还是人话吗
+ 提供全双工通信。双方都有发送缓存和接受缓存,可以同时收发
+ 面向字节流。TCP并不理解应用进程交给它的报文而只是看做无结构的字节流。所以他可以自己决定什么时候发送多大的字节流这样发送的时候就顺便加上流量控制和拥塞控制。只要接收方到同样的字节流就行。
+ 面向字节流。TCP并不理解应用进程交给它的报文而只是看做无结构的字节流。所以他可以自己决定什么时候发送多大的字节流这样发送的时候就顺便加上流量控制和拥塞控制。只要接收方到同样的字节流就行。
关于TCP的连接的话前面也说过了需要同时知道IP地址和端口号。所以TCP使用套接字作为连接的端点。其中套接字socket = (IP地址: 端口号)。比如说127.0.0.1:80就表示本机的80端口
@@ -83,7 +83,7 @@ TCP就比较强了提供的是可靠的传输。除此以外还提供了
+ 如果B收到数据则向A发送一个确认信息
+ 若A向B发送数据失败或是数据在传输过程中失真则B不会向A发送确认信息那么A由于没有受到B的确认信息A会向B重新发送信息
+ 若B向A发送的确认信息在传输过程中丢失那么A也不会受到这个确认信息A仍然会向B重新发送
+ 若A第一次发送的数据时延较大A向B第二次发送数据并成功接收。B过了一段时间后又到了A第一次发送的数据B收到以后就丢弃这个数据
+ 若A第一次发送的数据时延较大A向B第二次发送数据并成功接收。B过了一段时间后又到了A第一次发送的数据B收到以后就丢弃这个数据
可以看到,实现自动重传请求有两种方法
+ 停止等待协议A每发送一个数据就停止发送直到收到B发来的确认信息。停止等待协议固然很简单不需要维护太多信息。但是很明显信道的利用率太低。因此一般是使用滑动窗口协议。
@@ -92,7 +92,7 @@ TCP就比较强了提供的是可靠的传输。除此以外还提供了
要实现滑动窗口协议,需要满足
+ 发送方需要发送当前所发报文的是第几个报文以便让接收方将没有按序到达的报文组装起来。由于TCP是基于字节流的传输所以一般是发送当前报文的第一个字节序号
+ 接收方要对发送方发送的字节流进行确认。如果每个字节逐个确认那效率也太低了吧需要发送大量的信息。所以一般是使用_累积确认_的方式即只对按序到的数据中的最高字节进行确认,表示到这个字节以前接收方都已经收到了。发送方即可丢掉之前的数据。
+ 接收方要对发送方发送的字节流进行确认。如果每个字节逐个确认那效率也太低了吧需要发送大量的信息。所以一般是使用_累积确认_的方式即只对按序到的数据中的最高字节进行确认,表示到这个字节以前接收方都已经收到了。发送方即可丢掉之前的数据。
所以基于上面的讨论TCP报文的首部应该包含当前发送的_字节序号_以及_确认号_
@@ -100,35 +100,35 @@ TCP就比较强了提供的是可靠的传输。除此以外还提供了
#### 再谈滑动窗口协议
发送方A需要管理一个发送窗口里面包含当前已经发送但未收到确认的数据以及当前可以发送的数据。同理接收方B需要管理一个接窗口。很明显的是,发送方的发送窗口不能大于接收方的接窗口。
发送方A需要管理一个发送窗口里面包含当前已经发送但未收到确认的数据以及当前可以发送的数据。同理接收方B需要管理一个接窗口。很明显的是,发送方的发送窗口不能大于接收方的接窗口。
要实现上面那个条件就需要接收方告诉发送方自己当前的接收窗口有多大使得发送方可以调整自己的发送窗口。这样我们的TCP报文的首部就又增加了一个字段窗口大小
#### 超时重传时间
发送在没有收到来自接收方的信息时,会对之前的数据进行重传。可是这个重传时间应该怎么选择呢?如果重传时间太短,那么会占有网络的带宽,使得网络的负荷增大。重传时间太长又会使得数据传输的效率非常低。此外,网络的情况是在不断变化的,有时比较畅通,有时又会比较拥堵,因此常数的重传时间显然是行不通的。重传时间的选择需要根据网络的情况实现自适应。
发送在没有收到来自接收方的信息时,会对之前的数据进行重传。可是这个重传时间应该怎么选择呢?如果重传时间太短,那么会占有网络的带宽,使得网络的负荷增大。重传时间太长又会使得数据传输的效率非常低。此外,网络的情况是在不断变化的,有时比较畅通,有时又会比较拥堵,因此常数的重传时间显然是行不通的。重传时间的选择需要根据网络的情况实现自适应。
简单的想法是可以管理平均的往返时间(RTT, Rount Trip Time),取超时重传时间为 RTT_S + 4RTT_D, 其中RTT_S是平均的RTT值而RTT_D往返时间的标准差。按照正态分布考虑的话这个应该可以涵盖所有的正常情况。
简单的想法是可以管理平均的往返时间(RTT, Rount Trip Time),取超时重传时间为`RTT_S + 4RTT_D`, 其中`RTT_S`是平均的`RTT`值,而`RTT_D`往返时间的标准差。按照正态分布考虑的话,这个应该可以涵盖所有的正常情况。
上面的算法存在一个问题即RTT值不好计算。这是因为如果一个报文段发送了两次如何判断到的确认信息是对第一个报文的确认,还是对第二次发送报文的确认?
上面的算法存在一个问题即RTT值不好计算。这是因为如果一个报文段发送了两次如何判断到的确认信息是对第一个报文的确认,还是对第二次发送报文的确认?
一个改进方法是,既然不能算,那我就不算了(?)。只要报文重传了就不计算其RTT。但是这样的话如果某一时间时时延突然增大了很多使得报文发生重传。这以后RTT时间就无法进行更新因此报文就不断重传。
一个改进方法是,既然不能算,那我就不算了(?)。只要报文重传了就不计算其RTT。但是这样的话如果某一时时延突然增大了很多使得报文发生重传。这以后RTT时间就无法进行更新因此报文就不断重传。
所以最终方案是如果报文重传了就不计算它的RTT而是直接将之前的超时重传时间RTO加倍(真的简单粗暴)
所以最终方案是如果报文重传了就不计算它的RTT而是直接将之前的超时重传时间`RTO`加倍(真的简单粗暴)
### TCP流量控制
首先明确一个问题为什么TCP要进行流量控制
前面提到过TCP的发送方和接收方分别会管理一个发送缓存和接收缓存。设想如果接收方的接收缓存于发送方的发送缓存,那么发送方发送的部分数据就会被接收方所丢弃,从而不可避免地需要重传,提高了网络的负荷。所以前面说,发送缓存一定要小于接收缓存。
前面提到过TCP的发送方和接收方分别会管理一个发送缓存和接收缓存。设想如果接收方的接收缓存于发送方的发送缓存,那么发送方发送的部分数据就会被接收方所丢弃,从而不可避免地需要重传,提高了网络的负荷。所以前面说,发送缓存一定要小于接收缓存。
所以流量控制就是这样一个概念:让发送方的发送速率不要太快,以使得接收方来得及接。所以很明显这里可以直接利用前面提到过的_窗口_字段来进行流量控制--接方向发送方发送自己当前的接收窗口的大小,从而使发送方可以调整自己的发送窗口值。
所以流量控制就是这样一个概念:让发送方的发送速率不要太快,以使得接收方来得及接。所以很明显这里可以直接利用前面提到过的_窗口_字段来进行流量控制--接方向发送方发送自己当前的接收窗口的大小,从而使发送方可以调整自己的发送窗口值。
从上面讨论可以看到,流量控制是点对点通信量的控制,是端到端的问题。这有别于后面会提到的拥塞控制。
上面的利用窗口大小来进行流量控制的方法存在一个问题--设想如果某一时刻接收方将自己的接收窗口设置为零,并通知了发送方。于是发送方被禁止发送数据。这时由于某些原因,接收方又增大了自己的窗口值,但是这个新的窗口值报文在传输过程中丢失了。这以后发送方就一直在等待接收方非零窗口的通知,而接收方一直在等待发送方传来的数据。这种死锁局面将一直持续下去。
上面的利用窗口大小来进行流量控制的方法存在一个问题--设想某一时刻接收方将自己的接收窗口设置为零,并通知了发送方。于是发送方被禁止发送数据。这时由于某些原因,接收方又增大了自己的窗口值,但是这个新的窗口值报文在传输过程中丢失了。这以后发送方就一直在等待接收方非零窗口的通知,而接收方一直在等待发送方传来的数据。这种死锁局面将一直持续下去。
为了解决这个问题TCP为每个连接都设置了一个持续计时器。只要连接的一方到另一方的零窗口通知就启动计时器。计时器的时间到期就发送一个_零窗口检测报文段_对方在确认这个报文段时给出当前窗口值。如果窗口仍是零则重置持续计时器。若窗口不是零了死锁的僵局就被打破了。
为了解决这个问题TCP为每个连接都设置了一个持续计时器。只要连接的一方到另一方的零窗口通知就启动计时器。计时器的时间到期就发送一个_零窗口检测报文段_对方在确认这个报文段时给出当前窗口值。如果窗口仍是零则重置持续计时器。若窗口不是零了死锁的僵局就被打破了。
#### TCP报文段的发送时机
@@ -147,7 +147,7 @@ TCP就比较强了提供的是可靠的传输。除此以外还提供了
+ 应用进程把要发送的数据逐个字节给发送缓存
+ 发送方把第一个数据字节发送出去,后面到达的数据都缓存起来
+ 发送方收到对第一个字节的确认后,再把缓存中的所有数据组成一个报文段都发送出去。同时继续缓存后面到达的数据
+ 只有到对前一个报文段的确认后,才能发送后一个报文段(类似停止等待协议,但这是基于时间的发送)
+ 只有到对前一个报文段的确认后,才能发送后一个报文段(类似停止等待协议,但这是基于时间的发送)
+ 当到达的数据达到发送窗口的一半或已经达到最大报文段长度(MSS)时,立即发送(基于报文大小的发送)
上述基于滑动窗口的流量控制还有一个叫_糊涂窗口综合征_的问题。
@@ -169,12 +169,12 @@ TCP进行拥塞控制的算法有四种慢开始拥塞避免快重传
- 一开始发送方将自己的拥塞窗口设置为1发送一个报文段
- 发送方收到对这个报文段的确认后将拥塞窗口由1增大到2发送两个报文段
- 发送方收到对一个报文段的确认将将拥塞窗口增大1。因此经过一个传输轮次,拥塞窗口就加倍。
- 发送方收到对一个报文段的确认将将拥塞窗口增大1。因此经过一个传输轮次,拥塞窗口就加倍。
+ 拥塞避免
- 可以看到,采用慢开始算法后,拥塞窗口的大小呈现指数增长。为了防止慢开始算法使拥塞窗口增长太快,还需要设置一个慢开始门限。当拥塞窗口大于慢开始门限时,使用拥塞避免算法。
- 简单说来,拥塞避免算法就是减小拥塞窗口增大的幅度,使之缓慢增大。即经过一个传输轮次拥塞窗口增大1。这样拥塞窗口就以线性规律缓慢增大。
- 简单说来,拥塞避免算法就是减小拥塞窗口增大的幅度,使之缓慢增大。即经过一个传输轮次拥塞窗口增大1。这样拥塞窗口就以线性规律缓慢增大。
- 使用慢开始与拥塞避免算法的流程为
+ 一开始将拥塞窗口设置为1使用慢开始算法
@@ -186,20 +186,20 @@ TCP进行拥塞控制的算法有四种慢开始拥塞避免快重传
设想一种情况在TCP传输过程中某个报文段出现了超时重传TCP因此重新开始慢开始算法。但是实际上这个报文只是在传输过程中丢失了并不是因为网络出现了拥塞。此时采取慢开始算法反而会降低数据的传输效率。因此需要有办法可以检测出个别报文丢失的情况。这就是快重传算法。
- 接收方在收到数据时要立即发送确认,不要等待自己要发送数据才进行捎带确认。
-到失序的报文段时要发送对已收到报文的重复确认。即如果接收方已经接受了M1, M2并且发送了确认。这时没有到M3却收到了M4。此时接收方应该发送对M2的重复确认。
-到失序的报文段时要发送对已收到报文的重复确认。即如果接收方已经接受了M1, M2并且发送了确认。这时没有到M3却收到了M4。此时接收方应该发送对M2的重复确认。
- 发送方在收到对某个报文的重复确认后,就知道某个报文段在传输过程中丢失了,立即进行重传。这样就不会出现超时,发送方也不会误以为出现了网络拥塞。
+ 快恢复
- 在收到对某个报文段的重复确认后,发送方知道只是丢失了个别报文段,并不是网络出现了拥塞。因此不启动慢开始,而是执行快恢复算法
- 调整门限值为拥塞窗口的一,同时设置拥塞窗口等于门限值
- 调整门限值为拥塞窗口的一,同时设置拥塞窗口等于门限值
- 执行拥塞避免算法
#### 主动队列管理
拥塞控制是一个全局的控制,需要协调网络中各个部分。所以这里考虑一下路由器的问题。
当网络中出现拥塞时,路由器的队列长度不足以容纳要转发的所有分组,因此会将尾部的分组丢弃,即_尾部丢弃策略_。这样的尾部丢弃会导致一连串分组的丢失,使发送方出现超时重传,这些发送都进入了慢开始状态。这样一来全网的通信量会突然下降很多而网络恢复正常后通信量又会突然增大很多。即TCP的_全局同步_
当网络中出现拥塞时,路由器的队列长度不足以容纳要转发的所有分组,因此会将尾部的分组丢弃,即`尾部丢弃策略`。这样的尾部丢弃会导致一连串分组的丢失,使发送方出现超时重传,这些发送都进入了慢开始状态。这样一来全网的通信量会突然下降很多而网络恢复正常后通信量又会突然增大很多。即TCP的`全局同步`
为了避免全局同步,可以采取主动队列管理。主动是指路由器会主动的丢弃某一些分组,而不是被动等到队列满了以后才尾部丢弃。实现这个需要路由器维持两个参数:队列长度最小门限和最大门限
@@ -207,7 +207,7 @@ TCP进行拥塞控制的算法有四种慢开始拥塞避免快重传
+ 若平均队列长度大于最大门限,则把新到达的分组丢弃
+ 若介于两者之间,则按照某一个概率随机丢掉到达的分组
这样以一定概率随机丢掉新到达的分组使得拥塞控制只在个别的TCP上进行因而避免了全局同步的情况。
这样以一定概率随机丢掉新到达的分组使得拥塞控制只在个别的TCP上进行因而避免了`全局同步`的情况。
### TCP的连接
@@ -218,7 +218,7 @@ TCP进行拥塞控制的算法有四种慢开始拥塞避免快重传
考虑一种情况:
+ 客户端向服务器发送了两次请求报文段(ACK = 0, SYN = 1),第一次发送的请求报文段在某个网络结点长时间滞留了,因此发了第二次。
+ 服务器收到了第二次的请求报文段,因此客户端回送了确认连接(ACK = 1, SYN = 1)。若只需要两报文握手的话,此时连接已经建立
+ 服务器收到了第二次的请求报文段,因此客户端回送了确认连接(ACK = 1, SYN = 1)。若只需要两报文握手的话,此时连接已经建立
+ 连接释放以后,客户向服务器发送的第一个请求报文段终于发到了服务器。服务器误以为客户端再次请求连接,因此再次回送确认连接报文。
+ 由于客户端实际上并没有发送第二次连接请求,客户端会忽略服务器的确认报文。
+ 但是服务器误以为连接已经建立,因此白白消耗了服务器资源。
@@ -228,7 +228,7 @@ TCP进行拥塞控制的算法有四种慢开始拥塞避免快重传
#### 连接的释放: 四报文握手
+ 在某一时刻A和B处于TCP连接状态。若A已经没有要向B发送的数据了因此A首先发出连接释放请求。
+ B到请求后即发出确认。
+ B到请求后即发出确认。
若此时就完成连接的释放,会存在一些问题--若此时B还有需要向A发送的数据则这些数据得不到发送。因此B发出确认后TCP的连接并没有释放而只是释放A->B这个方向的连接。B->A方向仍然可以发送数据。此时TCP处于半关闭状态。故之后还有以下步骤
@@ -239,4 +239,4 @@ TCP进行拥塞控制的算法有四种慢开始拥塞避免快重传
此外等待两倍MSL的时间可以使本连接持续时间内所产生的所有报文段都从网络中消失这样下一次新的连接中就不会出现这种旧的连接请求报文段。
TCP的连接释放还有一种问题考虑客户和服务器建立连接后客户端因为某种原因而崩溃了此时客户显然无法向服务器发送连接释放报文这样服务器的资源就一直被白白占用。因此TCP还会设置一个保活计时器服务器收到一次客户端的数据就重新设置活计时器。若一定时间内还没有收到来自客户的数据,服务器就发送一个探测报文段。若一连发送十个探测报文段还没有来自客户的应,服务器就以为这个客户出现了故障,因而就关闭这个连接。
TCP的连接释放还有一种问题考虑客户和服务器建立连接后客户端因为某种原因而崩溃了此时客户显然无法向服务器发送连接释放报文这样服务器的资源就一直被白白占用。因此TCP还会设置一个`保活计时器`,服务器收到一次客户端的数据就重新设置`保活计时器`。若一定时间内还没有收到来自客户的数据,服务器就发送一个探测报文段。若一连发送十个探测报文段还没有来自客户的应,服务器就以为这个客户出现了故障,因而就关闭这个连接。

View File

@@ -1,5 +1,29 @@
Conclusion on BST
================
=================
## BST与BBST知识脉络
理解二叉搜索树的关键在于它的定义,即对于任一节点,其左子树的所有节点均不大于它,右子树的所有节点均不小于它,即`局部有序性`;通过二叉搜索树的`局部有序性`,可以证明得到它的`全局单调性`,因此可以仿照有序向量,在二叉搜索树上实现高效的查找算法。对二叉搜索树的动态操作,即插入与删除,关键也在于维护它的`局部有序性`。因此对于插入操作,首先需要调用一次`search`接口定位插入的位置;而对于删除操作,如果被删除节点没有左子树或者右子树,则可以用它的另一棵子树来替代它,如果同时具有左右子树,则需要用它的直接后继来替代它,随后删除一定没有左子树的直接后继。
容易注意到,二叉搜索树的搜索操作非常类似`二分查找`,但是它存在的问题是每次比较的节点未必就是中间节点,即它的`平衡性`问题——在最坏的情况下,二叉搜索树退化为单链表,其搜索操作的时间复杂度也退化为`O(n)`。为了保持二叉搜索树的平衡性,需要增加一些结构上的限制条件,即平衡条件,这样的二叉搜索树就成为了平衡二叉搜索树。对于任何一种`BBST`,都需要关注两个问题,即平衡条件和失衡调整算法。
`AVL树`使用`平衡因子`作为它的平衡条件,`平衡因子`本身也只是一个局部性质,但是通过`平衡因子`可以推导证明整棵`AVL树`的平衡性,即对于任意高度为`h``AVL树`,节点数量`n >= fib(h + 3) - 1`,即`h = O(logn)`。为了维护`平衡因子`条件,需要对动态操作即插入和删除进行一些修改——无论是插入还是删除,都首先调用`BST`的插入和删除算法,随后再利用`3+4重构`对失衡的节点进行修正。需要注意的是,插入操作至多进行一次`3+4`重构,全树即可恢复平衡;而删除操作至多却需要`O(logn)`次局部的修复才能恢复平衡。
相对于`AVL树``Splay树`则显得更加潇洒——它并没有维护任何的平衡条件。伸展树的基本思想是`局部性原理`,简单地通过`双层伸展策略`,伸展树即可保证其所有操作的分摊时间复杂度为`O(logn)`。需要注意的是,伸展树单次操作的时间性能可能有很大的波动,因此不适用于对性能要求很高的场合。在伸展树的插入和删除操作中,也需要贯彻`局部性原理`——对于插入操作,可以首先对待插入的关键码进行查找,随后比较其与新的根节点之间的大小,从而将新的节点插入到根节点;对于删除操作,也是首先查找一次待删除关键码,将它伸展到根节点,随后再删除;为了找到新的根,可以对右子树调用一次`search(e)`,该查找必然会失败,却会将被删除节点的直接后继移动到根节点,并且新的根必然没有左子树,因此实现左右子树的合并。
`B树`是为了解决多级存储介质速度不匹配的问题而产生的一种数据结构。它的本质是`多路平衡搜索树`,实际上,将二叉树的多层次节点合并,即可构成一个`B树``超级节点`。对于`m``B树`,它的定义是除了根节点外,所有节点的分支数都介于$\left \lceil m/2 \right \rceil \sim m$的多路平衡搜索树,对于根节点,其分支数则介于$\left \lceil 2 \right \rceil \sim m$之间。`B`树的搜索仍然是仿照`BST`的策略进行,为了维护`B`的结构,需要对它的插入和删除算法进行一些分析。对于插入,有可能会导致内部节点的`上溢`,解决`上溢`的方法是对超级节点进行分裂(split),将`上溢`的超级节点一分为二,同时将它的中间值添加到父节点中,容易看出,这可能会将`上溢`传递给父节点,一旦`上溢`传递到根节点,`B树`的高度就会增加一个单位;对于删除操作,则可能导致`下溢`,解决`下溢`有两种方法,即`左顾右盼``合并`,其中`合并`操作有可能将`下溢`向父节点传递,一旦传递至根节点,会导致`B`树高度减小一个单位。
`红黑树`则具有更加奇怪的平衡条件(四条),实际上`红黑树``B树`具有非常紧密的联系——将红黑树的红色节点向上依附于黑色节点,就构成了一棵`2-4B树`,因此`红黑树`的黑色高度(black height, bh)即等于与之对应的`B树`的高度,这样以来`红黑树`的平衡性就不证自明了。对于`红黑树`的失衡调整算法的理解,关键也在于将它转化为等效的`B树`,通过`B树`的上溢和下溢算法调整来理解。
对于插入操作,可能会出现`双红缺陷`(double red),此时需要对被插入节点的叔父节点进行讨论,如果叔父为黑色节点,则做一次`3+4重构`即可,等效于`B树`中交换相邻的红黑节点的颜色;如果叔父的红色节点,则对应于`B树`中的`上溢`,此时只需要将祖父染成黑色,而将父节点和叔父节点都染成红色,即可在这一局部解决`双红缺陷`,但需要注意的是,`双红缺陷`可能在祖父节点再次出现。这对应了`B树`中解决`上溢``split`操作,分裂节点后可能导致上一层节点继续`上溢`
对于删除操作,可能会出现`双黑缺陷`(double black),对应了`B树`中的`下溢`。为了解决`下溢`,首先需要`左顾右盼`,因此首先要找到下溢节点的邻居节点,记为`s`。当下溢节点的兄弟节点为黑色时,`s`即为该兄弟节点,如果`s`有红色的孩子,意味着从`s`借一个孩子即可解决`双黑缺陷`,对应了红黑树中对`s`的红色孩子进行一次`3+4重构`;如果`s`全是黑色的孩子,则无节点可借,为此只能进行节点的合并。如果父节点`p`为红色,则意味着上层超级节点有多余的节点可供合并,合并后并不会引起更高层的`下溢`,这对应了红黑树中将`p``s`交换颜色;如果父节点`p`为黑色,意味着上一层的超级节点则在`下溢`的边缘,合并后必然引起`下溢`向高层传递,这对应了红黑树中将`s`染红,然后`双黑缺陷`向上传递两层,即假想地认为刚刚删除了`p`的黑色父亲。
上面的讨论都是基于`s`就是被删除节点的兄弟节点,而如果`x`的兄弟节点是红色,`s`应该是`x`的兄弟节点的孩子节点,此后还需要对该孩子节点进行类似于上面的讨论,未免过于复杂。为了对问题进行简化,不妨简单地将`s``p`进行一次单旋转,并且交换它们的颜色,这对应了`B树`中将超级节点中的`s``p`交换颜色,此时虽然并没有解决`双黑缺陷`,可是这样交换以后,`s`必为黑色节点了,并且`p`是红色节点,因此问题就转化成了上面讨论过的第一种或者第二种情况。
容易看到,对于红黑树的失衡调整算法,一旦进行了一次结构调整,全树必然恢复平衡,因此红黑树的每次的结构调整量仅为`O(1)`,而`AVL树`只有插入的调整才能做到这点,其删除算法至多会进行`O(logn)`次结构调整。
`k-d树`(k-dimentional tree)是为了支持高效的多维数据`范围查询`问题而产生的,它的关键在于构造算法和查询算法。`kd树`的构造是依次按照不同的维度选择该维度的中位点对k维空间进行切分直至每个小区域内部只有一个节点。为了进行多维数据的`范围查询`,则需要递归地判断查询区间与当前区域的关系,即包含,不相交或是相交,只有在相交的情况下才需要递归地进行查询。
## 什么是二叉搜索树(BST, Binary Search Tree)