HashMap(红黑树篇)

本篇文章只分析红黑树的那块,其他部分请看本人的另一篇文章。

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

可以看到,TreeNode是Node的子类,所以TreeNode也拥有Node的next属性,记住这个,下面会讲到。

treeify(Node<K,V>[] tab):

final void treeify(Node<K,V>[] tab) {
    
    
    TreeNode<K,V> root = null;
    //this是头节点,从头开始遍历
    for (TreeNode<K,V> x = this, next; x != null; x = next) {
    
    
        //取当前节点的next
        next = (TreeNode<K,V>)x.next;
        x.left = x.right = null;
        //判断根节点是否为null
        if (root == null) {
    
    
            //把第一个节点设为根节点
            x.parent = null;
            x.red = false;
            root = x;
        }
        else {
    
    
            K k = x.key;
            int h = x.hash;
            Class<?> kc = null;
            //从根节点开始遍历
            for (TreeNode<K,V> p = root;;) {
    
    
                //dir是1代表插到右边,是-1代表插到左边
                //ph是当前节点的哈希值,这里的红黑树是以哈希值进行排序的。
                int dir, ph;
                K pk = p.key;
                //p是当前节点,h是要插入的节点的hash,判断p的hash是否大于当前节点的hash
                if ((ph = p.hash) > h)
                    dir = -1;
                else if (ph < h)
                    dir = 1;
                else if ((kc == null &&
                        (kc = comparableClassFor(k)) == null) ||
                        (dir = compareComparables(kc, k, pk)) == 0)
                    dir = tieBreakOrder(k, pk);

                TreeNode<K,V> xp = p;
                //根据dir判断左右,如果是null就可以插入,不为null就从当前节点的左边或者右边重新遍历
                //其实就是一个递归,不停的往左或者右遍历
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
    
    
                    //x是要插入的节点,xp是for里面的当前节点,把x.parent指向xp,插在xp的左或者右
                    x.parent = xp;
                    //根据dir判断把节点插到左还是右
                    if (dir <= 0)
                        xp.left = x;
                    else
                        xp.right = x;
                    //插入的修正
                    root = balanceInsertion(root, x);
                    break;
                }
            }
        }
    }
    //把根节点挪到链表的头节点
    moveRootToFront(tab, root);
}

看上去一堆代码,其实不复杂,插入节点这一步可以看成一个简单的二叉树,小于就往左边插,大于就往右边插:在这里插入图片描述

不过二叉树的缺点太明显了,某种情况下,会变成上图所示,活脱脱的就是一个链表。HashMap当前不会这么笨,HashMap用的红黑树,每次插入都会修正的。

真正讲修正之前,必须要先说清楚2个概念,左旋和右旋。

  • 左旋:孩子节点S把父节点E替掉,E变成S的左节点,S的左节点变成E的右节点。左旋的顶端节点必须要有右子节点,也就是必须要有S节点。
    在这里插入图片描述

看下红黑树里面的左旋,代码整合:

static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
                                      TreeNode<K,V> p) {
    
    
    //p相当于上面的E节点
    //r是S的右子节点,就是S节点,必须要有,因为左旋的顶端节点必须要有右子节点
    //pp是p的父节点,E的父亲,没有说明S就是根节点
    //rl相当于E的左子节点
    TreeNode<K,V> r, pp, rl;
    //左旋的顶端节点必须要有右子节点
    if (p != null && (r = p.right) != null) {
    
    
        //把l的left赋给p的right,想当于S的左子节点赋给E的右子节点
        if ((rl = p.right = r.left) != null)
            //不为null再把rl的parent指向p,这样就彻底替换成功
            rl.parent = p;
        //判断p.parent是否为null,相当于判断E是不是根节点
        if ((pp = r.parent = p.parent) == null)
            //为null说明S是根节点,把S变成新的根节点,且涂黑
            (root = r).red = false;
            //判断E在不在左边
        else if (pp.left == p)
            //在左边,就把pp的left指向r,相当于S把E顶掉了。
            pp.left = r;
        else
            //在右边,就把pp的right指向r,相当于S把E顶掉了。
            pp.right = r;
        //把r的left指向p,相当于E变成S的右节点
        r.left = p;
        //再把p的parent指向r,这个父子关系就成了
        p.parent = r;
    }
    return root;
}

