算法 {{哈希函数,哈希表,哈希冲突},取模哈希(探测法|拉链法),离散化,整数元组的哈希}

算法 {{哈希函数,哈希表,哈希冲突},取模哈希(探测法|拉链法),离散化,整数元组的哈希}

{哈希函数,哈希表,哈希冲突}

定义

一般当我们谈到哈希, 就是指: 将一个任意类型T 映射成一个uint32, 这称之为哈希函数; (参见qHash)
维护着这些要哈希的对象T 的集合, 叫做哈希表 (参见QHash);

具体过程如下:
0: 对于T t; 其哈希值为h, 则哈希表里 会维护一个{h, t}的pair; 哈希表里的所有pair, 一定满足: 其所有的t均不同(因为没有必要记录重复元素 因为哈希表就是维护对象与其哈希值 而一个确定的对象他的哈希值是唯一的), 但所有的h值 可能会发生重复(即哈希冲突); 比如{1, "a"}, {2, "b"}, {1, "c"}这是一个合法的哈希表;
1: 当我们要往哈希表里 放一个{h,t}时, 他会去查询 当前哈希表里 所有pair.first == h的pair 记作S, 然后再去比较 是否存在S.second == t, 如果存在 则操作无影响, 否则 把{h,t}放入哈希表里; (因此T类型 除了要支持哈希函数外 必须还要支持==运算符);
2: 此时 既然对象已经哈希成一个uint32了, 然后你可以再使用拉链法/探测法的取模哈希法 再进行进一步处理;

class ST{
	D0 d0;
	D1 d1;

	//> 这个函数 你可以发生*哈希冲突*, 比如两个不同的对象`!(a==b)` 他俩的哈希值一样`a.GetHash()==b.GetHash()`, 这是没问题的, 只不过会影响效率, 因为哈希冲突是不可避免的;
	uint32 GetHash(){ return d0.GetHash() ^ d1.GetHash();}
	
	//> 他非常重要, 你必须保证 你自己是*如何定义 两个ST对象的同一性的*;
	bool operator==( ST const& _a){ return (d0==_a.d0)&&(d1==_a.d1);}
};

错误

错误: #哈希函数 必须保证: 两个不同对象的哈希值 是不同的#;
这是错误的, 只能说尽可能不发生, 否则为什么还有哈希冲突这个术语呢? 他就是会发生的;
两个相同对象的哈希值 必须相同, 这句话是正确的;

整数元组的哈希

定义

对于(a0,a1,a2) 假设a0=[0,A0) , a1=[0,A1), a2=[0,A2), 这个元组 可以哈希成一个[0, A0*A1*A2)之间的整数 计算公式是a0*A1*A2 + a1*A2 + a2;
而且他支持逆哈希, 即给定哈希值d, 那么(a0=(d/A2/A1%A0), a1=(d/A2%A1), a2=(d%A2));

例题

比如状压DP, 就会用到这个技巧, 每个列都是[0, Ci)范围的数, 然后通过哈希 把他们变成一个整数 表示当前整个行(即所有列)的状态;

动态哈希(Map)

性质

理论上 他是 log ⁡ ( N ) \log(N) log(N), 但是 因为他是动态的过程, 所以 实际效率还不错 虽然会比(取模哈希)慢几个常数;

算法

模板代码

template< class _Type> class ___Hash_Dynamically{
public:
    std::map< _Type, int> __Map; // 所有元素的哈希值为`[0, Map.size()-1]`;

    void Initialize(){ __Map.clear();}
    void Insert( const _Type & _a){ if( __Map.find( _a) == __Map.end()){ __Map[ _a] = __Map.size();}}
    int Get_hash( const _Type & _a) const{ if( __Map.find( _a) == __Map.end()){ return -1;}  return __Map.at( _a);}
    int GetHash_andInsert( const _Type & _a){ if( __Map.find( _a) == __Map.end()){ __Map[ _a] = __Map.size();}  return __Map.at( _a);}
}; // class ___Hash_Dynamically

静态哈希(离散化)

代碼

template< class _element> struct ___Hash_Sorting{
    std::vector<_element> Map; // `b=Map[a]`表示`b`的哈希值為`a`, 即`a<->b`是一一對應的;

    void Initialize( int _maxCount){ // `maxCount: Add函数的最大调用次数`;
        Map.reserve( _maxCount);  Map.clear();
    }
    void Add( _element const& _a){ Map.push_back( _a);}
    void Work(){ std::sort( Map.begin(), Map.end()); Map.erase( std::unique( Map.begin(), Map.end()), Map.end());}
    int Get_lowerBound( const _element & _t) const{ // 滿足`>=t`的*最小元素的下標*(如果不存在返回`-1`);
        if( Map.empty()){ return -1;}
        int l = 0, r = Map.size()-1;
        while( l < r){ int mid = ( l + r) >> 1;  if( Map[ mid] >= _t){ r = mid;}else{ l = mid + 1;}}
        if( Map[l] < _t){ return -1;}
        return l;
    }
    int Get_hash( const _element & _tar) const{ // 如果不存在則返回`-1` 否則`Map[返回值]==tar`;
        auto ind = Get_lowerBound( _tar);  if( ind==-1 || Map[ind]!=_tar){ return -1;}
        return ind;
    }
}; // class ___Hash_Sorting

取模哈希(探测法|拉链法)

性质

需求: 对unordered_map/unordered_set的效率上的优化;
换句话说, 探测法|拉链法 都可以被(STL容器)所替代, 只不过 他效率更高;
. 即, 维护一个整数集合S, 然后有一些操作{查询某个整数是否在S里, 向S里添加一个整数, …}

@DELI;

給定 N N N個整數 a a a(可以是負數), 選擇一個 > N >N >N的質數 P P P, 定義數組T map[ P] 初始都置為Invalid(確保所有 a ≠ I n v a l i d a \neq Invalid a=Invalid), 對於a=map[b]: @IF(a!=Invalid)[a的哈希值為b], @ELSE[沒有元素的哈希值為b]; 即 任意兩個數 他倆的哈希值必須要保證是不同的;

哈希算法:
init = (a%P+P)%P非負餘數 作為a的初始哈希值, 顯然 對於 a + k ∗ P a + k*P a+kP這些數 他們的初始哈希值 都是相同的, 即出現了 哈希衝突;

哈希衝突:
1(拉鏈法): 將map變成vector< T> map[ P], 即對於衝突的元素 都放到map[ init]裡面去 來區分他們;
缺點:
. 1: 此時a的哈希值 不再是一個[0,P)的整數, 而是2個數{h1, h2} 表示a在map[ h1][ h2]這個位置, 這不好 假如我們希望他的哈希值是[0, P)範圍的;
. 2: 假如所有數都是a + k*P, 顯然此時 時間變成了 O ( N ) O(N) O(N), 超時了;
2(探測法): 為了讓a的哈希值 是一個[0, P)的整數 就不能使用上述的鏈錶, 依次的查詢x = [a/ a+(1*1)/ a+(2*2)/ a+(3*3)/ ...]的%P的非負餘數 如果map[x] = Invalid 那麼當前數的哈希值為x; 且要讓 P > 2 ∗ N P > 2*N P>2N 即讓P比較大;
缺點: 大多數情況沒問題, 但是跟上面的2:一樣 假如所有數都是a + k*P 此時同樣是退化到 O ( N ) O(N) O(N)超時;
. 解決辦法: 再哈希, 樸素做法是a + k*k 將他變成a + k*L 其中L = a%PP PP<P的質數;

算法

模板

拉鏈法等價於: set (即添加/查詢一個元素, 但你無法獲得一個元素的哈希值);
探測法等價於: map (在set的基礎上 還添加了 將一個元素映射到[0,P)的整數, 換句話說 等價於離散化哈希);
因此 探測法可以完全替代拉鏈法;

由於探測法和Hash_Mapping等價, 而且 其實unordered_map效率也很好 大多數情況不會被卡 大概也就慢個幾倍的時間, 因此 基本很少用探測法 你可以先使用Hash_Mapping 不行再用探測法;

代碼
class __Hash_Modular{
//< 取模哈希 對整數(可以是負數)進行哈希 得到一個`>=0`的哈希, 他可以完全被`Map< Type,int>`所替代 只不過後者是$O(\log{N})$ 而取模哈希是接近$O(1)$的;
//  . 當`@LINK:@LOC_0`發生, 說明衝突次數太多了, 可以增大`__MaxDetectionCount_` 但這不是好辦法, 最好是可以考慮*再哈希* 即將探測`k*k`改為`k*L` L為`_a % 99991`;
public:
    using Type_ = @TODO; // 必須是*整數*類型;
    static constexpr int __Modular_ = @TODO; // [ 100003, 500009, 1000003, 5000011, 10000019, 50000017];
    //< `N: 要進行哈希的元素個數`, 確保`Modular_ > 2*N`;
    static constexpr Type_ __Invalid_ = INT64_0x80; // 確保所有要進行哈希的元素 都不會等於該值;
    static constexpr int __MaxDetectionCount_ = 202; // 二次檢測的最大次數 (也對應`Add/Get_hash`函數的最大時間複雜度);
    Type_ __Map[ __Modular_]; // `a=Map[b]`: @IF(a!=`Invalid`)[a的哈希值為b], @ELSE[暫時還沒有元素的哈希值為b]
   
