图文结合、利于理解的数据结构学习笔记(6)——搜索树

机械工业出版社《数据结构与算法——Python语言实现》学习笔记。
画图工具:draw.io

1 二叉搜索树

树型数据结构在《树》一章中已有介绍。 搜索树 是树型数据结构的一个重要用途。本章中使用搜索树结构来实现 有序映射
二叉搜索树 是每个节点 p 存储一个键值对 (k,v) 的二叉树 T ,使得:

  • 存储在 p 的左子树的键都小于 k
  • 存储在 p 的右子树的键都大于 k

在这里插入图片描述

1.1 遍历二叉搜索树

命题:二叉搜索树的中序遍历是按照键增加的顺序进行的。

在二叉搜索树中计算某一位置的后继节点:

 Algorithm after(p):
    if right(p) id not None then
        walk = right(p)
        while left(walk) is not None do
            walk = left(walk)
        return walk
    else
        walk = p
        ancestor = parent(walk)
        while ancestor is not None and walk == right(ancestor) do
            walk = ancestor
            ancestor = parent(walk)
        return ancestor

1.2 搜索

二叉搜索算法:想象把二叉搜索树表示为决策树,在每个节点 p 的问题就是期望的键 k 是否小于、等于或大于存储在节点 p 的键,表示为 p.key()。如果“小于”,则继续搜索左子树。如果“等于”,则搜索成功终止。如果“大于”,则继续搜索右子树。最后,如果得到空的子树,就是没有搜索到。

在这里插入图片描述

二叉搜索的递归调用:

Algorithm TreeSearch(T, p, k):
    if k == p.key() then
        return p
    else if k < p.key() and T.left(p) is not None then
        return TreeSearch(T, T.left(p), k)
    else if k > p.key() and T.right(p) is not None then
        return TreeSearch(T, T.right(p), k)
    return p
1.2.1 二叉树搜索的分析

设每个节点的搜索时间为 O(1),则总搜索时间为 O(h),h 是二叉搜索树 T 的高度。
在有序映射 ADT 中,搜索将作为实现 _ _getitem_ _,_ _setitem_ _和_ _delitem_ _方法的子程序。树的高度为 h 时,所有这些操作在最坏情况下的时间复杂度为 O(h)。

1.3 插入和删除

1.3.1 插入

首先搜索键为 k 的项(假设映射不为空)。如果找到,该节点将被重新赋值;否则,新的节点可以放置在搜索失败结束时得到的空子树的位置。
伪代码:

Algorithm TreeInsert(T, k, v):
    Input:A search key k to be associated with value v
    p = TreeSearch(T, T.root(), k)
    if k == p.key() then
        Set p’s value to v
    else if k < p.key then
        add node with item (k,v) as left child of p
    else
        add node with item (k,v) as right child of p

在这里插入图片描述

1.3.2 删除

首先找到 T 中键等于 k 的节点的位置 p。如果搜索成功,则分以下两种情况:

  • 如果 p 最多有一个孩子,删除位置 p 的节点并用其子节点替换它。
  • 如果位置 p 有两个孩子,采用如下步骤:
    (1)通过公式 r = before§ 定位严格小于 p 处键的所有节点中拥有最大键的节点所在的位置 r。
    (2)使用位置 r 的节点作为位置 p 的替代。
    (3)使用第一种方法从树中删除 r 节点。(因为 r 节点无右子树)

在这里插入图片描述
在这里插入图片描述

1.4 Python实现

略。

1.5 二叉搜索树的性能

操作运行时间
k in TO(h)
T[k], T[k] = vO(h)
T.delete(p),del T[k]O(h)
T.find_position(k)O(h)
T.first(),T.last(),T.find_min(),T.find_max()O(h)
T.before(p),T.after(p)O(h)
T.find_lt(k),T.find_le(k),T.find_gt(k),T.find_ge(k)O(h)
T.find_range(start,stop)O(s + h)
iter(T),reversed(T)O(n)

通常来说,通过一系列随机的插入或删除操作生成的有 n 个键的二叉搜索树的期望复杂度是 O(log n)。证明略。

2 平衡搜索树

平衡搜索树用少量操作对标准二叉树进行扩展,以重新调整树并降低树的高度,因此能提供更强的性能保证。

平衡二叉搜索树的主要操作是 旋转。在旋转中,我们“旋转”大于其父亲节点的孩子节点:

在这里插入图片描述