一堆代码,仔细一看就是上面动图的步骤:

1:判断左旋的顶端节点必须要有右子节点

2:S有左节点就赋给E的right

3:然后S把E的位置顶掉。

下面看几个示意图:

S(4)有左子节点,p(2)是根节点的完整示意图:
在这里插入图片描述

S(2)没有左子节点,p(1)不是根节点,E(1)在整体的左边的完整示意图:
在这里插入图片描述
S(4)没有左子节点,p(3)不是根节点,E(3)在整体的右边的完整示意图:
在这里插入图片描述

  • 右旋:孩子节点E把父节点S替掉,S变成E的右节点,E的右节点变成S的左节点。右旋的顶端节点必须要有左子节点,也就是必须要有E节点。
    在这里插入图片描述

其实右旋跟左旋意思差不多,左旋理解了,右旋就是反一下,这里在简单描述一下,不写那么详细了。还是先上代码:

static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,
                                       TreeNode<K,V> p) {
    
    
    //p相当于上面的S节点
    //l是S的左子节点,就是E节点,必须要有,因为右旋的顶端节点必须要有左子节点
    //pp是p的父节点,S的父亲,没有说明S就是根节点
    //lr相当于E的右子节点
    TreeNode<K,V> l, pp, lr;
    //右旋的顶端节点必须要有左子节点
    if (p != null && (l = p.left) != null) {
    
    
        //把l的right赋给p的left,想当于E的右子节点赋给S的左子节点
        if ((lr = p.left = l.right) != null)
            //不为null再把lr的parent指向p,这样就彻底替换成功
            lr.parent = p;
        //判断p.parent是否为null,相当于判断S是不是根节点
        if ((pp = l.parent = p.parent) == null)
            //为null说明S是根节点,把E变成新的根节点,且涂黑
            (root = l).red = false;
            //判断S在不在右边
        else if (pp.right == p)
            //在右边,就把pp的right指向l,相当于E把S顶掉了。
            pp.right = l;
        else
            //在左边,就把pp的left指向l,相当于E把S顶掉了。
            pp.left = l;
        //把l的right指向p,相当于S变成E的右节点
        l.right = p;
        //p的parent指向l,父子关系成了
        p.parent = l;
    }
    return root;
}

大概注释都有了,这里就不行详细拆分看了,直接看几个完整的示意图吧。

E(2)没有右子节点,p(3)不是根节点,E在整体的右边的完整示意图:
在这里插入图片描述
E(4)有右子节点,p(6)是根节点的完整示意图:
在这里插入图片描述
建议大家自己动手画几幅图,基本上就能理解了,现在我们回到插入的修正。

插入的修正,balanceInsertion(),拆开说:

1:修正成功的两种情况:

//默认是插入的节点是红色的,所以先涂成红色
x.red = true;
//xp是新插入的节点的父节点,xpp是祖父节点
for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
    
    
    //判断下插入的节点的parent是否为null,相当于判断是不是根节点
    if ((xp = x.parent) == null) {
    
    
        x.red = false;
        return x;
  }
  //如果父节点是黑色的,或者父节点是根节点,不需要修正,完美插入
  else if (!xp.red || (xpp = xp.parent) == null)
      return root;

不停的循环,直至满足条件:当前是根节点、父节点是黑色的、或者父节点是根节点才跳出循环。

2:如果父节点在左边:

//判断新插入节点的父节点是否等于祖父节点的左节点,判断是不是在树的左边
if (xp == (xppl = xpp.left)) {

父节点在左边,右叔父节点不为null,且是红色:

//在左边,并且右叔父节点不为null,且是红色
if ((xppr = xpp.right) != null && xppr.red) {
    
    
    //把祖父节点的右节点涂黑
    xppr.red = false;
    //把父节点涂黑
    xp.red = false;
    //把祖父节点涂红
    xpp.red = true;
    //把祖父节点赋给x,继续往上追溯修正
    //因为要判断此时的xpp的父节点是不是红的,或者xpp是root呢?
    x = xpp;
}

父节点在左边,右叔父节点不为null,且是红色的示意图:
在这里插入图片描述
涂完之后,用祖父节点2重新循环,因为此时的祖父2是红色的。

  • 父节点在左边,右叔父节点为null:
else {
    
    
  //右叔父节点为null,自己在右边
  if (x == xp.right) {
    
    
      //在右边,左旋
      root = rotateLeft(root, x = xp);
      //左旋之后父节点不为null,就把新的祖父节点重新赋给xpp
      xpp = (xp = x.parent) == null ? null : xp.parent;
  }
  //判断父节点不为null
  if (xp != null) {
    
    
      //父节点涂黑
      xp.red = false;
      //判断是否有祖父节点
      if (xpp != null) {
    
    
          //祖父节点涂黑
          xpp.red = true;
          //右旋
          root = rotateRight(root, xpp);
    }
  }
}

父节点在左边,右叔父节点为null,插入的节点在左边的示意图:
在这里插入图片描述
右旋的图大家可以自己画一下,就是2上去,3下来。

3:父节点在右边:

  • 父节点在右边,左叔父节点不为null,且是红色:
else {
    
    
    //插入的父节点在右边,判断左叔父节点是否为null,是否是红色
    if (xppl != null && xppl.red) {
    
    
        //左叔父节点涂黑
        xppl.red = false;
        //父节点涂黑
        xp.red = false;
        //祖父节点涂红
        xpp.red = true;
        //把祖父节点赋给x,继续往上追溯修正
        //因为要判断此时的xpp的父节点是不是红的,或者xpp是root呢?
        x = xpp;
  }

父节点在右边,左叔父节点不为null,且是红色示意图:
在这里插入图片描述

  • 父节点在右边,左叔父节点为null:
else {
    
    
    //左叔父节点为null,自己在左边
    if (x == xp.left) {
    
    
        //在左边,右旋
        root = rotateRight(root, x = xp);
        //重新定义一下祖父节点
        xpp = (xp = x.parent) == null ? null : xp.parent;
    }
    if (xp != null) {
    
    
        //插入在右边,把父节点涂黑
        xp.red = false;
        //判断是否有祖父节点
        if (xpp != null) {
    
    
          //祖父节点涂红
          xpp.red = true;
          //左旋
          root = rotateLeft(root, xpp);
      }
    }
}

父节点在右边,左叔父节点为null,自己在左边示意图:
在这里插入图片描述
左旋之后大家可以自己画。

moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root):

static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
    
    
    int n;
    if (root != null && tab != null && (n = tab.length) > 0) {
    
    
        int index = (n - 1) & root.hash;
        //first是保存进此下标的第一个节点
        TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
        //判断此时的根节点是否等于当时的第一个节点
        if (root != first) {
    
    
            Node<K,V> rn;
            //把此时的下标指向根节点
            tab[index] = root;
            //取上个节点,注意此时的每个节点有prev有next
          	//还是每次新增的顺序,既是红黑树,又是双向链表。
            TreeNode<K,V> rp = root.prev;
            //判断根据是否有next
            if ((rn = root.next) != null)
                //把root的next的prev指向root的prev
              	//相当于root的后一个的前驱指向root的前一个,跳过root
                ((TreeNode<K,V>)rn).prev = rp;
            //判断root的前一个节点不为null
            if (rp != null)
                //把root的前一个节点的next指向root的后一个节点,这样就是一个删除掉root的双向链表
                rp.next = rn;
            if (first != null)
                //把first的前驱指向root
                first.prev = root;
            //root的后驱指向first,相当于把root排到第一个
            root.next = first;
            //root的前驱置为null,因为此时的root也变成了链表的头节点
            root.prev = null;
        }
        assert checkInvariants(root);
    }
}

其实很简单,相当于链表的删除,把root删掉,挪到链表的头部,链表的删除另一篇文章有详细示意图。

putTreeVal(HashMap<K,V> map, Node<K,V>[] tab, int h, K k, V v):

