1.红黑树与HashMap

1.红黑树的概念

红黑树,是一种二叉搜索树,但在每个结点上增加了一个存储位表示结点的颜色,可以使Red或Black。通过对任何一条从根到叶子的路径上各个结点着色的限制,红黑树确保没有一条路径会比其他路径长出两倍,因而近似平衡的

2.红黑树的性质

1) 每个结点不是红色就是黑色

2) 根结点是黑色的。

3) 如果一个结点是红色的,则它的两个孩子结点是黑色的.(从每个叶子到根的所有路径上不能有两个连续的红色结点)

4) 对于每个结点,从该结点到其所有后代叶子结点的简单路径上,均包含相同数目的黑色结点。(每条路径上的黑色结点数量相同)

5) 每个叶子结点都是黑色的(此处的叶子结点指定是空结点)

 为什么满足上面的性质,红黑树就能保证: 其最长路径中节点个数不会超过最短路径节点给树的两倍呢?

  1) 根据性质3和性质4,我们知道不能有连续的红节点,但是连续的黑节点是可以的,当某个路径中的节点全为黑时,就是该树的最短路径:

  2) 根据性质1和性质4,我们知道最长路径是由一个黑节点和一个红节点交替组成:

  3) 并且最后的叶子节点均是黑色结点:这样一来就能够保证最长路径中节点个数不会超过最短路径节点给树的两倍

3.红黑树的插入

   对于红黑树的插入来说,我们都是要通过构造红黑树节点来进行插入的,那么就有一个问题,究竟是构造红节点还是黑节点呢?

  对于左图的红黑树,插入的是黑色节点,破坏了性质4,就需要对红黑树进行调整,这里很显然插入一个黑色节点影响了所有路径:

   对于右图的红黑树,插入的是红色节点,破坏了性质3,但是性质4是具备的,这里显然只是局部发生的影响并没有影响整棵红黑树:

  以上图为例,当插入的是红节点时,其父节点如果是黑色,那么将不需要调整红黑树:如果是红色节点也只是影响局部,简单调整,但是插入黑色节点就不一样了,无论你插在哪里,对整棵树的影响很大;此时两种节点的插入所带来的性价比就很显然易见了:

  权衡利弊下:插入红节点最佳。所以我们在构造结点进行插入时,默认将结点的颜色设置为红色.

  红黑树的插入分两步:

  1)  按照二叉树的插入方法,找到并插入

  2)  若插入节点的父节点是红色,则需要对红黑树i进行调整

  首先规定如下: cur为当前(新增) 节点、p为父节点、g为祖父节点、u为叔叔节点(cur---current;  p---parent;  g---grandfather;   u---uncle)

  情况一:

  插入的新节点 N 是红黑树的根节点,这种情况下,我们把节点 N 的颜色由红色变为黑色,性质2(根是黑色)被满足。同时 N 被染成黑色后,红黑树所有路径上的黑色节点数量增加一个,性质5(从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点)仍然被满足。

             

   情况二:

  N 的父节点是黑色,这种情况下,性质4(每个红色节点必须有两个黑色的子节点)和性质5没有受到影响,不需要调整。

      

  情况三:

  p节点(父节点)为红色,g节点必为黑色,当u节点也是红色,由于p和cur节点均为红色,性质3被打破,此时需要调整:(注意: 叔叔存在且为红时,cur结点是parent的左孩子还是右孩子,调整方法都是一样的。)

  先将 P 和 U 的颜色染成黑色,再将 G 的颜色染成红色。此时经过 G 的路径上的黑色节点数量不变,性质5仍然满足。但需要注意的是 G 被染成红色后,可能会和它的父节点形成连续的红色节点,此时需要递归向上调整。

  

   情况四:

  cur为红,p为红,g为黑,u不存在/u为黑

  u的情况有两种

  (1) 如果u节点不存在,则cur一定是新插入节点,因为如果cur不是新插入节点,则cur和p一定有一人节点的颜色是黑色,就不满足性质4--每条路径黑色节点个数相同。

  (2) 如果u节点存在,则其一定是黑色的,那么cur节点原来的颜色一定是黑色的,现在看到其是红色的原因是因为cur的子树在调整的过程中将cur节点的颜色由黑色改成红色。(左图为情况3,操作后g节点对应情况4-(2))

   1)  p为g的左孩子,cur为p的左孩子,则进行右旋转

  

   2) p为g的右孩子,cur为p的右孩子,则进行左旋转,且p边黑,g变红

   情况五:  

  cur为红,p为红,g为黑,u不存在/u为黑

  (1).p为g的左孩子,cur为p的右孩子,则对p做左单旋转;转换成情况四,对g进行右单旋转

   (2).p为g的右孩子,cur为p的左孩子,则针对p做右单旋转;转换成了情况四,对g进行左单旋转

HashMap在jdk 1.8中使用用的是数组+链表+红黑树的结构来进行存储的,请看下图:

   当要对一个HashMap进行增删改查等操作时,一般情况下都是先根据key的Hash值定位到key在左侧数组桶的位置,然后判断当前的数组桶是使用的链表存储还是使用了红黑树存储。

   举一个简单的例子,我们要往HashMap中添加一个元素21,经过一个特定Hash算法得出的结果是索引0,所以我们把21这个元素放到了数组桶索引0的第一个位置上,因为这个时候索引0的位置上还没有元素,所以是以链表的方式存储的,接着继续添加33节点。。。如上图

   当某个索引(例如索引3)上的元素超过一个临界值后,如果还使用链表的方式进行存储的话效率比较低,所以把整个链表转换成了红黑树进行存储

