一点一滴成长

导航

散列

1、定义 

  散列(Hash,哈希)是一种对数据的处理方法,通过某种特定的算法将要检索的项与用来检索的索引(称为散列,或者散列值)关联起来,然后可以生成一种便于搜索的数据结构(称为散列表)。

  c++11中增加了获得hash值的方法,通过hash,hash是实现了operator()的类,所以其对象是函数对象:

    //template<class Key > struct hash;
    auto hashValue = std::hash<std::string>()("hello");

 

2、散列的常见应用:

  散列常用作一种数据安全的方法,由一串数据中经过散列算法计算出来的资料指纹(data fingerprint),经常用来识别资料是否有被窜改,以保证资料确实是由原创者所提供。
  散列算法的另一用途是用来加密的密码,由于散列算法所计算出来的散列值(Hash Value)具有不可逆(无法逆向演算回原本的数值)的性质,因此可有效的保护密码。
  散列表就是使用散列函数将键名和键值关联起来的数据结构,c++中的unordered_map、unordered_set底层实现使用这种数据结构来保存数据。

3、unordered_map

  unordered_map的基本原理:

   使用一个下标范围比较大的数组来存储元素。将每个键映射到0到数组大小这个范围中的某个数,将键值放到这个单元中存储,获得存储单元的索引位置的函数称为散列函数;散列函数具体来说就是使每个元素的键都与一个函数值(即数组下标)相对应,用这个数组下标所在的单元(称为桶)来存储这个元素。但是,不能够保证每个元素的键与存储单元一一对应的,有可能出现对于不同的元素却计算出了相同的桶号,在这种情况下不同的元素被存储到了相同的数组单元中,这就称为“冲突(碰撞)”,所以还要有方法来处理这种情况,即为“解决冲突”,比如如果得到的哈希地址冲突(该位置上已存储数据)的话就将这个数据与原数据组成链表存放,如下图的B、D、F桶处所示:

   

  对于unordered_map插入元素的过程是:

   ①、通过hash函数得到key的hash值
   ②、得到桶号(一般为hash值对整个数组大小求模获得)
   ③、存放key和value在桶内。

  unordered_map查找元素的过程是:

   ①、通过hash函数得到key的hash值
   ②、得到桶号(一般为hash值对整个数组大小求模获得)
   ③、比较桶的内部元素是否与key相等,若都不相等,则没有找到,相等的话取出value值。
根据unorder_map的查询和插入规则可知其查询、插入和删除操作不随数据量的增长而增大,时间复杂度是常数级别O(1)。

 4、map与unordered_map

  由于map内部使用的树形数据机构,所以map中元素会自动排序存放,unordered_map则不会。

   unordered_map中key如果是自定义类型的话定义unordered_map的时候需要传入模板参数列表的第三个和第四个参数,分别用来指定散列函数和解决冲突时使用的判断是否相等,eg:

#include <unordered_map>
#include "boost\functional\hash.hpp"
class CFoo
{
public:
    int m_iNum = 0;
    string m_str;
};
struct foo_hash_value
{
    size_t operator()(const CFoo& p)
    {
        //std::hash<int> h;
        //return h(p.m_iNum);
        size_t seed = 0;
        boost::hash_combine(seed, p.m_iNum);
        boost::hash_combine(seed, p.m_str);
        return seed;
    }
};

struct foo_is_equal
{
    bool operator()(const CFoo& f1, const CFoo& f2)
    {
        return f1.m_iNum == f2.m_iNum;
    }
};

std::unordered_map<CFoo, int, foo_hash_value, foo_is_equal> m;
View Code