if ((p = (dir <= 0) ? p.left : p.right) == null) {
    
    
    Node<K,V> xpn = xp.next;
    TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
    if (dir <= 0)
      	xp.left = x;
    else
      	xp.right = x;
    //跟上面的treeify几乎一样,除了这里。
    //因为treeify是链表转红黑树,已经是链表了。
    //这是红黑树的新增,所以加了next和prev,额外维护了一个双向链表。
    xp.next = x;
    x.parent = x.prev = xp;
    if (xpn != null)
        ((TreeNode<K,V>)xpn).prev = x;
         moveRootToFront(tab, balanceInsertion(root, x));
    return null;
}

跟上面的treeify几乎一样,除了上面注释的那2行。比treeify多维护了一个双向链表,而treeify已经是双向链表了。可以看得出,HashMap真的占空间,next、parent、left、right、prev,为了新增、删除、遍历的平衡,不容易。

treeifyBin(Node<K,V>[] tab, int hash):

  final void treeifyBin(Node<K,V>[] tab, int hash) {
    
    
        int n, index; Node<K,V> e;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            //小于MIN_TREEIFY_CAPACITY优先进行扩容而不是树化
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
    
    
            TreeNode<K,V> hd = null, tl = null;
            do {
    
    
                //把节点改成树结构的节点,由Node变成TreeNode
                TreeNode<K,V> p = replacementTreeNode(e, null);
                //判断tl是否为null,相当于判断是不是第一次进循环,取头结点
                if (tl == null)
                    //是第一次就把p赋给hd
                    hd = p;
                else {
    
    
                    //把p的prev指向tl,此时的p是当前节点,最后的tl = p是最后一步,此时还是上个节点。
                    p.prev = tl;
                    //把tl的next指向p,此时的tl是上个节点,
                  	//当前节点的prev指向上个节点,上个节点的next指向当前节点,组成了一个双向的链表
                    tl.next = p;
                }
                //每次记住当前节点
                tl = p;
                //刚开始的e其实是此下标的第一个节点,这里相当于从头开始遍历
            } while ((e = e.next) != null);
            //再说一下,此时还是链表,双向的,只不过每个节点从Node变成了TreeNode
            if ((tab[index] = hd) != null)
                //真正开始转树结构
                hd.treeify(tab);
        }
    }

很简单,把Node变成TreeNode,并且TreeNode还补上了next跟prev,相当于一个双向链表,图就不画了,另一篇文章的复制节点那里画过了。

getTreeNode(int h, Object k):

final TreeNode<K,V> getTreeNode(int h, Object k) {
    
    
  	return ((parent != null) ? root() : this).find(h, k, null);
}
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
    
    
    //h是要找的hash
    //p是每次遍历的节点
    //ph是遍历的节点的hash
    TreeNode<K,V> p = this;
    do {
    
    
        int ph, dir; K pk;
        //取遍历的节点的左右
        TreeNode<K,V> pl = p.left, pr = p.right, q;
        //判断当前节点的hash是否大于要找的hash
        if ((ph = p.hash) > h)
            //大于就把左节点赋给p重新循环
            p = pl;
        else if (ph < h)
            //小于就把右节点赋给p重新循环
            p = pr;
        //不是大于也不是小于,就判断key是否相等或者hash是否相等相等
        else if ((pk = p.key) == k || (k != null && k.equals(pk)))
            //相等就返回,找到了
            return p;
        else if (pl == null)
            //左边的为null就把右节点赋给p重新循环
            p = pr;
        else if (pr == null)
            //右边的为null就把左节点赋给p重新循环
            p = pl;
        else if ((kc != null ||
                  (kc = comparableClassFor(k)) != null) &&
                 (dir = compareComparables(kc, k, pk)) != 0)
          p = (dir < 0) ? pl : pr;
        else if ((q = pr.find(h, k, kc)) != null)
          return q;
        else
          p = pl;
    } while (p != null);
    return null;
 }

很简单,按哈希值的大小去找,小了就去左边找,大了就去右边找,直至找到或者找不到。

removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab, boolean movable):

分两步:

第一步:先删除链表:

