日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

HashMap的实现与优化

發(fā)布時(shí)間:2023/12/3 编程问答 36 豆豆
生活随笔 收集整理的這篇文章主要介紹了 HashMap的实现与优化 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

轉(zhuǎn)載自?HashMap的實(shí)現(xiàn)與優(yōu)化

HashMap的優(yōu)化與實(shí)踐

本文是基于作者在github上的Android 問題交流討論壇提問而產(chǎn)生的一篇文章,也是自己早打算開坑的一篇文章。文章首先介紹了hashMap的一些基本知識(shí),然后介紹了它在JDK8下的實(shí)現(xiàn)原理,最后著重介紹了幾個(gè)面試中處理大數(shù)據(jù)的方法,文章比較長(zhǎng),我也寫了好久,希望各位能夠讀完并發(fā)表意見。

Android 題交流討論壇是開源達(dá)人 Trinea 在gitHub上組建的一個(gè)討論組織,那里的提問與回答都非常靠譜。

HashMap的復(fù)雜度

如圖是ArrayList/LinkedList/HashMap三個(gè)數(shù)據(jù)結(jié)構(gòu)的復(fù)雜度對(duì)比,可以看出HashMap整體上性能都非常不錯(cuò),但是不穩(wěn)定,為O(N/Buckets),N就是以數(shù)組中沒有發(fā)生碰撞的元素。

?獲取查找添加/刪除空間?
ArrayListO(1)O(1)O(N)O(N)
LinkedListO(N)O(N)O(1)O(N)
HashMapO(N/Bucket_size)O(N/Bucket_size)O(N/Bucket_size)O(N)

注:發(fā)生碰撞實(shí)際上是非常稀少的,所以N/Bucket_size約等于1

HashMap是對(duì)Array與Link的折衷處理,Array與Link可以說是兩個(gè)速度方向的極端,Array注重于數(shù)據(jù)的獲取,而處理修改(添加/刪除)的效率非常低;Link由于是每個(gè)對(duì)象都保持著下一個(gè)對(duì)象的指針,查找某個(gè)數(shù)據(jù)需要遍歷之前所有的數(shù)據(jù),所以效率比較低,而在修改操作中比較快。

復(fù)雜度是如何考察的?

對(duì)于數(shù)據(jù)結(jié)構(gòu),在時(shí)間上我們需要考察Acessing ,Search, Deletion/Insertion的平均與最差的復(fù)雜度。在空間上,我們要考慮維護(hù)這個(gè)數(shù)據(jù)結(jié)構(gòu)所占用的內(nèi)存空間。

常見的數(shù)據(jù)結(jié)構(gòu)與排序的復(fù)雜度都在這里

HashMap的實(shí)現(xiàn)

本文以JDK8的API實(shí)現(xiàn)進(jìn)行分析

1. 什么是hash,什么是碰撞?

  • Hash:是一種信息摘要算法,它還叫做哈希,或者散列。我們平時(shí)使用的MD5,SHA1,SSL中的公私鑰驗(yàn)證都屬于Hash算法,通過輸入key進(jìn)行Hash計(jì)算,就可以獲取key的HashCode(),比如我們通過校驗(yàn)MD5來驗(yàn)證文件的完整性。
  • 碰撞:好的Hash算法可以出計(jì)算幾乎出獨(dú)一無二的HashCode,如果出現(xiàn)了重復(fù)的hashCode,就稱作碰撞;
<code>&gt;就算是MD5這樣優(yōu)秀的算法也會(huì)發(fā)生碰撞,即兩個(gè)不同的key也有可能生成相同的MD5。</code>

2. HashMap中是如何實(shí)現(xiàn)寫入與讀取的?

HashMap實(shí)現(xiàn)了Map接口,保存著K-V這樣的集合。我們以put操作為例

2.1. 對(duì)key進(jìn)行Hash計(jì)算

在JDK8中,由于使用了紅黑樹來處理大的鏈表開銷,所以hash這邊可以更加省力了,只用計(jì)算hashCode并移動(dòng)到低位就可以了