因为单个旋转修改了常数数量的父子关系,在一个二叉树中实现它用 O(1) 时间。

旋转在改变树的形状的同时维持了树的性质,这样的操作可以避免非常不平衡的树的结构。例如,上图中从左到右的旋转使子树 T1 中的每个节点的深度减少 1,同时使子树 T3 的每个节点的深度增加 1。

在一棵树内部,可以将一个或多个旋转合并来提供更广泛的平衡。这样的符合操作,我们称之为 trinode 重组。

我们考虑一个位置 x ,其父亲节点为 y,其祖父节点为 z。目标是重建以 z 为根的子树,以缩短到 x 位置和其子树的总路径长度。伪代码:

Algorithm restructure(x):
    INPUT: 二叉搜索树 T 的一个位置 x ,x 的父节点为 y ,祖父节点为 z
    OUTPUT: trinode 重组后的二叉搜索树,包括位置 x,y 和 z
1:让 (a,b,c) 为位置 x,y 和 z 的从左到右(中序)列表,令 (T1,T2,T3,T4) 为 x,y 和 z 的 4 个根不为 x,y 或 z 的子树的从左到右(中序)列表。
2:用根为 b 的子树来替换根为 z 的子树。
3:让 a 为 b 的左孩子,让 T1 和 T2 分别为 a 的左右子树。
4:让 c 为 b 的右孩子,让 T3 和 T4 分别为 c 的左右子树。

在实践中,由旋转重建造成的树 T 的修改可以通过单个旋转或者双旋转来实现。在任何情况下,旋转重建都可以在 O(1) 时间内完成。

在这里插入图片描述
在这里插入图片描述

3 AVL树

AVL树的定义

在二叉搜索树的基础上添加一条规则:树维持对数的高度。本节考虑在最长路径上 节点的数量 作为树的高度。
高度平衡属性:对于 T 中的每一个位置 p,p 的孩子的高度最多相差 1
任何满足高度平衡属性的二叉搜索树 T 被称为 AVL 树。

在这里插入图片描述

高度平衡属性带来的结果:(1)AVL 子树也是 AVL 树;(2)AVL 树可以保持高度最小。
命题:一棵存有 n 个节点的 AVL 树的高度是 O(log n) 证明略。

3.1 更新操作

给定一颗二叉搜索树,如果一个位置的子树高度之差的绝对值最多为 1,我们就说这个位置是 平衡的,否则这个位置就是 不平衡的。因此,AVL 树的高度平衡属性相当于 每个位置都是平衡的

3.1.1 插入

在叶子节点 p 的位置产生一个新节点,则 p 的祖先节点可能会变得不平衡。

“查找修复”策略来恢复二叉搜索树中节点的平衡:用 z 表示从根 T 到 p 的方向中遇到的第一个不平衡位置,用 y 表示 z 的具有更高高度的孩子 (y 必须是 p 的一个祖先) ,用 x 表示 y 的具有更高高度的孩子 (x 必须是 p 的一个祖先或者 p 自身) 。我们通过调用 trinode 重建方法 restructure(x) 对以 z 为根的子树进行再平衡。

在这里插入图片描述
更一般的再平衡过程示意图:
在这里插入图片描述

3.1.2 删除

如果 p 代表在树 T 中删除节点的父节点,最多可能有一个不平衡的节点在 p 到根节点之间的路径上。与插入一样,使用 trinode 重组恢复树 T 的平衡。

在这里插入图片描述

3.2 AVL 树的性能

由于有 n 个节点的 AVL 树的高度是 O(log n),易得性能:

操作运行时间
k in TO(log n)
T[k], T[k] = vO(log n)
T.delete(p),del T[k]O(log n)
T.find_position(k)O(log n)
T.first(),T.last(),T.find_min(),T.find_max()O(log n)
T.before(p),T.after(p)O(log n)
T.find_lt(k),T.find_le(k),T.find_gt(k),T.find_ge(k)O(log n)
T.find_range(start,stop)O(s + log n)
iter(T),reversed(T)O(n)

4 伸展树

伸展树结构从概念上完全不同于其他平衡搜索树,它在树的 高度上没有一个严格的对数上界 。伸展树 无需有额外的高度、平衡或与此树节点关联的其他辅助数据

