Python數據結構——散列表


散列表的實現常常叫做散列(hashing)。散列僅支持INSERT,SEARCH和DELETE操作,都是在常數平均時間執行的。需要元素間任何排序信息的操作將不會得到有效的支持。

散列表是普通數組概念的推廣。如果空間允許,可以提供一個數組,為每個可能的關鍵字保留一個位置,就可以運用直接尋址技術。

當實際存儲的關鍵字比可能的關鍵字總數較小時,采用散列表就比較直接尋址更為有效。在散列表中,不是直接把關鍵字用作數組下標,而是根據關鍵字計算出下標,這種

關鍵字與下標之間的映射就叫做散列函數。

1.散列函數

一個好的散列函數應滿足簡單移植散列的假設:每個關鍵字都等可能的散列到m個槽位的任何一個中去,並與其它的關鍵字已被散列到哪個槽位無關。

1.1 通常散列表的關鍵字都是自然數。

1.11 除法散列法

通過關鍵字k除以槽位m的余數來映射到某個槽位中。

hash(k)=k mod m

應用除法散列時,應注意m的選擇,m不應該是2的冪,通常選擇與2的冪不太接近的質數。

1.12 乘法散列法

乘法方法包含兩個步驟,第一步用關鍵字k乘上常數A(0<A<1),並取出小數部分,然后用m乘以這個值,再取結果的底(floor)。

hash(k)=floor(m(kA mod 1))

乘法的一個優點是對m的選擇沒有什么特別的要求,一般選擇它為2的某個冪。

一般取A=(√5-1)/2=0.618比較理想。

1.13 全域散列

隨機的選擇散列函數,使之獨立於要存儲的關鍵字。在執行開始時,就從一族仔細設計的函數中,隨機的選擇一個作為散列函數,隨機化保證了

沒有哪一種輸入會始終導致最壞情況發生。

1.2 如果關鍵字是字符串,散列函數需要仔細的選擇

1.2.1 將字符串中字符的ASCII碼值相加

def _hash(key,m):
    hashVal=0
    for _ in key:
        hashVal+=ord(_)
    return hashVal%m

由於ascii碼最大127,當表很大時,函數不會很好的分配關鍵字。

1.2.2 取關鍵字的前三個字符。

值27表示英文字母表的字母個數加上一個空格。

hash(k)=k[0]+27*k[1]+729*k[2]

1.2.3 用霍納法則把所有字符擴展到n次多項式。

用32代替27,可以用於位運算。

def _hash(key,m):
    hashval=0
    for _ in key:
        hashval=(hashval<<5)+ord(_)
    return hashval%m

2. 分離鏈接法

散列表會面臨一個問題,當兩個關鍵字散列到同一個值的時候,稱之為沖突或者碰撞(collision)。解決沖突的第一種方法通常叫做分離鏈接法(separate chaining)。

其做法是將散列到同一個值的所有元素保留到一個鏈表中,槽中保留一個指向鏈表頭的指針。

為執行FIND,使用散列函數來確定要考察哪個表,遍歷該表並返回關鍵字所在的位置。

為執行INSERT,首先確定該元素是否在表中。如果是新元素,插入表的前端或末尾。

為執行DELETE,找到該元素執行鏈表刪除即可。

散列表中元素個數與散列表大小的比值稱之為裝填因子(load factor)λ。

執行一次不成功的查找,遍歷的鏈接數平均為λ,成功的查找則花費1+(λ/2)。

分離鏈接散列的一般做法是使得λ盡量接近於1。

代碼:

class _ListNode(object):
    def __init__(self,key):
        self.key=key
        self.next=None
class HashMap(object):
    def __init__(self,tableSize):
        self._table=[None]*tableSize
        self._n=0  #number of nodes in the map
    def __len__(self):
        return self._n
    def _hash(self,key):
        return abs(hash(key))%len(self._table)
    def __getitem__(self,key):
        j=self._hash(key)
        node=self._table[j]
        while node is not None and node.key!=key :
            node=node.next
        if node is None:
            raise KeyError,'KeyError'+repr(key)
        return node        
    def insert(self,key):
        try:
            self[key]
        except KeyError:
            j=self._hash(key)
            node=self._table[j]
            self._table[j]=_ListNode(key)
            self._table[j].next=node
            self._n+=1
    def __delitem__(self,key):
        j=self._hash(key)
        node=self._table[j]
        if node is not None:
            if node.key==key:
                self._table[j]=node.next
                self._-=1
            else:
                while node.next!=None:
                    pre=node
                    node=node.next
                    if node.key==key:
                        pre.next=node.next
                        self._n-=1
                        break