12345static final int hash(Object key) {????int h;????//計(jì)算hashCode,并無符號(hào)移動(dòng)到低位????return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}

下面給出幾個(gè)常用的哈希碼的算法。

  • Object類的hashCode.返回對(duì)象的內(nèi)存地址經(jīng)過處理后的結(jié)構(gòu),由于每個(gè)對(duì)象的內(nèi)存地址都不一樣,所以哈希碼也不一樣。這個(gè)是native方法,這個(gè)取決于JVM的設(shè)計(jì),一般是某種地址的偏移。
  • String類的hashCode.根據(jù)String類包含的字符串的內(nèi)容,根據(jù)一種特殊算法返回哈希碼,只要字符串的內(nèi)容相同,返回的哈希碼也相同。
  • Integer等包裝類,返回的哈希碼就是Integer對(duì)象里所包含的那個(gè)整數(shù)的數(shù)值,例如Integer i1=new Integer(100),i1.hashCode的值就是100 。由此可見,2個(gè)一樣大小的Integer對(duì)象,返回的哈希碼也一樣。
  • int,char這樣的基礎(chǔ)類,它們不需要hashCode,如果需要存儲(chǔ)時(shí),將進(jìn)行自動(dòng)裝箱操作,計(jì)算方法同上。
  • 2.2. 獲取到當(dāng)前的位置

    計(jì)算了Hash,我們現(xiàn)在要把它插入數(shù)組中了

    1i = (tab.length - 1) & hash;

    通過位運(yùn)算,確定了當(dāng)前的位置,因?yàn)镠ashMap數(shù)組的大小總是2^n,所以實(shí)際的運(yùn)算就是 (0xfff…ff) & hash ,這里的tab.length-1相當(dāng)于一個(gè)mask,濾掉了大于當(dāng)前長(zhǎng)度位的hash,使每個(gè)i都能插入到數(shù)組中。

    2.3. 生成包裝類

    這個(gè)對(duì)象是一個(gè)包裝類,Node<K,V>,內(nèi)部有key,value,hash還有next,可以看出來它是一個(gè)鏈表。

    1234567static class Node<K,V> implements Map.Entry<K,V> {????????final int hash;????????final K key;????????V value;????????Node<K,V> next;????????//getter and setter .etc.}

    2.4. 插入包裝類到數(shù)組

    (1). 如果輸入當(dāng)前的位置是空的,就插進(jìn)去,如圖,左為插入前,右為插入后

    12345678910110?????????? 0|?????????? |1 -> null?? 1 - > null|?????????? |2 -> null?? 2 - > null|?????????? | ..-> null?? ..- > null|?????????? | i -> null?? i - > new node|?????????? |n -> null?? n - > null

    (2). 如果當(dāng)前位置已經(jīng)有了node,且它們發(fā)生了碰撞,則新的放到前面,舊的放到后面,這叫做鏈地址法處理沖突。

    12345678910110?????????? 0|?????????? |1 -> null?? 1 - > null|?????????? |2 -> null?? 2 - > null|?????????? | ..-> null?? ..- > null|?????????? | i -> old??? i - > new - > old|?????????? |n -> null?? n - > null

    我們可以發(fā)現(xiàn),失敗的hashCode算法會(huì)導(dǎo)致HashMap的性能下降為鏈表,所以想要避免發(fā)生碰撞,就要提高h(yuǎn)ashCode結(jié)果的均勻性。當(dāng)然,在JDK8中,采用了紅黑二叉樹進(jìn)行了處理,這個(gè)我們后面詳細(xì)介紹。

    什么是Hash攻擊?

    通過請(qǐng)求大量key不同,但是hashCode相同的數(shù)據(jù),讓HashMap不斷發(fā)生碰撞,硬生生的變成了SingleLinkedList

    1234567890|1 -> a ->b -> c -> d(撞!撞!撞!復(fù)雜度由O(1)變成了O(N))|2 -> null(本應(yīng)該均勻分布,這里卻是空的)|3 -> null|4 -> null

    這樣put/get性能就從O(1)變成了O(N),CPU負(fù)載呈直線上升,形成了放大版DDOS的效果,這種方式就叫做hash攻擊。在Java8中通過使用TreeMap,提升了處理性能,可以一定程度的防御Hash攻擊。

    3. 擴(kuò)容

    如果當(dāng)表中的75%已經(jīng)被占用,即視為需要擴(kuò)容了

    1(threshold = capacity * load factor ) < size

    它主要有兩個(gè)步驟:

    1. 容量加倍

    左移1位,就是擴(kuò)大了兩倍,用位運(yùn)算取代了乘法運(yùn)算

    12newCap = oldCap << 1;newThr = oldThr << 1;

    2. 遍歷計(jì)算Hash

    123456789101112131415161718192021222324252627282930313233343536373839404142434445for (int j = 0; j < oldCap; ++j) {????????????????Node<K,V> e;????????????????//如果發(fā)現(xiàn)當(dāng)前有Bucket????????????????if ((e = oldTab[j]) != null) {????????????????????oldTab[j] = null;????????????????????//如果這里沒有碰撞????????????????????if (e.next == null)????????????????????????//重新計(jì)算Hash,分配位置????????????????????????newTab[e.hash & (newCap - 1)] = e;????????????????????//這個(gè)見下面的新特性介紹,如果是樹,就填入樹????????????????????else if (e instanceof TreeNode)????????????????????????((TreeNode<K,V>)e).split(this, newTab, j, oldCap);????????????????????//如果是鏈表,就保留順序....目前就看懂這點(diǎn)????????????????????else { // preserve order????????????????????????Node<K,V> loHead = null, loTail = null;????????????????????????Node<K,V> hiHead = null, hiTail = null;????????????????????????Node<K,V> next;????????????????????????do {????????????????????????????next = e.next;????????????????????????????if ((e.hash & oldCap) == 0) {????????????????????????????????if (loTail == null)????????????????????????????????????loHead = e;????????????????????????????????else????????????????????????????????????loTail.next = e;????????????????????????????????loTail = e;????????????????????????????}????????????????????????????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;????????????????????????}????????????????????????if (hiTail != null) {????????????????????????????hiTail.next = null;????????????????????????????newTab[j + oldCap] = hiHead;????????????????????????}????????????????????}????????????????}????????????}

    由此可以看出擴(kuò)容需要遍歷并重新賦值,成本非常高,所以選擇一個(gè)好的初始容量非常重要。

    如何提升性能?

  • 解決擴(kuò)容損失:如果知道大致需要的容量,把初始容量設(shè)置好以解決擴(kuò)容損失;
    比如我現(xiàn)在有1000個(gè)數(shù)據(jù),需要 1000/0.75 = 1333 ,又 1024 < 1333 < 2048,所以最好使用2048作為初始容量。
  • 解決碰撞損失:使用高效的HashCode與loadFactor,這個(gè)…由于JDK8的高性能出現(xiàn),這兒?jiǎn)栴}也不大了。
  • 解決數(shù)據(jù)結(jié)構(gòu)選擇的錯(cuò)誤:在大型的數(shù)據(jù)與搜索中考慮使用別的結(jié)構(gòu)比如TreeMap,這個(gè)就是積累了,一般需要key排序時(shí),建議使用TreeMap;
  • HashMap與HashTable的主要區(qū)別

    在很多的Java基礎(chǔ)書上都已經(jīng)說過了,他們的主要區(qū)別其實(shí)就是Table加了線程同步保護(hù)

    • HashTable線程更加安全,代價(jià)就是因?yàn)樗直┑奶砑恿送芥i,所以會(huì)有性能損失。
    • 其實(shí)有更好的concurrentHashMap可以替代HashTable

    JDK8中HashMap的新特性

    如果某個(gè)桶中的鏈表記錄過大的話(當(dāng)前是TREEIFY_THRESHOLD = 8),就會(huì)把這個(gè)鏈動(dòng)態(tài)變成紅黑二叉樹,使查詢最差復(fù)雜度由O(N)變成了O(logN)。

    12345678910111213//e 為臨時(shí)變量,p為當(dāng)前的鏈for (int binCount = 0; ; ++binCount) {????if ((e = p.next) == null) {????????p.next = newNode(hash, key, value, null);????????if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st????????????treeifyBin(tab, hash);????????break;????}????if (e.hash == hash &&????????((k = e.key) == key || (key != null && key.equals(k))))????????break;????p = e;}

    JDK8在其他地方也有提升,更多的可以看這里。

    HashMap的裝箱空間效率

    在筆試題中,一般“內(nèi)存”是完全能夠使用的,而在現(xiàn)實(shí)中HashMap空間效率之低,你卻不一定知道。

    比如定義了一個(gè)?HashMap<Long,Long>

    1. Long的裝箱

    在對(duì)象頭中,加入額外的指針8Bype,加入8Bype的MarkWord(hashcode與鎖信息),這里就是16Byte

    也就是說,long在裝箱后,效率為 8/24 = 1/3

    2. Map.Entry的裝箱

    字段空間: hash(4) + padding(4) + next(8) = 16Byte,這里的padding是字節(jié)對(duì)齊

    對(duì)象頭: 16Byte,指針+MarkWord

    也就是說,維護(hù)一個(gè)Entry需要32Byte的空間

    1234567static class Node<K,V> implements Map.Entry<K,V>{??? ????final int hash;??? ????final K key;??? ????V value;??? ????Node<K,V> next;}

    3. 總效率

    8/(24 + 32) = 1/7

    計(jì)算結(jié)果可能有差異,本文主要在強(qiáng)調(diào)裝箱過程造成的損失

    在Android中使用SparseArray代替HashMap

    官方推薦使用SparseArray([spɑ:s] [?'re?],稀疏的數(shù)組)或者LongSparseArray代替HashMap,目前國(guó)內(nèi)好像涉及的比較少,容我先粘貼一段

    Note that this container keeps its mappings in an array data structure, using a binary search to find keys. The implementation is not intended to be appropriate for data structures that may contain large numbers of items. It is generally slower than a traditional HashMap, since lookups require a binary search and adds and removes require inserting and deleting entries in the array.

    For containers holding up to hundreds of items, the performance difference is not significant, less than 50%.
    To help with performance, the container includes an optimization when removing keys: instead of compacting its array immediately, it leaves the removed entry marked as deleted. The entry can then be re-used for the same key, or compacted later in a single garbage collection step of all removed entries. This garbage collection will need to be performed at any time the array needs to be grown or the the map size or entry values are retrieved.

    總的來說就是:

    • SparseArray使用基本類型(Primitive)中的int作為Key,不需要Pair<K,V>或者Entry<K,V>這樣的包裝類,節(jié)約了內(nèi)存;
    • SpareAraay維護(hù)的是一個(gè)排序好的數(shù)組,使用二分查找數(shù)據(jù),即O(log(N)),每次插入數(shù)據(jù)都要進(jìn)行排序,同樣耗時(shí)O(N);而HashMap使用hashCode來加入/查找/刪除數(shù)據(jù),即O(N/buckets_size);
    • 總的來說,就是SparseArray針對(duì)Android嵌入式設(shè)備進(jìn)行了優(yōu)化,犧牲了微小的時(shí)間性能,換取了更大的內(nèi)存優(yōu)化;同時(shí)它還有別的優(yōu)化,比如對(duì)刪除操作做了優(yōu)化;
    • 如果你的數(shù)據(jù)非常少(實(shí)際上也是如此),那么使用SpareArray也是不錯(cuò)的;

    在筆試中的使用

    1. 查重與分組問題

    某公司正在做一個(gè)尋找走失兒童的公益項(xiàng)目,現(xiàn)在有一個(gè)函數(shù),可以輸入兩個(gè)圖片,并返回這個(gè)兒童是否重復(fù)。請(qǐng)你設(shè)計(jì)一個(gè)系統(tǒng),幫助他們尋找兒童。

  • 網(wǎng)友可以同時(shí)上傳一批圖片
  • 系統(tǒng)能夠把所有圖片分類并歸為一組
  • 網(wǎng)友上傳圖片后,網(wǎng)頁要盡快返回該照片所在的組。
  • A:假設(shè)你現(xiàn)在有一個(gè)機(jī)器,請(qǐng)寫出你的數(shù)據(jù)結(jié)構(gòu)與處理流程,設(shè)計(jì)的思路。
    B:如果你有多臺(tái)機(jī)器,如果縮短請(qǐng)求的時(shí)間?

    A:我們可以把它分解為兩個(gè)部分,一個(gè)是數(shù)據(jù)結(jié)構(gòu)一個(gè)是上傳流程。

    (1). 對(duì)于數(shù)據(jù)結(jié)構(gòu)來說,一個(gè)是對(duì)兒童信息進(jìn)行包裝,另一個(gè)是實(shí)現(xiàn)兒童信息的高效查找。對(duì)于兒童信息包裝類來說,除了加入兒童的圖片,姓名,生日等基本信息外,特別要注意重寫equals與hashCode,這個(gè)equals就是題目所說的比較函數(shù)。對(duì)于查找的實(shí)現(xiàn)來說,首先我們建立一個(gè)HashSet,用于存儲(chǔ)兒童信息。網(wǎng)友上傳后,服務(wù)器通過對(duì)圖像計(jì)算出特征Hash值,并查Hash表,如果HashCode相同,則返回所在的組;如果不相同,就加入hash表中。(2). 對(duì)于多圖上傳問題,使用生產(chǎn)者-消費(fèi)者阻塞隊(duì)列就可以實(shí)現(xiàn)盡快的依次返回照片所在的組。

    B:

  • 考慮將Hash計(jì)算后取余,分配給其他計(jì)算機(jī)進(jìn)行存儲(chǔ)與查詢,我就想到這一個(gè)…..其它的嘛,大家?guī)臀蚁胂搿?/li>
  • 常規(guī)做法,Select反代,管道回調(diào)
  • TOP10的實(shí)現(xiàn)

    搜索引擎會(huì)通過日志文件把用戶每次檢索使用的所有檢索串都記錄下來,每個(gè)查詢串的長(zhǎng)度為1-255字節(jié)。
    假設(shè)目前有一千萬個(gè)記錄(這些查詢串的重復(fù)度比較高,雖然總數(shù)是1千萬,但如果除去重復(fù)后,不超過3百萬個(gè)。一個(gè)查詢串的重復(fù)度越高,說明查詢它的用戶越多,也就是越熱門。),請(qǐng)你統(tǒng)計(jì)最熱門的10個(gè)查詢串,要求使用的內(nèi)存不能超過1G。

    這個(gè)題目與上個(gè)題目類似,我們選擇使用HashMap,key為查詢值,val為計(jì)數(shù),內(nèi)存使用為 3 * 256 M == 768M < 1G,然后我們不斷的put就可以了,偽代碼

    1234567HashMap<String, Integer> map = new HashMap<String,Integer>();//如果內(nèi)存再多點(diǎn)的話,我們就可以把初始化容量湊個(gè)1024的整數(shù),減少擴(kuò)容損失。while(fileLog.hasNext()){????String queue = fileLog.next();????map.put(queue, map.get(queue) + 1);}

    接著使用堆排序遍歷即可,堆的大小為10,復(fù)雜度為10xO(LogN)。

    綜上,O(10^7) +10 * O(Log(3×10^6));

    使用WeakHashMap作為緩沖

    在動(dòng)態(tài)代理等耗時(shí)操作中,為了實(shí)現(xiàn)復(fù)用,使用了HashMap實(shí)現(xiàn)緩存,下面的一個(gè)例子是Picasso的Transformation操作

    123456789101112131415161718192021222324252627public final class PaletteTransformation implements Transformation {????private static final PaletteTransformation INSTANCE = new PaletteTransformation();????private static final Map<Bitmap, Palette> CACHE = new WeakHashMap<>();????public static PaletteTransformation instance() {????????return INSTANCE;????}????public static Palette getPalette(Bitmap bitmap) {????????return CACHE.get(bitmap);????}????private PaletteTransformation() {????}????@Override????public Bitmap transform(Bitmap source) {????????Palette palette = Palette.generate(source);????????CACHE.put(source, palette);????????return source;????}????@Override????public String key() {????????return "PaletteTransformation";????}}

    附錄一:集合中元素的排序方式:

    剛剛對(duì)比SparseArray與HashMap,我怕各位繞進(jìn)去了,講下Java中集合的排序方式

    • 按照添加/刪除的順序,比如FIFO,FILO,常見的比如Queue,Stack就是按照這個(gè)順序?qū)崿F(xiàn)的;
    • 按照Hash表進(jìn)行排序,比如HashMap的table中的元素是通過hash運(yùn)算隨機(jī)均勻排列的(至少理論上是),可以通過計(jì)算Key的Hash值快速查找到Value
    • 按照自定義的排序,比如按照數(shù)字大小,拼音首字母排序,常見的有線性順序表,二叉查找樹,以及高度封裝好的TreeMap,它們需要實(shí)現(xiàn)Comaparable接口以進(jìn)行進(jìn)一步的自定義CompareTo操作。

    附錄二:hashCode, == ,equals, Comparable的區(qū)別?

    • == : 這個(gè)就是內(nèi)存地址的比較,從C語言開始我們就知道這個(gè)了。
    • hashCode/equals:對(duì)于Object來說,hashCode就是地址的取樣運(yùn)算,而equals就是判斷地址是否相同;在實(shí)際使用中,特別是在集合(Set)中,特別是HashSet,為了防止插入的是兩個(gè)相同的值,我們更注重內(nèi)容上的對(duì)比而不是地址的對(duì)比,需要通過計(jì)算hashCode來判斷是否equals,所以兩個(gè)方法要同時(shí)重寫,比如String。
    • Comparable: 在集合需要排序(SortSet)的情況下,就需要給對(duì)象實(shí)現(xiàn)Comparable接口,比如TreeMap。這個(gè)在Android開發(fā)中實(shí)際需要手動(dòng)寫的情況并不多,畢竟包多,在ORM框架中一般都幫你寫好了。

    HashMap在Android項(xiàng)目中的使用

  • Bundle與Parcelable,實(shí)際上就是Android中對(duì)HashMap與Serializable的接口實(shí)現(xiàn)(當(dāng)然底層已經(jīng)是IPC了);
  • 反射時(shí)使用WeakReference做緩存,比如Retrofit等動(dòng)態(tài)代理框架;
  • 廣播的底層結(jié)構(gòu)是HashMap<IBinder, ReceiverList>;
  • 后記

    看這種JDK源碼又累又欣賞,特別是if((n.next=p) != null)這樣的代碼頻頻出現(xiàn)卻忘了運(yùn)行順序,說明了自己的基礎(chǔ)不足(然并卵,現(xiàn)在已經(jīng)能寫出這種WTF的代碼了)。這是我第一次寫分析而不是寫過程,希望有問題能夠提出,無論是文章排版還是技術(shù)上的問題都可以提出來。

    最后打個(gè)廣告
    我目前正在準(zhǔn)備技術(shù)面試,所以關(guān)于Java所有的文章有個(gè)總結(jié),不妨關(guān)注一下。

    Reference

  • http://bigocheatsheet.com/
  • https://github.com/android-cn/android-discuss
  • http://www.ibm.com/developerworks/cn/java/j-jtp08223/
  • http://www.jianshu.com/p/9a48bcbdfece
  • http://www.jianshu.com/p/4e13fcdf9ab5
  • http://blog.csdn.net/zimo2013/article/details/39692245
  • http://coderbee.net/index.php/java/20131018/519
  • http://www.guokr.com/blog/747926/
  • http://blog.csdn.net/v_JULY_v/article/details/6256463

  • 總結(jié)

    以上是生活随笔為你收集整理的HashMap的实现与优化的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。