<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          阿里-測(cè)試開發(fā)面經(jīng)(六)

          共 9143字,需瀏覽 19分鐘

           ·

          2021-05-02 11:36

          點(diǎn)擊藍(lán)字關(guān)注我們,獲取更多面經(jīng)








          HashMap的底層原理




          HashMap是基于哈希表的Map接口的非同步實(shí)現(xiàn)

          HashMap實(shí)際上是一個(gè)“鏈表散列”的數(shù)據(jù)結(jié)構(gòu),即數(shù)組和鏈表的結(jié)合體。

          首先來了解一下數(shù)據(jù)結(jié)構(gòu)中數(shù)組和鏈表來實(shí)現(xiàn)對(duì)數(shù)據(jù)的存儲(chǔ),但這兩者基本上是兩個(gè)極端。


          數(shù)組

          數(shù)組存儲(chǔ)區(qū)間是連續(xù)的,占用內(nèi)存嚴(yán)重,故空間復(fù)雜的很大。但數(shù)組的二分查找時(shí)間復(fù)雜度小,為O(1);數(shù)組的特點(diǎn)是:尋址容易,插入和刪除困難。


          鏈表

          鏈表存儲(chǔ)區(qū)間離散,占用內(nèi)存比較寬松,故空間復(fù)雜度很小,但時(shí)間復(fù)雜度很大,達(dá)O(N)。鏈表的特點(diǎn)是:尋址困難,插入和刪除容易。


          哈希表

          那么我們能不能綜合兩者的特性,做出一種尋址容易,插入刪除也容易的數(shù)據(jù)結(jié)構(gòu)?答案是肯定的,這就是我們要提起的哈希表。哈希表((Hash table)既滿足了數(shù)據(jù)的查找方便,同時(shí)不占用太多的內(nèi)容空間,使用也十分方便。

            哈希表有多種不同的實(shí)現(xiàn)方法,我接下來解釋的是最常用的一種方法—— 拉鏈法,我們可以理解為“鏈表的數(shù)組” ,如圖:

            從上圖我們可以發(fā)現(xiàn)哈希表是由數(shù)組+鏈表組成的,一個(gè)長(zhǎng)度為16的數(shù)組中,每個(gè)元素存儲(chǔ)的是一個(gè)鏈表的頭結(jié)點(diǎn)。那么這些元素是按照什么樣的規(guī)則存儲(chǔ)到數(shù)組中呢。一般情況是通過hash(key)%len獲得,也就是元素的key的哈希值對(duì)數(shù)組長(zhǎng)度取模得到。比如上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都存儲(chǔ)在數(shù)組下標(biāo)為12的位置。


           首先HashMap里面實(shí)現(xiàn)一個(gè)靜態(tài)內(nèi)部類Entry,其重要的屬性有 key , value, next,從屬性key,value我們就能很明顯的看出來Entry就是HashMap鍵值對(duì)實(shí)現(xiàn)的一個(gè)基礎(chǔ)bean,我們上面說到HashMap的基礎(chǔ)就是一個(gè)線性數(shù)組,這個(gè)數(shù)組就是Entry[],Map里面的內(nèi)容都保存在Entry[]里面。


              /**     * The table, resized as necessary. Length MUST Always be a power of two.     */    transient Entry[] table;


          1. HashMap的存取實(shí)現(xiàn)

          既然是線性數(shù)組,為什么能隨機(jī)存?。窟@里HashMap用了一個(gè)小算法,大致是這樣實(shí)現(xiàn):


          // 存儲(chǔ)時(shí):int hash = key.hashCode(); // 這個(gè)hashCode方法這里不詳述,只要理解每個(gè)key的hash是一個(gè)固定的intint index = hash % Entry[].length;Entry[index] = value;

          // 取值時(shí):int hash = key.hashCode();int index = hash % Entry[].length;return Entry[index];


          put()

          疑問:如果兩個(gè)key通過hash%Entry[].length得到的index相同,會(huì)不會(huì)有覆蓋的危險(xiǎn)?

            這里HashMap里面用到鏈?zhǔn)綌?shù)據(jù)結(jié)構(gòu)的一個(gè)概念。上面我們提到過Entry類里面有一個(gè)next屬性,作用是指向下一個(gè)Entry。打個(gè)比方, 第一個(gè)鍵值對(duì)A進(jìn)來,通過計(jì)算其key的hash得到的index=0,記:Entry[0] = A。一會(huì)后又進(jìn)來一個(gè)鍵值對(duì)B,通過計(jì)算其index也等于0,現(xiàn)在怎么辦?HashMap會(huì)這樣做:B.next = A,Entry[0] = B,如果又進(jìn)來C,index也等于0,那么C.next = B,Entry[0] = C;這樣我們發(fā)現(xiàn)index=0的地方其實(shí)存取了A,B,C三個(gè)鍵值對(duì),他們通過next這個(gè)屬性鏈接在一起。所以疑問不用擔(dān)心。也就是說數(shù)組中存儲(chǔ)的是最后插入的元素。到這里為止,HashMap的大致實(shí)現(xiàn),我們應(yīng)該已經(jīng)清楚了。

          public V put(K key, V value) {        if (key == null)            return putForNullKey(value); //null總是放在數(shù)組的第一個(gè)鏈表中        int hash = hash(key.hashCode());        int i = indexFor(hash, table.length);        //遍歷鏈表        for (Entry<K,V> e = table[i]; e != null; e = e.next) {            Object k;            //如果key在鏈表中已存在,則替換為新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) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<K,V>(hash, key, value, e); //參數(shù)e, 是Entry.next //如果size超過threshold,則擴(kuò)充table大小。再散列 if (size++ >= threshold) resize(2 * table.length);}


            當(dāng)然HashMap里面也包含一些優(yōu)化方面的實(shí)現(xiàn),這里也說一下。比如:Entry[]的長(zhǎng)度一定后,隨著map里面數(shù)據(jù)的越來越長(zhǎng),這樣同一個(gè)index的鏈就會(huì)很長(zhǎng),會(huì)不會(huì)影響性能?HashMap里面設(shè)置一個(gè)因子,隨著map的size越來越大,Entry[]會(huì)以一定的規(guī)則加長(zhǎng)長(zhǎng)度。


          get()

           public V get(Object key) {        if (key == null)            return getForNullKey();        int hash = hash(key.hashCode());        //先定位到數(shù)組元素,再遍歷該元素處的鏈表        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.equals(k)))                return e.value;        }        return null;}


          null key的存取

          null key總是存放在Entry[]數(shù)組的第一個(gè)元素。

             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;    }

          private V getForNullKey() { for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) return e.value; } return null; }


          確定數(shù)組index:hashcode % table.length取模

          HashMap存取時(shí),都需要計(jì)算當(dāng)前key應(yīng)該對(duì)應(yīng)Entry[]數(shù)組哪個(gè)元素,即計(jì)算數(shù)組下標(biāo);算法如下:

             /**     * Returns index for hash code h.     */    static int indexFor(int h, int length) {        return h & (length-1);    }


          按位取并,作用上相當(dāng)于取模mod或者取余%。

          這意味著數(shù)組下標(biāo)相同,并不表示hashCode相同。


          table初始大小

            public HashMap(int initialCapacity, float loadFactor) {        .....

          // Find a power of 2 >= initialCapacity int capacity = 1; while (capacity < initialCapacity) capacity <<= 1;

          this.loadFactor = loadFactor; threshold = (int)(capacity * loadFactor); table = new Entry[capacity]; init(); }

          注意table初始大小并不是構(gòu)造函數(shù)中的initialCapacity!!


          而是 >= initialCapacity的2的n次冪!?。?!


          再散列rehash過程


          當(dāng)哈希表的容量超過默認(rèn)容量時(shí),必須調(diào)整table的大小。當(dāng)容量已經(jīng)達(dá)到最大可能值時(shí),那么該方法就將容量調(diào)整到Integer.MAX_VALUE返回,這時(shí),需要?jiǎng)?chuàng)建一張新表,將原表的映射到新表中。

             /**     * Rehashes the contents of this map into a new array with a     * larger capacity.  This method is called automatically when the     * number of keys in this map reaches its threshold.     *     * If current capacity is MAXIMUM_CAPACITY, this method does not     * resize the map, but sets threshold to Integer.MAX_VALUE.     * This has the effect of preventing future calls.     *     * @param newCapacity the new capacity, MUST be a power of two;     *        must be greater than current capacity unless current     *        capacity is MAXIMUM_CAPACITY (in which case value     *        is irrelevant).     */    void resize(int newCapacity) {        Entry[] oldTable = table;        int oldCapacity = oldTable.length;        if (oldCapacity == MAXIMUM_CAPACITY) {            threshold = Integer.MAX_VALUE;            return;        }

          Entry[] newTable = new Entry[newCapacity]; transfer(newTable); table = newTable; threshold = (int)(newCapacity * loadFactor); }





          /** * Transfers all entries from current table to newTable. */ void transfer(Entry[] newTable) { Entry[] src = table; int newCapacity = newTable.length; for (int j = 0; j < src.length; j++) { Entry<K,V> e = src[j]; if (e != null) { src[j] = null; do { Entry<K,V> next = e.next; //重新計(jì)算index int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; } while (e != null); } } }


          hashmap的擴(kuò)容機(jī)制


          1、當(dāng)我們往hashmap中put元素的時(shí)候,先根據(jù)key的hash值得到這個(gè)元素在 數(shù)組中的位置(即下標(biāo)),然后就可以把這個(gè)元素放到對(duì)應(yīng)的位置中了。如果這個(gè)元素所在的位子上已經(jīng)存放有其他元素了,那么在同一個(gè)位子上的元素將以鏈表的 形式存放,新加入的放在鏈頭,比如a->b->c,新加入的d放到a的位置前面,最先加入的放在鏈尾,也就是c。最后變成d->a->b->c,從hashmap中g(shù)et元素時(shí),首先計(jì)算key的hashcode,找到數(shù)組中對(duì)應(yīng)位置的某一元素, 然后通過key的equals方法在對(duì)應(yīng)位置的鏈表中找到需要的元素。


          2、在hashmap中要找到某個(gè)元素,需要根據(jù)key的hash值來求得對(duì)應(yīng)數(shù)組中的位置。如何計(jì)算這個(gè)位置就是hash算法。前面說過hashmap的數(shù)據(jù)結(jié)構(gòu)是數(shù)組和鏈表的結(jié)合,所以我們當(dāng)然希望這個(gè)hashmap里面的元素位置盡量的分布均勻些,盡量使得每個(gè)位置上的元素?cái)?shù)量只有一個(gè),那么當(dāng)我們用hash算法求得這個(gè)位置 的時(shí)候,馬上就可以知道對(duì)應(yīng)位置的元素就是我們要的,而不用再去遍歷鏈表。所以我們首先想到的就是把hashcode對(duì)數(shù)組長(zhǎng)度取模運(yùn)算,這樣一來,元素的分布相對(duì)來說是比較均勻的。但是,“?!边\(yùn)算的消耗還是比較大的,能不能找一種更快速,消耗更小的方式那?java中是這樣做的

          staticintindexFor(inth,intlength){ returnh&(length-1); }


          首先算得keyhashcode值,然后跟數(shù)組的長(zhǎng)度-1做一次“與”運(yùn)算(&)。看上去很簡(jiǎn)單,其實(shí)比較有玄機(jī)。比如數(shù)組的長(zhǎng)度是2的4次方, 那么hashcode就會(huì)和2的4次方-1做“”運(yùn)算。很多人都有這個(gè)疑問,為什么hashmap的數(shù)組初始化大小都是2的次方大小時(shí),hashmap 的效率最高,我以2的4次方舉例,來解釋一下為什么數(shù)組大小為2的冪時(shí)hashmap訪問的性能最高。看下圖,左邊兩組是數(shù)組長(zhǎng)度為16(2的4次方),右邊兩組是數(shù)組長(zhǎng)度為15。兩組的hashcode均為8和9,但是很明顯,當(dāng)它們和1110“”的 時(shí)候,產(chǎn)生了相同的結(jié)果,也就是說它們會(huì)定位到數(shù)組中的同一個(gè)位置上去,這就產(chǎn)生了碰撞,8和9會(huì)被放到同一個(gè)鏈表上,那么查詢的時(shí)候就需要遍歷這個(gè)鏈 表,得到8或者9,這樣就降低了查詢的效率。同時(shí),我們也可以發(fā)現(xiàn),當(dāng)數(shù)組長(zhǎng)度為15的時(shí)候,hashcode的值會(huì)與14(1110)進(jìn)行“與”,那么 最后一位永遠(yuǎn)是0,而0001,0011,0101,1001,1011,0111,1101這幾個(gè)位置永遠(yuǎn)都不能存放元素了,空間浪費(fèi)相當(dāng)大,更糟的是 這種情況中,數(shù)組可以使用的位置比數(shù)組長(zhǎng)度小了很多,這意味著進(jìn)一步增加了碰撞的幾率,減慢了查詢的效率!所以說,當(dāng)數(shù)組長(zhǎng)度為2的n次冪的時(shí)候,不同的key算得得index相同的幾率較小,那么數(shù)據(jù)在數(shù)組上分布就比較均勻,也就是說碰撞的幾率小,相對(duì)的,查詢的時(shí)候就不用遍歷某個(gè)位置上的鏈表,這樣查詢效率也就較高了。說到這里,我們?cè)倩仡^看一下hashmap中默認(rèn)的數(shù)組大小是多少,查看源代碼可以得知是16,為什么是16,而不是15,也不是20呢,看到上面 annegu的解釋之后我們就清楚了吧,顯然是因?yàn)?6是2的整數(shù)次冪的原因,在小數(shù)據(jù)量的情況下16比15和20更能減少key之間的碰撞,而加快查詢 的效率。


          3、當(dāng)hashmap中的元素越來越多的時(shí)候,碰撞的幾率也就越來越高(因?yàn)閿?shù)組的長(zhǎng)度是固定的),所以為了提高查詢的效率,就要對(duì)hashmap的數(shù)組進(jìn)行 擴(kuò)容,數(shù)組擴(kuò)容這個(gè)操作也會(huì)出現(xiàn)在ArrayList中,所以這是一個(gè)通用的操作,很多人對(duì)它的性能表示過懷疑,不過想想我們的“均攤”原理,就釋然了, 而在hashmap數(shù)組擴(kuò)容之后,最消耗性能的點(diǎn)就出現(xiàn)了:原數(shù)組中的數(shù)據(jù)必須重新計(jì)算其在新數(shù)組中的位置,并放進(jìn)去,這就是resize。


          那么hashmap什么時(shí)候進(jìn)行擴(kuò)容呢?當(dāng)hashmap中的元素個(gè)數(shù)超過數(shù)組大小*loadFactor時(shí),就會(huì)進(jìn)行數(shù)組擴(kuò)容,loadFactor的 默認(rèn)值為0.75,也就是說,默認(rèn)情況下,數(shù)組大小為16,那么當(dāng)hashmap中元素個(gè)數(shù)超過16*0.75=12的時(shí)候,就把數(shù)組的大小擴(kuò)展為 2*16=32,即擴(kuò)大一倍,然后重新計(jì)算每個(gè)元素在數(shù)組中的位置,而這是一個(gè)非常消耗性能的操作,所以如果我們已經(jīng)預(yù)知hashmap中元素的個(gè)數(shù),那 么預(yù)設(shè)元素的個(gè)數(shù)能夠有效的提高h(yuǎn)ashmap的性能。


          比如說,我們有1000個(gè)元素new HashMap(1000), 但是理論上來講new HashMap(1024)更合適,不過上面annegu已經(jīng)說過,即使是1000,hashmap也自動(dòng)會(huì)將其設(shè)置為1024。但是new HashMap(1024)還不是更合適的,因?yàn)?.75*1000 < 1000, 也就是說為了讓0.75 * size > 1000, 我們必須這樣new HashMap(2048)才最合適,既考慮了&的問題,也避免了resize的問題。


          哈希表及處理hash沖突的方法


          看了ConcurrentHashMap的實(shí)現(xiàn), 使用的是拉鏈法.

          雖然我們不希望發(fā)生沖突,但實(shí)際上發(fā)生沖突的可能性仍是存在的。當(dāng)關(guān)鍵字值域遠(yuǎn)大于哈希表的長(zhǎng)度,而且事先并不知道關(guān)鍵字的具體取值時(shí)。沖突就難免會(huì)發(fā)生。

          另外,當(dāng)關(guān)鍵字的實(shí)際取值大于哈希表的長(zhǎng)度時(shí),而且表中已裝滿了記錄,如果插入一個(gè)新記錄,不僅發(fā)生沖突,而且還會(huì)發(fā)生溢出。

          因此,處理沖突和溢出是哈希技術(shù)中的兩個(gè)重要問題。


          哈希法又稱散列法、雜湊法以及關(guān)鍵字地址計(jì)算法等,相應(yīng)的表稱為哈希表。這種方法的基本思想是:首先在元素的關(guān)鍵字k和元素的存儲(chǔ)位置p之間建立一個(gè)對(duì)應(yīng)關(guān)系f,使得p=f(k),f稱為哈希函數(shù)。創(chuàng)建哈希表時(shí),把關(guān)鍵字為k的元素直接存入地址為f(k)的單元;以后當(dāng)查找關(guān)鍵字為k的元素時(shí),再利用哈希函數(shù)計(jì)算出該元素的存儲(chǔ)位置p=f(k),從而達(dá)到按關(guān)鍵字直接存取元素的目的。

             當(dāng)關(guān)鍵字集合很大時(shí),關(guān)鍵字值不同的元素可能會(huì)映象到哈希表的同一地址上,即 k1≠k2 ,但 H(k1)=H(k2),這種現(xiàn)象稱為沖突,此時(shí)稱k1和k2為同義詞。實(shí)際中,沖突是不可避免的,只能通過改進(jìn)哈希函數(shù)的性能來減少?zèng)_突。

          綜上所述,哈希法主要包括以下兩方面的內(nèi)容:

           1)如何構(gòu)造哈希函數(shù)

           2)如何處理沖突。








          更多面經(jīng)





          360-測(cè)試開發(fā)面經(jīng)(一)


          百度-測(cè)試開發(fā)面經(jīng)(一)


          字節(jié)跳動(dòng)-測(cè)試開發(fā)面經(jīng)(一)


              掃描二維碼

             獲取更多面經(jīng)

            扶搖就業(yè)  


          瀏覽 30
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美黄色大片网站 | 欧美精品久久久久黄片18试看 | 欧美日韩电影一区二区三区 | 午夜极品人妻 | 人人色人人看 |