map 學習(下)——C++ 中的 hash_map, unordered_map

map 學習(下)——C++ 中的 hash_map, unordered_map

接上篇《map 學習(一)——C++中 map 的使用》

一、hash_map

參考《C++ STL中哈希表 hash_map介紹》即可。博主寫的很詳細。

注:
hash_map 不是標準的。筆者寫該文檔時本來想嘗試些一個 hash_map 例程,但發現自己用 Qt + MSVC2010 編譯器出現了編譯錯誤。網上原因好像說是 STL 加入標準C++之時,hash_map系列當時還沒有完全實現,所以很多平臺上雖然安裝了 g++ 編譯器,但不一定有 hash_map 的實現。所以如果有平臺移植的內容,儘量少用 hash_map

二、unordered_map

以下內容翻譯自《unordered_map - C++ Reference》

1. 原型

template < class Key,                                    // unordered_map::key_type
           class T,                                      // unordered_map::mapped_type
           class Hash = hash<Key>,                       // unordered_map::hasher
           class Pred = equal_to<Key>,                   // unordered_map::key_equal
           class Alloc = allocator< pair<const Key,T> >  // unordered_map::allocator_type
           > class unordered_map;

2. 說明

unordered_map 是一種關聯容器,用於存儲由關鍵值 (Key Value,以下稱爲Key 值) 映射值 (Mapped Value,以下稱爲映射值) 組成的元素,並且允許根據其 Key 值快速檢索各個元素。
在 unordered_map 容器中,Key 值通常用來唯一標識元素,映射值是與該 Key 值關聯內容的對象。Key 值與映射值的類型可能不同。
在 unordered_map 內部,元素沒有按照其 Key 值與映射值的任何順序進行排序 ,而是根據它們的 Hash 值組織成桶,允許它們通過其 Key 值直接快速訪問單個元素(通常具有常數等級的平均時間複雜度)。
unordered_map 容器與 map 容器相比,通過 Key 值訪問各個元素的速度更快,然而通過其元素子集進行範圍迭代的效率通常較低。
unordered_map 實現了直接訪問操作符 (operator[]),它允許使用 Key 值作爲輸入參數,直接訪問映射值。
容器中的迭代器至少是前向迭代器。

3. 容器屬性

  • 關聯性
    • 關聯容器中的元素的參考地址指的是其 Key 值,而不是他們在容器中的絕對地址;
  • 無序性
    • 無序容器使用 Hash 表來組織元素,這些 Hash 表允許無序容器通過 Key 值快速訪問元素;
  • 映射
    • 每個元素將一個 Key 值與映射值關聯起來,Key 值用於標識其主要內容是映射值的元素;
  • 唯一關鍵值
    • 容器中不存在同時擁有相同 Key 值的兩個元素;
  • 分配器感知
    • map 容器使用分配器對象動態處理其存儲需求。

4. 模板參數

  • Key
    • Key 值的類型。在 unordered_map 中的每個元素都是由其 Key 值唯一指定的。
    • 別名爲成員類型 unordered_map::key_type
  • T
    • 映射值的類型。在 unordered_map 中的每個元素,都存儲了一些數據作爲其映射值。
    • 別名爲成員類型 unordered_map::mapped_type(注:不同於 unordered_map::value_type,詳細見下面)
  • Hash
    • 一個一元函數對象類型,它將與爲 Key 值同類型的對象作爲參數,並以此爲基礎返回類型爲 size_t 的唯一值。它可以使實現函數調用符的類,或是指向函數的指針(具體請詳細參閱示例的構造函數)。它的默認值是 hash <Key>,它返回一個碰撞概率接近於1.0/std::numeric_limits<sizet>::max() 的 Hash 值。
    • unordered_map 對象使用該函數返回的散列值,並在內部組織元素,加速了定位各個元素的過程。
    • 別名爲成員類型 unordered_map::hasher
  • Pred
    • 一個二元值,它接受兩個 Key 類型的參數,並返回一個布爾值。表達式 pred(a, b) 中,pred 是該類型的對象,a, b 是 Key 值,如果 a 被認爲與 b 等價,則返回 true。它可以使實現了函數調用運算符的類,或者指向函數的指針(具體請詳細參閱示例的構造函數)。它的默認值是 equal_to <Key>,它返回與等號運算符 operator(a==b) 相同的值。
    • unordered_map 對象使用該表達式,來確定兩個元素的 Key 值是否等價。在 unordered_map 容器中,沒有任何兩個元素可以使用該斷定產生 true 值(原句:No two elements in an unordered_map container can have keys that yield true using this predicate. ,也許翻譯的不對)。
    • 別名爲成員類型 unordered_map::key_equal
  • Alloc(通常使用默認值)
    • 用於定義存儲分配模型的分類器對象的類型。默認情況下,使用分配器類模板,它定義了最簡單的內存分配模型,並且與值無關。
    • 別名爲成員類型 unordered_map::allocator_type

在 unordered_map 成員函數的參考中,模板函數假定了相同的名稱:Key, T, Hash, Pred, Alloc
unordered_map 容器元素的迭代器可以訪問 Key 值與映射值。爲此 unordered_map 定義了一個對應的類 value_type,它的第一個值對應於 Key 值類型的常量版本,第二個值對應於映射值(即模板參數 T):

typedef pair<const Key, T> value_type;

unordered_map 容器的迭代器指向該 value_type 的元素。因此對於一個調用 value_type 的迭代器而言,迭代器指向 unordered_map 的一個元素,它的 Key 值與映射值可以分別用下面的方式進行訪問:

unordered_map<Key,T>::iterator it;
(*it).first;             // the key value (of type Key)
(*it).second;            // the mapped value (of type T)
(*it);                   // the "element value" (of type pair<const Key,T>)

當然可以用任何其他的直接訪問運算符,如 -> 或 []。例程如下:

it->first;               // same as (*it).first   (the key value)
it->second;              // same as (*it).second  (the mapped value) 

5. 常用函數

(1) bucket

以下內容譯自《unordered_map::bucket - C++ Reference》

原型

size_type bucket ( const key_type& k ) const;

說明
定位元素所在的桶,返回 Key 值爲輸入參數 k 的元素的所在桶號。
桶是容器內部 Hash 表中的一個槽,槽中的元素根據 Key 值分配元素。桶號的編號從 0 到 (bucket_count - 1)。
桶中單個元素可以通過 unordered_map::begin 和 unordered_map::end 返回的範圍迭代器進行訪問。

例程
下面例程片段摘自後面的程序示例:

    for (auto& x : mymap3) {
        std::cout << "Element [" << x.first << ":" << x.second << "]";
        // 返回元素所在桶號
        std::cout << " is in bucket #" << mymap3.bucket(x.first) << std::endl;
    }

(2) count

以下內容譯自《unordered_map::count - C++ Reference》

原型

size_type count ( const key_type& k ) const;

說明
使用給定的 Key 值計算元素。
搜索容器中 Key 值爲輸入參數 k 的元素,並返回找到元素的數量。由於 unordered_map 容器不允許存在重複的 Key 值,這說明如果容器中存在具有該 Key 值的元素,則該函數返回 1,否則返回 0。

(3) 其他

其他操作函數基本和 map 相同:

  • clear
    • 清除 map 中所有元素;
  • erase
    • 刪除 map 中指定位置的元素;
  • insert
    • 在 map 指定位置添加 pair 類型的元素;
  • find
    • 獲取 map 中元素的迭代器;
  • begin, end
    • map 的正向迭代器的起始位置與終點位置;

6. 示例

(1) 示例 1

以下示例從《C++11中std::unordered_map的使用》挑選,並加以註釋說明。

#include <iostream>
#include <string>
#include <unordered_map>

// reference: http://www.cplusplus.com/reference/unordered_map/unordered_map/at/
typedef std::unordered_map<std::string, std::string> stringmap;

// 將 a, b 融合爲一個 unordered_map
stringmap merge(stringmap a, stringmap b) {
    // unordered_map 複製構造函數
    stringmap temp(a);
    // 範圍插入,將 b 全部插入進 a 中
    temp.insert(b.begin(), b.end());
    return temp;
}