伸展树的效率取决于某一位置移动到根的操作(称为 伸展 ),每次在插入、删除或者搜索都要 从最底层的位置 p 开始 。直观上,伸展树会使得被频繁访问的元素更快接近于根,从而减少典型的搜索时间。伸展树保障了插入、删除、搜索操作具有 对数运行时间

4.1 伸展

已知二叉搜索树 T 的一个节点 x,通过一系列的重组将 x 移动到 T 的根来对 x 进行 伸展。将 x 向上移动的特定操作取决于 x 、其父节点 y 和 x 的祖先节点 z(如果存在的话)的相对位置。考虑以下三种情况:

  • zig-zig 型:
    在这里插入图片描述
  • zig-zag 型:
    在这里插入图片描述
  • zig 型:
    在这里插入图片描述

有了以上三种操作,我们就可以对节点 x 进行重复的伸展,直到节点 x 变为伸展树的根节点。
在这里插入图片描述
在这里插入图片描述

4.2 何时进行伸展

伸展规则如下:

  • 搜索键 k 时,如果在位置 p 处找到 k ,则伸展 p;否则,在搜索失败的位置伸展叶子节点。
  • 当插入键 k 时,我们将伸展新插入的内部节点 k。

在这里插入图片描述

  • 当删除键 k 时,在位置 p 进行伸展,其中 p 是被移除节点(此被移除节点不是被删除的节点)的父节点。
    在这里插入图片描述

4.3 Python实现

略。

4.4 伸展树的摊销分析*

分析过程略。

结论:伸展树执行一个搜索、插入或删除的摊销运行时间复杂度是 O(log n),其中 n 是伸展树的大小。

伸展树的优势:(1)仅使用一棵不需要存储每个节点的附加平衡信息的简单二叉树就能实现这样的性能。(2)实现简单,仅需要对标准二叉树进行简单的改编。

5 (2, 4) 树

5.1 多路搜索树

5.1.1 多路搜索树的定义

令 w 为有序树的一个节点,如果 w 有 d 个孩子,则称 w 是 d-node。
多路搜索树定义为一棵有以下属性的有序树 T:

  • T 的每个内部节点至少有两个孩子。也就是说每个内部节点是一个 d-node,其中 d >= 2。
  • T 的每个内部 d-node w(其孩子为 c1,…,cd )按顺序存储 d-1 个键-值对 (k1, v1),…,(kd-1,vd-1),k1 <= … <= kd-1。
  • 通常定义 k0 = -∞ 和 kd = +∞。每个条目 (k,v) 存储在一个以 ci 为根的 w 的子树的一个节点上,其中 i=1, …, d, ki-1 <= k <= ki。

如果认为存储在 w 的键的集合包含特殊的虚拟键 k0 = -∞ 和 kd = + ∞,那么存储在以孩子节点 ci 为根的 T 的子树上的键 k 一定是存储在 w 上的两个键“之间”的一个。
根据上述定义, 多路搜索的外部节点不存储任何数据并且仅仅作为“占位符”。这些外部节点可以有效地以 None 引用表示。
键值对的数目和外部节点的数目存在有趣的关系。
命题:一棵有 n 个键值对的多路搜索树有 n+1 个外部节点

5.1.2 多路树搜索

在 T 中从根节点开始跟踪路径执行搜索。在搜索 d-node 节点 w 时,比较键 k 和存储在 w 上的键 k1, …, kd-1。如果 k = ki,搜索成功;否则,继续搜索 w 的孩子 ci,使得 ki-1 < k < ki(通常定义 k0 = -∞ 和 kd = + ∞ )。如果到达外部节点,搜索不成功并终止。
在这里插入图片描述

5.1.3 主要的多路径搜索树数据结构

在多路搜索树中搜索键 k 时,基本操作是找到键比 k 大或者相等的节点中最小的节点。所以我们将节点本身的信息作为一个有序映射。这样的映射可以作为 二级 数据结构来支持由整个多路搜索树表示的 初级 数据结构。
多路搜索树的首要效率目标是保持高度尽可能小。接下来讨论的策略是: dmax 距离限制在 4 ,同时保证高度 h 是 n 的对数,其中 n 为保存在映射中节点的总数。

5.2 (2, 4)树的操作

  • 大小属性:每个内部节点最多有 4 个孩子。
  • 深度属性:所有外部节点具有相同的深度。

在这里插入图片描述

命题:存储 n 个节点的 (2, 4) 树的高度为 O(log n) 证明略。

