hashMap原理

線性表的弊端

線性表的順序存儲,佔用的空間地址是連續的,查找方便,但是容易造成內存空間的浪費。插入和刪除,需要大規模的移動數據,既不安全速度又慢。鏈式存儲雖然插入刪除方便但是由於其空間的不連續,查找速度慢。當我們既要對數據增刪又要查詢顯然線性表已經不能滿足我們的需求了

散列表(hashTable)

散列表的使用分爲兩步:

  • 第一步:使用散列函數將被查找的Key值轉換爲數組的一個索引,理想情況下不同的key值都能轉換爲不同的索引值,但是這只是理想情況,很有可能兩個Key或者多個Key都會映射爲相同的索引值這就需要散列查找的第二步解決散列碰撞衝突
  • 第二步:解決散列碰撞: 拉鍊法,線性探測法

這裏寫圖片描述

散列函數

散列函數的計算,這個過程會將鍵值轉換爲數組的索引,這個散列函數應該易於計算並且能夠均勻分佈所有的鍵。這裏主要主要介紹兩種方法(jdk中)。

  • 第一種方法:先計算key的hash值,一個32位int類型數字,和
    數組的長度M做取模運算得到的數字一定在M內。
  • 第二種方法:先計算key的hash值,一個32位int類型數字,
    和數組的長度M-1,做&運算,那麼得到的數字一定在M內。

解決散列碰撞

在Java中hashMap解決散列碰撞使用的是拉鍊法,即將大小爲M的數組中的每個元素指向一個 鏈表,鏈表的每一個節點都存儲了散值爲該元素的鍵值對,這種方法被稱爲拉鍊法。
這裏寫圖片描述

這裏寫圖片描述

hsahMap源碼

 //初始容量  
    static final int DEFAULT_INITIAL_CAPACITY = 16;
    //最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30;
    //默認的負載因子,和擴容相關的
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //散列表,是一個Entry數組
    transient Entry<K,V>[] table;
    //容器的大小
    transient int size;
       ...
 public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);

        // Find a power of 2 >= initialCapacity
        //散列表的大小必須是2的N次方冪
        int capacity = 1;
        while (capacity < initialCapacity)
            capacity <<= 1;

        this.loadFactor = loadFactor;
        threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        table = new Entry[capacity];
        useAltHashing = sun.misc.VM.isBooted() &&
                (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        init();
    }


    public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

默認的無參構造是初始化一個16個大小的散列表

  • Entry保存key,key的hash值,vlaue,和指針
static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;

        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }

        public final K getKey() {
            return key;
        }

        public final V getValue() {
            return value;
        }
        。。。。
    }
  • 散列函數
static int indexFor(int h, int length) {
        return h & (length-1);
    }
  • 添加元素的方法:put
public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);//鍵爲空的時候
        int hash = hash(key); //計算key的hash值,32位的int類型
        int i = indexFor(hash, table.length);//散列函數,計算通過鍵計算數組的索引
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            //存入的Key值一樣,並且hash也相同,新的value覆蓋原來的
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        //添加元素
        addEntry(hash, key, value, i);
        return null;
    }

     void addEntry(int hash, K key, V value, int bucketIndex) {
         //判斷是否超出可數組的長度
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }
    //添加null
     private V putForNullKey(V value) {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        //添加到散列表的第一個位置
        addEntry(0, null, value, 0);
        return null;
    }

    void createEntry(int hash, K key, V value, int bucketIndex) {
        //添加的是頭節點
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }

注意的是:hash值相同,Key值不一定相同,Key值相同hash值一定相同,所以在判斷key是否存在一定要注意。允許存入key爲null,但是一定存放在散列表的第一個位置

  • 獲得元素的方法get
public V get(Object key) {
         if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }

  final Entry<K,V> getEntry(Object key) {
        int hash = (key == null) ? 0 : hash(key);
        //計算哈希表索引,遍歷鏈表
        for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null;  e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))

                return e;
        }
        return null;
    }
  • 刪除元素
 final Entry<K,V> removeEntryForKey(Object key) {
        int hash = (key == null) ? 0 : hash(key);
        int i = indexFor(hash, table.length);
        Entry<K,V> prev = table[i];//頭元素
        Entry<K,V> e = prev;

        while (e != null) {
            Entry<K,V> next = e.next;
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k)))) {
                modCount++;
                size--;
                if (prev == e)//如果第一個元素就是
                    table[i] = next;
                else
                    prev.next = next;
                e.recordRemoval(this);
                return e;
            }
            prev = e;
            e = next;//向下遍歷
        }
  • 再哈希
    當哈希表的表空間不足的時候我們就需要對哈希表進行擴容,但是散列表的值不能大於2的30次冪,擴充後的哈希表,由於長度變了所以必須重新使用散列函數計算散列值
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章