int main()
{
    //============================
    //   1. unordered_map 元素計算與基礎遍歷
    //============================
    // 定義第一個 unordered_map
    std::unordered_map<std::string, int> mymap = { { "Mars", 3000 }, { "Saturn", 60000 }, { "Jupiter", 70000 } };

    // 對元素進行計算
    mymap.at("Mars") = 3396;
    mymap.at("Saturn") += 272;
    mymap.at("Jupiter") = mymap.at("Saturn") + 9638;

    // auto:自動判斷類型
    // 基於範圍的 for 循環,遍歷 mymap
    for (auto& x : mymap) {
        std::cout << x.first << ": " << x.second << std::endl;
    }
    std::cout << "mymap.size() is " << mymap.size() << std::endl << std::endl;

    //============================
    //   2. iterator, 迭代器遍歷
    //============================
    // 定義第二個 unordered_map
    std::unordered_map<std::string, std::string> mymap2 = { { "Australia", "Canberra" }, { "U.S.", "Washington" }, { "France", "Paris" } };
    std::cout << "mymap2 contains:" << std::endl;

    // 遍歷 mymap2
    for (auto it = mymap2.begin(); it != mymap2.end(); ++it)
        std::cout << " " << it->first << ":" << it->second << std::endl;
    std::cout << std::endl;

    // mymap2 分配的各桶中的元素
    std::cout << "mymap2's buckets contain:\n";
    for (unsigned i = 0; i < mymap2.bucket_count(); ++i) {
        std::cout << "bucket #" << i << " contains:";
        for (auto local_it = mymap2.begin(i); local_it != mymap2.end(i); ++local_it)
            std::cout << " " << local_it->first << ":" << local_it->second;
        std::cout << std::endl;
    }

    //============================
    //   3. bucker, 桶操作
    //============================
    // 定義第三個 unordered_map
    std::unordered_map<std::string, std::string> mymap3 = {
            { "us", "United States" },
            { "uk", "United Kingdom" },
            { "fr", "France" },
            { "de", "Germany" }
    };

    // 遍歷 mymap3
    for (auto& x : mymap3) {
        std::cout << "Element [" << x.first << ":" << x.second << "]";
        // 返回元素所在桶號
        std::cout << " is in bucket #" << mymap3.bucket(x.first) << std::endl;
    }

    //============================
    //   4. count ,判斷元素是否在容器中
    //============================
    // 定義第四個 unordered_map
    std::unordered_map<std::string, double> mymap4 = {
            { "Burger", 2.99 },
            { "Fries", 1.99 },
            { "Soda", 1.50 } };

    // 遍歷 mymap4
    for (auto& x : { "Burger", "Pizza", "Salad", "Soda" })
    {
        // 判斷 x 是否在容器中
        if (mymap4.count(x)>0)
            std::cout << "mymap4 has " << x << std::endl;
        else
            std::cout << "mymap4 has no " << x << std::endl;
    }

    //============================
    //   5. erase ,刪除操作
    //============================
    // 定義第五個 unordered_map
    std::unordered_map<std::string, std::string> mymap5;
    mymap5["U.S."] = "Washington";
    mymap5["U.K."] = "London";
    mymap5["France"] = "Paris";
    mymap5["Russia"] = "Moscow";
    mymap5["China"] = "Beijing";
    mymap5["Germany"] = "Berlin";
    mymap5["Japan"] = "Tokyo";

    // 通過迭代器刪除
    mymap5.erase(mymap5.begin());
    // 通過 Key 值刪除
    mymap5.erase("France");
    // 通過迭代器範圍刪除
    mymap5.erase(mymap5.find("China"), mymap5.end());

    // 基於範圍的 for 循環,遍歷展示刪除後的 mymap
    for (auto& x : mymap5)
        std::cout << x.first << ": " << x.second << std::endl;

    //============================
    //   6. find ,搜索操作
    //============================
    // 定義第六個 unordered_map
    std::unordered_map<std::string, double> mymap6 = {
            { "mom", 5.4 },
            { "dad", 6.1 },
            { "bro", 5.9 } };

    std::string input;
    std::cout << "who? ";
    // 輸入 mom, dad, bro 中的一個,否則搜索失敗返回 Not Found
    getline(std::cin, input);

    // 根據輸入參數 Key 值進行搜索,返回一個迭代器
    std::unordered_map<std::string, double>::const_iterator got = mymap6.find(input);

    // find 返回值若爲 unordered_map 的尾部,則沒有在容器中找到
    if (got == mymap6.end())
        std::cout << "not found";
    else
        std::cout << got->first << " is " << got->second;
    std::cout << std::endl;

    //============================
    //   6. insert ,插入操作
    //============================
    // 定義第七、八個 unordered_map
    std::unordered_map<std::string, double>
        myrecipe,
        mypantry = { { "milk", 2.0 }, { "flour", 1.5 } };

    // 定義插入元素,類型爲 pair 的對象
    std::pair<std::string, double> myshopping("baking powder", 0.3);

    // 複製插入
    myrecipe.insert(myshopping);
    // 移動插入
    myrecipe.insert(std::make_pair<std::string, double>("eggs", 6.0));
    // 範圍插入
    myrecipe.insert(mypantry.begin(), mypantry.end());  // range insertion
    // 初始化列表插入
    myrecipe.insert({ { "sugar", 0.8 }, { "salt", 0.1 } });    // initializer list insertion

    std::cout << "myrecipe contains:" << std::endl;
    for (auto& x : myrecipe)
        std::cout << x.first << ": " << x.second << std::endl;

    std::cout << std::endl;

    //============================
    //   7. 等於運算符 = 操作
    //============================
    // 初始化列表
    stringmap first = { { "AAPL", "Apple" }, { "MSFT", "Microsoft" } };
    stringmap second = { { "GOOG", "Google" }, { "ORCL", "Oracle" } };
    // 移動
    stringmap third = merge(first, second);
    // 複製
    first = third;

    std::cout << "first contains:";
    for (auto& elem : first) std::cout << " " << elem.first << ":" << elem.second;
    std::cout << std::endl;

    return 0;
}