final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
                          boolean movable) {
    
    
    int n;
    if (tab == null || (n = tab.length) == 0)
      return;
    //得到下标
    int index = (n - 1) & hash;
    TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;
    //取当前节点的next,prev
    //此时是TreeNode调的removeTreeNode方法
    //removeTreeNode继承于Node,所以可以直接调next
    TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;
                          //判断前驱是否为null
                          if (pred == null)
    //为null说明是头节点,就把后驱赋给first
    tab[index] = first = succ;
    else
        //把前驱的next指向后驱
        pred.next = succ;
    //判断后驱是否不为null
    if (succ != null)
        //不为null说明不是最后一个,就把后驱的前驱指向删除节点的前驱,相当于跳过了要删除的节点
        succ.prev = pred;
    if (first == null)
      	return;
    if (root.parent != null)
      	root = root.root();
    if (root == null || root.right == null ||
        (rl = root.left) == null || rl.left == null) {
    
    
        tab[index] = first.untreeify(map);  // too small
        return;
}

链表的删除比较简单,图在另一篇文章也画过了。

第二步:红黑树的删除:

删除也可以大概分成两种情况(下面以字母p代替需要删除的节点,以replacement代替真正被删掉的节点,代码里也是这2个字母):

  • 第一种:replacement就是p本身,有两种可能,此时是先修正后删除:

可能1:p没有子节点,示意图:
在这里插入图片描述
p没有子节点,代码整合:

else
    //左右都为null,被删除的节点就是自己
    replacement = p;

这个时候是最简单的,p就是自己,先修正颜色,后删除。(修正balanceDeletion后面说)

可能2:p有左右子节点,p的右子节点的最下面一层有左节点,示意图:
在这里插入图片描述
p有左右子节点,p的右子节点的最下面一层有左节点,代码整合:

if (pl != null && pr != null) {
    
    
    //s就是p的右子节点的最小的值
    TreeNode<K,V> s = pr, sl;
    while ((sl = s.left) != null)
        //一直找到最下面的左子节点,就是最小值,把sl赋给s
        s = sl;
    //把s和p的颜色互换
    boolean c = s.red; s.red = p.red; p.red = c; 
    TreeNode<K,V> sr = s.right;
    TreeNode<K,V> pp = p.parent;
    if (s == pr) {
    
    
          //如果没有孙子节点,把p挪到s的位置
          p.parent = s;
          s.right = p;
      }
    else {
    
    
        TreeNode<K,V> sp = s.parent;
        if ((p.parent = sp) != null) {
    
    
            //s是最下面的左节点,sp是parent
            if (s == sp.left)
                //把p挪下来,挪到s的位置
                sp.left = p;
            else
                //按1.8的源码这里是永远走不到的
                //如果上面的s取的左子节点最下面的右节点就会进这里
                //可能别的版本改了,这里为了兼容
                sp.right = p;
        }
        if ((s.right = pr) != null)
            //把s挪上去,挪到p的位置
            pr.parent = s;
      }
  		p.left = null;
      if ((p.right = sr) != null)
        	//sr是s的right,不为null代表最后一层只有右节点
        	//最小值s在倒数第二层
        	//把sr赋给p的right,因为p会换到s的位置
          sr.parent = p;
      if ((s.left = pl) != null)
          //原来的left赋给挪上去的s
          pl.parent = s;
      if ((s.parent = pp) == null)
          //删除的节点的父亲是根节点,就把root指向s
          root = s;
      else if (p == pp.left)
          //把原来父亲的left指向挪上去的s,这样就完全替换掉了。
          pp.left = s;
      else
          //把原来父亲的right指向挪上去的s
          pp.right = s;
      if (sr != null)
        	//把最下面一层的右节点赋给replacement
          replacement = sr;
      else
          replacement = p;

一堆代码,总结一下步骤:

1:找到p的右边的最小值s,把p和s的颜色互换,位置互换。

2:s如果有右子节点,就把p的right指向这个右子节点,因为p挪到s的位置了。

3:把p以前的left赋给s,因为s挪到p位置了。

4:再把s指向p以前的parent,p是左边就是left指向s,是右边就是left指向s。要是parent是根节点,就把s赋给根节点。

总结起来就是一句话:要删除的节点和右边最小值(左边最大值也行)颜色互换,位置互换。

因为右边最大值和要删除节点互换位置,必定大于左边,小于右边的,不影响二叉查找树的性质。而要删除的节点最后会删除的,所以可以忽略。

p有左右子节点,p的右子节点的最下面一层有左节点的完整示意图:
在这里插入图片描述

  • 第二种:replacement不是p本身,有三种可能,此时是先删除后修正:
    可能一和二:p只有左子节点或者右子节点,示意图:
    在这里插入图片描述
    p只有左子节点或者右子节点,代码整合:
else if (pl != null)
    //只有左子节点,左子节点就是要被删除的节点
    replacement = pl;
else if (pr != null)
    //左子节点为null,右子节点就是要被删除的节点
    replacement = pr;

可能三:p有左右子节点,p的右子节点的最后的左节点下面还有且仅有一个右节点,示意图:
在这里插入图片描述
p有左右子节点,p的右子节点的最后的左节点下面还有且仅有一个右节点,代码整合:

代码上面已经贴过了,步骤是一样的,下面贴个完整示意图:
在这里插入图片描述
看完了各种情况,就做了一件事:要删除的节点和右边最小值(左边最大值也行)颜色互换,位置互换。

换完了之后,两种可能,一个是要删除的节点在最后一层,此时就先修正后删除,一个要删除的节点不在最后一层,此时就先删除后修正。

节点换完位置以后,其实都一样的了。

在最后一层的示意图:
在这里插入图片描述
不在最后一层的示意图:
在这里插入图片描述
一模一样,就这2种情况,看下删除的代码:

if (replacement != p) {
    
    
    TreeNode<K,V> pp = replacement.parent = p.parent;
    if (pp == null)
        //p.parent为null,说明要删除的节点是root
        root = replacement;
    else if (p == pp.left)
        //如果p在左边,就把实际被删除的节点赋给父节点的left
        pp.left = replacement;
    else
        //如果p在右边,就把实际被删除的节点赋给父节点的right
        pp.right = replacement;
    //把应该删除的节点的左、右、父都置为null
    p.left = p.right = p.parent = null;
}

就是把要删除的节点替掉,没啥好说的,看下示意图:
在这里插入图片描述
接着看修正:balanceDeletion(TreeNode<K,V> root, TreeNode<K,V> x):

//p是红的就不需要修正
TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);

这时候的p已经是跟右边最小值s互换颜色了,不管先删还是后删,实际删除的都是s。而删掉的s是红色的话,是不影响黑高的,只有黑的才会。

删除很复杂,讲代码太绕,我这里只分析一下最复杂的情况,保证看得懂。而且可以跟HashMap的源码不一样的方法,因为修正不是固定的,只要修正之后满足规则就行。

只能说HashMap的源码是满足所有情况的,并且综合来讲肯定是修正的步骤最少的,性能最高的。这是大佬的心血,我们直接抄过来就行,但是抄之前得先理解。

最复杂的情况:
在这里插入图片描述
我们不看代码,就看上面这个情况,要删除9该怎么办?

此时的9肯定自己这一边的最后一层,因为如果9还有右子节点,会先删除的,保证9在最后一层。

而9的兄弟节点14有值,且是红色,说明14的左右子节点有且必须是黑色,至于在下面的红的不管。

9马上会被删除,所以必须想办法从14那边挪一个过来,但是14那边全是大于9的父亲11,不能挪,怎么遍?左旋呗,把9的父亲变大点。好,我们先左旋:
在这里插入图片描述
这个时候,二叉平衡树的特性是满足的,但是不满足红黑树的规则,所以我们需要继续修正,可以继续左旋:
在这里插入图片描述
这个时候,一通乱涂:
在这里插入图片描述
这样也算修正成功了,下面看下红黑树的修正步骤:
在这里插入图片描述
其实跟我们自己瞎想的也差不多,所以具体怎么修正的代码我们可以不理解,但是意思一定要知道,是为了满足规则。为什么满足规则?因为这样的规则也是大佬的心血,遵循就行,但是要先理解意思。

好了,删除就不细说代码,大家可以去看,主要是代码太难懂了(我也不会…),直接搬过来用吧。

终于结束了,写了好久好久,写不下去了,水平还是不够啊。

猜你喜欢

转载自blog.csdn.net/couguolede/article/details/105539281