HashMap的一些基础属性:

    
   // 默认的初始容量 默认16
   static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 /** * 最大容量,2的30次方 */ static final int MAXIMUM_CAPACITY = 1 << 30; /** * 默认扩展因子,比如初始情况下,当键值对的数量大于 16 * 0.75 = 12 时,就会触发扩容 */ static final float DEFAULT_LOAD_FACTOR = 0.75f; /** * 当某个箱子中,链表长度大于 8 时,有可能会转化成树 */ static final int TREEIFY_THRESHOLD = 8; /** * 哈希表扩容时,如果发现链表长度小于 6,则会由树重新退化为链表 */ static final int UNTREEIFY_THRESHOLD = 6; /** * 在转变成树之前,还会判断一次,只有键值对数量大于 64 才会发生转换。 */ static final int MIN_TREEIFY_CAPACITY = 64;

   // 链表(树)数组
   transient
Node<K,V>[] table; /** * Holds cached entrySet(). Note that AbstractMap fields are used * for keySet() and values(). */ transient Set<Map.Entry<K,V>> entrySet; /** * hashMap的大小(键值对的数量) */ transient int size;
  /**
  *
  modCount:记录当前集合被修改的次数
  * (1)添加
  * (2)删除
  * 这两个操作都会影响元素的个数。
   * 当我们使用迭代器或foreach遍历时,如果你在foreach遍历时,自动调用迭代器的迭代方法,
  * 此时在遍历过程中调用了集合的add,remove方法时,modCount就会改变,
   * 而迭代器记录的modCount是开始迭代之前的,如果两个不一致,就会报异常,
   * 说明有两个线路(线程)同时操作集合。这种操作有风险,为了保证结果的正确性,
  * 避免这样的情况发生,一旦发现modCount与expectedModCount不一致,立即保错。
  */
    transient int modCount;

    /**
     * 当前 HashMap 所能容纳键值对数量的最大值,超过这个值,则需扩容((threshold = 容量*实际扩展因子))
     */
    int threshold;

    /**
     * 实际的扩展因子
     */
    final float loadFactor;

HashMap存储数据所使用的链表数据结构:

static class Node<K,V> implements Map.Entry<K,V> {
     // hash值
final int hash; final K key; V value;
     // 下一个节点 Node
<K,V> next; Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + "=" + value; } public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals(Object o) { if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry<?,?> e = (Map.Entry<?,?>)o; if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true; } return false; } }

HashMap方法是如何保证容量大小始终为2的整次幂的?见:

1.HashMap源码学习(jdk8) - Kavins - 博客园 (cnblogs.com)

为2的整次幂原因:

1)  减少hash碰撞

  HashMap采用hash&(length- 1)来获取存储数据的下标位置,当HashMap的容量是2的n次幂时,(length-1)的2进制也就是1111111***111这样形式的,这样与添加元素的hash值进行位运算时,能够充分的散列,使得添加的元素均匀分布在HashMap的每个位置上,减少hash碰撞。

2)  扩容时减少链表上元素移动代价

  JDK.8中HashMap的扩容机制按2次幂的扩展(每次扩容为原来2倍),会出现原来链表上元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置(原索引+oldCap)。如下图,扩容前的key1和key2确定的索引位置 和 图扩容后key1和key2确定索引位置,可以看出扩容后只有key2的索引位置发生变化, 且变为原索引+oldCap。

 扩容前和扩容后index的位置只需要判断(e.hash & oldCap)==0,oldCap为扩容前数组的容量10000,当(e.hash & oldCap)==0时,hash值(第5位)是为0,索引值不变,不为0时,索引值为旧索引值+oldCap(16)

oldCap 是table的容量,按照源码的策略,一定是2^n次方,也就代表着该2进制一定只有一个1,剩余的都是0;如果想要保证e.hash&oldCap ==0,则只需要e.hash在对应oldCap二进制位在1的那位为0,就可以保证最后的按位与操作(&)结果为0;如果e.hash&oldCap !=0 ,则只需要e.hash在对应oldCap二进制位在1的那位为1,就可以保证最后的按位与 操作(&)不等于0

  (1) 当(e.hash & oldCap)==0时,e.hash的对应的oldCap的最高位的值为0

      (2oldCap-1)相当于(oldCap -1)前面增加了一个1(11111    1111)

  所以扩容后(2oldCap-1)& e.hash =(oldCap -1)& e.hash(增加的那个1对应的e.hash值为0),所以相等

  (2)(e.hash & oldCap)不等于0时,说明e.hash的对应的oldCap的最高位的值为1,(2oldCap-1)& e.hash与(oldCap -1)& e.hash相比,相当于前几位值不变,最高位值变成了1

  此时(2oldCap-1)& e.hash =  (oldCap -1)& e.hash + oldCap

HashMap的hash()方法

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

h>>>16(无符号右移16位),移位后,高16位的值变到低16位,然后与key的hashcode进行异或操作

假如key的hashcode为

10000000  11111111 11000000 11111110

右移动16位后

00000000  00000000 10000000  11111111

hashmap存储数据的数组下标(i)使用如下公式计算的:   tab[i = (n - 1) & hash]   其中n为数组长度(n一定为2的整数次幂,所以n-1后,所有有效为都为1,这样与添加元素的hash值进行位运算时,能够充分的散列,使得添加的元素均匀分布在HashMap的每个位置上,减少hash碰撞)

一般情况下,数组长度小于2的16次方,如果不与高16位进行异或操作, 当数组长度小于2的16次方时, (n - 1) & hash公式中只有hashcode的低16为参数运算,会增加hash冲突的概率

右移16位,然后进行异或,是为了让高16位参与到运算中,减少hash冲突(&操作会使二进制结果向0靠拢,|操作会是二进制结果向1靠拢)