(2) 示例 2

摘選自 Leetcode 問題 Two Sum:給出一個整數數組,返回兩個數的下標值,令其和等於一個指定的目標值。
例:

Given nums = [2, 7, 11, 15], target = 9,

Because nums[0] + nums[1] = 2 + 7 = 9,
return [0, 1].

有解如下:

#include <unordered_map>
class Solution {
public:
    vector<int> twoSum(vector<int> &numbers, int target)
    {
        //Key is the number and value is its index in the vector.
        unordered_map<int, int> hash;
        vector<int> result;
        for (int i = 0; i < numbers.size(); i++) {
            int numberToFind = target - numbers[i];

            //if numberToFind is found in map, return them
            if (hash.find(numberToFind) != hash.end()) {
                result.push_back(hash[numberToFind]);
                result.push_back(i);            
                return result;
            }

            //number was not found. Put it in the map.
            hash[numbers[i]] = i;
        }
        return result;
    }
};

算法本身遍歷一次,花費了 O(n) 的時間複雜度,遍歷過程中的 find() 方法本身花費 O(log n),所以該算法總時間複雜度爲 O(nlog n)。

三、map, hash_map, unordered_map 的區別

參考網址:
《c++中map與unordered_map的區別》
《C++中map和hash_map的區別》

1. 頭文件

  • map
    • #include <map>
  • hash_map
    • #include <hash_map>
  • unordered_map
    • #include <unordered_map>

2. 內部實現機理

  • map
    • map 內部實現了一個紅黑樹,該結構具有自動排序的功能,因此map內部的所有元素都是有序的,紅黑樹的每一個節點都代表着map的一個元素,因此,對於map進行的查找,刪除,添加等一系列的操作都相當於是對紅黑樹進行這樣的操作,故紅黑樹的效率決定了map的效率,map只需要提供比較函數(一般爲小於函數)即可完成比較;
  • hash_map
    • hash_map 需要提供 hash 函數,以及等於函數;
  • unordered_map
    • unordered_map 內部實現了一個 Hash 表,所以其元素的排列順序是雜亂無序的。

3. 優缺點

  • map
    • 優點:
      • 有序性:這是map結構最大的優點,其元素的有序性在很多應用中都會簡化很多的操作;
      • 紅黑樹,內部實現一個紅黑書使得 map 的很多操作在 log n 的時間複雜度下就可以實現,因此效率非常的高;
    • 缺點:
      • 空間佔用率高,因爲 map 內部實現了紅黑樹,雖然提高了運行效率,但是因爲每一個節點都需要額外保存父節點,子節點以及紅/黑性質,使得每一個節點都佔用大量的空間;
    • 適用於具有順序要求的問題;
  • hash_map
    • 優點:
      • hash_map 查找速度會比map快,而且查找速度基本和數據量大小無關,屬於常數級別(但不能說一定比 map 的 log n 級別快,因爲 hash 函數本身也有耗時);
    • 缺點:
      • 空間佔用多,如果對內存使用很嚴格,需要認真考慮是否使用 hash_map ;特別是當 hash_map 對象特別多時,更加難以控制;
    • 適用於對效率要求較高的環境;
  • unordered_map
    • 優點:
      • 內部實現了 Hash 表,所以查找速度很快;
    • 缺點:
      • Hash 表的建立比較比較費時;
    • 適用於查找問題;
發佈了60 篇原創文章 · 獲贊 259 · 訪問量 47萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章