Android 从零学数据结构与算法(3)——HashMap和LinkedHashMap

    本博客的原创文章,都是本人平时学习所做的笔记,不做商业用途,如有侵犯您的知识产权和版权问题,请通知本人,本人会即时做出处理删除文章。

  • HashMap

    基于哈希表(散列表)的Map接口的实现,允许使用null键和null值,HashMap是非线程安全的,数据元素存取迭代是无序,顺便提一下HashTable,HashTable是线程安全的,除了线程安全和null键值的区别,HashMap和HashTable大致相同。


    上图是哈希表的结构图,这种表结构查找效率高,如果我们把0-15的顺序线表的每个地址看成一个"桶",而下标是"桶"的编号,我们通过计算key的hash值“与”上线性表的长度得到得到0-15的值,然后将value放入对应的"桶"内,这样我们查找的时候不用遍历所有的元素,直接去对应的桶里面查找就可以了。

    下面我们一起看HashMap的源码,我是用Android studio查看的api25的源码,不同api版本源码会有不同,首先来看看成员变量。

    //最少容量
    static final int DEFAULT_INITIAL_CAPACITY = 4;
    //最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30;
    //扩容因子(当容量到75%的时候就要开始扩容了)
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //空表
    static final HashMapEntry<?,?>[] EMPTY_TABLE = {};
    //键值对的数组
    transient HashMapEntry<K,V>[] table = (HashMapEntry<K,V>[]) EMPTY_TABLE;
    //非空元素的长度
    transient int size;
    //容量
    int threshold;
    //扩容因子相关
    final float loadFactor = DEFAULT_LOAD_FACTOR
    //操作计数器
    transient int modCount;

    我们再来看看put方法:

public V put(K key, V value) {
	//存储key为null的值,如果之前有值就覆盖
        if (key == null)
            return putForNullKey(value);
	//获取key的hash值
        int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);
	//通过hash&(length-1)得到元素长度内的值,也就是"桶"的下标,(&运算不熟悉的自行百度)
        int i = indexFor(hash, table.length);
	//通过桶的下标,遍历"桶"里面的元素(需要看一下HashMapEntry的实现),如果hash值相同,key相等,那就覆盖value,返回旧的value
        for (HashMapEntry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
	//操作计数
        modCount++;
	//如果没有相同的key,就添加新元素
        addEntry(hash, key, value, i);
        return null;
    }
	void addEntry(int hash, K key, V value, int bucketIndex) {
	//如果元素个数大于等于扩容因子容量(目前容量的75%),那就扩容原来的一倍
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? sun.misc.Hashing.singleWordWangJenkinsHash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
	//添加数据元素(内容简单,代码就不贴了)
        createEntry(hash, key, value, bucketIndex);
    }
	void resize(int newCapacity) {
        HashMapEntry[] oldTable = table;
        int oldCapacity = oldTable.length;
	//如果达到最大了就不扩容了
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        HashMapEntry[] newTable = new HashMapEntry[newCapacity];
	//通过遍历,把旧数组中的元素添加到新数组中
        transfer(newTable);
        table = newTable;
	//计算扩容因子容量,loadFactor=0.75f
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }
  void transfer(HashMapEntry[] newTable) {
        int newCapacity = newTable.length;
	//双层循环遍历把元素添加到新数组里面,因为indexFor()方法中的leng变了,
	//"桶"的下标就有可能变,所以需要遍历全部元素,通过key的hash值重新计算桶的下标
        for (HashMapEntry<K,V> e : table) {
            while(null != e) {
                HashMapEntry<K,V> next = e.next;
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

    上面是put流程的部分源码,还需要自己对照源码看一看。如果能把put看明白,get就很简单了。下面我们看一下remove方法源码:

public V remove(Object key) {
        Entry<K,V> e = removeEntryForKey(key);
        return (e == null ? null : e.getValue());
    } 
	final Entry<K,V> removeEntryForKey(Object key) {
        if (size == 0) {
            return null;
        }
	//通过key的hash值获取"桶"的下标
        int hash = (key == null) ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key);
        int i = indexFor(hash, table.length);
        HashMapEntry<K,V> prev = table[i];
        HashMapEntry<K,V> e = prev;
	//遍历这个"桶"内的元素
        while (e != null) {
            HashMapEntry<K,V> next = e.next;
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k)))) {
                modCount++;
                size--;
		//只有第一个元素就是要删除元素的时候prev才会等于e
                if (prev == e)
                    table[i] = next;
                else
                    prev.next = next;
                e.recordRemoval(this);
                return e;
            }
            prev = e;
            e = next;
        }

        return e;
    }
    一些逻辑还需要自己认真屡一下。HashMap核心内容也就这些。下面我们一起看看LinkedHashMap。
  • LinkedHashMap

    LinkedHashMap是HashMap的子类,具有HashMap所有特性,只是额外维护一个双向循环链表来保持迭代顺序,当然,是牺牲了一定的性能。LinkedHashMap支持LRU算法,LruCache就是基于LinkedHashMap来实现的。