5、CRC、MD5、SHA、HAMC

  CRC、MD5、SHA都是通过对数据进行计算,来生成一个校验值,该校验值可以用来校验数据的完整性。

  CRC不是hash算法,而是采用多项式除法,其校验值的长度跟其多项式有关系,一般为16位(CRC16)或32位(CR32),其计算效率比MD5和SHA1要高很多,但其安全性相对于MD5和SHA1要弱很多,一般只用作通信数据的校验。

  MD5和SHA1使用的是hash算法,由于hash值具有不可逆性,无法通过hash值获得对应的原值,所以它们又经常用作数据加密。生成的校验值MD5是128位(16个字节),SHA1是160位(20个字节),SHA1的安全性比MD5还要高一点。SHA-2是SHA-1的后继者,其安全性更高,但耗时更高,包括SHA-224、SHA-256、SHA-512等。如果我们想要通过加密后的值获得原加密之前的值,我们可以使用自己的加密算法或者使用非对称加密。比如使用非对称加密即为使用公钥对数据加密后进行保存,通过私钥对加密后的数据进行解密。常用的非对称加密算法有RSA、Elgamal。

  一般我们是通过https传输注册的用户名和密码,服务端对于用户密码,保存其MD5值而不是原始密码(也可以对MD5值进行乱序后保存)下次用户登录的时候,对输入的密码进行MD5计算后判断是否与保存的MD5值相同。MD5或者SHA-1通过穷举法或者彩虹表也可以被破解出来,比如我们的数据库被黑客获取到了的话,黑客就知道了各个用户的用户名和密码MD5值,然后他使用一个数据量非常庞大的数据字典,这个字典里都是些常用的密码及其对应的MD5值,黑客通过这个表来查询他获取到的用户的MD5密码,很有可能就能查得到其对应的原始密码。

  可以使用给密码加“盐(salt)”的做法来防止破解,一般来说就是将用户的key+salt一起Hash,即Hash(key+salt),所以服务端也需要保存salt。因为加了个salt,并且salt值一般是随机生成的,所以就增加了破解密码的难度。 salt 不一定要加在密码的最前面或最后面,也可以插在中间,也可以分开插入,也可以倒序,以增加破解难度。可以使用CSPRNG算法来生成salt,CSPRNG不是普通的随机数算法,是加密安全的,C/C++中为CryptGenRandom,Java中为Java.security.SecureRandom。也可以直接使用Bcrypt算法来防止哈希破解,它就是使用的哈希加盐的思想。

  HMAC是一种更高级别的加密方法,它使用hamckkey(秘钥)+hash(散列,可以使用MD5或SHA)结合的加密方式。另外还有一种HMAC与时间结合的加密方式。

6、布隆过滤器

  布隆过滤器可以有效的支持判断一个元素是否不存在,其本质是一个bit向量或bit数组。当向过滤器插入元素的时候,会通过几个哈希方法来计算出元素在过滤的位置,比如插入元素"Foo"的话,通过第一个哈希方法计算出结果为100,那么就将索引100处的bit位置为1,通过第二个哈希方法计算出结果为220,那么就将索引220位置处的bit置为1,这样如果位置100或者220处的bit不是1的话,那么"Foo"是肯定不存在的。因为哈希方法计算出来的结果可能相同,比如元素"Bar"通过第一个方法计算出来的结果也是220,所以布隆过滤器只能判断一个元素可能存在。传统的布隆过滤器不支持删除操作。但是名为 Counting Bloom filter 的变种支持元素删除。

  如果我们来设计布隆过滤器的话,其长度很短的话那么很快所有的 bit 位均为 1,那么查询任何值都会返回“可能存在”,所以其适合设置的比较大。哈希函数的个数也需要权衡,个数越多布隆过滤器的效率越低,如果太少的话,判断可能存在的误报率会变高。另外,性能很低的哈希函数不是个好选择,推荐 MurmurHash、Fnv 这些。

  Redis 因其支持 setbit 和 getbit 操作,且纯内存性能高等特点,因此可以作为布隆过滤器来使用。

posted on 2018-04-24 14:10  整鬼专家  阅读(934)  评论(0编辑  收藏  举报