【算法导论】笔记-第三部分 数据结构

第三部分 数据结构

  • 动态集合:算法操作的集合能在整个过程中增大,缩小或发生其他变化。
  • 动态集合操作:
    • 查询操作(返回信息
      • SEARCH(S, k):返回指向S中某个元素的指针x,使得x.key=k
      • MINIMUM(S):返回指向S中具有最小关键字元素的指针
      • MAXIMUM(S):返回指向S中具有最大关键字元素的指针
      • SUCCESSOR(S, x):返回集合S中比x大的下一个元素的指针
      • PREDECESSOR(S, x):返回集合S中比x小的前一个元素的指针
    • 修改操作(改变集合
      • INSERT(S, x):将由X指向的元素加入集合S中
      • DELETE(S, x):从集合S中删除x

第9章 基本数据结构

9.1 栈与队列

    • 栈是限定在一端进行插入和删除的线性表。

    • 操作:INSERT称作压入(PUSH),DELETE称作弹出(POP)

    • 特点:

      • 只能在栈顶进行插入和删除
      • 先进后出, 后进先出
      • 栈底指针 bottom ,栈顶指针 top
      • 栈底指针不变,栈中元素随栈顶指针的变化而动态变化
      • 栈具有记忆功能
      • 栈支持子程序调用
    • 伪代码:

      • STACK-EMPTY(S):测试是否为空栈

        if S.top == 0
            return TRUE
        else return FALSE
        
      • PUSH(S, x)

        S.top = S.top + 1
        S[S.top] = x 
        
      • POP(S)

        if STACK-EMPTY(S)
            error "underflow"
        else S.top = S.top - 1
            return S[S.top + 1]
        
    • 运行时间: O ( 1 ) O(1) O(1)

  • 队列

    • 队列是指允许在一端进行插入,而在另一端进行删除的线性表。

    • 操作:INSERT称作入队(ENQUEUE),DELETE称作出队(DEQUEUE)

    • 特点:

      • 队列只允许在队尾进行插入,而在队头进行删除
      • 先进先出 ,后进后出
      • 队头指针 front ,队尾指针 rear
      • 队列中元素随队头指针和队尾指针的变化而动态变化
    • 循环队列:计算:

      • rear>front: s=rear-front
      • rear<front: s= 容量 +rear-front
      • rear=front: s=1 或者 s=0
    • 伪代码:

      • ENQUEUE(Q, x)

        Q[Q.tail] = x
        if Q.tail == Q.length
            Q.tail = 1
        else Q.head = Q.head + 1
        return x
        
      • DEQUEUE(Q)

        x = Q[Q.head]
        if Q.head = Q.length
            Q.head = 1
        else Q.head = Q.head + 1
        return x
        
    • 运行时间: O ( 1 ) O(1) O(1)

9.2 链表

  • 双向链表:每个元素都是一个对象,每个对象都有一个关键字 k e y key key和两个指针: n e x t 和 p r e v next和prev nextprev

    • x . n e x t x.next x.next:指向它在链表中的后继元素
    • x . p r e v x.prev x.prev:指向它在链表中的前驱元素
  • 链表: x . p r e v = N I L x.prev=NIL x.prev=NIL,即没有前驱

    • 链表的头 h e a d head head:链表的第一个元素
    • 链表的尾 t a i l tail tail:链表的最后一个元素
    • 特点:
      • L . h e a d = N I L L.head=NIL L.head=NIL,则链表为空
      • 各数据结点的存储空间可以不连续
      • 各数据元素的存储顺序和逻辑循序可以不一致
      • 线性表的链式存储所占存储空间大于顺序存储结构
      • 查找结点时链式储存要比顺序存储慢
      • 链式存储插入删除元素比顺序存储灵活
    • 线性链表的操作:在线性链表中进行插入与删除,不需要移动链表中的元素。
    • 类别:
      • 单链接:省略每个元素的 p e r v perv perv指针
      • 双链接:双向链表
      • 已排序:链表的线性顺序与元素中关键字的线性顺序一致
      • 未排序:元素无顺序排列
  • 链表的搜索:

    • 采用简单的线性搜索方法

    • 伪代码:LIST-SEARCH(L, k)

      x = L.head
      while x != NIL and x.key != k
          x = x.next
      return x
      
    • 最坏情况运行时间: O ( n ) O(n) O(n)

  • 链表的插入

    • 伪代码:LIST-INSERT(L, x)

      x.next = L.head
      if L.head != NIL
          L.head.prev = x
      L.head = x
      x.prev = NIL
      
    • 运行时间: O ( 1 ) O(1) O(1)

  • 链表的删除

    • 伪代码:LIST-DELETE(L, x)

      if x.prev != NIL
          x.prev.next = x.next
      else L.head = x.next
      if x.next != NIL
          x.next.prev = x.prev
      
    • 运行时间: O ( 1 ) O(1) O(1)

  • 哨兵

    • 哨兵是一个哑对象,其作用是简化边界的处理
  • 假设在链表L中设置一个对象 L . n i l L.nil L.nil,该对象代表NIL,但具有其他对象相同的各个属性。对于链表代码中出现的每一处对NIL的引用,都代之以对哨兵 L . n i l L.nil L.nil的引用。这样调整将一个常规的双向链表转变成一个有哨兵的双向循环链表,哨兵L.nil位于表头和表尾之间。

    • 属性L.nil.next指向表头,L.nil.prev指向表尾。同样表尾的next属性和表头的prev属性同时指向L.nil。
    • 因为L.nil.next指向表头,我们就可以去掉属性L.head,并把对它的引用代替为对L.nil.next的引用。
    • 一个空的链表只由一个哨兵组成,L.nil.next和L.nil.prev同时指向L.nil。

9.3 指针和对象的实现

  • 对象的多数组表示:

    • 对一组具有相同域的对象,每一个对象都可以用一个数组来表示 。
    • 对于一个给定的数组下标x,三个数组项 k e y [ x ] , n e x t [ x ] , p r e v [ x ] key[x], next[x], prev[x] key[x],next[x],prev[x]一起表示链表中的一个对象。
  • 对象的单数组表示:一个对象占据存储中的一组连续位置 。

  • 对象的分配与释放:使用垃圾收集器来确定链表中哪些对象空间未被使用

9.4 有根树的表示

  • 二叉树:利用属性 p , l e f t , r i g h t p, left, right p,left,right存放指向父节点,左子叶,右子叶的指针。属性 T . r o o t T.root T.root指向整棵树的根结点。
  • 分支无限制的有根树:将二叉树的表示方法推广到多个结点的任意类型的树
    • 左兄弟右兄弟表示法:减少存储空间的浪费
    • 只使用两给指针:
      • x . l e f t − c h i l d x.left-child x.leftchild指向结点x最左边的根结点
      • x . r i g h t − c h i l d x.right-child x.rightchild指向x右侧相邻的根结点

第10章 散列表

  • 散列表是实现字典操作的一种有效数据结构。

10.1 直接寻址表

  • 直接寻址表:表示动态集合,记作 T [ 0.. m − 1 ] T[0..m-1] T[0..m1]。其中每个位置称作
  • 槽:槽k指向集合中一个关键字为k的元素。若没有关键字为k的元素,则 T ( k ) = N I L T(k)=NIL T(k)=NIL
  • 缺点:全域U很大,存储不现实。并且浪费空间。

10.2 散列表

  • 利用散列函数h,由关键字k计算出槽的位置。函数h将关键字的全域U映射到散列表 T [ 0.. m − 1 ] T[0..m-1] T[0..m1]的槽位上: h : U → { 0 , 1 , ⋅ ⋅ ⋅ , m − 1 } h:U\rightarrow \{0,1,···,m-1\} h:U{0,1,,m1} h ( k ) h(k) h(k)即为关键字k的散列表
  • python提供的散列表为字典
  • 冲突:两个关键字可能映射到同一个槽中。
    • 解决方法:
      • 使散列函数h尽可能的随机,以避免冲突或者使冲突的次数减少
      • 链接法
      • 开放寻址法
  • 通过链接法解决冲突:把散列到同一槽中所有元素都放在一个链表中。槽 j j j中有一个指针,它指向存储所有散列到 j j j的元素的链表的表头。

10.3 散列函数

  • 好的散列函数的特点:每个关键字都被等可能地散列到m个槽位中任何一个。

  • 构造好的散列函数:

    • 启发式方法:用除法进行散列和用乘法进行散列。
    • 全域散列
  • 将关键字转换为自然数:如果所给关键字不是自然数,则必须有一种方法来将它们解释为自然数。

  • 除法散列表:通过取k除以m的余数,将关键字k映射到m个槽中的某一个上

    • 散列函数: h ( k ) = k   m o d   m h(k)=k\ mod\ m h(k)=k mod m
  • 乘法散列表:

    • 步骤:
      • 用关键字k乘上常数A(0<A<1),并提取kA的小数部分。
      • 用m乘以这个值,再向下取整
    • 散列函数: h ( k ) = ⌊ m ( k A m o d    1 ) ⌋ h(k)=\lfloor m(kA\mod 1)\rfloor h(k)=m(kAmod1)
  • 全域散列法:从一组精心设计的函数中,随机地选择散列函数

10.4 开放寻址法

  • 开放寻址法:每个表项或包含动态集合的一个元素,或包括NIL。当查找某个元素时,要系统地检查所有的表项,直到找到所需的元素,或最终查明该元素不在表中,这一操作称为探查

  • 伪代码:

    • HASH-INSERT(T, k):插入

      i = 0
      repeat
      j = h(k, i)
      if T[j] == NIL
          T[j] = k
          return j
      else i = i + 1
      until i == m
      error "hash table overflow"
      
    • HASH-SEARCH(T, k):查找

      i = 0
      repeat
          j = h(k, j)
          if T[j] == k
              return j
          i = i + 1
      until T[j] == NIL or i == m
      return NIL
      
    • python代码:

      ## 除法散列表
      def h(k, m):
          return k % m
      
      ## 插入
      def hash_insert(T, k):
          m = len(T)
          i = 1
          while i < m:
              j = h(k, i)
              if T[j] == None:
                  T[j] = k
                  return j
              else:
                  i = i + 1
          return "hash table overflow"
      
      ## 搜索
      def hash_search(T, k):
          m = len(T)
          i = 1
          j = h(k, i)
          while i < m and T[j] != None:
              j = h(k, i)
              if T[j] == k:
                  return j
              else:
                  i = i + 1
          return None
      
      T = [None]*10 ## 初始化散列表
      for k in [79,69,98,72,14,50]:
      	print(hash_insert(T, k))
          
      for k in [79,98,14,900]:
      	print(hash_search(T, k))
      
      
  • 计算开放寻址法中的探查序列

    • 线性探查:给定一个散列函数 h ′ : U → { 0 , 1 , ⋅ ⋅ ⋅ , m − 1 } h':U\rightarrow \{0,1,···,m-1\} h:U{0,1,,m1},称之为辅助散列函数
      • 散列函数: h ( k , i ) = ( h ′ ( k ) + i )   m o d   m ,   i = 0 , 1 , ⋅ ⋅ ⋅ , m − 1 h(k,i)=(h'(k)+i)\ mod\ m,\ i=0,1,···,m-1 h(k,i)=(h(k)+i) mod m, i=0,1,,m1
      • 缺点:一次群集问题,即随着时间的推移,连续被占用的槽不断增加,平均查找时间也随着不断增加。
    • 二次探查:
      • 散列函数: h ( k , i ) = ( h ′ ( k ) + c 1 i + c 2 i 2 )   m o d   m ,   i = 0 , 1 , ⋅ ⋅ ⋅ , m − 1 h(k,i)=(h'(k)+c_1i+c_2i^2)\ mod\ m,\ i=0,1,···,m-1 h(k,i)=(h(k)+c1i+c2i2) mod m, i=0,1,,m1
      • 初始的探查位置是 T [ h ′ ( k ) ] T[h'(k)] T[h(k)]
      • 如果两个关键字的初始探查位置相同,那么它们的探查序列也是相同的,这一性质导致一种程度较轻的群集现象,称为二次群集。
    • 双重散列
      • 散列函数: h ( k , i ) = ( h ( k ) + i h 2 ( k ) )   m o d   m ,   i = 0 , 1 , ⋅ ⋅ ⋅ , m − 1 h(k,i)=(h(k)+ih_2(k))\ mod\ m,\ i=0,1,···,m-1 h(k,i)=(h(k)+ih2(k)) mod m, i=0,1,,m1
      • 初始的探查位置是 T [ h 1 ( k ) ] T[h_1(k)] T[h1(k)]

第11章 二叉搜索树

11.1 什么是二叉搜索树

  • 二叉搜索树是以一颗二叉树来组织的

  • 设x是二叉搜索树中的一个结点。如果y是x左子树中的一个结点,那么 y . k e y < = x . k e y y.key<=x.key y.key<=x.key。如果y是x左子树中的一个结点,那么 y . k e y > = x . k e y y.key>=x.key y.key>=x.key

  • 中序遍历:输出的子树根的关键字在其左右子树的关键字之间

  • 先序遍历:根的关键字在其左右子树的关键字之前

  • 后序遍历:根的关键字在其左右子树的关键字之后

  • 三种遍历方式子数都是向左再右,不同在于根的位置

  • 中序遍历输出一颗二叉搜索树T中的所有元素

    • 伪代码:INORDER-TREE-WALK(x)

      if x != NIL
          INORDER-TREE-WALK(x.left)
          print x.key
          INORDER-TREE-WALK(x.right)
      
    • 遍历一棵有n个结点的二叉搜索树需要耗费 θ ( n ) \theta(n) θ(n)时间。

11.2 查询二叉搜索树

  • 查找:

    • 伪代码:

      • TREE-SEARCH(x, k)

        if x == NIL or k == x.key
            return x
        if k < x.key
            return TREE-SEARCH(x.left, k)
        else return TREE-SEARCH(x.right, k)
        
      • ITEATIVE-TREE-SEARCH(x, k)

        while x != NIL and k != x.key
            if k<x.key
                x = x.left
            else x = x.right
        return x
        
    • python代码:

      def tree_search(self, root, val):
          if root == None:
              return False
          if root.val == val:
              return True
          elif val < root.val:
              return self.tree_search(root.left, val)
          elif val > root.val:
              return self.tree_search(root.right, val)
      
  • 最大关键字元素和最小关键字元素

    • 思路:如果y是x左子树中的一个结点,那么 y . k e y < = x . k e y y.key<=x.key y.key<=x.key。如果y是x左子树中的一个结点,那么 y . k e y > = x . k e y y.key>=x.key y.key>=x.key

    • 伪代码:

      • TREE-MINIMUM(x)

        while x.left != NIL
            x = x.left
        return x
        
      • TREE-MAXIMUM(x)

        while x.left != NIL
            x = x.right
        return x
        
    • python代码:

      def tree_minimum(self, root):
          if root.left:
              return self.tree_minimum(root.left)
          else:
              return root
      
      def tree_maximum(self, root):
          if root.right:
              return self.tree_maximum(root.right)
          else:
              return root
      
  • 后继和前驱:

    • 给定一棵二叉搜索树中的一个结点,有时需要按中序遍历的次序查找它的后继。如果所有的关键字互不相同,则一个结点x的后继是大于x.key的最小关键字的结点。一棵二叉搜索树的结构允许我们通过没有任何关键字的比较来确定一个结点的后继。

    • 伪代码:TREE-SUCCESSOR(x)

      if x,right != NIL
          return TREE-MINIMUM(x, right)
      y = x.p
      while y != NIL and x == y.right
          x = y
          y = y.p
      return y
      

11.3 插入与删除

  • 插入:

    • 思路:从根开始,x指针记录了一条向下的简单路径,并查找要替换的输入项z的NIL。在找到合适的位置后,我们更新z的父结点以及z的父结点的孩子结点信息从而完成INSERT过程。

    • 伪代码:TREE-INSERT(T, z)

      y = NIL
      x = T.root
      while x != NIL
          y = x
          if z.key < x.key
              x = x.left
          else x = x.right
      z.p = y
      if y == NIL
          T.root = z
      elseif z.key < y.key
          y.left = z
      else y.right = z
      
    • python代码:

      class tree:
      	'''二叉搜索树节点的定义'''
      	def __init__(self, val):
      		self.val = val
      		self.left = None
      		self.right = None
              
      def tree_insert(self, root, val):
          if root == None:
              root = tree(val)
          elif val < root.val:
              root.left = self.tree_insert(root.left, val)
          elif val > root.val:
              root.right = self.tree_insert(root.right, val)
          return root
      
  • 删除:

    • 三种基本情况:

      • 如果z没有孩子结点,那么只是简单地将它删除,并修改它的父结点,用null作为孩子来替换z。
      • 如果z只有一个孩子结点,那么将这个孩子提升到树中z的位置上,并修改它的父结点,用z的孩子来替换z。
      • 如果z有两个孩子结点,那么找z的后继(一定在z的右子树中)y,并让y占据树中 z的位置。z的原来右子树部分成为y的新的右子树,z的原来左子树部分成为y的新的左子树。
    • 子过程TRANSPLANT:为了完成二叉搜索树中结点的DELETE过程,我们需要定义一个子过程TRANSPLANT,它是用另一棵子树来替换一棵子树并成为其双亲的孩子结点。

      • 伪代码:TRANSPLANT(T, u, v)

        if u.p == NIL
            T.root = v
        elseif u == u.p.left
            u.p.left = v
        else u.p.right = v
        if v != NIL
            v.p = u.p
        
    • TREE-DELETE(T, z)

      if z.left == NIL
          TRANSPLANT(T, z, z.right)
      elseif z.right == NIL
          TRANSPLANT(T, z, z.left)
      else y = TREE-MINIMUM(z.right)
          if y.p != z
              TRANSPLANT(T, y, y.right)
              y.right = z.right
              y.right.p = y
          TRANSPLANT(T, z, y)
          y.left = z.left
          y.left.p = y
      
    • python代码:

      def tree_delete(self, root, val):
          '''删除二叉搜索树中值为val的点'''
          if root == None:
              return 
          if val < root.val:
              root.left = self.tree_delete(root.left, val)
          elif val > root.val:
              root.right = self.tree_delete(root.right, val)
          # 当val == root.val时,分为三种情况:只有左子树或者只有右子树、有左右子树、即无左子树又无右子树
          else:
              if root.left and root.right:
                  # 既有左子树又有右子树,则需找到右子树中最小值节点
                  temp = self.tree_minimum(root.right)
                  root.val = temp.val
                  # 再把右子树中最小值节点删除
                  root.right = self.tree_delete(root.right, temp.val)
              elif root.right == None and root.left == None:
                  # 左右子树都为空
                  root = None
              elif root.right == None:
                  # 只有左子树
                  root = root.left
              elif root.left == None:
                  # 只有右子树
                  root = root.right
          return root
      
    • 运行时间:在一棵高度为h的二叉搜索树中,DELETE和DELETE过程的运行时间为 O ( h ) O(h) O(h)


第12章 红黑树

12.1 红黑树的性质

  • 红黑树在每个结点上添加一个存储位来表示结点的颜色,可以是RED或BLACK。

  • 通过对任何一条从根到叶子的简单路径上各个结点的颜色进行约束,红黑树确保没有一条路径会比其它路径长出2倍,因而近似于平衡的

  • 树中每个结点包含5个属性: c o l o r , k e y , l e f t , r i g h t , p color, key, left, right, p color,key,left,right,p

  • 红黑树性质:

    • 二叉搜索树

    • 结点或是红色,或是黑色

    • 根结点是黑色结点

    • 每个叶子结点都带有两个空的黑色结点(NIL结点)

    • 如果一个结点是红色,则它的两个子结点都是黑色(或者说从每个叶子到根的所有路径上不能有两个连续的红色节点)

    • 对每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点

  • 为了便于处理红黑树代码边界条件,使用一个哨兵来代表NIL。哨兵 T . n i l T.nil T.nil与树中普通结点有相同属性。它的 c o l o r color color属性为BLACK,其它属性为任意值。所以我们可以使用一个哨兵 T . n i l T.nil T.nil来代表所有的NIL

  • 黑高:从某一结点出发(不包含该节点)到达一个叶结点的任意一条简单路径上黑色结点个数称为该结点的黑高,记为 b h ( x ) bh(x) bh(x)

  • 一颗有 n n n个内部结点的红黑树的高度至多为 2 lg ⁡ ( n + 1 ) 2\lg(n+1) 2lg(n+1)

  • 红黑树的python实现:

    class ColorEnum(Enum):
        RED = "red"
        BLACK = "black"
     
     
    class RBTreeNode():
        def __init__(self):
            self.value = None
            self.left = None
            self.right = None
            self.p = None
            self.color = None
    

12.2 旋转

  • 指针结构的修改是通过旋转来完成的,这种操作能保持二叉搜索树性质。

  • 左旋:假设在某个结点x上做左旋,它的右孩子为y。左旋则以x到y的链为“支链”进行,它使得y称为该子树新的根结点,x为y的左孩子,y的左孩子成为x的右孩子。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mWBVJAcz-1614768342258)(img/20181031000037863.png)]

  • 伪代码:LEFT-ROTATE(T, x)

    y = x.right
    x.right = y.left
    if y.left != T.nil
        y.left.p = x
    y.p = x.p
    if x.p == T.nil
        T.root = y
    elseif x == x.p.left
        x.p.left = y
    else x.p.right = y
    y.left = x
    x.p = y
    
  • python代码:

    class RB_Tree(object):
        def __init__(self):
            self.root = None
     
        def left_rotate(self, x: RBTreeNode):
            y = x.right
            x.right = y.left
            y.left.p = x
            y.p = x.p
            if not x.p:
                self.root = y
            elif x == x.p.left:
                x.p.left = y
            else:
                x.p.right = y
            y.left = x
            x.p = y
     
        def right_rotate(self, y: RBTreeNode):
            x = y.left
            y.left = x.right
            x.right.p = y
            y.p = x.p
            if not y.p:
                self.root = x
            elif y == y.p.right:
                y.p.left = x
            else:
            y.p.right = x
            x.right = y
            y.parent = x
    

12.3 插入

  • 插入过程与二叉搜索树差不太多,搜索树中为NULL的地方,现在被替换成nil哨兵节点,其次插入的节点我们总是将其着色为红色。在完成插入之后,为保证红黑性质能继续保持,我们调用一个辅助程序RB-INSERT-FIXUP来对结点重新着色并旋转。

  • 伪代码:

    • RB-INSERT(T, z)

      y = T.nil
      x = T.root
      while x != T.nil
          y = x
          if z.key < x.key
          x = x.left
          else x = x.right
      z.p = y
      if y == T.nil
          T.root = z
      elseif z.key < y.key
          y.left = z
      else y.right = z
      z.left = T.nil
      z.right = T.nil
      z.color = RED
      BR-INSERT-FIXUP(T, z)
      
    • BR-INSERT-FIXUP(T, z)

      while z.p.color == RED
          if z.p == z.p.p.right
              y = z.p.p.left
              if y.color == RED
                  z.p.color = BLACK
                  y.color = BLACK
                  z.p.p.color = RED
                  z = z.p.p
              else if z = z.p.right
                  z = z.p
                  LEFT-ROTATE(T, z)
              z.p.color = BLACK
              z.p.p.color = RED
              RIGHT-ROTATE(T, z, p, p)
          else
              T.root.color = BLACK
      
  • python代码:

    def rb_insert(self, z: RBTreeNode):
        y = None
        x = self.root
        while x:
            y = x
            if z.value < x.value:
                x = x.left
            else:
                x = x.right
        z.p = y
        if not y:
            self.root = z 
        elif z.value < y.value:
            y.left = z
        else:
            y.right = z
        z.left = None
        z.right = None
        z.color = ColorEnum.RED
        self.rb_insert_fixup(z) 
    
    def rb_insert_fixup(self, z):
        while z.p.color == ColorEnum.RED:
            if z.p.p.left == z.p:
                y = z.p.p.right
                if y.color == ColorEnum.RED:
                    z.p.color = ColorEnum.BLACK
                    y.color = ColorEnum.BLACK
                    z.p.p.color = ColorEnum.RED
                    z = z.p.p
                elif y.color == ColorEnum.BLACK and z == z.p.right:
                    z = z.p
                    self.left_rotate(z)
                else:
                    z.p.color = ColorEnum.BLACK
                    z.p.p.color = ColorEnum.RED
                    self.right_rotate(z.p.p)
            else:
                y = z.p.p.left
                if y.color == ColorEnum.RED:
                    z.p.color = ColorEnum.BLACK
                    y.color = ColorEnum.BLACK
                    z.p.p.color = ColorEnum.RED
                    z = z.p.p
                elif y.color == ColorEnum.BLACK and z == z.p.left:
                    z = z.p
                    self.right_rotate(z)
                else:
                    z.p.color = ColorEnum.BLACK
                    z.p.p.color = ColorEnum.RED
                    self.left_rotate(z.p.p)
    
  • 我们之前的插入操作,可能出现问题的就是性质2,4,对于2号性质,如果插入的是一颗空树(只有nil节点),那么根结点就是红的,就不满足2性质。如果插入的节点的父节点是红色,而我们插入的也是一个红色的节点,那么就不满足4性质了。

  • 处理:

    • 性质2:在调整函数最后加上一句root->color=BLACK即可,将根结点染黑。

    • 性质4:我们需要调整的判断条件就是父节点如果为红,就需要继续调整。这里实际上就要分三种情况,首先都已经默认z的父节点z->p的颜色为红色,循环条件为while(z.color==RED):

      • 情况一:z的叔节点y是红色,其中z为要调整的节点(下同):

        ​ 将z的父节点和叔节点都染黑,而将z的祖父节点染红,然后这时我们再将z点指向z的祖父节点位置,然后继续进入循环

      • 情况二:z的叔节点y是黑色的且z是一个右孩子

        ​ 先让z=z.p,然后再以z为支点进行左旋,因为左旋会让z下降一级,所以实际上z还是指向的原来那一层的节点,z.p.p的位置还是没有变。

      • 情况三:z的叔节点y是黑色的且z是一个左孩子

        ​ z.p染黑,z.p.p染红,然后以z.p.p为支点右旋

12.4 删除

  • 步骤:

    • 查找要删除的值所在的节点,如果不存在,删除失败,若存在,执行步骤2
    • 如果要删除的节点不是叶子节点,用要删除节点的后继节点替换(只进行数据替换即可,颜色不变,此时也不需要调整结构),然后删除后继节点。
  • 伪代码:

    • RB-TRANSPLANT(T, u, v)

      if u.p == T.nil
          T.root = v
      elseif u == u.p.left
          u.p.left = v
      else u.p.right = v
      v.p = u.p
      
    • RB-DELETE(T, z)

      y = z
      y-original-color = y.color
      if z.left == T.nil
          x = z.right
          RB-TRANSPLANT(T, z, z.right)
      elseif z.right == T.nil
          x = z.right
          RB-TRANSPLANT(T, z, z.right)
      else y = TREEE-MINIMUM(z.right)
          y-original-color = y.color
          x = y.right
          if y.p == z
              x.p = y
          else RB-TRANSPLANT(T, y, y.right)
              y.right = z.right
              y.right.p = y
          RB-TRANSPLANT(T, z, y)
          y.left = z.left
          y.left.p = y
          y.color = z.color
      if y.original-color == BLACK
          RB-DELETE-FIXUP(T, x)
      
    • RB-DELETE-FIXUP(T, x):恢复红黑树性质

      while x != T.root and x.color == BLACK
          if x == x.p.left
             w = x.p.right    
             if w.color = RED
                 w.color = BLACK
                 x.p.color = RED
                 LEFT-ROTATE(T, x.p)
                 w = x.p.right
             if w.left.color == BLACK and w.right.color == BLACK 
                 w.color = RED				
                 x = x.p
             else if w.right.color == BLACK
                 w.left.color = BLACK
                 w.color = RED 	
                 RIGHT-ROTATE(T, w)
                 w = x.p.right
              w.color = x.p.color
             x.p.color = BLACK
             w.right.color = BLACK
             LEFT_ROTATE(T, x.p)
             x = root[T]
         else
              x.color = BLACK
      
  • 红黑树T中删除结点z可能出现的4种情况:

    • z没有左孩子(即左孩子为T.nil),则用其右孩子来替换z,这个右孩子可以是T.nil,也可以不是,所以这种情况包含了两种情形:z没有孩子、z只有一个右孩子。

    • z有左孩子,没有右孩子,则用其左孩子来替换z。

    • z既有左孩子又有右孩子。我们找z的右子树的最小结点y来替换z,因为这样能继续保证左孩子<父结点<右孩子。这种情况下y可以分为两种情况处理:

      • y是z的右孩子,则直接用y替换z。

      • y不是z的右孩子,则先用y的右孩子替换y,然后再用y替换z。

        这两种情况用y替换z后,y的颜色要变为z的颜色。

  • python代码:

    class RB_Tree(object):    
        def rb_delete(self, z):
            if not z.left or not z.right:
                y = z
            else:
                y = self.find_successor(z)
            if y.left:
                x = y.left
            else:
                x = y.right
            x.p = y.p
            if not y.p:
                self.root = x
            elif y == y.p.left:
                y.p.left = x
            else:
                y.p.right = x
            if y != z:
                z.value = y.value
            if y.color == ColorEnum.BLACK:
                self.rb_delete_fixup(x)
            return y
     
        def rb_delete_fixup(self, x):n:
            while self.root != x and x.color == ColorEnum.BLACK:
                if x == x.p.left:
                    w = x.p.right
                    if w.color == ColorEnum.RED:
                        w.color = ColorEnum.BLACK
                        x.p.color = ColorEnum.RED
                        self.left_rotate(x.p)
                        w = x.p.right
                    if w.left.color == ColorEnum.BLACK and w.right.color == ColorEnum.BLACK:
                        w.color = ColorEnum.RED
                        x = x.p
                    elif w.right.color == ColorEnum.BLACK:
                        w.left.color = ColorEnum.BLACK
                        w.color = ColorEnum.RED
                        self.right_rotate(w)
                        w = x.p.right
                    w.color = x.p.color
                    x.p.color = ColorEnum.BLACK
                    w.right.color = ColorEnum.BLACK
                    self.left_rotate(x.p)
                    self.root = x
                else:
                    w = x.p.left
                    if w.color == ColorEnum.RED:
                        w.color = ColorEnum.BLACK
                        x.parent.color = ColorEnum.RED
                        self.right_rotate(x.p)
                        w = x.p.left
                    if w.right.color == ColorEnum.BLACK and w.left.color == ColorEnum.BLACK:
                        w.color = ColorEnum.RED
                        x = x.p
                    elif w.left.color == ColorEnum.BLACK:
                        w.right.color = ColorEnum.BLACK
                        w.color = ColorEnum.RED
                        self.left_rotate(w)
                        w = x.p.left
                    w.color = x.p.color
                    x.p.color = ColorEnum.BLACK
                    w.left.color = ColorEnum.BLACK
                    self.right_rotate(x.p)
                    self.root = x
            x.color = ColorEnum.BLACK
    

第13章 数据结构的扩张

13.1 动态顺序统计

  • 一棵顺序统计量树T通过简单地在红黑树的每个结点存入附加信息而成。在一个结点x内,除了原有的域,还包括 x . s i z e x.size x.size,这个域包含以结点x为根的子树的节点数: x . s i z e = x . l e f t . s i z e + x . r i g h t . s i z e + 1 x.size=x.left.size+x.right.size+1 x.size=x.left.size+x.right.size+1

  • 查找具有给定秩的元素:OS-SELECT(x, i)

    r = x.left.size + 1
    if i == r
        return x
    elseif i < r
        return OS-SELECT(x.left, i)
    else return OS-SELECT(x.right, i-r)
    
  • 确定一个元素的秩:OS-RANK(T, x)

    r = x.left.size + 1
    y = x
    while y != T.root
        if y == y.p.right
            then r = r + y.p.left.size + 1
        y = y.p
    return r
    
  • 对子树规模的维护:
    给点每个结点的size域后,OS-SLELECT和OS-RANK能迅速地计算出所需的顺序统计信息。然而除非能够用红黑树上基本的修改操作对这些size域加以有效的维护,否则,就达不到期望的目的。

    • 第一阶段:插入结点:在寻找插入位置的过程中,所经路径上的结点size域都需加1
    • 第二阶段:旋转:如果插入结点后,需要旋转某些结点,可以验证,可以在常数时间内更新相关结点的size域。

13.2 扩展数据结构

  • 步骤
    • 选择一种基础数据结构
    • 确定基础数据结构中要维护的附加信息
    • 检验基础数据结构上的基本操作能否维护附加信息
    • 设计新的操作

13.3 区间树

  • 闭区间是一个实数的有序对 [ t 1 , t 2 ] [t_1,t_2] [t1,t2],其中 t 1 < = t 2 t_1<=t_2 t1<=t2

  • 区间便于示占用连续时间的事件

  • 我们可以把一个区间 [ t 1 , t 2 ] [t_1,t_2] [t1,t2]表示成一个对象 i i i,其中 l o w [ i ] = t 1 low[i]=t_1 low[i]=t1为低端点, h i g h [ i ] = t 2 high[i]=t_2 high[i]=t2为高端点。若两个区间 i i i j j j重叠,则满足区间三定律之一:

    • i i i j j j重叠
    • i i i j j j左边,即 h i g h [ i ] < l o w [ j ] high[i]<low[j] high[i]<low[j]
    • i i i j j j右边,即$high[j]<low[i] $
  • 区间树:一种对动态集合进行维护的红黑树。

    • 操作:

      • INTERVAL-INSERT(T, x)
      • INTERVAL-DELETE(T, x)
      • INTERVAL-SEARCH(T, i)
    • 构造区间树:

      • 基础数据结构
        选择红黑树,其中每个结点x包含区间域x.int,关键字为区间的低端点x.int.low。这样对树进行中序遍历就可以按低端点的次序输出区间。

      • 附加信息
        每个结点除了区间信息之外,还包含一个值max[x],即以x为根的子树所有区间的端点的最大值。

      • 对信息的维护
        验证对max域的维护能在常规的插入、删除操作中完成:
        x . m a x = m a x ( x . i n t . h i g h t , x . l e f t . m a x , x . r i g h t . m a x ) x.max=max(x.int.hight,x.left.max,x.right.max) x.max=max(x.int.hight,x.left.max,x.right.max),这样更新max域只需要常数时间。

      • 设计新的操作
        INTERVAL-SEARCH(T, i)

        x = T.root
        while x != T.nil and i dose not overlap x.int
            if x.left != T.nil and x.left.max >= i.low
                x = x.left
            else x = x.right
        return x
        
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

From Star.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值