下面一起看看源码,看一下仅有的两个成员变量。

    //头结点
    private transient LinkedHashMapEntry<K,V> header;
    //如果是true通过访问排序,如果是false通过插入排序,默认为false
    private final boolean accessOrder;

    如果你去看一下Lrucache的构造方法,你会发现它传入的就是true。LinkedHashMap里面并没有put的方法,只是重写了addEntry()和createEntry()方法,HashMap的put方法最后会调用addEntry()和createEntry()方法。

 void addEntry(int hash, K key, V value, int bucketIndex) {
	//双向循环链表头节点(header后的节点)如果是按插入排序就是最先插入的元素,
	//如果是按访问排序就是访问最少的元素,把访问最少或最老的元素赋值给eldest,
        LinkedHashMapEntry<K,V> eldest = header.after;
        if (eldest != header) {
            boolean removeEldest;
            size++;
            try {
		//removeEldestEntry()方法交给子类实现,通过判断返回值来删除eldest元素
		//默认返回false,所以不删除
                removeEldest = removeEldestEntry(eldest);
            } finally {
                size--;
            }
		//删除最少或最老的元素
            if (removeEldest) {
                removeEntryForKey(eldest.key);
            }
        }

        super.addEntry(hash, key, value, bucketIndex);
    }

    void createEntry(int hash, K key, V value, int bucketIndex) {
	//取出数组中存的节点
        HashMapEntry<K,V> old = table[bucketIndex];
	//创建新节点
        LinkedHashMapEntry<K,V> e = new LinkedHashMapEntry<>(hash, key, value, old);
	//将新节点放在数组中
        table[bucketIndex] = e;
	//将新元素节点添加到双向循环链表,为什么传header呢,因为要把元素插入在header前面
	//header前面是尾节点,header后面是头节点,新添加的元素需要添加到尾节点
        e.addBefore(header);
        size++;
    }
    private void addBefore(LinkedHashMapEntry<K,V> existingEntry) {
	    //将新元素添加到header前面的尾节点,也就是header和之前的尾节点之间插入新元素节点
            after  = existingEntry;
            before = existingEntry.before;
            before.after = this;
            after.before = this;
        }

    上面就是LinkedHashMap添加新元素的核心代码。配合结构图能更好的理解。每次新添加的元素都添加到header的前面,也就是尾节点,而header的后面的节点就是头结点,所以在Lru算法中,无论是访问排序还是插入排序,需要删除元素时,都是删除头结点。

  public V get(Object key) {
	//调用父类的获取元素方法
        LinkedHashMapEntry<K,V> e = (LinkedHashMapEntry<K,V>)getEntry(key);
        if (e == null)
            return null;
	//记录访问
        e.recordAccess(this);
        return e.value;
    }
	void recordAccess(HashMap<K,V> m) {
            LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
		//如果是访问排序,就把访问的数据元素放在尾节点
            if (lm.accessOrder) {
		//记录操作
                lm.modCount++;
		//删除自己
                remove();
		//把自己添加到header前面的尾节点
                addBefore(lm.header);
            }
        }

    上面试get()的核心代码,如果是插入排序,只需要遍历查找就可以了,如果是访问排序,就需要把访问的元素放在尾节点。

   public Map.Entry<K, V> eldest() {
        Entry<K, V> eldest = header.after;
        return eldest != header ? eldest : null;
    }

    eldest()方法是返回头节点。以上就是LinkedHashMap比较核心的源码了。

  • 下篇预告

    下节我们一起学习树。

------------------------------------------------       

    想要继续跟我一起学习一起成长,请关注我的公众号:程序员持续发展方案




  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值