ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

TreeMap分析

2021-09-06 21:57:48  阅读:132  来源: 互联网

标签:分析 HashMap TreeMap next key Entry null


TreeMap分析

TreeMap和HashMap不同点

  1. TreeMap的数据结构和HashMap不一样,TreeMap是基于红黑树实现的。而HahsMap是数组 + 链表(红黑树)来实现的。

  2. TreeMap不需要调用Hash方法来算hash值。

  3. TreeMap一开始就是红黑树,不像HashMap一样,达到特定的条件之后才会变为红黑树。

  4. TreeMap不需要扩容。但是HashMap会扩容。

  5. TreeMap没有负载因子,也没有初始容量。但是这些HashMap都有。

    数组和链表的差距,主要是同样的空间的前提下,链表结构的空间利用率高。数组需要连续的存储空间。这也就导致了慢。首先要知道,他是基于红黑树实现的,所以,他并不需要连续的存储空间。所以,从跟上就不会有扩容的问题出现,只有在内存不够的情况下,直接OOM。

  6. TreeMap里面存放的key必须要实现Comparable接口,或者在构造函数里面指定Comparator,并且key能在Comparator比较。而HashMap在hash冲突之后,并且链表变为红黑树,hash值一样的情况下,会调用Comparable接口来做比较,这就要求要key要实现Comparable接口。在日常的使用中,直接放就行, 但是自己可以仔细的看看,这些类上面都实现了Comparable接口。如果类型不一样或者不是Comparable接口,就会抛出异常ClassCastException

  7. TreeMap实现的接口比HashMap多。

在这里插入图片描述

​ (TreeMap)

在这里插入图片描述

​ (HashMap)

  1. Entry不一样。TreeMap的是TreeMap##Entry,他的这些属性都是红黑树所需要的。HashMap的是HashMap##NodeHashMap##TreeNode 其实我觉得TreeMap的Entry是可以复用HashMap的TreeNode的。

  2. 迭代器不一样

    Treemap的迭代器是TreeMap##EntryIterator

    HashMap的迭代器是HashMap##EntryIterator

    因为HashMap和TreeMap的实现不一样,所以迭代器的具体功能也是不一样的。

    具体放在下一章节说

  3. TreeMap不支持key为null,但是HashMap支持。TreeMap是需要用来做排序的,并且没有调用hashcode方法,所以key不能为null,HashMap在转为红黑树的时候,是用key里面的hash值来做比较的。对于null,HashMap产生的hash值是 0

    //HashMap里的hash方法  
    static final int hash(Object key) {
            int h;
            return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
        }
    

    以上是我能想到的,可能不太全面,如果有还有不一样的话,请补充补充。

TreeMap会有Hash冲突吗?