HashMap的put方法

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
  // 如果当前位置已存在一个值,false是替换,true是不替换
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        // 定义数组tab
        HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i;
        // 如果hashmap为空,则对table进行扩容(初始化),n的值为2的m次幂
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // n为2的整数次幂,n-1后,有效位的值都为1,和hash值进行&操作,即可得到存储数据的索引值
        // 若当前索引没有存储数据,则将新的数据存入
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        // 如果该位置存在元素
        else {
            HashMap.Node<K,V> e; K k;
            // 如果链表第一个元素p的key和hash值都与要存入的key和hash相同,则p的值赋给e(key存在了)
            if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            // 如果key或hash不同,且p为红黑树,则将该数据用红黑树存储,详见下面的putTreeVal方法
            else if (p instanceof HashMap.TreeNode)
                e = ((HashMap.TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            // 否则进行如下操作
            else {
                // 遍历p
                for (int binCount = 0; ; ++binCount) {
                    // 当遍历到p的最后一个节点时
                    if ((e = p.next) == null) {
                        // 创建一个新节点连接到p尾部
                        p.next = newNode(hash, key, value, null);
                        // 如果链表长度超过阈值,则将当前索引的链表转换为红黑树(其他数组元素依旧为链表) treeifyBin方法见下一个方法解析
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        // 跳出遍历
                        break;
                    }
                    // 如果该链表下某个元素的hash值和key值与要插入的值一样,则跳出循环(key存在了)
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    // 将e赋值给p,继续下一轮循环
                    p = e;
                }
            }
            // 如果key在链表中已经存在了
            if (e != null) { // existing mapping for key
                // 获取该key对应的旧的value
                V oldValue = e.value;
                // 赋值新的value,返回旧的value
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                // 不进行任何操作()LinkedHashMap中才用倒
                afterNodeAccess(e);
                return oldValue;
            }
        }
        // 添加元素时,modCount加一
        ++modCount;
        // 超过阈值时扩容
        if (++size > threshold)
            resize();
        // 不进行任何操作()LinkedHashMap中才用倒
        afterNodeInsertion(evict);
        return null;
    }

TreeNode(红黑树)的treeifyBin方法

    final void treeifyBin(HashMap.Node<K,V>[] tab, int hash) {
        int n, index; HashMap.Node<K,V> e;
        // 如果数组为空或者数组长度小于MIN_TREEIFY_CAPACITY(64),直接对链表数组扩容,不进行红黑树转换
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        // 根据hash值得到数组索引,e为索引位置的链表
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            // 创建红黑树,hd为头节点,tl为尾节点
            HashMap.TreeNode<K,V> hd = null, tl = null;
            // 遍历链表e,将链表的值赋给红黑树,此时只用了TreeNode的prev和next
            // 还未用到左右子树
            do {
                // 根据e创建红黑树节点
                HashMap.TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            // hd:将索引处的链表转换后的红黑树链表双向链表(此时还未使用左右子树),并用双向链表替换原有链表
            if ((tab[index] = hd) != null)
                // 将hd彻底转换为红黑树(开始使用左右子树),treeify见下个方法解析
                hd.treeify(tab);
        }
    }

红黑树的treeify方法

/**
     * 转为红黑树时,先判断hash值大小,若hash值相等(hash冲突),则根据key去比较大小
     * 因为红黑树是根据节点值的大小来确定当前节点是左右子树的,所以必须比较出
     * 两个节点大小(不能相等)
     */
    final void treeify(HashMap.Node<K,V>[] tab) {
        HashMap.TreeNode<K,V> root = null;
        // this为调用treeify时的对象(比如treeifyBin的hd)
        for (HashMap.TreeNode<K,V> x = this, next; x != null; x = next) {
            next = (HashMap.TreeNode<K,V>)x.next;
            // 左右子树赋null
            x.left = x.right = null;
            // 如果根节点为null,设置当前节点为根节点,并设置根节点为黑色
            if (root == null) {
                x.parent = null;
                x.red = false;
                root = x;
            }
            // 根节点不为null
            else {
                K k = x.key;
                int h = x.hash;
                Class<?> kc = null;
                for (HashMap.TreeNode<K,V> p = root;;) {
                    int dir, ph;
                    K pk = p.key;
                    // 根节点的hash值大于当前节点的hash值时,dir=-1
                    if ((ph = p.hash) > h)
                        dir = -1;
                    // 根节点的hash值小于当前节点的hash值时,dir=1
                    else if (ph < h)
                        dir = 1;
                    // 当前节点hash值与父节点hash值一样时
                    // 视图通过key的值来确定两个节点的大小
                    else if ((kc == null &&
                            (kc = comparableClassFor(k)) == null) ||
                            (dir = compareComparables(kc, k, pk)) == 0)
                        // 当key没有实现Compareble接口或者比较结果为0时,根据如下方法获取
                        // 两个key值的比较结果
                        dir = tieBreakOrder(k, pk);
                    // 将p(根节点)赋值给xp
                    HashMap.TreeNode<K,V> xp = p;
                    // 当dir小于0时,将p的左子树赋值给p,否则将p的右子树赋值给p,当p为null时,将x插入对应子树位置,不为null时,进入下一个循环
if ((p = (dir <= 0) ? p.left : p.right) == null) { // 设置当前节点的父节点为xp x.parent = xp; // 如果dir<=0,设置xp的左子树为x if (dir <= 0) xp.left = x; // 否则设置xp的右子树为p else xp.right = x; // 将节点x插入红黑树,balanceInsertion方法解析见下个方法 root = balanceInsertion(root, x); break; } } } } moveRootToFront(tab, root); }

 TreeNode(红黑树)的结构和balanceInsertion方法如下

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    TreeNode<K,V> parent;  // red-black tree links
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;    // needed to unlink next upon deletion
    boolean red;
    TreeNode(int hash, K key, V val, Node<K,V> next) {
        super(hash, key, val, next);
    }

    static <K,V> HashMap.TreeNode<K,V> balanceInsertion(HashMap.TreeNode<K,V> root,
                                                        HashMap.TreeNode<K,V> x) {
        // 新插入的节点设置为红色
        x.red = true;
        for (HashMap.TreeNode<K,V> xp, xpp, xppl, xppr;;) {
            // 1.为xp节点赋值(值为x节点(待插入节点)的父节点)
            // 2.若xp为null,则代表插入节点为第一个节点(根节点)--->对应情况一
            if ((xp = x.parent) == null) {
                // 设置根节点为黑色
                x.red = false;
                // 返回根节点
                return x;
            }
            // 如果x的父节点xp节点为黑色或者xpp节点(xp父节点)为空(代表xp节点为空节点),
            // 则插入的红色节点x不影响红黑树结构,直接返回根节点即可--->对应情况二
            else if (!xp.red || (xpp = xp.parent) == null)
                return root;
            // 下面代码执行的前提是x的父节点为红色(为黑色时,上面return了)
            // 如果父节点(xp)为祖父节点(xpp)的左子节点
            if (xp == (xppl = xpp.left)) {
                // 祖父右节点(叔叔节点)不为空且为红色节点时
                if ((xppr = xpp.right) != null && xppr.red) {
                    // 祖父右节点(叔叔节点)设置为黑色,父节点设置为黑色,祖父节点设置为红色,
                    // x节点替换为祖父节点继续下一个循环--->对应情况三
                    xppr.red = false;
                    xp.red = false;
                    xpp.red = true;
                    x = xpp;
                }
                // 祖父右节点(叔叔节点)不存在或者存在为黑色时
                else {
                    // 如果x为父节点的右节点
                    if (x == xp.right) {
                        // 对xp做左单旋转--->对应情况五(1)
                        root = rotateLeft(root, x = xp);
                        // xp赋值为父节点,xpp赋值为祖父节点
                        xpp = (xp = x.parent) == null ? null : xp.parent;
                    }
                    // 情况五(1)左单旋转后的后续操作--->对应情况4 1)
                    if (xp != null) {
                        xp.red = false;
                        if (xpp != null) {
                            xpp.red = true;
                            root = rotateRight(root, xpp);
                        }
                    }
                }
            }
            // 如果父节点(xp)为祖父节点(xpp)的右子节点
            else {
                // 祖父节点的左子节点不为空,且为红色
                if (xppl != null && xppl.red) {
                    // 叔叔节点染成黑色,父节点设置为黑色,祖父节点设置为红色,
                    xppl.red = false;
                    xp.red = false;
                    xpp.red = true;
                    // x节点替换为祖父节点继续下一个循环--->对应情况三
                    x = xpp;
                }
                // 祖父左子节点为空或者为黑色
                else {
                    // 若x为父节点的左节点
                    if (x == xp.left) {
                        // 对xp做右单旋转--->对应情况五(2)
                        root = rotateRight(root, x = xp);
                        // xp赋值为父节点,xpp赋值为祖父节点
                        xpp = (xp = x.parent) == null ? null : xp.parent;
                    }
                    // 情况五(2)右单旋转后的后续操作--->对应情况4 2)
                    if (xp != null) {
                        xp.red = false;
                        if (xpp != null) {
                            xpp.red = true;
                            root = rotateLeft(root, xpp);
                        }
                    }
                }
            }
        }
    }

    static <K,V> HashMap.TreeNode<K,V> rotateLeft(HashMap.TreeNode<K,V> root,
                                                  HashMap.TreeNode<K,V> p) {
        HashMap.TreeNode<K,V> r, pp, rl;
        // 父节点不为空,r为父节点的右节点且不为空
        if (p != null && (r = p.right) != null) {
            // rl,p.right为r的左节点
            if ((rl = p.right = r.left) != null)
                rl.parent = p;
            // pp,r.parent为p的父节点,
            if ((pp = r.parent = p.parent) == null)
                (root = r).red = false;
                // 当root的左节点为p时
            else if (pp.left == p)
                // root的左节点改为原来p节点的左子节点
                pp.left = r;
            else
                pp.right = r;
            // r(root的子节点)的左子节点改为原来的父节点
            r.left = p;
            // 原来的父节点的父节点设置为r
            p.parent = r;
        }
        return root;
    }

    static <K,V> HashMap.TreeNode<K,V> rotateRight(HashMap.TreeNode<K,V> root,
                                                   HashMap.TreeNode<K,V> p) {
        HashMap.TreeNode<K,V> l, pp, lr;
        // 父节点不为空,l赋值为p节点的左节点且不为null
        if (p != null && (l = p.left) != null) {
            // lr,p.left赋值为l的右节点值
            if ((lr = p.left = l.right) != null)
                // l的右节点不为null时
                lr.parent = p;
            // pp,l.parent赋值为p的父节点
            if ((pp = l.parent = p.parent) == null)
                (root = l).red = false;
                // pp的右节点等于p时
            else if (pp.right == p)
                pp.right = l;
            else
                // pp左节点设置为l
                pp.left = l;
            // l的右节点设置为p
            l.right = p;
            // p的父节点设置为l
            p.parent = l;
        }
        return root;
    }
}

TreeNode的moveRootToFront方法

    /**
     * 当我们删除或者增加红黑树节点的时候,root节点在双链表中的位置可能会变动,
     * 为了保证每次红黑树的根节点都在链表的第一个位置,在操作完成之后 需要moveRootToFront方法来进行调整。
     * @param root:调整红黑树之后的 root节点
     */
    static <K,V> void moveRootToFront(HashMap.Node<K,V>[] tab, HashMap.TreeNode<K,V> root) {
        int n;
        if (root != null && tab != null && (n = tab.length) > 0) {
            // 获取红黑树根节点的应该放入的下标位置,该位置的元素即是双向链表,又是红黑树
            int index = (n - 1) & root.hash;
            // 获取双向链表的第一个节点
            HashMap.TreeNode<K,V> first = (HashMap.TreeNode<K,V>)tab[index];
            // 如果该节点不是root节点,说明root节点位置发生了变动
            if (root != first) {
                HashMap.Node<K,V> rn;
                // 将索引所在链表的第一个位置 指向root节点
                tab[index] = root;
                // 获取根节点(双向链表)的前驱节点
                HashMap.TreeNode<K,V> rp = root.prev;
                // 获得root后继,将前驱和后继相连接 root节点从原链表中脱离出来了
                if ((rn = root.next) != null)
                    ((HashMap.TreeNode<K,V>)rn).prev = rp;
                if (rp != null)
                    rp.next = rn;
                // 将root与原来双链表的第一个节点相连,
                // 这样root又回到双链表当中 并且在双链表的第一个位置上
                if (first != null)
                    first.prev = root;
                root.next = first;
                // root节点的前驱节点设置为null  因为他双链表的第一个节点 没有前驱
                root.prev = null;
            }
       // 验证红黑树的正确性
assert checkInvariants(root); } }

TreeNode的putTreeVal方法

    /**
     * TreeNode的putTreeVal方法
     */
    final HashMap.TreeNode<K,V> putTreeVal(HashMap<K,V> map, HashMap.Node<K,V>[] tab,
                                           int h, K k, V v) {
        Class<?> kc = null;
        boolean searched = false;
        // 获取红黑树的跟节点
        HashMap.TreeNode<K,V> root = (parent != null) ? root() : this;
        for (HashMap.TreeNode<K,V> p = root;;) {
            int dir, ph; K pk;
            // 根节点的hash值大于要插入元素的hash值时,dir=-1,小于时,dir=1
            if ((ph = p.hash) > h)
                dir = -1;
            else if (ph < h)
                dir = 1;
            // 根节点的hash值等于要插入元素的hash值,且key值相同时(此时不需要插入,只需替换)
            // 返回根节点(替换旧值的逻辑在调用putTreeVal的方法中)
            else if ((pk = p.key) == k || (k != null && k.equals(pk)))
                return p;
            // 当hash值一样,key值不一样时,比较当前节点和根节点的key值,并将比较结果赋值给dir
            else if ((kc == null &&
                    (kc = comparableClassFor(k)) == null) ||
                    (dir = compareComparables(kc, k, pk)) == 0) {
                // 如果根节点的key和要插入的节点的key不一样,则到它的左右子树中去查找
                if (!searched) {
                    HashMap.TreeNode<K,V> q, ch;
                    // 设置标志位true
                    searched = true;
                    if (((ch = p.left) != null &&
                            (q = ch.find(h, k, kc)) != null) ||
                            ((ch = p.right) != null &&
                                    (q = ch.find(h, k, kc)) != null))
                        return q;
                }
                dir = tieBreakOrder(k, pk);
            }
            // 如果没有找到该key对应的元素,则插入并返回null
            HashMap.TreeNode<K,V> xp = p;
            // 当dir小于0时,将p的左子树赋值给p,否则将p的右子树赋值给p,且p为null时
            if ((p = (dir <= 0) ? p.left : p.right) == null) {
                // xpn为xp节点的下一个节点
                HashMap.Node<K,V> xpn = xp.next;
                // 新建一个红黑树节点
                HashMap.TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
                // 如果dir<=0,设置xp的左子树为x
                if (dir <= 0)
                    xp.left = x;
                // 否则设置xp的右子树为p
                else
                    xp.right = x;
                // xp下一个节点指向x
                xp.next = x;
                // x的前驱节点和父节点指向xp(前驱节点是双线链表的角色,父节点是红黑树的角色)
                x.parent = x.prev = xp;
                // xpn如果不为空,xpn的前驱节点设置为x(相当于将x插入到了xp节点与xpn节点中间)
                if (xpn != null)
                    ((HashMap.TreeNode<K,V>)xpn).prev = x;
                // 平衡插入x节点,并将调整后的二叉树的root节点放到双向链表的第一位
                moveRootToFront(tab, balanceInsertion(root, x));
                return null;
            }
        }
    } 

HashMap的get方法

    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
    final HashMap.Node<K,V> getNode(int hash, Object key) {
        HashMap.Node<K,V>[] tab; HashMap.Node<K,V> first, e; int n; K k;
        // 判断数组不为空,根据hash码获取索引位置,并将索引位置的链表赋值给first
        if ((tab = table) != null && (n = tab.length) > 0 &&
                (first = tab[(n - 1) & hash]) != null) {
            // 如果链表的第一个元素就是要找的值,直接返回
            if (first.hash == hash && // always check first node
                    ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            // 获取链表下一个元素
            if ((e = first.next) != null) {
                // 如果链表是红黑树,则进行红黑树的查找
                if (first instanceof HashMap.TreeNode)
                    return ((HashMap.TreeNode<K,V>)first).getTreeNode(hash, key);
                // 遍历链表,查找目标值
                do {
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        // 找不到则返回null
        return null;
    }

TreeNode的getTreeNode方法

    /**
     * 获取红黑树的根节点
     */
    final HashMap.TreeNode<K,V> root() {
        for (HashMap.TreeNode<K,V> r = this, p;;) {
            if ((p = r.parent) == null)
                return r;
            r = p;
        }
    }
    /**
     * 调用find方法查找目标值
     */
    final HashMap.TreeNode<K,V> getTreeNode(int h, Object k) {
        return ((parent != null) ? root() : this).find(h, k, null);
    }
    /**
     * @param h hash值
     * @param k key值
     * @param kc key值的类型
     */
    final HashMap.TreeNode<K,V> find(int h, Object k, Class<?> kc) {
        // 将调用find方法的对象赋值给p
        HashMap.TreeNode<K,V> p = this;
        do {
            int ph, dir; K pk;
            HashMap.TreeNode<K,V> pl = p.left, pr = p.right, q;
            // 如果p的hash值大于查询节点的hash值(说明查询结果在p节点的左子树),将左子树赋值给p
            if ((ph = p.hash) > h)
                p = pl;
            // 如果p的hash值小于查询节点的hash值(说明查询结果在p节点的右子树),将右子树赋值给p
            else if (ph < h)
                p = pr;
            // 如果p的hash值,key值都与要查询节点的hash值,key值相等,则p就是要查找的节点,返回p
            else if ((pk = p.key) == k || (k != null && k.equals(pk)))
                return p;
            // 左子树为null,将右子树赋值给p
            else if (pl == null)
                p = pr;
            // 右子树为null,将左子树赋值给p
            else if (pr == null)
                p = pl;
            // 如果hash值相等,key值不相等,且左右子树不为null
            // 且(kc不为null或者k实现了Comparable接口)且(pk与k通过compareTo比较结果不为0时)
            // 将比较结果赋值给dir
            else if ((kc != null ||
                    (kc = comparableClassFor(k)) != null) &&
                    (dir = compareComparables(kc, k, pk)) != 0)
                // 根据key的比较结果来确定给p赋左子树还是右子树
                p = (dir < 0) ? pl : pr;
            // 如果上述没查到,则从右子树递归查找
            else if ((q = pr.find(h, k, kc)) != null)
                return q;
            // 上述没找到的话,将左子树赋值给p,从左子树开始查找
            else
                p = pl;
        } while (p != null);
        return null;
    }

HashMap的remove方法

    /**
     * 调用removeNode方法,根据key值删除
     */
    public V remove(Object key) {
        HashMap.Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
                null : e.value;
    }
    /**
     * matchValue:删除时,是否匹配value值remove时没有匹配
     */
    final HashMap.Node<K,V> removeNode(int hash, Object key, Object value,
                                       boolean matchValue, boolean movable) {
        HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, index;
        // table不为null,且根据hash值查到的索引所在链表不为null,
        if ((tab = table) != null && (n = tab.length) > 0 &&
                (p = tab[index = (n - 1) & hash]) != null) {
            HashMap.Node<K,V> node = null, e; K k; V v;
            // hash值相等,key相等则p为要删除的节点
            if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            // 若p不是要删除的节点,遍历p
            else if ((e = p.next) != null) {
                // 如果p是红黑树,则通过getTreeNode方法查找节点
                if (p instanceof HashMap.TreeNode)
                    node = ((HashMap.TreeNode<K,V>)p).getTreeNode(hash, key);
                // p是普通链表,则遍历p查找节点
                else {
                    do {
                        if (e.hash == hash &&
                                ((k = e.key) == key ||
                                        (key != null && key.equals(k)))) {
                 // 如果能找到node,则p是node的上一个节点 node
= e; break; } p = e; } while ((e = e.next) != null); } } // 若找到节点,则进行删除操作,需要匹配value时,进行value值验证 if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { // 如果node是红黑树,则进行红黑树节点删除removeTreeNode方法解析见下一个方法 if (node instanceof HashMap.TreeNode) ((HashMap.TreeNode<K,V>)node).removeTreeNode(this, tab, movable); // 如果node是链表的第一个节点 else if (node == p) tab[index] = node.next; // node不是链表的第一个节点,删除node节点(p指向node节点的下一个节点) else p.next = node.next; ++modCount; --size; // 此处为空方法 afterNodeRemoval(node); return node; } } return null; }

TreeNode的removeTreeNode方法

 

HashMap的扩容方法

    final HashMap.Node<K,V>[] resize() {
        // 扩容前旧的数组链表(树)
        HashMap.Node<K,V>[] oldTab = table;
        // 扩容前数组链表长度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        // 旧的键值对容量阈值(超过该数量会扩容)
        int oldThr = threshold;
        // 新数组的长度和容纳键值对的最大值
        int newCap, newThr = 0;
        if (oldCap > 0) {
            //如果超过了数组的最大容量值,则扩容到Integer.MAX_VALUE
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // 否则newCap(容量)*2(newCap<最大容量且>=初始容量)
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                    oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        // 旧的容量阈值大于0的话,新的数组长度设置为oldThr
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        // 零初始阈值表示使用默认值
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        // newThr 为 0 时,按阈值计算公式进行计算
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                    (int)ft : Integer.MAX_VALUE);
        }
        // 扩容后的容量阈值
        threshold = newThr;
        // 创建新的数组,数组大小为newCap
        HashMap.Node<K,V>[] newTab = (HashMap.Node<K,V>[])new HashMap.Node[newCap];
        // 将新创建的数组赋值给table
        table = newTab;
        // 如果旧的桶数组不为空,则遍历桶数组,将旧数组的元素放到新的数组table
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                HashMap.Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    // 如果e链表只有一个值,则直接将该值放入新的链表
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    // 当数组中的值为红黑树时
                    else if (e instanceof HashMap.TreeNode)
                        // 对红黑树进行拆分
                        ((HashMap.TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else {
                        // 索引值不变的链表
                        HashMap.Node<K,V> loHead = null, loTail = null;
                        // 索引值变的链表
                        HashMap.Node<K,V> hiHead = null, hiTail = null;
                        HashMap.Node<K,V> next;
                        do {
                            // 获取下一个元素
                            next = e.next;
                            // 如果当前元素的hash值与oldCap(扩容前数组长度,2的整数次幂)&后结果为0
                            // 则(2oldCap-1)& e.hash =(oldCap -1)& e.hash,该值的索引值不变
                            // 将索引值不变的值放入lo链表中
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            // 将索引值不变的值放入hi链表中
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        // 将索引值不变的链表放入对应的索引中
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        // 索引值改变的链表放入j + oldCap中
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

HashMap的comparableClassFor方法

/**
     * Returns x's Class if it is of the form "class C implements
     * Comparable<C>", else null.
     * x参数为hashmap的key值,当x实现了Compareble接口,且泛型参数为x的Class类型时,返回x的Class对象
     */
    static Class<?> comparableClassFor(Object x) {
        // 判断是否实现了Comparable接口
        if (x instanceof Comparable) {
            Class<?> c; Type[] ts, as; Type t; ParameterizedType p;
            // // 如果是String类型,直接返回String.class
            if ((c = x.getClass()) == String.class) // bypass checks
                return c;
            // 判断是否有直接实现的接口
            if ((ts = c.getGenericInterfaces()) != null) {
                // 遍历接口
                for (int i = 0; i < ts.length; ++i) {
                    // 1.该接口实现了泛型,2.getRawType()获取不带泛型的Class对象,并判断是否为Comparable
                    // 3.获取泛型参数数组.4.泛型参数数组只有一个泛型参数且为c
                    if (((t = ts[i]) instanceof ParameterizedType) &&
                            ((p = (ParameterizedType)t).getRawType() ==
                                    Comparable.class) &&
                            (as = p.getActualTypeArguments()) != null &&
                            as.length == 1 && as[0] == c) // type arg is c
                        return c;
                }
            }
        }
        return null;
    } 

 HashMap的compareComparables方法

    /**
     * @param kc k的Class类型
     * @param k k的值
     * @param x 与k进行比较的值
     * compareComparables方法与comparableClassFor一起出现
     * compareComparables出现在如下语境
     * else if ((kc != null ||
     * (kc = comparableClassFor(k)) != null) &&
     * (dir = compareComparables(kc, k, pk)) != 0)
     * 且comparableClassFor返回非null时(此时kc为实现了Comparable接口类的对象),
     * 才调用compareComparables,所以当x为kc类型时,可以用compareTo进行比较
     */
    static int compareComparables(Class<?> kc, Object k, Object x) {
        return (x == null || x.getClass() != kc ? 0 :
                ((Comparable)k).compareTo(x));
    }

 红黑树节点的删除https://blog.csdn.net/qq_40843865/article/details/102498310

组合1:被删节点无子节点,且被删结点为红色

这是最简单的一种情况,直接将节点删除即可,不破坏任何红黑树的性质

组合2:被删结点无子结点,且被删结点为黑色

这是最复杂的情况,我们稍后再讨论

组合3:被删结点有一个子结点,且被删结点为黑色(不存在为红色的情况,如果为红色,一个节点必为黑色,违反性质4))

 这种组合下,被删结点node的一个子结点value必然为红色(若为黑色,则违反性质4)),此时直接将node删掉,用value代替node的位置,并将value着黑即可。

组合4&5:被删结点有两个子结点,且被删结点为黑色或红色

当被删结点node有两个子结点时,我们先找到这个被删结点的后继结点successor(前驱节点也可以),然后用successor替换node的值,不用改变颜色,此时转换为删除node的后继节点successor。

这里使用node的后继节点替换node,必然是下图两种形态中的一种:

 若是(a)的情形,用successor代替node后,转换为successor被删,若successor为红色,则变成了组合1;若successor为黑色,则变成了组合2。

若是(b)的情形,用successor代替node后,转换为successor被删,若successor为红色,则变成了组合1;若successor为黑色,则变成了组合2或3。

综上所述:组合4,5被转换为前面几种组合,我们真正要进行删除的只有组合1 2 3,对于组合1和3,删除操作相当简单,这里不再多讲,接下来再逐一分析组合2可能的几种情形。

再论组合2:被删结点无子结点,且被删结点为黑色

 因为删除黑色结点会破坏红黑树的性质4),所以为了不破坏性质4),将node删除后用一个拥有额外黑色的null替代它(可以想象是将node删除后,在这个位置放了一个黑色的权值),剩下的就是调平的过程,最终这个游离的黑色权值被扔掉,整个删除操作完成。

 然后再结合node的父结点father和其兄弟结点brother来分析。

情形一:brother为黑色,且brother有一个与其方向一致的红色子结点son

所谓方向一致,是指brother为father的左子结点,son也为brother的左子结点;或者brother为father的右子结点,son也为brother的右子结点。

 

图(c )中,白色代表随便是黑或是红,方形结点存储的是一个游离的黑色权值。将brother和father旋转(是左旋还是右旋自己根据情景体会,下同),并重新上色后,变成了图(d),将游离的黑色权值扔掉,此时不违背任何红黑树的性质,删除操作完成。

图(c )中的情形颠倒过来,也是一样的操作。

情形二:brother为黑色,且brother有一个与其方向不一致的红色子结点son

图(e)中,将son和brother旋转,重新上色后,变成了图(f),转化为情形一。

图(e)中的情形颠倒过来,也是一样的操作。

情形三:brother为黑色,且brother无红色子结点

 此时若father为红,则重新着色即可,删除操作完成。如图下图(g)和(h)

 

此时若father为黑,则重新着色,将游离的黑色权值存到father(此时father的黑色权重为2),将father作为新的结点进行情形判断,遇到情形一、情形二,则进行相应的调整,完成删除操作;如果没有,则结点一直上溯,直到根结点存储额外的黑色,此时将该额外的黑色扔掉,即完成了删除操作。

 

注意: 这里第二种情况有点不好理解,之所以要加一个游离的黑色是因为删除node后造成node子树和brother子树的黑节点个数不平衡,这里的操作实际是将brother子树中黑节点个数减少一个,而将father黑色权重设为2,这样father左右两边的子树不用加游离的权值都是平衡的了。接下来将问题转移到了father、father的father、father的brother三个之间,这样向上递归,如果一直是这种情况,最终必定转化为root的左子节点或者右子节点权重为2,同样的将root黑色权重设为2,将另外一边的黑色权重减小1,再将root的黑色权重扔掉一个,此时整棵树重回平衡,只是root到叶节点路径的黑节点少了一个。

情形四:brother为红色,则father必为黑色。

 

图(i)中,将brother和father旋转,重新上色后,变成了图(j),新的brother(原来的son)变成了黑色,这样就成了情形一、二、三中的一种。如果将son和brother旋转,无论怎么重新上色,都会破坏红黑树的性质4或5,例如图(k)。
图(i)中的情形颠倒过来,也是一样的操作。

/**
     * 下面用node代替要删除的节点
     */
    final void removeTreeNode(HashMap<K,V> map, HashMap.Node<K,V>[] tab,
                              boolean movable) {
        int n;
        if (tab == null || (n = tab.length) == 0)
            return;
        // 根据node的hash值获取node所在数组的索引值
        int index = (n - 1) & hash;
        // 获取双向链表的第一个元素first,并赋值给root
        HashMap.TreeNode<K,V> first = (HashMap.TreeNode<K,V>)tab[index], root = first, rl;
        // 获取node的后驱节点succ,前驱节点pred
        HashMap.TreeNode<K,V> succ = (HashMap.TreeNode<K,V>)next, pred = prev;
        // 如果pred为null,说明node为双向链表的第一个节点
        // 此时直接用succ作为双向链表的第一个节点
        if (pred == null)
            tab[index] = first = succ;
        // 否则pred下一个节点指向succ(node的前驱节点直接指向node的后驱节点,相当于删除node)
        else
            pred.next = succ;
        // 如果succ不等于null,设置succ的前驱节点为pred
        if (succ != null)
            succ.prev = pred;
        // 如果first为null,说明first在第一个if(tab[index] = first = succ;)处赋值了null
        // 因为node必定存在,first作为双向链表的第一个节点必定不为null
        // 此时succ为null,且pred为null,说明该双向链表只有一个节点,且为node,
        // 前面已经给table[index]=null,所以直接返回即可
        if (first == null)
            return;
        // 如果root不是红黑树的根节点,那么获取红黑树的根节点
        if (root.parent != null)
            root = root.root();
        // 根据根节点及左右子树数量来判断红黑树存储元素数量,满足条件时将红黑树转换为链表
        // (红黑树最长路径最多不超过最短路径的两倍)
        if (root == null || root.right == null ||
                (rl = root.left) == null || rl.left == null) {
            // 将红黑树转换为链表,untreeify方法解析见下面
            tab[index] = first.untreeify(map);  // too small
            return;
        }
        // 上面只是完成node作为双向链表角色的删除,下面要完成node作为红黑树角色的删除
        // p为node,pl为node的左子树,pr为node的右子树,repacement为删除node后替换node的节点
        HashMap.TreeNode<K,V> p = this, pl = left, pr = right, replacement;
        // 左右子树都不为null时
        if (pl != null && pr != null) {
            // s赋值为node的右子树
            HashMap.TreeNode<K,V> s = pr, sl;
            // 不断获取s的左子树,并赋值给sl,sl最终值为s最最左边的叶子节点(即大于node的最小值),后继节点
            while ((sl = s.left) != null) // find successor
                s = sl;
            // 交换p节点和s节点的颜色
            boolean c = s.red; s.red = p.red; p.red = c; // swap colors
            // sr:s的右节点
            HashMap.TreeNode<K,V> sr = s.right;
            // pp:p的父节点
            HashMap.TreeNode<K,V> pp = p.parent;
            // 若s==pr,说明p是s的父节点
            if (s == pr) { // p was s's direct parent
                p.parent = s;
                s.right = p;
            }
            else {
                HashMap.TreeNode<K,V> sp = s.parent;
                if ((p.parent = sp) != null) {
                    if (s == sp.left)
                        sp.left = p;
                    else
                        sp.right = p;
                }
                if ((s.right = pr) != null)
                    pr.parent = s;
            }
            p.left = null;
            if ((p.right = sr) != null)
                sr.parent = p;
            if ((s.left = pl) != null)
                pl.parent = s;
            if ((s.parent = pp) == null)
                root = s;
            else if (p == pp.left)
                pp.left = s;
            else
                pp.right = s;
            if (sr != null)
                replacement = sr;
            else
                replacement = p;
        }
        else if (pl != null)
            replacement = pl;
        else if (pr != null)
            replacement = pr;
        else
            replacement = p;
        if (replacement != p) {
            HashMap.TreeNode<K,V> pp = replacement.parent = p.parent;
            if (pp == null)
                root = replacement;
            else if (p == pp.left)
                pp.left = replacement;
            else
                pp.right = replacement;
            p.left = p.right = p.parent = null;
        }

        HashMap.TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);

        if (replacement == p) {  // detach
            HashMap.TreeNode<K,V> pp = p.parent;
            p.parent = null;
            if (pp != null) {
                if (p == pp.left)
                    pp.left = null;
                else if (p == pp.right)
                    pp.right = null;
            }
        }
        // 将红黑树的跟节点移动到双向链表的第一个节点
        if (movable)
            moveRootToFront(tab, r);
    }

   ConcurrentHashMap是如何保证线程安全的?

  https://blog.csdn.net/weixin_38192427/article/details/122537790

 

posted @ 2023-05-18 11:09  TriggerMaker  阅读(8)  评论(0)    收藏  举报