3.開放定址法

 在開放定址散列算法中,如果有沖突發生,那么就要嘗試選擇另外的單元,直到找出空的單元為止。

   h(k,i)=(h'(k)+f(i)) mod m,i=0,1,...,m-1   ,其中f(0)=0

3.1 線性探測法

函數f(i)是i的線性函數

  h(k,i)=(h'(k)+i) mod m 

相當於逐個探測每個單元

線性探測會存在一個問題,稱之為一次群集。隨着被占用槽的增加,平均查找時間也會不斷增加。當一個空槽前有i個滿的槽時,該空槽為下一個將被占用

槽的概率是(i+1)/m。連續被占用槽的序列會越來越長,平均查找時間也會隨之增加。

如果表有一半多被填滿的話,線性探測不是個好辦法。

3.2 平法探測

 平方探測可以取消線性探測中的一次群集問題。

h(k,i)=(h'(k)+c1i+c2i2) mod m

平方探測中,如果表的一半為空,並且表的大小是質數,保證能夠插入一個新的元素。

平方探測會引起二次群集的問題。

3.3 雙散列

雙散列是用於開放定址法的最好方法之一。

h(k,i)=(h1(k)+ih2(k)) mod m

為能查找整個散列表,值h2(k)要與m互質。確保這個條件成立的一種方法是取m為2的冪,並設計一個總產生奇數的h2。另一種方法是取m為質數,並設計一個總是產生

較m小的正整數的h2。

例如取:

h1(k)=k mod m,h2(k)=1+(k mod m'),m'為略小於m的整數。

給定一個裝填因子λ的開放定址散列表,插入一個元素至多需要1/(1-λ)次探查。

給定一個裝填因子λ<1的開放定址散列表,一次成功查找中的期望探查數至多為(1/λ)ln(1/1-λ)。

4.  再散列

如果表的元素填得太滿,那么操作的運行時間將開始消耗過長。一種解決方法是當表到達某個裝填因子時,建立一個大約兩倍大的表,而且使用一個相關的新散列函數,

掃描整個原始散列表,計算每個元素的新散列值並將其插入到新表中。

 為避免開放定址散列查找錯誤,刪除操作要采用懶惰刪除。

代碼

class HashEntry(object):
    def __init__(self,key,value):
        self.key=key
        self.value=value
class HashTable(object):
    _DELETED=HashEntry(None,None)  #用於刪除
    def __init__(self,tablesize):
        self._table=tablesize*[None]
        self._n=0
    def __len__(self):
        return self._n
    def __getitem__(self,key):
        found,j=self._findSlot(key)
        if not found:
            raise KeyError
        return self._table[j].value
    def __setitem__(self,key,value):
        found,j=self._findSlot(key)
        if not found:
            self._table[j]=HashEntry(key,value)
            self._n+=1
            if self._n>len(self._table)//2:
                self._rehash()
        else:
            self._table[j].value=value
    def __delitem__(self,key):
        found,j=self._findSlot(key)
        if found:
            self._table[j]=HashTable._DELETED   # 懶惰刪除
    def _rehash(self):
        oldList=self._table
        newsize=2*len(self._table)+1
        self._table=newsize*[None]
        self._n=0
        for entry in oldList:
            if entry is not None and entry is not HashTable._DELETED:
                self[entry.key]=entry.value
                self._n+=1            
    def _findSlot(self,key):
        slot=self._hash1(key)
        step=self._hash2(key)
        firstSlot=None
        while True:
            if self._table[slot] is None:
                if firstSlot is None:
                    firstSlot=slot
                return (False,firstSlot)
            elif self._table[slot] is HashTable._DELETED:
                firstSlot=slot
            elif self._table[slot].key==key:
                return (True,slot)
            slot=(slot+step)%len(self._table)
    def _hash1(self,key):
        return abs(hash(key))%len(self._table)
    def _hash2(self,key):
        return 1+abs(hash(key))%(len(self._table)-2)

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM