『数据结构』红黑树(red-black tree)

作者: mbinary | 来源:发表于2018-07-14 17:56 被阅读7次

    1. 定义与性质

    红黑树是一种平衡的二叉查找树

    1.1. 数据域

    每个结点有 5 个数据域

    • color: red or black
    • key: keyword
    • left: pointer to left child
    • right:pointer to right child
    • p: pointer to nil leaf

    1.2. 红黑性质

    满足下面的 红黑性质 的二叉查找树就是红黑树:

    • 每个结点或是红色或是黑色
    • 根是黑
    • nil leaf 是 黑
    • 红结点的孩子是黑
    • 从每个结点出发,通过子孙到达叶子结点的各条路径上 黑结点数相等

    如,叶子结点 是 nil, 即不存储任何东西, 为了编程方便,相对的,存有数据的结点称为内结点


    为了节省空间, 可以如下实现, 只需要一个 nil 结点


    nil leaf

    1.3. 黑高度

    从某个结点 x 到叶结点的黑色结点数,称为此结点的黑高度, 记为 h_b(x)
    树的黑高度是根的黑高度

    1. 以 x 为 根的子树至少包含 2^{h_b(x)}-1个结点
    2. 一颗有 n 个内结点的红黑树高度至多为2lg(n+1)

    可用归纳法证明1
    证明 2:
    设树高 h
    由红黑性质4, 根结点到叶子路径上的黑结点数至少 \frac{h}{2},即 h_b(root)\geqslant \frac{h}{2}
    再由1,
    n \geqslant 2^{h_b(x)} -1 \geqslant 2^{\frac{h}{2}} -1

    h\leqslant 2lg(n+1)

    2. 旋转

    由于上面证明的红黑树高为 O(logn),红黑树的 insert, delete, search 等操作都是, O(logn).
    进行了 insert, delete 后可能破坏红黑性质, 可以通过旋转来保持.

    下面是对结点 x 进行 左旋与右旋.
    注意进行左旋时, 右孩子不是 nil(要用来作为旋转后 x 的双亲), 同理 右旋的结点的左孩子不是nil


    左旋与右旋

    总结起来就是: 父亲旋转,顺时针就是右旋,逆时针就是左旋, 旋转的结果是儿子成为原来父亲的新父亲, 即旋转的结点下降一层, 它的一个儿子上升一层.

    3. 插入

    插入的过程:

    • 先同二叉查找树那样插入, 做为叶子(不为空)
    • 然后将新结点的 左右孩子设为 nil , 颜色设为红色
    • 最后再进行颜色调整以及旋转(维持红黑性质)

    这是算法导论[1]上的算法

    RB-INSERT(T, z)  
     y ← nil[T]                        // 新建节点“y”,将y设为空节点。
     x ← root[T]                       // 设“红黑树T”的根节点为“x”
     while x ≠ nil[T]                  // 找出要插入的节点“z”在二叉树T中的位置“y”
         do y ← x                      
            if key[z] < key[x]  
               then x ← left[x]  
               else x ← right[x]  
     p[z] ← y                          // 设置 “z的父亲” 为 “y”
     if y = nil[T]                     
        then root[T] ← z               // 情况1:若y是空节点,则将z设为根
        else if key[z] < key[y]        
                then left[y] ← z       // 情况2:若“z所包含的值” < “y所包含的值”,则将z设为“y的左孩子”
                else right[y] ← z      // 情况3:(“z所包含的值” >= “y所包含的值”)将z设为“y的右孩子” 
     left[z] ← nil[T]                  // z的左孩子设为空
     right[z] ← nil[T]                 // z的右孩子设为空。至此,已经完成将“节点z插入到二叉树”中了。
     color[z] ← RED                    // 将z着色为“红色”
     RB-INSERT-FIXUP(T, z)             // 通过RB-INSERT-FIXUP对红黑树的节点进行颜色修改以及旋转,让树T仍然是一颗红黑树
    

    3.1. 二叉查找树的插入

    可以用python 实现如下

        def insert(self,nd):
            if  not isinstance(nd,node):
                nd = node(nd)
            elif nd.isBlack: nd.isBlack = False
    
            if self.root is None:
                self.root = nd
                self.root.isBlack = True
            else:
                parent = self.root
                while parent:
                    if parent == nd : return None
                    if parent>nd:
                        if parent.left :
                            parent = parent.left
                        else:
                            parent.left  = nd
                            break
                    else:
                        if parent.right:
                            parent = parent.right
                        else:
                            parent.right = nd
                            break
                self.fixUpInsert(parent,nd)
    

    3.2. 颜色调整与旋转

    3.2.1. 问题

    在插入后,可以发现后破坏的红黑性质只有以下两条(且互斥)

    1. root 是红 (这可以直接将root 颜色设为黑调整)
    2. 红结点的孩子是黑

    所以下面介绍如何保持 红结点的孩子是黑 , 即插入结点的双亲结点是红的情况.

    下面记 结点 x 的 双亲为 p(x), 新插入的结点为 x, 记 uncle 结点 为 u(x)

    由于 p(x) 是红色, 而根结点是黑色, 所以 p(x)不是根, p(p(x))存在

    3.2.2. 情况

    有如下三种情况

    每种情况的解决方案如下

    3.2.2.1. case1: x 的叔叔是红色的

    这里只需改变颜色, 将 p(x)变为 黑, p(p(x))变为红, u(x) 变为黑色 (x为右孩子同样)


    3.2.2.2. case2: x 的叔叔是黑色, x,p(x), p(p(x)),方向为 left-right 或者 right-left

    即 x,p(x), p(p(x)) 成折线状

    3.2.2.3. case3: x 的叔叔是黑色, x,p(x), p(p(x)),方向为 left-left 或者 right-right

    即 x,p(x), p(p(x)) 成直线状

    当 x 为右孩子时, 通过旋转变成p(x) 的双亲, 然后相当于 新插入 p(x)作为左孩子, 再进行转换.

    即将新结点的双亲向上一层旋转,颜色变为黑色, 而新节点的祖父向下一层, 颜色变为红色

    3.2.3. 总体解决方案

    我最开始也没有弄清楚, 有点绕晕的感觉, 后来仔细读了书上伪代码, 然后才发现就是一个状态机, 画出来就一目了然了.

    现在算是知其然了, 那么怎样知其所以然呢? 即 为什么要分类这三个 case, 不重不漏了吗?

    其实也简单, 只是太繁琐.
    就是将各种情况枚举出来, 一一分析即可. 我最开始试过, 但是太多,写在代码里很容易写着写着就混了.
    而算法导论上分成这三个case , 很简洁, 只是归纳了一下而已. 如果想看看枚举情况的图与说明,可以参考[2] .

    算法导论上的伪代码

    RB-INSERT-FIXUP(T, z)
    while color[p[z]] = RED                                                  // 若“当前节点(z)的父节点是红色”,则进行以下处理。
        do if p[z] = left[p[p[z]]]                                           // 若“z的父节点”是“z的祖父节点的左孩子”,则进行以下处理。
              then y ← right[p[p[z]]]                                        // 将y设置为“z的叔叔节点(z的祖父节点的右孩子)”
                   if color[y] = RED                                         // Case 1条件:叔叔是红色
                      then color[p[z]] ← BLACK                    ▹ Case 1   //  (01) 将“父节点”设为黑色。
                           color[y] ← BLACK                       ▹ Case 1   //  (02) 将“叔叔节点”设为黑色。
                           color[p[p[z]]] ← RED                   ▹ Case 1   //  (03) 将“祖父节点”设为“红色”。
                           z ← p[p[z]]                            ▹ Case 1   //  (04) 将“祖父节点”设为“当前节点”(红色节点)
                      else if z = right[p[z]]                                // Case 2条件:叔叔是黑色,且当前节点是右孩子
                              then z ← p[z]                       ▹ Case 2   //  (01) 将“父节点”作为“新的当前节点”。
                                   LEFT-ROTATE(T, z)              ▹ Case 2   //  (02) 以“新的当前节点”为支点进行左旋。
                              color[p[z]] ← BLACK                 ▹ Case 3   // Case 3条件:叔叔是黑色,且当前节点是左孩子。(01) 将“父节点”设为“黑色”。
                              color[p[p[z]]] ← RED                ▹ Case 3   //  (02) 将“祖父节点”设为“红色”。
                              RIGHT-ROTATE(T, p[p[z]])            ▹ Case 3   //  (03) 以“祖父节点”为支点进行右旋。
           else (same as then clause with "right" and "left" exchanged)      // 若“z的父节点”是“z的祖父节点的右孩子”,将上面的操作中“right”和“left”交换位置,然后依次执行。
    color[root[T]] ← BLACK
    

    我用python 实现如下. 由于左右方向不同, 如果向上面伪代码那样实现, fixup 代码就会有两份类似的(即 right left 互换), 为了减少代码冗余, 我就定义了 setChild, getChild 函数, 传递左或是右孩子这个方向的数据(代码中是isLeft), 所以下面的就是完整功能的 fixup, 可以减少一般的代码量, haha😄,
    (下文 删除结点同理)

    其实阅读代码也简单, 可以直接当成 isLeft 取真值.

        def fixUpInsert(self,parent,nd):
            ''' adjust color and level,  there are two red nodes: the new one and its parent'''
            while not self.checkBlack(parent):
                grand = self.getParent(parent)
                isLeftPrt = grand.left is parent 
                uncle = grand.getChild(not isLeftPrt)
                if not self.checkBlack(uncle):
                    # case 1:  new node's uncle is red
                    self.setBlack(grand, False)
                    self.setBlack(grand.left, True)
                    self.setBlack(grand.right, True)
                    nd = grand
                    parent = self.getParent(nd)
                else:
                    # case 2: new node's uncle is black(including nil leaf)
                    isLeftNode = parent.left is nd
                    if isLeftNode ^ isLeftPrt:
                        # case 2.1 the new node is inserted in left-right or right-left form
                        #         grand               grand
                        #     parent        or            parent
                        #          nd                   nd
                        parent.setChild(nd.getChild(isLeftPrt),not isLeftPrt)
                        nd.setChild(parent,isLeftPrt)
                        grand.setChild(nd,isLeftPrt)
                        nd,parent = parent,nd
                    # case 2.2 the new node is inserted in left-left or right-right form
                    #         grand               grand
                    #      parent        or            parent
                    #     nd                                nd
                    grand.setChild(parent.getChild(not isLeftPrt),isLeftPrt)
                    parent.setChild(grand,not isLeftPrt)
                    self.setBlack(grand, False)
                    self.setBlack(parent, True)
                    self.transferParent(grand,parent)
            self.setBlack(self.root,True)
    

    4. 删除

    算法导论上的算法

    写的很简练👍


    rb-delete

    4.1. 二叉查找树删除结点

    下面 z 是要删除的结点, y 是 其后继或者是它自己, x 是 y 的一个孩子(如果 y 的孩子为 nil,则为 nli, 否则 y 只有一个非 nil 孩子, 为 x)

    • 当 z 孩子全是 nil (y==z): 直接让其双亲对应的孩子为 nil
    • 当 z 只有一个非 nil 孩子 x (y==z):
      1. 如果 z 为根, 则让 x 为根.
      2. 让 y 的双亲连接到 x
    • 当 z 有两个非nil孩子(y!=z): 复制其后继 y 的内容到 z (除了指针,颜色) , 将其后继 y 的孩子(最多只有一个 非 nil ,不然就不是后继了)连接到其后继的双亲, 删除 其后继y,

    [3] 如果要删除有两个孩子的结点 z , 则找到它的后继y(前趋同理), 可以推断 y 一定没有左孩子, 右孩子可能有,可能没有. 也就是最多一个孩子.
    所以将 y 的值复制到 x 位置, 现在相当于删除 y 处的结点.
    这样就化为 删除的结点最多一个孩子的情况.

    4.2. 调整颜色与旋转

    可以发现只有当 y 是黑色,才进行颜色调整以及旋转(维持红黑性质), 因为如果删除的是红色, 不会影响黑高度, 所有红黑性质都不会破坏
    伪代码如下, (我的python代码见文末)


    如果被删除的结点 y 是黑色的, 有三种破坏红黑性质的情况

    1. y是根, 则 y 的一个红色孩子成为新根
    2. 进行删除结点过程中, p(y) 的孩子有 x, 两者都是红色
    3. 删除 y 导致包含y 的路径上的黑结点 少 1个

    修复3的思路:
    如果可能,在兄弟一支,通过旋转,改变颜色修复
    否则, 将红结点一直向上推(因为当前路径上少了一个黑结点,向上推的过程中使红结点所在的子树都少一个黑结点), 直到到达树根, 那么全部路径都少一个黑结点, 3就修复了, 这时只需将根设为黑就修复了 1

    代码中的 while 循环的目的是将额外的黑色沿树上移,直到

    • x 指向一个红黑结点
    • x 指向根,这时可以简单地消除额外的黑色
    • 颜色修改与旋转

    在 while 中, x 总是指向具有双重黑色的那个非根结点, 在第 2 行中要判断 x 是其双亲的左右孩子
    w 表示 x 的相抵. w 不能为 nil(因为 x 是双重黑色)

    算法中的四种情况如图所示


    • x 的兄弟 w 是红色的


    • x 的兄弟 w 是黑色的, w的两个孩子都是黑色的

    • x 的兄弟 w 是黑色的, w 的左孩子是红,右孩子是黑

    • x 的兄弟 w 是黑色的, w 的孩子是红色的

    注意上面都是先考虑的左边, 右边可以对称地处理.

    同插入一样, 为了便于理解, 可以作出状态机.
    而且这些情形都是归纳化简了的, 你也可以枚举列出基本的全部情形.


    5. 数据结构的扩张

    5.1. 平衡树的扩张

    通过在平衡树(如红黑树上的每个结点 加上 一个数据域 size (表示以此结点为根的子树的结点数.) 可以使获得第 i 大的数 的时间复杂度为 O(logn)

    O(n) 时间内建立, python代码如下

    def setSize(root):
        if root is None:return 0
        root.size = setSize(root.left) + setSize(root.right)+1
    

    O(logn)时间查找,

    def find(root,i):
        r =  root.left.size +1
        if r==i:
            return root
        if r > i:
            return find(root.left,i)
        else:
            return find(root.right,i-r)
    

    6. python 代码

    github地址

    我用了 setChild, getChild 来简化代码量, 其他的基本上是按照算法导论上的伪代码提到的case 来实现的. 然后display 只是测试的时候,为了方便调试而层序遍历打印出来

    效果如下


    '''
    #########################################################################
    # File : redBlackTree.py
    # Author: mbinary
    # Mail: zhuheqin1@gmail.com
    # Blog: https://mbinary.coding.me
    # Github: https://github.com/mbinary
    # Created Time: 2018-07-12  20:34
    # Description: 
    #########################################################################
    '''
    from functools import total_ordering
    from random import randint, shuffle
    
    @total_ordering
    class node:
        def __init__(self,val,left=None,right=None,isBlack=False):
            self.val =val
            self.left = left
            self.right = right
            self.isBlack  = isBlack
        def __lt__(self,nd):
            return self.val < nd.val
        def __eq__(self,nd):
            return nd is not None and self.val == nd.val
        def setChild(self,nd,isLeft = True):
            if isLeft: self.left = nd
            else: self.right = nd
        def getChild(self,isLeft):
            if isLeft: return self.left
            else: return self.right
        def __bool__(self):
            return self.val is not None
        def __str__(self):
            color = 'B' if self.isBlack else 'R'
            return f'{color}-{self.val:}'
        def __repr__(self):
            return f'node({self.val},isBlack={self.isBlack})'
    class redBlackTree:
        def __init__(self,unique=False):
            '''if unique is True, all node'vals are unique, else there may be equal vals'''
            self.root = None
            self.unique = unique
    
        @staticmethod
        def checkBlack(nd):
            return nd is None or nd.isBlack
        @staticmethod
        def setBlack(nd,isBlack):
            if nd is not None:
                if isBlack is None or isBlack:
                    nd.isBlack = True
                else:nd.isBlack = False
        def sort(self,reverse = False):
            ''' return a generator of sorted data'''
            def inOrder(root):
                if root is None:return
                if reverse:
                    yield from inOrder(root.right)
                else:
                    yield from inOrder(root.left)
                yield root
                if reverse:
                    yield from inOrder(root.left)
                else:
                    yield from inOrder(root.right)
            yield from inOrder(self.root)
        def getParent(self,chd):
            '''note that use is to find real node when different nodes have euqiv val'''
            if self.root is chd:return None
            nd = self.root
            while nd:
                if nd>chd and  nd.left is not None:
                    if nd.left is  chd: return nd
                    else: nd = nd.left
                elif nd<chd and  nd.right is not None:
                    if nd.right is  chd: return nd
                    else: nd = nd.right
        def find(self,val):
            nd = self.root
            while nd:
                if nd.val ==val:
                    return nd
                elif nd.val>val:
                    nd = nd.left
                else:
                    nd = nd.right
        def getSuccessor(self,nd):
            if nd:
                if nd.right:
                    nd = nd.right
                    while nd.left:
                        nd = nd.left
                    return nd
                else:return self.getParent(nd)
        def transferParent(self,origin,new):
            if origin is  self.root:
                self.root = new
            else:
                prt = self.getParent(origin)
                prt.setChild(new, prt.left is origin)
    
        def insert(self,nd):
            if  not isinstance(nd,node):
                nd = node(nd)
            elif nd.isBlack: nd.isBlack = False
    
            if self.root is None:
                self.root = nd
                self.root.isBlack = True
            else:
                parent = self.root
                while parent:
                    if parent == nd : return None
                    if parent>nd:
                        if parent.left :
                            parent = parent.left
                        else:
                            parent.left  = nd
                            break
                    else:
                        if parent.right:
                            parent = parent.right
                        else:
                            parent.right = nd
                            break
                self.fixUpInsert(parent,nd)
        def fixUpInsert(self,parent,nd):
            ''' adjust color and level,  there are two red nodes: the new one and its parent'''
            while not self.checkBlack(parent):
                grand = self.getParent(parent)
                isLeftPrt = grand.left is parent 
                uncle = grand.getChild(not isLeftPrt)
                if not self.checkBlack(uncle):
                    # case 1:  new node's uncle is red
                    self.setBlack(grand, False)
                    self.setBlack(grand.left, True)
                    self.setBlack(grand.right, True)
                    nd = grand
                    parent = self.getParent(nd)
                else:
                    # case 2: new node's uncle is black(including nil leaf)
                    isLeftNode = parent.left is nd
                    if isLeftNode ^ isLeftPrt:
                        # case 2.1 the new node is inserted in left-right or right-left form
                        #         grand               grand
                        #     parent        or            parent
                        #          nd                   nd
                        parent.setChild(nd.getChild(isLeftPrt),not isLeftPrt)
                        nd.setChild(parent,isLeftPrt)
                        grand.setChild(nd,isLeftPrt)
                        nd,parent = parent,nd
                    # case 2.2 the new node is inserted in left-left or right-right form
                    #         grand               grand
                    #      parent        or            parent
                    #     nd                                nd
                    grand.setChild(parent.getChild(not isLeftPrt),isLeftPrt)
                    parent.setChild(grand,not isLeftPrt)
                    self.setBlack(grand, False)
                    self.setBlack(parent, True)
                    self.transferParent(grand,parent)
            self.setBlack(self.root,True)
    
        def copyNode(self,src,des):
            '''when deleting a node which has two kids, 
                copy its succesor's data to his position
                data exclude left, right , isBlack
            '''
            des.val = src.val
        def delete(self,nd):
            '''delete node in a binary search tree'''
            if not isinstance(nd,node):
                nd = self.find(nd)
            if nd is None: return
            y = None
            if nd.left and nd.right:
                y= self.getSuccessor(nd)
            else:
                y = nd
            py = self.getParent(y)
            x = y.left if y.left else y.right
            if py is None:
                self.root = x
            elif y is py.left:
                py.left = x
            else:
                py.right = x
            if y != nd:
                self.copyNode(y,nd)
    
            if self.checkBlack(y): self.fixUpDel(py,x)
    
     
        def fixUpDel(self,prt,chd):
            ''' adjust colors and rotate '''
            while self.root != chd and self.checkBlack(chd):
                isLeft = prt.left is  chd 
                brother = prt.getChild(not isLeft)
                # brother is black
                lb = self.checkBlack(brother.getChild(isLeft))
                rb = self.checkBlack(brother.getChild(not isLeft))
                if  not self.checkBlack(brother):
                    # case 1: brother is red.   converted to  case 2,3,4
                    # prt (isLeft) rotate
                    prt.setChild(brother.getChild(isLeft), not isLeft)
                    brother.setChild(prt, isLeft)
    
                    self.setBlack(prt,False)
                    self.setBlack(brother,True)
    
                    self.transferParent(prt,brother)
                elif lb and rb: 
                    # case 2: brother is black and two kids are black. 
                    # conveted to the begin case
                    self.setBlack(brother,False)
                    chd = prt
                    prt = self.getParent(chd)
                else:
                    if  rb:
                        # case 3: brother is black and left kid is red and right child is black
                        # uncle's son is nephew, and niece for uncle's daughter
                        nephew = brother.getChild(isLeft)
                        self.setBlack(nephew,True)
                        self.setBlack(brother,False)
    
                        # brother (not isLeft) rotate
                        prt.setChild(nephew,not isLeft)
                        brother.setChild(nephew.getChild(not isLeft),isLeft)
                        nephew.setChild(brother, not isLeft)
                        brother = nephew
    
                    # case 4: brother is black and right child is red
                    brother.isBlack = prt.isBlack
                    self.setBlack(prt,True)
                    self.setBlack(brother.getChild(not isLeft),True)
    
                    # prt left rotate
                    prt.setChild(brother.getChild(isLeft),not isLeft)
                    brother.setChild(prt,isLeft)
    
                    self.transferParent(prt,brother)
    
                    chd = self.root
            self.setBlack(chd,True)
    
    
        def display(self):
            def getHeight(nd):
                if nd is None:return 0
                return max(getHeight(nd.left),getHeight(nd.right)) +1
            def levelVisit(root):
                from collections import deque
                lst = deque([root])
                level = []
                h = getHeight(root)
                ct = lv = 0
                while 1:
                    ct+=1
                    nd = lst.popleft()
                    if ct >= 2**lv:
                        lv+=1
                        if lv>h:break
                        level.append([])
                    level[-1].append(str(nd))
                    if nd is not None:
                        lst += [nd.left,nd.right]
                    else:
                        lst +=[None,None]
                return level
            def addBlank(lines):
                width = 5
                sep = ' '*width
                n = len(lines)
                for i,oneline in enumerate(lines):
                    k  = 2**(n-i) -1
                    new = [sep*((k-1)//2)]
                    for s in oneline:
                        new.append(s.ljust(width))
                        new.append(sep*k)
                    lines[i] = new
                return lines
    
            lines = levelVisit(self.root)
            lines = addBlank(lines)
            li = [''.join(line) for line in lines]
            li.insert(0,'red-black-tree'.rjust(48,'-')  + '-'*33)
            li.append('end'.rjust(42,'-')+'-'*39+'\n')
            return  '\n'.join(li)
           
        def __str__(self):
            return self.display()
    

    测试代码

    def genNum(n =10):
        nums =[]
        for i in range(n):
            while 1:
                d = randint(0,100)
                if d not in nums:
                    nums.append(d)
                    break
        return nums
    
    def buildTree(n=10,nums=None,visitor=None):
        if nums is None or nums ==[]: nums = genNum(n)
        rbtree = redBlackTree()
        print(f'build a red-black tree using {nums}')
        for i in nums:
            rbtree.insert(i)
            if visitor:
                visitor(rbtree)
        return rbtree,nums
    def testInsert():
        def visitor(t):
            print(t)
        rbtree,nums = buildTree(visitor = visitor)
        print('-'*5+ 'in-order visit' + '-'*5)
        for i,j in enumerate(rbtree.sort()):
            print(f'{i+1}: {j}')
    
    def testSuc():
        rbtree,nums = buildTree()
        for i in rbtree.sort():
            print(f'{i}\'s suc is {rbtree.getSuccessor(i)}')
    
    def testDelete():
        #nums = [2,3,3,2,6,7,2,1]
        nums = None
        rbtree,nums = buildTree(nums = nums)
        print(rbtree)
        for i in nums:
            print(f'deleting {i}')
            rbtree.delete(i)
            print(rbtree)
    
    if __name__=='__main__':
        #testSuc()
        #testInsert()
        testDelete()
    

    7. 参考


    1. 算法导论

    2. https://www.jianshu.com/p/a5514510f5b9?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

    3. https://www.jianshu.com/p/0b68b992f688?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

    相关文章

      网友评论

        本文标题:『数据结构』红黑树(red-black tree)

        本文链接:https://www.haomeiwen.com/subject/hkbypftx.html