在 (2, 4) 树中执行搜索需要 O(log n) 的时间复杂度,且节点的二级数据结构的具体实现不是一个关键的设计选择,因为最大孩子数量 dmax 是一个常数。
对 (2, 4) 树进行插入和删除后,保持大小和深度属性需要一些操作。

5.2.1 插入

插入一个键为 k 的新节点 (k, v) 到 (2, 4) 树 T 中,首先对键 k 执行搜索。假设 T 中没有键为 k 的节点,这个搜索非正常终止于外部节点 z 中。令 w 为 z 的父节点。我们在节点 w 上插入新的项,并且在 z 的左边对 w 添加一个新的孩子节点 y(外部节点)。

上述插入方法维持了深度属性,但可能违反大小属性。如果一个节点 w 以前是 4-node,那么插入后它将成为一个 5-node,导致 T 树不再是 (2,4) 树。这种情况称为在 w 节点 溢出,为了修复 w 节点的溢出问题,我们对 w 执行以下分裂操作:

  • 用 w’ 和 w" 来代替 w,其中 w’ 是存储 k1 和 k2 的 3-node (其孩子节点为 c1,c2,c3);w" 是存储 k4 的 2-node(其孩子节点为 c4,c5)。
  • 如果 w 是 T 的根节点,创建一个新的根节点 u,让 u 为 w 的父节点。
  • 插入键值 k3 到 u 中,并使得 w" 和 w’ 成为 u 的孩子节点。如果 w 是 u 的第 i 个孩子,那么 w’ 和 w" 将分别为 u 的第 i 个和第 i+1 个孩子节点。

在这里插入图片描述

由于节点 w 的分裂操作,w 的父节点 u 可能会发生溢出。如果发生溢出,它会在节点 u 继续触发分裂。执行插入操作的总时间是 O(log n)。

5.2.2 删除
  • 要删除的项存储在无外部孩子的节点 z:
    假设要删除的键为 k 的项存储在节点 z 的第 i 个项 (ki, vi),则选出如下节点 w 中的项并与 (ki, vi) 交换:
    (1)w 在以 z 的第 i 个孩子为根的子树上; w 是最右边的子树的内部节点;w 的所有孩子节点都是外部节点。
    (2)用 w 的最后一个节点交换节点 z 的 (ki, vi)。
    由于节点 w 为只有外部孩子的节点,此时可以转化为下面的情况。

  • 要删除的项存储在只有外部孩子的节点 w:
    (1)如果删除项后不怕破坏大小属性,此时可直接删除项并删除 w 的第 i 个外部节点。(见下图中 g),h))
    (2)如果删除项后破坏大小属性,这种情况称为在节点 w 下溢。为了修复下溢,我们检查 w 的兄弟节点是否是一个 3-node 或 4-node。如果发现这样一个兄弟 s,就进行 转移 操作,也就是将 s 的一个孩子移到 w 上,将 s 的一个键移到 w 和 s 的父节点 u 上,将 u 的一个键移动到 w (见下图中 b),c))。如果 w 只有一个兄弟或者兄弟都是 2-node ,就进行 融合 操作,合并 w 及其一个兄弟,创建一个新节点 w’ 并将 w 的父节点 u 的键移动到 w’。(见下图中 e),f))

在这里插入图片描述
在这里插入图片描述

节点 w 处的融合操作可能导致一个新的下溢发生在 w 的父节点 u 上,进而触发 u 交换或合并。如果下溢一直传播到根,那么根被删除。

5.2.3 (2, 4) 树的性能

有 n 个键-值对的 (2, 4) 树的时间复杂度的分析基于以下几点:

  • 存储 n 个节点的 (2,4) 树的高度是 O(log n)。
  • 分裂、交换或合并操作需要 O(1) 时间。
  • 搜索、插入或删除一个节点需要访问 O(log n) 个节点。

6 红黑树

红黑树在一次更新之后,使用 O(1) 次结构变化来保持平衡。

红黑树是一棵带有红色和黑色节点的二叉搜索树,具有以下属性:

  • 根属性:根节点是黑色的。
  • 红色属性:红色节点(如果有的话)的子节点是黑色的。
  • 深度属性:具有零个或一个子节点的所有节点都具有相同的 黑色深度 (被定义为黑色祖先节点的数量)。一个节点是它自己的祖先。

在这里插入图片描述
红黑树和 (2,4) 树之间存在着一个对应关系,它们之间可以相互转换:

在这里插入图片描述
因此我们能够得出命题:有 n 个条目的红黑树的高度是 O(log n)

6.1 红黑树的操作

6.1.1 插入

首先在 T 中搜索 k,直到达到一个空子树,然后在这个位置插入一个新的叶子节点 x,存储项。如果 x 是根节点,将其设为黑色,否则将其设为红色。

上述操作可能违反红色属性,即 x 和 x 的父节点 y 都是红色的,我们将这种情况称为 双红色
为了解决双红色问题,考虑以下两种情况:
情况1: y 的兄弟姐妹为黑色(或无)。此时进行 T 的 trinode 重组:

在这里插入图片描述
可以注意到重组后任何路径中的黑色节点数不改变,因此树的 黑色深度不受影响

情况2: y 的兄弟姐妹是红色的。此时双红色表示在相应的 (2,4) 树中溢出。为解决这个问题,进行一个相当于 分裂 的操作,即 重新着色
在这里插入图片描述

与 (2,4) 树中一样,在节点 x 上重新着色可能将双红问题传播到 x 的祖先节点 z 上去,此时需要继续深度搜索 T 进行重新着色直到无双红问题出现。

6.1.2 删除

从红黑树 T 中删除键为 k 的项和二叉搜索树的删除过程相似。在结构上,这种处理结果导致删除至多有一个孩子的节点(要么是最初包含 k 的节点要么是它的前继),并提升其剩余的节点(如果有的话)。
在这里插入图片描述

如果删除节点是红色的,这种结构性变化不会影响树中任何路径的黑色深度,也不会违反红色属性,所以结果树仍然是有效的红黑树。在相应的 (2,4) 树中,这表示 3-node 或 4-node 的萎缩。

如果删除的节点是黑色的,那么它要么没有孩子要么有一个子节点,这个子节点是一个红色的叶子节点(因为删除的节点的空子树黑色高度为 0,由深度属性和红色属性,此子节点必须是红色的叶子节点)。在后一种情况下,将除去的节点代表一个相应的 3-node 的黑色部分,我们通过重新将提升的孩子着色为黑色来恢复红黑属性:
在这里插入图片描述

更为复杂的情况是一个(非根)黑色叶节点被删除。考虑一个更一般的设置,用一个已知有两个子树的 z 节点、T_heavy 和 T_light,T_light (如果有)在删除前的根节点是黑色,同时 T_heavy 的黑色深度恰好比 T_light 高 1,如下图所示。在一个除去黑色叶子的情况下,z 是该叶子的父亲,T_light 是删除之后仍然存在的空子树。用 y 表示 T_heavy 的根(y 一定存在,因为 T_heavy 的黑色深度至少为 1)
在这里插入图片描述
分三种情况:
情况1:节点 y 是黑色的,同时有一个红色的孩子节点 x。 对于该情况执行 trinode 重组。这种情况对应于 (2,4) 树 T’ 中在 z 的两个子节点之间的转换操作。
在这里插入图片描述

情况2:节点 y 是黑色,并且 y 的两个子节点是黑色(或无)。 对于该情况执行 重新着色 。这种情况相当于在 (2,4) 树中进行一个融合操作。
在这里插入图片描述

情况3:节点 y 是红色的。 因为 y 是红色的,同时 T_heavy 有至少为 1 的黑色深度,所以 z 必须是黑色的,且 y 的两个子树必须每一个都有一个黑色的根,并且黑色的深度等于 T_heavy 的深度。

这种情况下,我们把 y 和 z 进行旋转,然后重新将 y 着为黑色,将 z 着为红色。

这并不能立即解决不足,因此还需要继续采用情况 1 或者情况 2 ,且一定能在下一次终止(对应情况 2图中 的 a))。
在这里插入图片描述

6.1.3 红黑树的性能

红黑树的渐进性能与 AVL 树或 (2,4) 树的渐进性能相同,对于大多数操作保证了 对数时间界限。红黑树的主要优点在于插入或删除只需要常数步的调整操作(这是相对于 AVL 树和 (2,4) 树)

命题:在一棵存储 n 个项目的红黑树中插入一个项可在 O(log n) 的时间内完成,并且需要 O(log n) 的重新着色且至多需要一次的 trinode 重组。

命题:在一棵存储 n 个项目的红黑树中删除一个项可在 O(log n) 的时间内完成,并且需要 O(log n) 的重新着色且最多需要两次调整操作。

6.2 Python 实现

略。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值