    __Hash_Modular(){}
    void Initialize(){
        memset( __Map, 0x80, sizeof( __Map));
    }
    int Add( Type_ _a){ // 返回值為 `a`的哈希值;
        ASSERT_WEAK_( _a != __Invalid_);
        int init = _a % __Modular_;  if( init < 0){ init += __Modular_;}
        if( __Map[ init] == __Invalid_){ __Map[ init] = _a; return init;}
        else if( __Map[ init] == _a){ return init;}

        for( int k = 1; k <= __MaxDetectionCount_; ++k){
            int id = (init + k * k) % __Modular_;
            if( __Map[ id] == __Invalid_){ __Map[ id] = _a; return id;}
            else if( __Map[ id] == _a){ return id;}
        }
        ASSERT_( 0); // @MARK:@LOC_0
        return -1;
    }
    int Get_hash( Type_ _a){
    //< 返回值: @IF(`!= -1`)[表示a的哈希值 說明其之前調用過`Add`函數] @ELSE[該元素不存在];
        ASSERT_WEAK_( _a != __Invalid_);
        int init = _a % __Modular_;  if( init < 0){ init += __Modular_;}
        if( __Map[ init] == __Invalid_){ return -1;}
        else if( __Map[ init] == _a){ return init;}

        for( int k = 1; k <= __MaxDetectionCount_; ++k){
            int id = (init + k * k) % __Modular_;
            if( __Map[ id] == __Invalid_){ return -1;}
            else if( __Map[ id] == _a){ return id;}
        }
        ASSERT_( 0); // @MARK:@LOC_0
        return -1;
    }
}; // class __Hash_Modular
__Hash_Modular Hash_Modular;
@TODO( Hash_Modular.Initialize());

筆記

#取模数为质数#
要放入一个LL的值A, 他的下标是A % Hash_size_, 然后再探测; 即Ll_ Hash_index[ Hash_size_]
Hash_size_, 取模数(也是哈希表长度), 必须是质数 (否则容易冲突)

@DELI;

#探测算法#
要放入一个值A, int init = A % Hash_size_, 然后从init位置开始, 开始探测;

不要写成: int init = Hash_index[ A % Hash_size_];
这点反映对哈希的理解程度, Hash_index里存放的是: 键值; 键值通过% Hash_size_后, 是index, 数组下标;
现在init要获取的是(下标), 不是(键值)

#二次探测#
目前看来, 二次探测要比(线性探测)要更好
但应该没必要循环k = [0, .., Hash_size)吧? 这太大了, 而且k * k就爆int了, 还得转LL;

int Get_hash( int _key){
    int init = _key % Hash_size_;
    int id;
    for( int k = 0; k < 10000; ++k){
        id = (init + k * k) % Hash_size_;
        if( ( Hash_key[ id] == Hash_invalid) || (Hash_key[ id] == _key)){
            Hash_key[ id] = _key;
            return id;
        }
    }
    ASSERT_( 0);
    return id;
}

这样, k*k + init也是int范围的;
应该不会超过10000(虽然理论上是应该到Hash_size_), 但假如他循环比10000次还多, 那应该设哈希表设计的问题, 跟平方探测应该没问题;

取模哈希(四則運算)

定義

對於整數a, 執行a += x, a -= x, a *= x (且x也是整數 不涉及到除法運算), 然後判斷 兩個整數a,b 是否相等;
如果所有數 都是在long long範圍裡 這當然很簡單, 但是假如他會爆出long long, 那麼此時就可以使用哈希;

算法

模板

很簡單, 選擇一個取模數M=1e9+7, 對於(判斷兩個整數a,b是否相等這個操作 他等價於a%M == b%M), 對於(四則運算 因為取模也是可以進行+,-,*的);
具體代碼也很簡單, 你之前是: long long a = ?; (即所有涉及到的整數 都是long long類型), 現在就把他變成為Modular a = ?%Mod類型即可;

雙哈希

上面的算法 也可以稱為(單哈希), 假如說 對於Modular a = 1e9, 執行a += 7, 此時a==0, 然後此時a就代表所有0 + k*M, 假如此時你要判斷a是否等於0 (此時a=1e9+7 顯然他不等於0) 他倆的哈希值都是0, 即發生了哈希衝突;
為了解決哈希衝突, 此時我們再設置一個Mod2 = 1e9+9(與1e9+7互為孿生質數), 原來是Modular a, 現在變成pair<Modular, Mod2> a, (對於四則運算 即a.first += ?, a.second += ?) (對於判斷相等 即對應為 同時判斷這個2個哈希值是否相等);
. 以上述例子, 對於1e9他的哈希值為{1e9, 1e9}, 進行+=7操作後為{0, 1e9+7}, 那麼判斷他是否等於0 (0的哈希值為{0,0}), 顯然{0,1e9+7} != {0,0};
具體代碼也很簡單 原來是Modular a = ?%Mod; 現在變成pair<Mod1, Mod2> a = {?%M1, ?%M2}即可;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值