【算法與數據結構 09】哈希表——高效查找的利器


前言:python

以前,咱們前後學習了線性表、數組、字符串和樹,它們廣泛都存在這樣的缺陷,那就是數據數值條件的查找,都須要對所有數據或者部分數據進行遍歷。那麼,有沒有一種方法能夠省去數據比較的過程,從而進一步提高數值條件查找的效率呢?數組

答案固然是:有!這一課時咱們就來介紹這樣一種高效率的查找神器:哈希表。bash

在這裏插入圖片描述


1、什麼是哈希表

哈希表名字源於 Hash,也能夠叫做散列表。哈希表是一種特殊的數據結構,它與數組、鏈表以及樹等咱們以前學過的數據結構相比,有很明顯的區別。數據結構

1.1 哈希表的原理

哈希表是一種數據結構,它使用哈希函數組織數據,以支持快速插入和搜索。哈希表的核心思想就是使用哈希函數將鍵映射到存儲桶。更確切地說:app

  • 當咱們插入一個新的鍵時,哈希函數將決定該鍵應該分配到哪一個桶中,並將該鍵存儲在相應的桶中;
  • 當咱們想要搜索一個鍵時,哈希表將使用相同的哈希函數來查找對應的桶,並只在特定的桶中進行搜索。

下面舉一個簡單的例子,咱們來理解下:less

在這裏插入圖片描述
在示例中,咱們使用 y = x % 5 做爲哈希函數。讓咱們使用這個例子來完成插入和搜索策略:
函數

  • 插入:咱們經過哈希函數解析鍵,將它們映射到相應的桶中。 例如,1987 分配給桶 2,而 24 分配給桶 4。
  • 搜索:咱們經過相同的哈希函數解析鍵,並僅在特定存儲桶中搜索。 例如,若是咱們搜索 23,將映射 23 到 3,並在桶 3 中搜索。咱們發現 23 不在桶 3 中,這意味着 23 不在哈希表中。

1.2 設計哈希函數

哈希函數是哈希表中最重要的組件,該哈希表用於將鍵映射到特定的桶。在以前的示例中,咱們使用 y = x % 5 做爲散列函數,其中 x 是鍵值,y 是分配的桶的索引學習

散列函數將取決於鍵值的範圍桶的數量。下面是一些哈希函數的示例:
在這裏插入圖片描述
哈希函數的設計是一個開放的問題。其思想是儘量將鍵分配到桶中,理想狀況下,完美的哈希函數將是鍵和桶之間的一對一映射。然而,在大多數狀況下,哈希函數並不完美,它須要在桶的數量和桶的容量之間進行權衡。

this

固然,咱們也能夠自定義一些哈希函數。通常的方法有:spa

  • 直接定製法。哈希函數爲關鍵字到地址的線性函數。如,H (key) = a * key + b。 這裏,a 和 b 是設置好的常數。
  • 數字分析法。假設關鍵字集合中的每一個關鍵字 key 都是由 s 位數字組成(k1,k2,…,Ks),並從中提取分佈均勻的若干位組成哈希地址。
  • 平方取中法。若是關鍵字的每一位都有某些數字重複出現,而且頻率很高,咱們就能夠先求關鍵字的平方值,經過平方擴大差別,而後取中間幾位做爲最終存儲地址。
  • 摺疊法。若是關鍵字的位數不少,能夠將關鍵字分割爲幾個等長的部分,取它們的疊加和的值(捨去進位)做爲哈希地址。
  • 除留餘數法。預先設置一個數 p,而後對關鍵字進行取餘運算。即地址爲 key % p。

2、解決哈希衝突

理想狀況下,若是咱們的哈希函數是完美的一對一映射,咱們將不須要處理衝突。不幸的是,在大多數狀況下,衝突幾乎是不可避免的。例如,在咱們以前的哈希函數(y = x % 5)中,1987 和 2 都分配給了桶 2,這就是一個哈希衝突。

解決哈希衝突應該要思考如下幾個問題:

  • 如何組織在同一個桶中的值?
  • 若是爲同一個桶分配了太多的值,該怎麼辦?
  • 如何在特定的桶中搜索目標值?

那麼一旦發生衝突,咱們該如何解決呢?

經常使用的方法有兩種:開放定址法和鏈地址法

2.1 開放定址法

即當一個關鍵字和另外一個關鍵字發生衝突時,使用某種探測技術在哈希表中造成一個探測序列,而後沿着這個探測序列依次查找下去。當碰到一個空的單元時,則插入其中。

經常使用的探測方法是線性探測法。 好比有一組關鍵字 {12,13,25,23},採用的哈希函數爲 key % 11。當插入 12,13,25 時能夠直接插入,地址分別爲 一、二、3。而當插入 23 時,哈希地址爲 23 % 11 = 1。

然而,地址 1 已經被佔用,所以沿着地址 1 依次往下探測,直到探測到地址 4,發現爲空,則將 23 插入其中。以下圖所示:
在這裏插入圖片描述

2.2 鏈地址法

將哈希地址相同的記錄存儲在一張線性鏈表中。例如,有一組關鍵字 {12,13,25,23,38,84,6,91,34},採用的哈希函數爲 key % 11。以下圖所示:
在這裏插入圖片描述

3、哈希表的應用

3.1 哈希表的基本操做

在不少高級語言中,哈希函數、哈希衝突都已經在底層完成了黑盒化處理,是不須要開發者本身設計的。也就是說,哈希表完成了關鍵字到地址的映射,能夠在常數級時間複雜度內經過關鍵字查找到數據。

至於實現細節,好比用了哪一個哈希函數,用了什麼衝突處理,甚至某個數據記錄的哈希地址是多少,都是不須要開發者關注的。接下來,咱們從實際的開發角度,來看一下哈希表對數據的增刪查操做。

