读《算法——符号表》有感——符号表之从链表到红黑二叉查找树

读《算法(第4版)》——符号表之从链表到红黑二叉查找树

什么是符号表?说人话就是,一系列的key-value结构,e.g.PHP的关联数组、JAVA的map

用什么数据结构表示,以及存储和查询的效率如何都是研究的关键。当然,笔者作为一个渣渣,简便地理解其中的奥义更重要。

在日常使用中,查询key对应的value比存储key-value更频繁,所以一般情况下,查询效率比存储效率要重要。

先从最简单的实现讲起,链表。

链表中的节点存储这个key-value结构

struct Node

{

  type key;

  type value;

  Node next;

}

e.g.将S、E、A、R、C、H、E、X、A、M、P、L、E这些键放入符号表,值默认为1,如果该键已存在在符号表中则修改那个已存在的节点,使其值+1

得到 key/value->S/1->E/3->A/2->R/1->C/1->H/1->X/1->M/1->P/1->L/1

在插入的过程中,总是需要遍历一遍链表来确定链表中不存在这个key的节点,然后在链表末端添加新的节点

此外,如果查询该符号表不存在的键,例如我想查询键Z对应的值,但该符号表中不存在键Z。查询必然导致链表的遍历。

当链表长得令人发指的时候,遍历链表耗时会十分严重。

大部分查找算法会要求按顺序排列,于是我们要求键按ASCII码顺序

上面的链表优化后会变成下面这个样子

key/value->A/2->C/1->E/3->H/1->L/1->M/1->P/1->R/1->S/1->X/1

键有序化,能使比较操作递推下去。说人话就是,A<B && B<C  => A<C

首先能大大缩短插入的时间,例如我想在上述链表中插入B这个键,当遍历操作走到键C(第一个大于待插入键的键)这个节点的时候就知道后面不会有键B了,就可以在键C前插入键B

查询时间也能缩短,例如我想查询F这个键,当遍历操作走到键H(第一个大于查询键的键)这个节点的时候,就知道后面不会有键F了,所以链表中不存在键F的节点

但问题依然很尖锐,当链路很长的时候需查询的键在链表末端,依然会耗费很长时间,使用双向链表也是治标不治本,而且理论上双向链表的比较操作是单向链表的两倍。

在学习链表和数组的时候,我们总会比较这两者的优缺点。我们知道数组的一大优势就是查询快速,能迅速定位对应下标的值。(注意,这里提到的数组是JAVA、C中的纯索引数组)

那用数组实现符号表可以吗?

当然可以,不过要2个数组,一个用于存key,一个用于存value,key-value关系通过数组的下标维系。

大概长这个样子

KeyArr

下标

0

1

2

3

4

5

6

7

8

9

A

C

E

H

L

M

P

R

S

X

ValueArr

下标

0

1

2

3

4

5

6

7

8

9

2

1

3

1

1

1

1

1

1

1

都用上数组了,不会还有小傻瓜一个一个遍历找key吧 [Doge],二分查找搞起来呀,能有效减短找key的平均时间。

但是,往数组中插入元素导致后续元素移位的问题是硬伤,数组本身无法避免。也就是说当这个由数组构成的符号表庞大到一定程度的时候,每添加一对key-value都要耗费大量时间。

2种最基础的数据结构对符号表的实现都不尽人意,就要考虑树了。基于分治思想,树无论是插入节点,还是查询,都有两把刷子。

二叉查找树:每个结点的键都大于其左子树中的任意结点的键,每个结点的键都小于其右子树中的任意结点的键 的二叉树。

struct Node

{

    type key;

    type value;

    Node left;

    Node right;

    //int N; 该结点为根结点的树的结点数量

}

因为

每个结点的键都大于其左子树中的任意结点的键,每个结点的键都小于其右子树中的任意结点的键,

所以

对二叉查找树的中根遍历总是键有序!!!

基于二叉查找树“左子树小右子树大”的特点,在完全二叉查找树的理想情况下,最坏查询时间级为O(lgN)

P.S.这里的自然对数不是以10为底,而是以2为底,因为“二叉”[Doge]

这里强调理想情况是因为,有的树长得枝繁叶茂,有的树长得像链表。

例如下图中2个都是二叉查找树,但因为插入顺序不同,长成了截然不同的样子。应验了那句话,人同妖都喺阿妈生嘅,......——罗家英《大话西游》。

根据前面对由链表构成的符号表的描述,我们就能大概知道这条长得像链表的二叉查找树的插入和查询效率有多差。

那如何让二叉查找树长得像树呢?(说人话就是,如何减少树的高度?)

那就增加树的高度利用率,也就是树的每一层都塞满节点。毫无疑问,完全二叉树是利用率最高的,因为每一层都塞满了节点。但完全二叉树要求节点数量为2^k-1 (k为深度),如果节点数量不符合2^k-1就无法实现完全二叉树。

这时候我们引出一个骚操作,一个节点存2对key-value, 三个子树,其中中间的子树的key取值范围为该节点的2个key之间。

大概长下图{EJ}节点这个样子

《算法》称这种存2对key-value和3个子树的节点为3-结点,存放一对key-value和2个子树的节点为2-结点。借助这个骚操作,就总是能通过2-结点和3-结点共同使用搭建完全二叉树。这种树称为2-3查找树。

2-3查找树的难点在于插入操作,

①往树的2-叶子结点插入结点,可以将该叶子结点转换成3-结点,树的结点数量没有变哦。

②往树的3-叶子结点插入结点,这里引入一个临时的数据结构4-结点,存放3对key-value和4个子树,4-结点是可以轻易转变成3个2-结点的

为了不破坏完全二叉树,我们需要将由4-结点转变成的2-结点的“中键”往父级节点塞。如果父级节点也是3-结点,那就往爷爷结点塞,以此类推直到根结点,根结点的4-结点可以放心地转换为3个2-结点,不会破坏完全二叉树哦。

2-3树的高度被控制在最低高度,插入和查询效率杠杠的。但是,一棵树要维护2-结点和3-结点2种结点的数据结构,将结点从一种数据结构转换成另一种数据结构(往2-结点插入新的key-value,会将2-结点转变成3-结点)等很多操作都是劝退开发者的,实现2-3查找树代码量会超级多。

接下来,我们换个角度看3-结点。3-结点太复杂,那我们用2-结点表示它。怎么表示呢?看下图,原3-结点变成了用红链接相连的两个2-结点。

附上两种链接的含义

红链接:连接两个结点表示3-结点

黑链接:2-3树中的普通链接

对于任意的2-3树,只要对结点进行转换,我们都可以立即派生出一棵对应的二叉查找树,我们将用这种方式表示2-3树的二叉查找树称为红黑二叉查找树 (简称红黑树)

因为红黑树是由2-3树加“红线”生成的,所以抛开红链接不看,红黑树的黑链接是完美平衡的(任意空链接到根节点的路径上的黑链接数量相同)

红黑树的相关知识后续还要再补一补课。肚子里没墨水,开口就只剩粗话。

大学的时候,教数据结构的老师意气风发,从二叉查找树讲到红黑树,中间好像没有2-3树过渡(也可能是我当年睡着了)。虽然期末考试不考,但从那天开始,“红黑树”三个字好像就成了我的数据结构认知中的噩梦,每次看见它的反应就是避而远之。

写于 2021-08-15

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值