不会,因为他根本就不会调用hashcode方法。可以看看put的源码

    public V put(K key, V value) {
       //如果根节点没有值。这个if就是给根节点赋值。
        Entry<K,V> t = root;
        if (t == null) {
          //这个compare,就是为了检查类型和null
            compare(key, key); // type (and possibly null) check
           //简单的构建Entry,赋值给root
            root = new Entry<>(key, value, null);
            size = 1;
           // 快速失败,继续修改次数
            modCount++;
            return null;
        }
        int cmp;
        Entry<K,V> parent;
        // split comparator and comparable paths
      
      //下面就是在有根节点的情况下,遍历整个树,利用compare来比较,可以看到
      // 小于就是左
      // 大于就是右
      // 等于就是覆盖值。
        Comparator<? super K> cpr = comparator;
        if (cpr != null) {
            do {
                parent = t;
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        else {
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
                Comparable<? super K> k = (Comparable<? super K>) key;
            do {
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        Entry<K,V> e = new Entry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
      
      //在平衡
        fixAfterInsertion(e);
        size++;
      // 快速失败
        modCount++;
        return null;
    }

可以看到,压根和hash没有一点点的关系,所以就不会存在hash冲突的问题。主要是利用Comparable接口和Comparator。并且Comparator的优先级更高。

TreeMap遍历(迭代)顺序是什么?和HashMap有啥不一样

TreeMap的迭代

先上例子

     @Test
   public void testTreeMap(){
       TreeMap<Integer, Integer> treeMap = new TreeMap<>();
        treeMap.put(1,1);
        treeMap.put(2,2);
        treeMap.put(3,3);
        treeMap.put(4,4);
        treeMap.put(5,5);
        treeMap.put(6,6);
        treeMap.put(7,7);
        treeMap.put(8,8);
        treeMap.put(9,9);
        treeMap.put(10,10);
        treeMap.put(11,11);
        treeMap.put(12,12);


       Iterator<Map.Entry<Integer, Integer>> iterator = treeMap.entrySet().iterator();
       while (iterator.hasNext()){
           System.out.print(iterator.next() + ",");
       }
   }

在这里插入图片描述

要知道在TreeMap的数据结构是红黑树。首先,他是一种二叉排序树。怎么遍历的时候就出现这种结果了。他是按照什么样的遍历顺序来遍历的呢?二叉排序树按照遍历根节点和子节点的顺序不同,可以分为好多种,经常用的是三种,中序遍历,先序遍历,后序遍历。这里用的是那种遍历?

直接debug,看看他生成的树的结构是什么。然后再看看迭代器相关的代码。就能确定了。

在这里插入图片描述

构建出来的树就是这个样子,在看看迭代器是怎么遍历的?

 //  Iterator<Map.Entry<TreeDo, Integer>> iterator = hashMap.entrySet().iterator();从这里debug进来。      
 public Iterator<Map.Entry<K,V>> iterator() {
            return new EntryIterator(getFirstEntry());
        }
    
//  getFirstEntry() 看看第一个节点是什么?
// 这个方法很简单,从根节点开始,查找左子树,一直找到最左的子树。
  final Entry<K,V> getFirstEntry() {
        Entry<K,V> p = root;
        if (p != null)
            while (p.left != null)
                p = p.left;
        return p;
    }

// 在看看EntryIterator里面next方法

  final Entry<K,V> nextEntry() {
          // 这个next就是上面查找出来的最左的节点
            Entry<K,V> e = next;
            if (e == null)
                throw new NoSuchElementException();
     // 快速失败
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
    
     // 找到下一个要访问节点,重点就是这里
            next = successor(e);
            lastReturned = e;
            return e;
        }

//重点就是successor方法
  static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
        if (t == null)
            return null;
    // 这里的逻辑就是重点中的重点,一块看看
    
     //如果说,右子树不为null,并且有左子树,就找到这个右子树最左的节点。
        else if (t.right != null) {
            Entry<K,V> p = t.right;
            while (p.left != null)
                p = p.left;
            return p;
        } else {
           //右子树为null
            Entry<K,V> p = t.parent; //父节点
            Entry<K,V> ch = t; //当前节点
          
           //如果p不为null并且当前的节点是p的右子树的话。一直往上找。
            while (p != null && ch == p.right) {
                ch = p;
                p = p.parent;
            }
            return p;
        }
    }

所以,结合图,还有这里的代码发现,这里的遍历顺序是中序遍历

HashMap的迭代

hashMap都已经很熟悉了,就直接看他的迭代器的方法吧

主要分为两个部分,一个是new 迭代器的时候的赋值操作,一个是调用next方法的取值操作

  1. 赋值操作
 // 只要debug从下面的代码进去,就能看到,要注意EntryIterator可是继承HashIterator,所以得看到构造方法里面,这就是在构造方法里面赋值的
// Iterator<Map.Entry<TreeDo, Integer>> iterator = hashMap.entrySet().iterator();

HashIterator() {
          // 快速失败
            expectedModCount = modCount;
            Node<K,V>[] t = table;
            current = next = null;
            index = 0;
  
           //看这里的操作,do里面没看什么事,重点是while。
  				// index初始为0,在hashmap的table里面一直找,找到了第一个下标不为null的元素就返回。
            if (t != null && size > 0) { // advance to first entry
                do {} while (index < t.length && (next = t[index++]) == null);
            }
        }
  1. next取值操作

     
    // 这个方法还是在 HashIterator里面。
    final Node<K,V> nextNode() {
                Node<K,V>[] t;
                Node<K,V> e = next;
               //快速失败
                if (modCount != expectedModCount)
                    throw new ConcurrentModificationException();
                if (e == null)
                    throw new NoSuchElementException();
      
               // 重点是这里。do while里面的和上面一样。重点是if里面
           // 如果将e.next赋值给了next,只有当链表或者红黑树的没有节点了,才会再次获取hashMap中下一个索引位置不为null的值。
                if ((next = (current = e).next) == null && (t = table) != null) {
                    do {} while (index < t.length && (next = t[index++]) == null);
                }
                return e;
            }
    

    问题?

    如果变为链表的话,next肯定是有的,自然而然的就是下一个节点,但是要是红黑树的话,next是啥?

    // 分为两种情况
    // 1. 链表变为红黑树的时候
       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)
                resize();
            else if ((e = tab[index = (n - 1) & hash]) != null) {
                TreeNode<K,V> hd = null, tl = null;
                do {
                    TreeNode<K,V> p = replacementTreeNode(e, null);
                    if (tl == null)
                        hd = p;
                    else {
                       //看这里next,这里的代码就是将node替换为TreeNode,别的什么都没有变。所以,这里的顺序还是链表的顺序。
                        p.prev = tl;
                        tl.next = p;
                    }
                    tl = p;
                } while ((e = e.next) != null);
                if ((tab[index] = hd) != null)
                    hd.treeify(tab);
            }
        }
    // 2. 已经变为红黑树的时候,这里的代码比较长,我就不在这里写,在 HashMap#  final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,int h, K k, V v) 中。
                    //父节点
                    TreeNode<K,V> xp = p; 
                    if ((p = (dir <= 0) ? p.left : p.right) == null) {
                        //父节点的next赋值给new出来的节点的next
                        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;
                         // 当前节点变为父节点的next。
                        xp.next = x;
                        x.parent = x.prev = xp;
                        if (xpn != null)
                            ((TreeNode<K,V>)xpn).prev = x;
                        moveRootToFront(tab, balanceInsertion(root, x));
                        return null;
                    }
    父节点之前next就是子节点的next,然后把子节点变为父节点next。 这好像是头插法。
    

关于TreeMap就分析到这里了。如有不正确的地方,欢迎指出。谢谢。

标签:分析,HashMap,TreeMap,next,key,Entry,null
来源: https://blog.csdn.net/daliucheng/article/details/120145745

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有