哈希表中的增長和刪除數據操做,不涉及增刪後對數據的挪移問題(數組須要考慮),所以處理就能夠了。

哈希表查找的細節過程是:對於給定的 key,經過哈希函數計算哈希地址 H (key)。

  • 若是哈希地址對應的值爲空,則查找不成功。
  • 反之,則查找成功。

雖然哈希表查找的細節過程還比較麻煩,但由於一些高級語言的黑盒化處理,開發者並不須要實際去開發底層代碼,只要調用相關的函數就能夠了。

3.2 哈希表的優缺點

  • 優點:它能夠提供很是快速的插入-刪除-查找操做,不管多少數據,插入和刪除值須要接近常量的時間。在查找方面,哈希表的速度比樹還要快,基本能夠瞬間查找到想要的元素。
  • 不足:哈希表中的數據是沒有順序概念的,因此不能以一種固定的方式(好比從小到大)來遍歷其中的元素。在數據處理順序敏感的問題時,選擇哈希表並非個好的處理方法。同時,哈希表中的
    key 是不容許重複的,在重複性很是高的數據中,哈希表也不是個好的選擇。

4、 設計哈希映射

4.1 設計要求

要求:

不使用任何內建的哈希表庫設計一個哈希映射具體地說,設計應該包含如下的功能:

  • put(key, value):向哈希映射中插入(鍵,值)的數值對。若是鍵對應的值已經存在,更新這個值。
  • get(key):返回給定的鍵所對應的值,若是映射中不包含這個鍵,返回-1。
  • remove(key):若是映射中存在這個鍵,刪除這個數值對。

示例:

MyHashMap hashMap = new MyHashMap();
hashMap.put(1, 1);          
hashMap.put(2, 2);         
hashMap.get(1);            // 返回 1
hashMap.get(3);            // 返回 -1 (未找到)
hashMap.put(2, 1);         // 更新已有的值
hashMap.get(2);            // 返回 1 
hashMap.remove(2);         // 刪除鍵爲2的數據
hashMap.get(2);            // 返回 -1 (未找到)

注意:

全部的值都在 [0, 1000000]的範圍內。
 操做的總數目在[1, 10000]範圍內。
 不要使用內建的哈希庫。

4.2 設計思路

哈希表是一個在不一樣語言中都有的通用數據結構。例如,Python 中的 dict 、C++中的 map 和 Java 中的 Hashmap。哈希表的特性是能夠根據給出的 key 快速訪問 value。

最簡單的思路就是用模運算做爲哈希方法,爲了下降哈希碰撞的機率,一般取素數的模,例如 模 2069。

定義 array 數組做爲存儲空間,經過哈希方法計算數組下標。爲了解決 哈希碰撞 (即鍵值不一樣,但映射下標相同),利用桶來存儲全部對應的數值。桶能夠用數組或鏈表來實現,在下面的具體實現中, Python 中用的是數組。

定義哈希表方法,get(),put() 和 remove(),其中的尋址過程以下所示:

  • 對於一個給定的鍵值,利用哈希方法生成鍵值的哈希碼,利用哈希碼定位存儲空間。對於每一個哈希碼,都能找到一個桶來存儲該鍵值所對應的數值。
  • 在找到一個桶以後,經過遍從來檢查該鍵值對是否已經存在。

在這裏插入圖片描述

4.3 實際案例

Python實現以下:

class Bucket:
    def __init__(self):
        self.bucket = []

    def get(self, key):
        for (k, v) in self.bucket:
            if k == key:
                return v
        return -1

    def update(self, key, value):
        found = False
        for i, kv in enumerate(self.bucket):
            if key == kv[0]:
                self.bucket[i] = (key, value)
                found = True
                break

        if not found:
            self.bucket.append((key, value))

    def remove(self, key):
        for i, kv in enumerate(self.bucket):
            if key == kv[0]:
                del self.bucket[i]


class MyHashMap(object):

    def __init__(self):
        """ Initialize your data structure here. """
        # better to be a prime number, less collision
        self.key_space = 2069
        self.hash_table = [Bucket() for i in range(self.key_space)]


    def put(self, key, value):
        """ value will always be non-negative. :type key: int :type value: int :rtype: None """
        hash_key = key % self.key_space
        self.hash_table[hash_key].update(key, value)


    def get(self, key):
        """ Returns the value to which the specified key is mapped, or -1 if this map contains no mapping for the key :type key: int :rtype: int """
        hash_key = key % self.key_space
        return self.hash_table[hash_key].get(key)


    def remove(self, key):
        """ Removes the mapping of the specified value key if this map contains a mapping for the key :type key: int :rtype: None """
        hash_key = key % self.key_space
        self.hash_table[hash_key].remove(key)


# Your MyHashMap object will be instantiated and called as such:
# obj = MyHashMap()
# obj.put(key,value)
# param_2 = obj.get(key)
# obj.remove(key)

複雜度分析:

  • 時間複雜度:每一個方法的時間複雜度都爲 O(N/K),其中 N 爲全部可能鍵值的數量,K 爲哈希表中預約義桶的數量,在這裏 K 爲 2069。這裏咱們假設鍵值是均勻地分佈在全部桶中的,桶的平均大小爲 N/K​,在最壞狀況下須要遍歷完整個桶,所以時間複雜度爲 O(N/K)。
  • 空間複雜度:O(K+M),其中 K 爲哈希表中預約義桶的數量,M 爲哈希表中已插入鍵值的數量。

今天的分享就到這裏啦,但願對你的學習有所幫助!

在這裏插入圖片描述

養成習慣,先贊後看!你的支持是我創做的最大動力!

相關文章
相關標籤/搜索