hashmap containsvalue时间复杂度_面试宝典:数据结构HashMap
常用數據結構在新增、查找等基礎操作上的性能
1、數組
采用一段連續的存儲單元來存儲數據
對于指定下標的查找,時間復雜度為O(1)
通過給定值進行查找,需要遍歷數組,逐一比對給定關鍵字和數組元素,時間復雜度為O(n)
對于有序數組,則可采用二分查找,插值查找,斐波那契查找等方式,可將查找復雜度提高為O(logn)
對于一般的插入刪除操作,涉及到數組元素的移動,其平均復雜度也為O(n)
2、線性鏈表
對于鏈表的新增,刪除等操作(在找到指定操作位置后),僅需處理結點間的引用即可,時間復雜度為O(1)
查找操作需要遍歷鏈表逐一進行比對,復雜度為O(n)
3、二叉樹
對一棵相對平衡的有序二叉樹,對其進行插入,查找,刪除等操作,平均復雜度均為O(logn)
4、哈希表
- 在哈希表中進行添加,刪除,查找等操作,性能十分之高 不考慮哈希沖突的情況下 僅需一次定位即可完成,時間復雜度為O(1)
數據結構的物理存儲結構
1、順序存儲結構2、鏈式存儲結構
順序存儲結構和鏈式存儲結構
JDK 1.8 hashmap put邏輯圖
注意:JDK8之后
如果哈希表單向鏈表中元素超過8個,那么單向鏈表這種數據結構會變成紅黑樹數據結構當
紅黑樹上的節點數量小于6個,會重新把紅黑樹變成單向鏈表數據結構
hash沖突
概念
對某個元素進行哈希運算,得到一個存儲地址,然后要進行插入的時候,發現已經被其他元素占用了,其實這就是所謂的哈希沖突,也叫哈希碰撞
沖突必然性
數組是一塊連續的固定長度的內存空間,再好的哈希函數也不能保證得到的存儲地址絕對不發生沖突
解決沖突方式
哈希沖突的解決方案有多種:
1、開放定址法(發生沖突,繼續尋找下一塊未被占用的存儲地址)
2、再散列函數法
3、鏈地址法 (HashMap即是采用了鏈地址法,也就是數組+鏈表的方式)
深入源碼分析實現原理
Entry是HashMap中的一個靜態內部類
static?class?Entry?implements?Map.Entry?{????????final?K?key;
????????V?value;
????????Entry?next;//存儲指向下一個Entry的引用,單鏈表結構
????????int?hash;//對key的hashcode值進行hash運算后得到的值,存儲在Entry,避免重復計算
????????/**
?????????*?Creates?new?entry.
?????????*/
????????Entry(int?h,?K?k,?V?v,?Entry?n)?{
????????????value?=?v;
????????????next?=?n;
????????????key?=?k;hash?=?h;
????????}?
hashmap總體結構
HashMap中的鏈表出現越少,性能才會越好
重要字段
/**實際存儲的key-value鍵值對的個數*/transient?int?size;
/**閾值,當table ==?{}時,該值為初始容量(初始容量默認為16);當table被填充了,也就是為table分配內存空間后,
threshold一般為 capacity*loadFactory。HashMap在進行擴容時需要參考threshold,后面會詳細談到*/
int?threshold;
/**負載因子,代表了table的填充度有多少,默認是0.75
加載因子存在的原因,還是因為減緩哈希沖突,如果初始桶為16,等到滿16個元素才擴容,某些桶里可能就有不止一個元素了。
所以加載因子默認為0.75,也就是說大小為16的HashMap,到了第13個元素,就會擴容成32。
*/
final?float?loadFactor;
/**HashMap被改變的次數,由于HashMap非線程安全,在對HashMap進行迭代時,
如果期間其他線程的參與導致HashMap的結構發生變化了(比如put,remove等操作),
需要拋出異常ConcurrentModificationException*/
transient?int?modCount;
initialCapacity默認為16,loadFactory默認為0.75
構造器
沒有為數組table分配內存空間(有一個入參為指定Map的構造器例外)
而是在執行put操作的時候才真正構建table數組
put函數
public?V?put(K?key,?V?value)?{????????//如果table數組為空數組{},進行數組填充(為table分配實際內存空間),入參為threshold,
????????//此時threshold為initialCapacity?默認是1<<4(24=16)
????????if?(table?==?EMPTY_TABLE)?{
????????????inflateTable(threshold);
????????}
???????//如果key為null,存儲位置為table[0]或table[0]的沖突鏈上
????????if?(key?==?null)
????????????return?putForNullKey(value);
????????int?hash?=?hash(key);//對key的hashcode進一步計算,確保散列均勻
????????int?i?=?indexFor(hash,?table.length);//獲取在table中的實際位置
????????for?(Entry?e?=?table[i];?e?!=?null;?e?=?e.next)?{
????????//如果該對應數據已存在,執行覆蓋操作。用新value替換舊value,并返回舊value
????????????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++;//保證并發訪問時,若HashMap內部結構發生變化,快速響應失敗
????????addEntry(hash,?key,?value,?i);//新增一個entryreturn?null;
????}
inflateTable函數
用于為主干數組table在內存中分配存儲空間
通過roundUpToPowerOf2(toSize)可以確保capacity為大于或等于toSize的最接近toSize的二次冪,比如toSize=13,則capacity=16;to_size=16,capacity=16;to_size=17,capacity=32.
????????int?capacity?=?roundUpToPowerOf2(toSize);//capacity一定是2的次冪
????????/**此處為threshold賦值,取capacity*loadFactor和MAXIMUM_CAPACITY+1的最小值,
????????capaticy一定不會超過MAXIMUM_CAPACITY,除非loadFactor大于1?*/
????????threshold?=?(int)?Math.min(capacity?*?loadFactor,?MAXIMUM_CAPACITY?+?1);
????????table?=?new?Entry[capacity];
????????initHashSeedAsNeeded(capacity);
????}
roundUpToPowerOf2函數
使得數組長度一定為2的次冪,Integer.highestOneBit是用來獲取最左邊的bit(其他bit位為0)所代表的數值.
private?static?int?roundUpToPowerOf2(int?number)?{????????//?assert?number?>=?0?:?"number?must?be?non-negative";
????????return?number?>=?MAXIMUM_CAPACITY
??????????????????MAXIMUM_CAPACITY
????????????????:?(number?>?1)???Integer.highestOneBit((number?-?1)?<????}
hash函數
用了很多的異或,移位等運算
對key的hashcode進一步進行計算以及二進制位的調整等來保證最終獲取的存儲位置盡量分布均勻
????????int?h?=?hashSeed;
????????if?(0?!=?h?&&?k?instanceof?String)?{
????????????return?sun.misc.Hashing.stringHash32((String)?k);
????????}
????????h?^=?k.hashCode();
????????h?^=?(h?>>>?20)?^?(h?>>>?12);
????????return?h?^?(h?>>>?7)?^?(h?>>>?4);
????}
indexFor
以上hash函數計算出的值,通過indexFor進一步處理來獲取實際的存儲位置
/***?返回數組下標
*/
static?int?indexFor(int?h,?int?length)?{
????return?h?&?(length-1);
}??
1、h&(length-1)保證獲取的index一定在數組范圍內
舉個例子,默認容量16,length-1=15,h=18,轉換成二進制計算為index=2
2、位運算對計算機來說,性能更高一些(HashMap中有大量位運算)
終存儲位置的確定流程
addEntry函數
void?addEntry(int?hash,?K?key,?V?value,?int?bucketIndex)?{????????if?((size?>=?threshold)?&&?(null?!=?table[bucketIndex]))?{
????????????resize(2?*?table.length);//當size超過臨界閾值threshold,并且即將發生哈希沖突時進行擴容
????????????hash?=?(null?!=?key)???hash(key)?:?0;
????????????bucketIndex?=?indexFor(hash,?table.length);
????????}
????????createEntry(hash,?key,?value,?bucketIndex);
????}
當發生哈希沖突并且size大于閾值的時候,需要進行數組擴容
擴容時,需要新建一個長度為之前數組2倍的新的數組
然后將當前的Entry數組中的元素全部傳輸過去,擴容后的新數組長度為之前的2倍
擴容相對來說是個耗資源的操作
為何HashMap的數組長度一定是2的次冪
resize擴容函數
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,?initHashSeedAsNeeded(newCapacity));
????????table?=?newTable;
????????threshold?=?(int)Math.min(newCapacity?*?loadFactor,?MAXIMUM_CAPACITY?+?1);
????}
如果數組進行擴容,數組長度發生變化,而存儲位置 index = h&(length-1),index也可能會發生變化,需要重新計算index
transfer函數
void?transfer(Entry[]?newTable,?boolean?rehash)?{????????int?newCapacity?=?newTable.length;
?????//for循環中的代碼,逐個遍歷鏈表,重新計算索引位置,將老數組數據復制到新數組中去(數組不存儲實際數據,所以僅僅是拷貝引用而已)
????????for?(Entry?e?:?table)?{while(null?!=?e)?{
????????????????Entry?next?=?e.next;if?(rehash)?{
????????????????????e.hash?=?null?==?e.key???0?:?hash(e.key);
????????????????}
????????????????int?i?=?indexFor(e.hash,?newCapacity);
????????????????//將當前entry的next鏈指向新的索引位置,newTable[i]有可能為空,有可能也是個entry鏈,如果是entry鏈,直接在鏈表頭部插入。
????????????????e.next?=?newTable[i];
????????????????newTable[i]?=?e;
????????????????e?=?next;
????????????}
????????}
????}
將老數組中的數據逐個鏈表地遍歷 扔到新的擴容后的數組中
數組索引位置的計算是通過 對key值的hashcode進行hash擾亂運算后
通過和 length-1進行位運算得到最終數組索引位置
1、
HashMap的數組長度一定保持2的次冪
比如16的二進制表示為 10000,那么length-1就是15,二進制為01111
同理擴容后的數組長度為32,二進制表示為100000,length-1為31,二進制表示為011111
會保證低位全為1,而擴容后只有一位差異,也就是多出了最右位的1
在通過 h&(length-1)的時候,只要h對應的最左邊的那一個差異位為0,就能保證得到的新的數組索引和老數組索引一致(大大減少了之前已經散列良好的老數組的數據位置重新調換)
2、
數組長度保持2的次冪,length-1的低位都為1,會使得獲得的數組索引index更加均勻
3、
&運算,高位是不會對結果產生影響的(hash函數采用各種位運算可能也是為了使得低位更加散列)
只關注低位bit,如果低位全部為1,那么對于h低位部分來說,任何一位的變化都會對結果產生影響,也就是說,要得到index=21這個存儲位置,h的低位只有這一種組合
4、
如果不是2的次冪,也就是低位不是全為1此時,要使得index=21,h的低位部分不再具有唯一性了,哈希沖突的幾率會變的更大
index對應的這個bit位無論如何不會等于1了,而對應的那些數組位置也就被白白浪費了
get函數
public?V?get(Object?key)?{?????//如果key為null,則直接去table[0]處去檢索即可。
????????if?(key?==?null)
????????????return?getForNullKey();
????????Entry?entry?=?getEntry(key);return?null?==?entry???null?:?entry.getValue();
?}
get方法通過key值返回對應value,如果key為null,直接去table[0]處檢索
getEntry函數
final?Entry?getEntry(Object?key)?{if?(size?==?0)?{return?null;????????}
????????//通過key的hashcode值計算hash值
????????int?hash?=?(key?==?null)???0?:?hash(key);
????????//indexFor?(hash&length-1)?獲取最終數組索引,然后遍歷鏈表,通過equals方法比對找出對應記錄for?(Entry?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;
????}????
key(hashcode)–>hash–>indexFor–>最終索引位置,找到對應位置table[i],再查看是否有鏈表,遍歷鏈表,通過key的equals方法比對查找對應的記錄
e.hash == hash 這里是否有必要判斷?從而引出下個話題
重寫equals方法需同時重寫hashCode方法
如果沒有重寫hashCode方法
put:key(hashcode1)–>hash–>indexFor–>最終索引位置
get:通過key取出value的時候 key(hashcode1)–>hash–>indexFor–>最終索引位置
由于hashcode1不等于hashcode2,導致沒有定位到一個數組位置而返回邏輯上錯誤的值null
小結:
在重寫equals的方法的時候,必須注意重寫hashCode方法
同時還要保證通過equals判斷相等的兩個對象,調用hashCode方法要返回同樣的整數值
而如果equals判斷不相等的兩個對象,其hashCode可以相同(只不過會發生哈希沖突,應盡量避免)
HashMap的數據結構
特點簡介
- 無序
因為不一定掛到哪一個單向鏈表上的,因此加入順序和取出也不一樣
- 不可重復
1、使用equals方法來保證HashMap集合key不可重復,如key重復來,value就會覆蓋
2、存放在HashMap集合key部分的元素,其實就是存放在HashSet集合中,則HashSet集合也需要重寫equals和hashCode方法
參考文章
https://blog.csdn.net/woshimaxiao1/article/details/83661464總結
以上是生活随笔為你收集整理的hashmap containsvalue时间复杂度_面试宝典:数据结构HashMap的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: css点击a标签显示下划线_好程序员HT
- 下一篇: python取中间值的函数_tensor