哈希表(三级)
關于查找我已經講了三種了,一種是線性表的查找,一種是樹的,查找樹,下面我們講另外一種結構,它是哈希表前面的我們查找不管你是順序查找,還是折半查找,還是樹里面查找,都要做一個操作,都需要進行比較,拿著我要找的關鍵字,跟表里面或者樹里面進行比較,看是不是相同,那既然要比較的話,就涉及到比較的效率,理想的方法是,不需要進行比較,就可以直接找到我們要找的內容,有人說這怎么可能呢,有沒有這樣的可能,是有的,對于我們的數組來說,如果我們要按照索引來說的話,索引查詢,我要查詢第5個,我要查詢第10個,是不是可以直接定位,直接定位,只要計算一次公式,他有數組的首地址,有每個元素的長度,只要套一個公式,既可以直接定位到索引是5索引是10的元素,找第5個,找第10個,找第1個,找第1萬個,花的時間都是一樣的,并且一次計算就可以得到,效率會特別的快,但是我們如果按照內容來找的話,我不是找5個元素,也不是找第10個元素,我是找值是10的元素,那他就不是索引的值了,是內容,那我們就會逐個的比對,順序查找效率是比較低的,折半查找要限制條件,要求是有序的,我們按照內容查找的時候也能和按照索引查找這么高的效率,不需要比較,直接定位,這就是我們哈希表要做的一件事情,所以我們就知道哈希表他有多重要了,有多神奇了,那怎么來理解哈希表呢,哈希表底層它是采用一個什么樣的結構,結構和他的神奇是有一定的關系,光有這個結構還不行,還有他具體的算法,我們看一下哈希表是怎么添加數據的,和哈希表是怎么查詢數據的,通過這兩個具體的操作,知道明白哈希表為什么神奇,它是怎么快的,最后我們還有一些細節的內容,講里面的一些細節,比如說怎么減少沖突啊,怎么構建哈希碼啊,在JAVA里面有兩個重要的方法,使用哈希表的話有兩個重要的方法,這兩個方法到底有什么樣的作用,那我們就開始了
1. 先來看哈希表的結構和特點:哈希表英文單詞是這個單詞hashtable,也按照英譯的話也是哈希表,按照意義譯的話也是散列表他有個特點,快,非常快,神奇的快,他底層是什么結構啊,底層有多種結構,多種實現方式,下面我給出的是最容易理解的,最常用的一種方式,就是順序表加鏈表,他的主結構大家看,主結構它是一張順序表,我們你這里從0到12一共有13個數,那這個13個不存放具體的內容,每個后面會引一個鏈表,會引鏈表的,主結構是一個順序表,每個順序表的節點,它是用來引鏈表的,如果我有一個值是11,我有一個值是11,那經過我的運算之后,11的位置不往數組里面放,后面拉一個鏈表,數組里面的指針指向他就行了,這是我們哈希表的主結構,哈希表最容易理解的主結構是一個數組,每個元素可以拉鏈表,這是一個,那下面我們來看一下哈下表是怎么來添加數據的,那我們這里有一個哈希表初始操作狀態,我們來理解一下,這上面是什么,上面就是我們要添加的數據,23,36,一直到47,我們要加這些數,而下面我們這一塊就是我們的哈希表,我們是不是創建了一個哈希表他的主結構它是多少啊,是不是11,從0到10,一共有11個元素,注意畫的這個是什么啊,只不過這個是縱向畫的,我們這邊是橫向來畫的,注意就是這條線往下,就是哈希表的主結構了,目前都是空的,剛開始的情況下你可以認為有沒有值啊,有,都是null,這個地方都是null,沒有什么值的,我們就不再一個一個標記了,那下邊我們就要看,我們就要把這些數放在哈希表里面,怎么快的,首先你想一下數組里面要想加一個數,你如果不是加到最后,加到中間的話,就要移動,我們這會不需要移動,你看是怎么來做的,我們要加這個23,這一列是是什么啊,這是我們要加入的內容,加入的數據,只不過這一列比較特殊,正好是整數,不管是什么數據,到最后你都得生成一個哈希碼,生成一個哈希碼它是一個整數類型,而對于整數來說,哈希碼就取他自己就可以了,所以這個X就是hashcode就是哈希碼,不是那個函數哈希碼,他就代表一個哈希碼,對于整數來說,哈希碼取他自己就可以了,整數的哈希碼取自身即可,因為它本身也是一個整數,所以下面一大堆就在這里解決了,每個哈希碼我們又寫了一遍他自己,然后y=k(x),它是一個哈希函數,這是需要一個哈希函數的,比如我們往哈希表里放數據,他不需要比較,但是他需要計算,X是誰啊,X是那個哈希碼,y是什么意思啊,y就是要存放的地址,只要計算這一次,馬上就能找到地址,你把這個數放到這個地址,就可以了,不需要大量的比較,也不需要移動,那我們制定的這個函數是什么意思,讓這個哈希碼除以11,取余數,為什么我們這里除以11,因為我們哈希表的主結構,長度是11,如果你對11取余數的話,不正好是0到10嗎,那正好落在我們指定的下標位置,那實際上我們這么來做,所以大家初步已經知道了,哈希碼不需要比較,不需要移動,只需要計算,計算得到地址哪有這么快,我們按照索引在數據里面找元素也要計算,也要定位,再往下我們挨個來看,我們現在解決第一個問題,看這兒了,第一步
2. 哈希表是如何添加數據的?第一步是計算哈希碼,整數的哈希碼是他自己第二步是套到哈希函數里面去,得到地址第三步然后存到哈希表:一次添加成功,一次就成功了,什么是一次就成功了,我現在加23唄,23除以11取余數,后面的余數已經給大家寫好了,告訴我23該怎么存,23余數不是1嗎,要不要把23擦掉把23存到這里,要不要,不要,這個地方不寫值,這個地方寫什么呢,我們要這么來寫,這個引入一個鏈表,引入一個節點,當然這個節點里面是分兩部分的,這里面就放了一個23,放地址后面還有沒有元素,后面是null了,后面沒有節點了,如果這個節點是0X1012的話,實際上我們是要把這個地址放到這兒的,0X1012,這個大家應該是沒有任何問題的,鏈表直接加到這里就可以了,23就加到這里,得到哈希碼計算哈希函數,得到結果是1唄,寫到這里就成了,添加這么做,36依次類似嗎,余數是3,那你就在這又創建一個節點,再創建一個節點,那這兒的值是多少,這兒的值是36,后面的值是null,同樣,我們不再寫具體的地址了,是這么來指的,48與此類似,我們在這又創建一個,再創建一個啊,到這兒來,我們把這個復制一份吧,一會直接用它就可以了,我們在這放一個48,這邊有一個null,同樣指向他,這么來指向,就可以了,77和他是一樣的,余數是0,放到這兒來,這邊放的是誰,這邊放的是77,這邊要寫上一個null,把這個null要擦掉,不是null了,指向他,還有嗎,86,86的余數是9,選擇一下,把它拖過來,9在這兒,那這邊寫的是86,我想大家已經感覺到他添加的一個快速性了,添加基本都是常數級別的,直接一步兩步三步,就到位了,特別的快,這是一個操作,再往下還有別的嗎,76也類似,我們先跳過去,76在這兒呢,這個67我們先把他放一下,這三個先排除,我們先把其他的一次性的寫完,76在哪兒呢,76在這兒呢,76寫到這,這邊寫一個null,然后再畫一個指針,指向他就可以了,其他的都是有沖突的呢,我們來看67吧,67該怎么辦,我們講的一個操作,一次添加成功特別快,但是有時候你會發現需要多次添加成功,什么叫多次添加成功,67除以11余數是1,1的話怎么辦,直接寫到這兒,結果往這里寫的時候發現這里已經不是空了,不是空意味著下面已經有值了,有值了怎么辦,那我們就在上面,就在這一塊,把這個拿過來,拖到這兒,一會可能還會再拖,我們就先來寫一下,先拖到這兒,這邊改寫誰了,這邊叫67,這邊還是null,那這個就不是null,這么來指向他,這個效率就有點降低了,第一次添加不成功,有沖突的,關鍵之不同但是得到的地址是一樣的,當我們再來放一個23,23是另外一種情況,23在這里畫一下,現在來看56,56怎么了,56的余數還是1,那就應該往這里放,有值了,往這兒走,這兒也不是空,往下走,他這兒是空,意味著它是這里的最后一個節點,然后我們在這里來寫,寫一個56,然后把這個內容去掉,去掉之后再拉一個鏈條,56這個呢現在是末節點,它是null的,還有一個78,78怎么辦,78余數還是1,那我們就得順著這個位置一直往下找,往下列表找不行,我們這個78是23嗎,不是,是67嗎,不是,是56嗎,不是,那我們一直往下加,最后把78加到這兒,我們再連接起來最后一個狀態,到這兒來,把這個去了,那我們對添加操作就寫到這兒,發現沖突是絕對避免不了的,難免會有沖突,如果沖突的概率比較低的話,最終他整體的速度還是可以的那我們樹的加載操作還差最后一步,23我們該怎么加,這我們已經講了幾種情況了,一次添加成功的,多次添加成功,多次添加成功就出現了沖突,就會拿出我們要加的值,現有的值調用equals方法,進行比較,到最后也會相等,那就創建個新節點,存儲數據,加到鏈表的后面就行了,可是我們現在加的比較特殊,這是個23,23怎么辦,之前已經加過23了,這個時候怎么辦,23除以11的余數是1,往這兒一放,要拿這個23和這個23進行比較,比較啊,從JAVA的角度來說是一個Integer對象,他最終比較是要調用equals來進行比較的,結果那這個23和這個23進行一比,相同,或者你往里邊加的是67,這里邊是不是也有了,那怎么辦,已經有重復的數據,那就不加了,所以最終導致我們的哈希表里有沒有重復的數據,沒,是沒有重復數據的,所以不添加,出現沖突,調用equals方法比較,有重復的就不添加了,通過這個添加的過程,我們應該得到一個結論,整體的速度還是比較快的,計算就得到了位置,大家想一下數組里面是怎么添加的,他加在某個位置要大量的移動,他的效率也要比較高,添加的時候要得出一個結論,添加的時候要快的,第二,這里面有沒有重復的數據,沒,有重復的就不加了,并請問這里面的數據有沒有順序,無序的,這真的是沒有順序的,23,67,56,有什么順序,沒有順序,所以我們要得到這三個結論,哈希表添加數據特別的快,如果不考慮沖突的話,3步就可以了,常數級別的,數據元素是唯一的,不會重復,然后結論他還是無序的,哈希表的原理就講到這
下面我們來看,添加快了,我們來看一下哈希表是如何來查詢數據的?查詢數據又分為3種情況:1. 查詢數據和添加數據過程是相同的,基本上相同的,但是又有不同之處,怎么不同啊,添加可能一次添加成功,也有可能多次添加成功,我們的查詢可能一次找到,有可能多次找到,但是我們添加的時候,如果有重復的,就不添加了,你查詢的時候可能查詢哈希表里面就沒有的值,我在這里找100,就沒有100,根本就沒有,我們看這個查詢是怎么來實現的,我們舉個例子來說,我要找23,我要在這里面找23,怎么辦,和剛才添加的過程是一樣的,首先我要找到整數23的哈希碼,整數的哈希碼就是他自己,然后套到這個公式里面,余數是1,索引是1的位置找,一下子就找到了23,你要這個48,計算哈希碼就是他自己,套哈希函數得到結果4,來4這個位置直接找48,一次就找到了,也非常快,那我們再來一個,再來一個多次找到的,找一個67,67是怎么找到的,67得到他的哈希碼還是67,套入哈希函數,余數是1,那就來這個位置找67了,一看這個值是67嗎,不是,再往下找,67,你看,找到了,多次找到的,78和這個一樣,只不過他要多比對,多比對幾次,或者你把這個沖突盡量避免的話,他的效率還是比較高的,再找一個100,100可怎么辦,這里面有沒有100,沒有,我現在要在這里面找一個100了,100除以11余數是幾,是1吧,99加1,100怎么辦,1來這個位置找,不是100,不是100,不是100,不是100,再往后沒有了,100如果存在就肯定在這個鏈表里了,而這個鏈表從頭找到最后,是不是也沒有找到100,那說明什么,那說明100是不存在的,我現在畫的這個表,根現在這個表,你看結構是一樣的,只不過一個是橫著畫一個豎著畫,通過我們剛才查找的這個過程,大家應該知道,得出什么結論,第一個哈希表的查詢順序是比較快的,跟添加速度是一樣的,查詢到之后就可以刪除了,比如我想把56刪除了怎么辦,我想把這個56刪了,那你就直接改指針就行了,改一下指針指向他就行了,更新就要看更新什么了,我找到67想把它改成77,那不能這么改的,為什么啊,當你把它改成77的話,他是不是排在這個位置了,那不能隨便改的,一改的話下次再改就找不著了,但是我們現在存著整數來說,可能這個哈希碼我們要看,我們要根據情況來看,如果一改這個值影響存儲位置,那你就要考慮其他方法了,比如先刪除再添加,也相當于是更新了,其他的算法了,會有這種解決方案,更新就要好好考慮了,更新就影響哈希碼,影響哈希碼就要采用其他方法來解決了,但是你即使先刪除再添加,那速度也不慢,因為它引的是鏈表,便于刪除也便于添加,這個就講到這里了
講到這里哈希表的主題內容就已經講了,如果問到哈希表就從這三步來說,首先要說哈希表的特點是什么,特點是快,為什么快,跟他的結構有關,然后再講一下它是怎么添加的,把這個說明白,說明白之后得到這個結論,是非常快的,然后再講一下它是怎么查詢的,和添加的過程是基本相同的,然后逐個來說明就可以了,講到這哈希表就可以了,我們再講一些細節,請問hashcode這個方法是做什么的,JAVA里面有這個方法,是計算哈希碼的,它是計算哈希碼的,我們打開JAVA,hashcode是每個類都要有的一個方法,它是從Object繼承過來的,找一個方法,hashcode方法有沒有,整數的哈希碼就直接返回哈希碼這個數,public int hashCode()return Integer.hashCode(value);public static int hashCode(int value),return value;整數的哈希碼取他自身就可以了不同數的哈希碼肯定是不一樣的,哈希碼是通過一個計算得到一個整數,如果我們這里面存的是字符串,存的是學生,你必須把字符串和學生變成一個整數值,算法放到hashcode里邊就可以了,我們得到一個整數,這是他的一個內容,再往下看還有什么,這是我們hashcode的一個作用,equals是干什么的,就是看這個圖,hashcode是在這里使用的,equals是怎么用的,當你往這里放的時候,里邊出現了沖突,可能是添加,可能是查詢,一般出現沖突之后需要比對了,你找的是67,那要和每個逐個比較看是不是這個內容,那通過equals來比較,equals是什么,equals是出現了沖突之后,通過equals來比較,判斷內容是否相同,這是一個內容
下面再講一個內容,各種類型的哈希碼應該如何獲取?1. 簡單一句話就是調用hashcode方法,關鍵是我們使用者是調用hashcode方法,就得到他的哈希碼了,這個hashcode的開發者呢,在里面寫什么代碼,得到一個整數,那我們說了,如果是整數,取自身2. double該怎么辦,如果里面存的是double的話,那有人說那不簡單,要整數取整唄,3.14,3.15,3.145,我們都取整,取整不是找沖突嗎,為什么,3.14,3.15,3.145一取整,是不是都是3,不同的數哈希碼一樣,那最終都得存到一個位置,那明顯是沖突的,所以如果你這么來設計hashcode的算法,那以后這種算法是很失敗的,會導致沖突,double肯定不能這么來寫,那怎么來寫呢,這我們不用操心,我們知道就可以了,好多數學家就是做這個的,學數學的,學二進制的,他就是用來解決這些問題的,我們來看Double底層的哈希碼是怎么來做的,return Double.hashCode(value);這什么意思,再來看,public static int hashCode(double value),long bits = doubleToLongBits(value);這什么意思,value就是3.14,先要把double變成一個long類,然后要得到一個long數,return (int)(bits ^ (bits >>> 32));然后這個數要做什么,是不是麻煩,先要向右移動32位,然后要與原來的bits做異或運算,這個里面就不看了,比較復雜,他做這么復雜的一個目的是什么,目的就是很簡單,我這邊不同的double數,經過你這個運算之后,得到一個整數,這個整數盡量要是不一樣的,他就是為了這樣的一個目的,為什么整數這么簡單,其自身不同就是不同被3. 如果是一個String的話,字符串的哈希碼該怎么辦,java,oracle,這跟整數也沒關系,這個時候該怎么辦,有人說我是有辦法的,比如說JAVA怎么辦,JAVA是不是由4個字母組成的,那我就把4個字母的編碼值都有它的unicode編碼嗎,它的編碼不就是整數嗎,相加不就可以嗎,也能得到一個整數,那也是一個很糟的方案,比如說,abc中國農業銀行,cba中國籃球聯賽,bac是什么,我知道bat是什么意思,請問如果按照我們剛才的算法的話,a的編碼是97,b的編碼是98,c的編碼是99,我們這三個一求,把他們的編碼相加,結果都是一樣的,結果你這三個不同的字符串,他們的哈希碼又是一樣的,那一存又存在一個位置了,又出現沖突了,這種方案又是不好的,那不好我們怎么辦,比如我們這么來做,你不是順序不同嗎,abc 1*97+2*98+3*99,abc這么來的,cba 1*99+2*98+3*97,這么一來數就不一樣了,應該就不一樣了,這是一種思路,但這是不是最好的思路,我覺得最好的思路基本上就在這兒,我就看JAVA底層是怎么來實現的,人家肯定是采用非常好的算法,JAVA的hashcode是怎么來做的,你看什么意思,JAVA的hashcode,int h = hash;這是一個h的值,if (h == 0 && value.length > 0),value字符串底層是一個字符數組,取他的長度,把每個字符都取出來,char val[] = value; for (int i = 0; i < value.length; i++),h = 31 * h + val[i];把每個字符取出來,然后乘以31,再加上value[i],就是h是誰,h是他的哈希值,總之他又是一套的算法,他的算法是這么來實現的,保證不同的字符串生成的哈希碼肯定是整數,并且值是不一樣的,這大家明確了,下邊怎么辦,我要在這里來了一個類,整數Double都是基本類型,我還來個學生
最后我們來看如何減少沖突?沖突不能夠百分百的避免,肯定會有沖突,沒有誰說設計哈希表的時候不沖突的,我們只能減少這個沖突,所以我們來看,如何減少這個沖突,第一個大家想一下,現在這個長度是不是11,現在這個讀取的長度是不是11,我要往里面存20個數,有沒有沖突,肯定沒有沖突的,你存的數比哈希表的長度還要長呢,那肯定是有沖突的,那如何避免,哈希表的長度和表中記錄數的長度,概念叫填充因子,表的記錄數,如果我要往里面放20個數據,但是我哈希表的長度是11,這個一除大于1的,那肯定有沖突,肯定是有沖突的,那這個比例至少是小于1的,等于1也是有沖突的,光小于1也不行,根據實際文獻證明,填充因子在0.5左右的時候,性能是比較好的,也就是你的長度是11的話,怎么辦呢,你這里面的數最好不要超過5,6個,那這時候就會降低沖突,當然反過來說你降低沖突會浪費空間,那就會浪費空間,所以大家大概知道這樣一個理論,裝填因子=表中的記錄數/哈希表的長度,經驗值是0.5,稍微高一些也可以,這是一個內容了,沖突減少了,哈希表的總長度和表中要放的記錄數是有關系的,這是一個了,哈希函數的選擇,因為選一個比較好的哈希函數,這個從理論上面有很多的方法,直接定址法,折疊法,除留余數法,大家可以去查詢相關的資料,怎么去取一個好的哈希函數,那我們這里使用的哈希函數是y=x%11,這個就是除留取余法,這個大家明確一下,我們的哈希函數是使用這種來做的還有一個我們怎么來處理沖突,減少沖突,如果你能夠把這個沖突處理好,可能能夠進一步的減少這個沖突,該如何來處理這個沖突啊,好多方法,鏈地址法,開放地址法,再散列法,建立一個公共的溢出區,把公共的數據放在溢出區里面,那我們剛才使用的是哪一種方法,沖突了就往鏈表后面加節點,我們用的就是鏈地址法,我們剛才處理沖突的方法叫鏈地址法,講到這里我們就把哈希表相關的理論就講了總結一下:如果問到哈希表了,哈希表最大的特點就是快,怎么快啦,添加快,查詢快,怎么達到這一點的呢,跟他的結構有關,說一下他最流行,最簡單的結構,然后來講一下它是如何來添加的,再講一下查詢的,講查詢和添加的時候要貫穿一條主線,就是快,幾步就可以了,要貫穿這條主線,講到這基本上就可以結尾了,如果下面再繼續交流的話,我們就可以把后面的這些內容說一下,我建議大家再加上一句話,那句話啊,我們JAVA里面有一個類,叫HashSet,是不是還有一個叫HashMap,還有一個過時了,叫Hashtable,他們底層用的是什么啊,都是哈希表,只要遇到Hash這四個單詞,這4個字母哈希的意思,他的底層結構就是哈希,關于哈希的理論和算法呢,就都給大家講了
package com.learn.search;/*** 如果我要往哈希表里面存學生的話* 他的哈希碼該怎么辦,* 這是一個復雜類型,* 但是他的屬性是基本類型,* Student比較復雜,* 但是你這里不是有這么多的屬性嗎* 我得到你每個屬性的基本哈希碼,* 然后按照某種算法添加啊,* 不是可以得到一個整數了嗎* 你的Class不是比較復雜嗎* * 學生的哈希碼又該怎么辦* 我來了一個學生的姓名,年齡都不一樣的* @author Leon.Sun**/
public class Student {private int id;private String name;private int age;/*** 因為我們這里寫的是小寫的double,* 如果是大寫的Double他就會直接調用它里面的hashCode方法*/private double score;/*** 但是還有一種情況,* 班級Class,* 這個clazz當然不存在*/private Clazz clazz;/*** 我們來產生以下它的hashCode方法* 好復雜啊,* 最終還是得到一個整數,* 返回就可以了* 我們又給大家回答了一個問題,* 什么問題呢,* 各種數據類型的哈希碼該如何獲取* 這個理論大家知道*/@Overridepublic int hashCode() {final int prime = 31;int result = 1;/*** 年齡*/result = prime * result + age;/*** 班級直接調用hashCode方法得到結果就行了*/result = prime * result + ((clazz == null) ? 0 : clazz.hashCode());/*** id* 整數直接取自身*/result = prime * result + id;/*** 姓名* 字符串的話就直接調用hashCode*/result = prime * result + ((name == null) ? 0 : name.hashCode());long temp;/*** 實際上和我們開始見到的double處理機制是一樣的*/temp = Double.doubleToLongBits(score);result = prime * result + (int) (temp ^ (temp >>> 32));return result;}@Overridepublic boolean equals(Object obj) {if (this == obj)return true;if (obj == null)return false;if (getClass() != obj.getClass())return false;Student other = (Student) obj;if (age != other.age)return false;if (clazz == null) {if (other.clazz != null)return false;} else if (!clazz.equals(other.clazz))return false;if (id != other.id)return false;if (name == null) {if (other.name != null)return false;} else if (!name.equals(other.name))return false;if (Double.doubleToLongBits(score) != Double.doubleToLongBits(other.score))return false;return true;}}
package com.learn.search;/*** Clazz里面還是基本類型的* 可以得到一個哈希碼* 最終按照某個機制來就可以了* 我們哈希表里既用到hashcode也用到equals* equals我們經常用,* 所以我們同時來實現hashcode和equals* @author Leon.Sun**/
public class Clazz {/*** 這里面可能比較簡單*/private int id;private String name;/*** 仔細看一下他的hashCode是怎么來的,* 最終保證不同*/@Overridepublic int hashCode() {/*** 還是一個31*/final int prime = 31;int result = 1;/*** 哈希碼的整數就是他自己*/result = prime * result + id;/*** 字符串的哈希碼就是調用它的hashCode方法*/result = prime * result + ((name == null) ? 0 : name.hashCode());return result;}@Overridepublic boolean equals(Object obj) {if (this == obj)return true;if (obj == null)return false;if (getClass() != obj.getClass())return false;Clazz other = (Clazz) obj;if (id != other.id)return false;if (name == null) {if (other.name != null)return false;} else if (!name.equals(other.name))return false;return true;}}
?
總結
- 上一篇: 查找树(二级)
- 下一篇: 排序及其分类(一级)