diff --git a/zh/9.md b/zh/9.md index 4b01a8dd76a4519ff8e69a452daf88ff0c633b89..ff2057102e880823b421eeb3d9622d331e04b70e 100644 --- a/zh/9.md +++ b/zh/9.md @@ -16,7 +16,7 @@ 5. 所有空孩子都出现在树的同一层。 图9.1包含一个4阶排序树的示例。在实际实现中,B-树一般用在在二级存储(如磁盘等)上,根据实际需要读入它们的节点。我们选择m阶树(多叉数)的目的是尽可能快地从二级存储器传输数据。对于磁盘操作而言更关注的是单次磁盘访问所需要时间越小越好, -因此m的值范围越大,从不同节点中读取的时间差异越小。在这种情况下,**m**取的值过小很明显不是一个很好的主意。 +因此m的值范围越大,从不同节点中读取的时间差异越小。在这种情况下,**m**取的值过小很明显不是一个很好的主意。__ 我们将用一个被称为**BTreeNode**的结构来表示B-树的节点,并且有如下的定义: **B.child(i)**:B-树节点**B**的第*i*个孩子,**0 <= i < m**。 @@ -329,11 +329,217 @@ class InnerTrie extends Trie { 如此精密的封装是有代价的:这将使插入操作变得很复杂。当向现有节点添加新的子节点时,所需的槽可能已被其他数组使用,首先从打包的存储区域中删除非空条目,从而有必要将节点移动到新位置。找到另一个点并将其条目移动到该点,最后更新指向父节点中正在移动的节点的指针。有一些方法可以消除这种情况,但我们不会在这里讨论它们。 ## 9.3 旋转自平衡树 +另一种二叉树平衡的方法是通过选择一个新的根节点,将子树高度较深一侧的节点移动到子树高度较低的一侧从而达到平衡,并且依然保持二叉搜索树的属性。 +最简单的实现方式就是树的**旋转(rotations)**。图9.11展示了两棵节点元素集合相同的二叉搜索树。首先考虑右旋转(左旋转是镜像操作)。首先,旋转之后依然需要保证二叉树的属性,在未旋转的树中,A节点小于右侧的B节点,D节点大于 +B节点,而且右子树C节点也是大于B节点的。你需要保证,在进行旋转后D节点下面的子节点仍然保持二叉树的性质。 +![figure_9_9](https://github.com/Abel-Huang/cs61b-textbook-zh/blob/master/zh/img/figure_9_9.jpg) + 图9.9:使用压缩字典树的数据结构。 + + 让我们使用符号$\H_A$,$\H_C$,$\H_E$,$\H_B$和$\H_D$来表示子树A,C和E以及根是节点B和D的子树的高度。子树A,C,E可以为空,这种情况下我们将其高度设为-1。左边树的高度为1+max($\H_E$, 1 + $\H_A$, 1 + $\H_C$ )。右边树的高度为1+max($\H_A$, 1 + $\H_C$, 1 + $\H_E$ )。因此,只要$\H_A$ > max($\H_C$ + 1, $\H_E$)(例如,在左倾树中就会出现这种情况),右侧树的高度将小于左侧树的高度。另一个方向也是相似的情况。 + + 实际上,可以通过旋转将任何二叉搜索树转换为任何其他包含相同键的二叉搜索树。这相当于通过旋转,我们可以将二叉搜索树的任何节点移动到树的根节点,同时保留二叉搜索树属性(为什么这是一个充分条件?)。这个论点是对树结构的归纳。 + * 对于空树或者只有根节点的树显然是成立。 + * 对于一个更大的树,我们假设可以旋转所有较小的树以使它们的任何节点成为根节点,进行如下假设: + * 如果我们期望的新的根节点已经是根节点,那么我们就完成该操作; + * 如果我们期望的新的根节点在左子树(归纳假设),然后在整个树上执行右旋转; + * 如果该节点在右子树会进行类似的操作。 ### 9.3.1 AVL树 +当然,知道通过旋转可以重排列二叉搜索树,但是我们并不知道如何具体进行旋转操作。AVL树是该技术的一种具体示例,用于跟踪子树的高度并在它们差距过大时执行旋转操作。AVL树是一种二叉搜索树,并且满足**AVL属性** :任意节点的左子树和右子树的高度差小于1。 + +在这种树的底部增加或者删除节点(正如6.1节点简单二叉搜索树的插入和删除操作)会破坏所谓的AVL属性,但是可以通过从插入或删除点开始向根处理并执行某些选定的旋转来恢复,这取决于需要纠正的不平衡的性质。在下面的图示中,子树中的表达式代表它们的高度。 + +![figure_9_A](https://github.com/Abel-Huang/cs61b-textbook-zh/blob/master/zh/img/figure_9_A.jpg) + +一个不平衡的子树可以通过单次做旋转重新平衡,如下图所示的AVL树形式: + +![figure_9_B](https://github.com/Abel-Huang/cs61b-textbook-zh/blob/master/zh/img/figure_9_B.jpg) + +最后我们在考虑一个不平衡的二叉树的示例 + +![figure_9_C](https://github.com/Abel-Huang/cs61b-textbook-zh/blob/master/zh/img/figure_9_C.jpg) + +$\h^'$和$\h^''$中至少有一个是h,并且另外一个也是h或者h-1。这里我们可以通过两次旋转实现平衡,首先是在节点C进行右旋转,然后在节点进行左旋转,得到正确的AVL树 + +![figure_9_D](https://github.com/Abel-Huang/cs61b-textbook-zh/blob/master/zh/img/figure_9_D.jpg) + +另外可能的通过增加或者删除单个节点导致的重新平衡是上述的镜像操作。 + +因此,我们跟踪所有子树的高度,我们总是可以恢复AVL属性,从树的底部插入或删除的点开始并向上进行旋转实现平衡。事实上证明,我们没有必要知道所有子树精确的高度,仅仅需要每个节点属于三种情况:两棵子树有相同的高度,左子树比右子树高度高一层,或者左子树比右子树高度低一层。 ## 9.4 伸展树 +旋转允许我们在保持二叉树的属性的前提下,尽可能的将二叉树的节点移动到离根节点更近的位置。因此,至少我们可以在不平衡树中使用旋转来快速查找经常搜索的关键字。事实证明,我们可以做的更好。伸展树(Splay Tree)是一种自调整二叉搜索树,即使不通过改变树内容的操作也可以调整其结构以加速后续操作。这种数据结构具有一些有趣的特性,即某些单独的操作可能花费$\Theta(N)$时间(对于树中的N个节点),但是对于K个同样操作的均摊时间成本(参考1.4节点)仍然是$\Theta(\lgK)。此外,它是基础(不平衡)二叉搜索树的特别简单的修改。 + +不出所料,此树中的定义操作是splaying,以特定方式将特定节点旋转成根节点。伸展节点意味着使用一系列展开步骤,以便将节点置于树的顶部。伸展具体有三种类型: + + 1. 给定节点**t** **y** 是其子节点之一,围绕**t** 旋转**y** (即,根据需要向左或向右旋转,以使**y** 到达顶部)。 + 2. 给定一个节点**t** ,它的一个子节点**y** 和**y** 的子节点**z** 在同一侧,然后绕**y** 旋转**z** 。 + 3. 给定一个节点**t**,它的一个子节点**y** 和**y** 的子节点**z** 在不同侧,先围绕**y**旋转**z**,然后绕**t**旋转。 + +图9.12展示了这具体的三种步骤。 + +我们通常会从二叉搜索树根节点开始的遍历查找给定值,从而找到接受此操作的节点。要了解这种特定操作背后的动机,请考虑图9.13。 +图左侧的树是典型的最坏时间复杂度的二叉搜索树。在展开节点0之后,我们得到图右侧的树,其大约是前者的一半高度。确实,我们必须进行7次旋转以伸展此节点,但是为了在左侧创建树,我们进行了8次常数时间的插入,因此(到目前为止),所有9次操作的摊销成本(8次插入加1次))每个只有2个左右。 + +从普通二叉搜索树中搜索,插入和删除的操作都涉及搜索包含特定值的节点,或者尽可能接近特定值的节点。在伸展树中,找到此节点后,我们将其展开,将其带到树的根部并重新组织树的其余部分。为此,可以方便地修改用于在BST中搜索值的通常算法,以便它同时适用于伸展树。图9.15显示了一种在BST类型的树上可能的实现方式,如图9.14所示。这种特殊类型提供了旋转和替换将执行左或右操作的子节点的操作,允许我们将案例嵌套折叠成几个。 + +**splayFind** 函数是我们可以用来在二叉搜索树上实现通用操作的工具,如图9.16和9.17所示。 + +### 9.4.1 伸展树分析 +创建非常不平衡的伸展树很容易。按顺序向树中插入数据项就可以实现。因此,将按顺序搜索树中的所有数据项。因此,如果N是树中节点(及关键字)的数量,则隔离的任何特定操作的代价是θ(N)。但是,你永远不会在一棵大树上执行单个操作;毕竟,你必须先创建树,而这当然必须花费至少与其大小成比例的时间。因此,如果我们要求在整个序列上分摊操作的时间,我们可能会得到不同的结果。在本节中,我们将展示实际上在伸展树上搜索,插入和删除操作的分摊时间界限为O(lgN),就像其他平衡二叉树的最坏情况边界一样。 + +为此,我们首先在树上定义一个势函数,如§1.4所述,它将跟踪我们执行了多少便宜(和不平衡)操作,从而表明我们可以花多少时间在 昂贵的操作上,同时仍然保证一系列操作的总累积代价保持在适当的限制内。正如我们在公式1.1所做的,我们定义序列中第i个操作的摊余成本ai为 + +ci为实际的开销,φk是第k次操作之前数据结构中“存储电位”的数量。对于ci,我们可以很方便用旋转执行的次数来表示,当一个操作不涉及旋转时用1表示。这为我们提供了与实际工作量成比例的ci值。挑战是我们要找到一个φ允许我们吸收ci的峰值;当ai>ci时,我们保存φ中的“操作信用”并在ci变大的步骤中释放它(导致φ(i + 1) <φi)。 为了合适,我们必须一直确保φi>φ0。 + +对于包含一组节点的伸展树T,我们将使用它作为我们的势函数 + +其中s(x)是以x为根的子树的大小(节点数)。r(x) = lgs(x)称为x的等级。 因此,对于图9.13左侧的完全线性树,φ = P1≤i≤8lg i = lg8!≈15.3,而该图右侧的树具有 φ=4lg1+lg3+lg5+lg7+lg8≈9.7,表明通过降低电位来大大抵消了展开0的成本。 + +每个操作(搜索,插入或删除)中的所有工作都在splayFind中进行,因此分析它就足够了。我声明在根为t的树中查找和展开节点x的摊销代价 ≤3(r(t)-r(x))+1。因为t是根,我们知道r(t)≥r(x)≥0。此外,由于s(t)=N,N为树中节点的数量,证明展开的摊销代价必须为O(lgN),如期望的那样。 + +我们用C(t,x)表示在以t为根的树中查找并展开节点x的分摊代价。即: + C(t, x) = max(1, 执行的旋转数) + + 树的最终电位 − 树的初始电位. +我们按照图9.15中程序的结构递归地进行,以表明 + C(t, x) ≤ 3(r(t) − r(x)) + 1 = 3 lg(s(t)/s(x)) + 1. (9.1) + +我们很方便使用符号s'(z)表示“在展开步骤结束时s(z)的值”,且r'(z)表示“r(z)在结束处的值"。 + + 1. 当t是空树或v在它的根时,没有旋转,电位没有变化,我们将实际成本设为1。在这种情况下,断言9.1显然是正确的。 + 2. 当x=y是t的孩子时(“zig”情况,如图9.12顶部所示),我们执行一次旋转,总实际代价为1.为了计算电位的变化,我们首先注意到我们必须考虑的节点只有t和x,因为所有其他节点的等级不会改变,因此当我们从旧节点中减去新的电位时会取消。因此,电位的变化是 + r′(t) + r′(x) − r(t) − r(x) + = r′(t) − r(x), since r′(x) = r(t) + < r(t) − r(x), since r′(t) < r(t) + < 3(r(t) − r(x)), since r(t) − r(x) > 0 + 因此,加上一个旋转的开销,摊销代价 <3(r(t) - r(x))+ 1。 + 3. 在zig-zig的例子中,开销包括首先将x展开为t的孙子(图9.12的第二行中的节点z),然后执行两次旋转。根据假设,第一个展开步骤的摊余代价是C(z,x)≤3(r(z)-r(x))+ 1 (r(z)是在展开到z的前一个位置之后的x的等级,因为展开不会改变树的根的等级。我们会稍微滥用符号并将此展开后的节点x称为z,这样我们仍然可以使用r(x)作为x的原始等级)。旋转的代价是2,并且由这两个旋转引起的电位的变化仅取决于它在t,y和z的等级中引起的变化。 总结这些,这个例子的摊销成本是 + C(t, x) = 2 + r′(t) + r′(y) + r′(z) − r(t) − r(y) − r(z) + C(z, x) + = 2 + r′(t) + r′(y) − r(y) − r(z) + C(z, x), since r′(z) = r(t) + ≤ 2 + r′(t) + r′(y) − r(y) − r(z) + 3(r(z) − r(x)) + 1 + 通过归纳假设 + = 3(r(t) − r(x)) + 1 + 2 + r′(t) + r′(y) − r(y) + 2r(z) − 3r(t) + 所以我们想要的结果如下 + 2 + r′(t) + r′(y) − r(y) + 2r(z) − 3r(t) ≤ 0. (9.2) + 9.2展示如下: + 2 + r′(t) + r′(y) − r(y) + 2r(z) − 3r(t) + ≤ 2 + r′(t) + r(z) − 2r(t) + since r(y) > r(z) and r(t) > r′(y). + = 2 + lg(s′(t)/s(t)) + lg(s(z)/s(t)) + 通过r的定义和lg的属性。 + 现在,如果你在图9.12的zig-zig情况下检查树,你可以看到s'(t)+ s(z)+ 1 = s(t),所以s'(t)/s(t)+s(z)/s(t)<1。因为lg是一个凹的,增加的函数,这反过来告诉我们(如§1.6中所讨论的), + 2 + lg(s′(t)/s(t)) + lg(s(z)/s(t)) ≤ 2 + 2 lg(1/2) = 0. + 4. 最后,在zig-zig的例子中,如果我们能够证明上面的不等式9.2,我们再次得到想要的结果。 这次,我们有s'(y)+s'(t)+1 = s(t),所以我们可以继续 + 2 + r′(t) + r′(y) − r(y) + 2r(z) − 3r(t) + ≤ 2 + r′(t) + r′(y) − 2r(t) + since r(y) > r(z) and r(t) > r(z). + = 2 + lg(s′(t)/s(t)) + lg(s′(y)/s(t)) + 结果遵循与zig-zig例子相同的推理。 +从而结束了证明。 + +插入和搜索的操作为展开时间增加了一个常数时间,删除时增加了常量和常数因子2(因为它涉及两个展开操作)。因此,对展开树的所有操作都具有O(lgN)的摊销时间(对于任何给定的操作序列,使用N为最大值)。 + +这种界限实际上是悲观的。正如我们所见,遍历有序树要花费树大小的线性时间。由于展开树只是一个BST,我们可以得到相同的界限。如果我们在遍历它们时将每个节点展开到根(这对于伸展树来说似乎是自然的),我们的分摊界限是O(NlgN)而不是O(N)。不仅如此,在遍历之后,我们的树将被转换为“串状”链表。然而,奇怪的是,可以证明,当遍历每个节点时,顺序遍历BST的成本实际上是O(N)。 然而,作者认为他已经将这个问题打败了,并且会为您提供详细信息。 + +## 9.5 跳表 +B树是搜索树的一个例子,其中节点具有可变数量的子节点,每个子节点代表一些有序的键集。它通过将每个节点上的键细分为不相交的键范围来加速搜索,并且设法使这些序列具有可比较的长度,从而平衡树。在这里,我们看看另一个结构做了很多相同的事情,除了它根据需要使用旋转来近似平衡树,它只是以高概率而不是确定性实现这种平衡。考虑图9.1中的同一组整数键,排列在搜索树中,其中每个节点都有一个键和任意数量的子节点,并且任何节点的子节点都具有至少与其父节点一样大的键。图9.18显示了一种可能的安排。 键出现的最大高度根据规则独立地选择,该规则给出出现在高度k(0为底部)的键的(1 - p)p^k的概率。也就是说,0 < p < 1是任意常数,其表示高度大于等于e的所有高度的大于等于e的节点的大致比例。我们在左边添加了一个最小的(-∞)键高度作为整棵树的根。图9.18显示了使用p = 0.5创建的示例。为了查找一个关键字,我们可以从任何级别开始从左到右扫描此树并向下工作。从底部开始(0层)只是给我们一个简单的线性搜索。在更高层级,我们搜索树木森林,根据其根节点的值选择更密切地检查哪个森林。例如,要查看127是否在其中,我们可以看: + * 0级的前15个条目(不包括-∞)[15个条目]; 或者 + * 前7个1级条目,然后是120个[9个条目]下面的2个0级项目; 或者 + * 前3个2级条目,然后是1级条目140,然后是120个[6个条目]下的2个0级项目; 或者 + * 等级3条目90,然后是等级2条目120,然后是等级1条目140,然后是低于120 [5个条目]的2个等级0项目。 + +我们可以将这个树表示为一种有序的线性节点列表(见图9.19),其中节点具有随机数的下一个链接,并且每个节点中的第i个下一个链接(从0开始编号)连接到 具有至少i + 1个链接的下一个节点。这个类似列表的表示,有一些链接“跳过”任意数量的列表元素,这个数据结构被称为:跳跃链表,简称跳表。 + +搜索非常简单。如果我们将这些节点之一的值表示为**L.value**(这里,我们将使用整数键),并将高度为k的下一个指针表示为**L.next [k]**,则: + +```java + /** True iff X is in the skip list beginning at node L at + * a height <= K, where K>=0. */ + static boolean contains (SkipListNode L, int k, int x) { + if (x == L.next[k].value) + return true; + else if (x > L.next[k].value) + return contains (L.next[k], k, x); + else if (k > 0) + return contains (L, k-1, x); + else + return false; + } +``` + +我们可以在k≥0的任何级别开始直到树的高度。 事实证明,开始包含N个节点的列表的合理位置是$\log(1/p)*N$,如下所述。 + +要在列表中插入或删除,我们可以找到上述过程要添加或删除的节点的位置,跟踪我们遍历的节点。 添加或删除项目时,这些是可能需要更新其指针的节点。当我们插入节点时,我们随机选择一个高度,使得高度为k + 1的节点数大致为pk,其中p是一些概率(典型值可能是0.5或0.25)。也就是说,如果我们正在操作一个n层的搜索树,我们让p = 1 / n。 合适的过程可能如下所示: + +```java + /** A random integer, h, in the range 0 .. MAX such that + * Pr(h ≥ k) = Pk, 0 ≤ k ≤ MAX. */ + static int randomHeight (double p, int max, Random r) { + int h; + h = 0; + while (h < max && r.nextDouble () < p) + h += 1; + return h; + } +``` +一般来说,容纳任意大的高度是没有意义的,所以我们施加一些最大值,通常是人们期望需要的最大关键字个数的对数(基数1/p)。直观地,任何其高度至少为k的M个插入节点的序列将围绕每个1/p个节点随机地断开一个高度严格大于k的节点。同样,对于高度至少为k+1的节点,依此类推。 因此,如果我们的列表包含N个数据项,并且我们开始查看$\log(1/p)*N$层,我们期望看大多数(1/p)$\log(1/p)*N$个关键字(也就是说,每个$\log(1/p)*N$级别的1/p个关键字)。换句话说,θ(lgN)平均来说,这就是我们想要的。不可否认,这种分析有点过时,但真正的差距并没有很大。由于插入和删除包括查找节点,加上与节点高度成比例的一些插入或删除时间,因此我们实际上在搜索,插入和删除时具有θ(lgN)预期界限。 + +## 练习 + +**9.1** 根据注释完善代码 + +```java + /** Return a modified version of T containing the same nodes + * with the same inorder traversal, but with the node containing + * label X at the root. Does not create any new Tree nodes. */ + static Tree rotateUp (Tree T, Object X) { + // FILL IN + } +``` + +**9.2** 包含N个节点的5-B树的最大高度是多少? 最小高度是多少? 什么序列的关键字获得最大高度(即,给出这些序列的一般特征)。 什么序列的关键字获得最小高度? + +**9.3** 图9.15中给出的splayFind算法几乎不是人们可以想象的这个过程的最有效版本。 原始论文具有相同函数的迭代版本,它使用常量额外空间而不是我们的splayFind版本的线性递归。 它跟踪两棵树:L,包含小于v的节点,R,包含大于v的节点。当它从根向下迭代地向下进行时,它将当前节点的子树添加到L和R,直到它 到达它正在寻找的节点x。 此时,它通过将x的左右子树分别附加到L和R,然后将L和R作为新子项来完成。 在此过程中,子树附加到L以便增加标签,并按照标签减少的顺序附加到R. 重写splayFind以使用此策略。 + +**9.4** 为跳表编写包含函数的非递归版本(第9.5节)。 + +**9.5** 定义使用跳表表示的SortedSet接口的实现。 + +![figure_9_10](https://github.com/Abel-Huang/cs61b-textbook-zh/blob/master/zh/img/figure_9_10.jpg) + +图9.8中的trie的打包版本。 该图中的每个trie节点表示为由字符索引的子节点数组,作为子节点索引的字符存储在上面的行中(对应于数组**edgeLabels**)。 指向子项本身的指针位于下一行(对应于**allKids**数组)。 顶部的空框表示未使用的位置(**NOEDGE**值)。 为了压缩图表,我改变了字符集编码,使□为0,'a'为1,'b'为2,等等。下排的交叉框表示空节点。右侧(未示出)还必须有另外24个空条目记录存储的最右边的trie节点的c-z条目。 搜索算法使用edgeLabel来确定条目实际上何时属于它当前正在检查的节点。 例如,根节点应包含“a”,“b”和“f”的条目。事实上,如果您从上面的“根”框中算出1,2和6,您将找到边标签为'a','b'和'f'的条目。另一方面,如果从根盒中计算超过3,查找不存在的'c'边缘,则会找到边缘标签'e',告诉您根节点没有'c'边缘。 +![figure_9_11](https://github.com/Abel-Huang/cs61b-textbook-zh/blob/master/zh/img/figure_9_11.jpg) + +二叉搜索树中的旋转。 三角形表示子树,圆圈表示单个节点。 二进制搜索树关系由两个操作维护,但各个节点的级别受到影响。 + +![figure_9_12](https://github.com/Abel-Huang/cs61b-textbook-zh/blob/master/zh/img/figure_9_12.jpg) + + 基本的展开步骤。 当y在t的另一侧时,存在镜像图像。 最后一行说明了完整的splaying操作(在节点3上)。 从底部开始,我们执行“zig”步骤,然后执行“zig-zig”,最后在节点3上执行“Zig-zag”,最后是右侧的树。 + +![figure_9_13](https://github.com/Abel-Huang/cs61b-textbook-zh/blob/master/zh/img/figure_9_13.jpg) + +在完全不平衡的树中伸展节点0。 结果树大约变成原高度的一半,这样大大加快了后续搜索速度。 + +![figure_9_14](https://github.com/Abel-Huang/cs61b-textbook-zh/blob/master/zh/img/figure_9_14.jpg) + +用于我们的伸展树的二叉搜索树结构。 这只是一个普通的二叉搜索树,为旋转或更换孩子节点提供统一的操作。 + +![figure_9_15](https://github.com/Abel-Huang/cs61b-textbook-zh/blob/master/zh/img/figure_9_15.jpg) + +用于查找和展开节点的**splayFind** 方法。 用于插入,删除和搜索。 + +![figure_9_16](https://github.com/Abel-Huang/cs61b-textbook-zh/blob/master/zh/img/figure_9_16.jpg) + +伸展树上的标准集合操作。 该接口采用Java集合类的样式。 图9.17说明了这些方法。 + +![figure_9_17](https://github.com/Abel-Huang/cs61b-textbook-zh/blob/master/zh/img/figure_9_17.jpg) + +展开树上的基本BST操作。(a)是原始树。(b)是对值21或24中的任何一个执行**splayFind**的结果。(c)是将值21加到(a)中的结果; 第一步是创建展开的树(b)。(d)是从原树(a)中删除24的结果; 再次,第一步是创建(b),之后我们将24的左子项显示为值24,这保证大于该子项中的任何值。 + +![figure_9_18](https://github.com/Abel-Huang/cs61b-textbook-zh/blob/master/zh/img/figure_9_18.jpg) + +跳过列表的抽象视图,显示其与(非二进制)搜索树的关系。 除-∞之外的每个密钥都复制到随机高度。 我们可以从任何级别开始搜索这个结构。 在最好的情况下,要搜索(失败)目标值127,我们只需要查看着色节点中的键。 较暗的阴影节点表示大于127的键绑定搜索。 -### 9.4.1 分析 +![figure_9_19](https://github.com/Abel-Huang/cs61b-textbook-zh/blob/master/zh/img/figure_9_19.jpg) -## 9.5 跳表 \ No newline at end of file +图9.18中的跳过列表,显示了可能的表示。 数据结构是一个有序列表,其节点包含指向后面节点的随机数量的指针(允许在搜索期间跳过列表中的中间项;因此名称)。 如果一个节点至少有k个指针,那么它包含一个指向下一个节点的指针,该节点至少有k个指针。 右边的∞节点允许我们避免测试null。 同样,在搜索127期间查看的节点是阴影; 较暗的阴影表示限制搜索的节点。 \ No newline at end of file diff --git a/zh/img/figure_9_10.jpg b/zh/img/figure_9_10.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f002d79822426944241529fd3935cf18e8276a78 Binary files /dev/null and b/zh/img/figure_9_10.jpg differ diff --git a/zh/img/figure_9_11.jpg b/zh/img/figure_9_11.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7fd8a23c38d7eb455bf10a2df46ef54b923fc914 Binary files /dev/null and b/zh/img/figure_9_11.jpg differ diff --git a/zh/img/figure_9_12.jpg b/zh/img/figure_9_12.jpg new file mode 100644 index 0000000000000000000000000000000000000000..74abe5a6e41fde6f88599f40f9914ef893a3a828 Binary files /dev/null and b/zh/img/figure_9_12.jpg differ diff --git a/zh/img/figure_9_13.jpg b/zh/img/figure_9_13.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3922a310ffcafeb9def17db61e46397434976682 Binary files /dev/null and b/zh/img/figure_9_13.jpg differ diff --git a/zh/img/figure_9_14.jpg b/zh/img/figure_9_14.jpg new file mode 100644 index 0000000000000000000000000000000000000000..04e6e934d4536386b41a0e54f17e8fa131176421 Binary files /dev/null and b/zh/img/figure_9_14.jpg differ diff --git a/zh/img/figure_9_15.jpg b/zh/img/figure_9_15.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7337ebfebed0005e00bfe595691732dfaaa11d88 Binary files /dev/null and b/zh/img/figure_9_15.jpg differ diff --git a/zh/img/figure_9_16.jpg b/zh/img/figure_9_16.jpg new file mode 100644 index 0000000000000000000000000000000000000000..86cc6a9ae3158da12c74ab63190bc4caa7f9b29b Binary files /dev/null and b/zh/img/figure_9_16.jpg differ diff --git a/zh/img/figure_9_17.jpg b/zh/img/figure_9_17.jpg new file mode 100644 index 0000000000000000000000000000000000000000..15c01d91546b471f398c9286600e772ccb8fdb1f Binary files /dev/null and b/zh/img/figure_9_17.jpg differ diff --git a/zh/img/figure_9_18.jpg b/zh/img/figure_9_18.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a2e374760293ea29be72150852926740daf2668e Binary files /dev/null and b/zh/img/figure_9_18.jpg differ diff --git a/zh/img/figure_9_19.jpg b/zh/img/figure_9_19.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5ba44cc62d2cbe40950d1cef81389375ad01bd79 Binary files /dev/null and b/zh/img/figure_9_19.jpg differ diff --git a/zh/img/figure_9_9.jpg b/zh/img/figure_9_9.jpg new file mode 100644 index 0000000000000000000000000000000000000000..570011c47f9d88305339c14931689453d2ac43c2 Binary files /dev/null and b/zh/img/figure_9_9.jpg differ diff --git a/zh/img/figure_9_A.jpg b/zh/img/figure_9_A.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c79e7479d30ce5436a618fcca53e262008c8fad6 Binary files /dev/null and b/zh/img/figure_9_A.jpg differ diff --git a/zh/img/figure_9_B.jpg b/zh/img/figure_9_B.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f33c46e918725d698662a27c03145452f82235a2 Binary files /dev/null and b/zh/img/figure_9_B.jpg differ diff --git a/zh/img/figure_9_C.jpg b/zh/img/figure_9_C.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6a3813ed7894c57553d7d9793b718e3a0f2aff32 Binary files /dev/null and b/zh/img/figure_9_C.jpg differ diff --git a/zh/img/figure_9_D.jpg b/zh/img/figure_9_D.jpg new file mode 100644 index 0000000000000000000000000000000000000000..54d10526031aabedbf8eab73a84d2bc8c2332cf2 Binary files /dev/null and b/zh/img/figure_9_D.jpg differ