【面试题】给40亿个不重复无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中。

●  在看到这个题后最先想到的方法是遍历这40亿个数,依次进行判断,但此做法需要的内存很大,大约为15G(4000000000 * 4 ÷(1024*1024*1024)),可见此算法不可取。

 如果内存够的话,我们可以通过位图实现,位图一个数组每个数据的每个二进制位表示一个数据,每一位用0,1表示当前这个位置上是否存有值,同样是利用哈希存储的方法。此做法可以大大减少内存,对于此题是一个int类型就可以编程32个位,需要的内存空间从15G降到500M

具体实现如下:

  1. #pragma
  2. class BitMap//位图
  3. {
  4. public:
  5.     BitMap()
  6.     :_size(0)
  7.     {}
  8.     BitMap(size_t size)//size表示多少位,不是数据个数
  9.     : _size(size)
  10.     {//调整大小为size / 32 + 1即右移5位加1(加1:需要的大小要包含size,例如10%8=1,大小应为2)
  11.      _a.resize((size >> 5) + 1);
  12.     }
  13.     //位图中,注意1在移位时为左移num不是左移32-num;
  14.     void Set(size_t x)//存入x位,置1
  15.     {
  16.      size_t index = x >> 5;
  17.      size_t num = x % 32;//eg:x = 35,num = 3,则在位图中为_a[1]中设为001
  18.      ++_size;
  19.      _a[index] |= 1 << num;//1左移3位,进行|使_a中对应处为
  20.     }
  21.     void Remove(size_t x)//删除x位,置0
  22.     {
  23.      size_t index = x >> 5;
  24.      size_t num = x % 32;//eg:x = 35,num = 3,则在位图中为_a[1]中设为110
  25.      --_size;
  26.      _a[index] &= (~(1 << num));//1右移3位取反0,进行&使_a中对应处为0
  27.     }
  28.     bool Test(size_t x)//判断是否存在
  29.     {
  30.      size_t index = x >> 5;
  31.      size_t num = x % 32;
  32.      if (_a[index] & (1 << num))//如果当前位为1,则存在
  33.      {
  34.      return true;
  35.      }
  36.      return false;
  37.     }
  38.     void Resize(size_t size)//重置大小
  39.     {
  40.      _a.resize((size >> 5) + 1);
  41.     }
  42.     size_t Size()//返回位图的总位数
  43.     {
  44.      return _size;
  45.     }
  46.     size_t Capacity()//返回int数据个数
  47.     {
  48.      return _a.size();
  49.     }
  50.     void Print()
  51.     {
  52.      for (size_t i = 0; i < _a.size(); i++)
  53.      {
  54.      cout << _a[i] << " " << endl;
  55.      }
  56.      cout << endl;
  57.     }
  58. private:
  59. vector<size_t> _a;
  60. size_t _size;
  61. };
  62. void TestBitMap()
  63. {
  64. BitMap bm(65);
  65. bm.Set(3);
  66. bm.Set(4);
  67. bm.Set(5);
  68. bm.Print();
  69. cout << "is 4 EXTST? " << bm.Test(4) << endl;
  70. cout << "is 8 EXTST? " << bm.Test(8) << endl;
  71. bm.Remove(4);
  72. cout << "is 4 EXTST? " << bm.Test(4) << endl;
  73. bm.Print();
  74. cout << "size: " << bm.Size() << endl;
  75. cout << "capacity: " << bm.Capacity() << endl;
  76. bm.Resize(100);
  77. cout << "capacity: " << bm.Capacity() << endl;
  78. }

●  Bloom Filter 是一种空间效率很高的随机数据结构,Bloom filter 可以看做是对 bit-map 的扩展, 它的原理是:

      当一个元素被加入集合时,通过 K 个 Hash函数将这个元素映射成一个位阵列(Bit array)中的 K 个点,把它们置为 1。检索时,只要看看这些点是不是都是 1 就知道集合中有没有它了。

1、如果这些点有任何一个 0,则被检索元素一定不在;

2、如果都是 1,则被检索元素可能在。

用了素数表和6个哈希算法:

  1. #pragma
  2. size_t _GetNextPrime(size_t size)//素数表:获取下一个素数
  3. {
  4. const int _PrimeSize = 28;
  5. static const unsigned long _PrimeList[_PrimeSize] =
  6. {
  7. 53ul97ul193ul389ul769ul,
  8. 1543ul3079ul6151ul12289ul24593ul,
  9. 49157ul98317ul196613ul393241ul786433ul,
  10. 1572869ul3145739ul6291469ul12582917ul25165843ul,
  11. 50331653ul100663319ul201326611ul402653189ul805306457ul,
  12. 1610612741ul3221225473ul4294967291ul
  13. };
  14. for (size_t i = 0; i < _PrimeSize; ++i)
  15. {
  16. if (_PrimeList[i] > size)
  17. {
  18. return _PrimeList[i];
  19. }
  20. return _PrimeList[i - 1];
  21. }
  22. return _PrimeList[_PrimeSize];//如果size大于或等于素数表中数据,就返回表中最大数
  23. }
  24. //6种字符串哈希算法
  25. template<class T>
  26. size_t BKDRHash(const T * str)
  27. {
  28. register size_t hash = 0;
  29. while (size_t ch = (size_t)*str++)
  30. {
  31. hash = hash * 131 + ch;   // 也可以乘以31、131、1313、13131、131313..         
  32. }
  33. return hash;
  34. }
  35. template<class T>
  36. size_t SDBMHash(const T *str)
  37. {
  38. register size_t hash = 0;
  39. while (size_t ch = (size_t)*str++)
  40. {
  41. hash = 65599 * hash + ch;
  42. //hash = (size_t)ch + (hash << 6) + (hash << 16) - hash;  
  43. }
  44. return hash;
  45. }
  46. template<class T>
  47. size_t RSHash(const T *str)
  48. {
  49. register size_t hash = 0;
  50. size_t magic = 63689;
  51. while (size_t ch = (size_t)*str++)
  52. {
  53. hash = hash * magic + ch;
  54. magic *= 378551;
  55. }
  56. return hash;
  57. }
  58. template<class T>
  59. size_t APHash(const T *str)
  60. {
  61. register size_t hash = 0;
  62. size_t ch;
  63. for (long i = 0; ch = (size_t)*str++; i++)
  64. {
  65. if ((i & 1) == 0)
  66. {
  67. hash ^= ((hash << 7) ^ ch ^ (hash >> 3));
  68. }
  69. else
  70. {
  71. hash ^= (~((hash << 11) ^ ch ^ (hash >> 5)));
  72. }
  73. }
  74. return hash;
  75. }
  76. template<class T>
  77. size_t JSHash(const T *str)
  78. {
  79. if (!*str)        // 这是由本人添加,以保证空字符串返回哈希值0  
  80. return 0;
  81. register size_t hash = 1315423911;
  82. while (size_t ch = (size_t)*str++)
  83. {
  84. hash ^= ((hash << 5) + ch + (hash >> 2));
  85. }
  86. return hash;
  87. }
  88. template<class T>
  89. size_t DEKHash(const T* str)
  90. {
  91. if (!*str)        // 以保证空字符串返回哈希值0  
  92. return 0;
  93. register size_t hash = 1315423911;
  94. while (size_t ch = (size_t)*str++)
  95. {
  96. hash = ((hash << 5) ^ (hash >> 27)) ^ ch;
  97. }
  98. return hash;
  99. }
  100. //6个仿函数分别进行6种字符串算法的调用
  101. template<class T>
  102. struct _HashFunc1
  103. {
  104. size_t operator()(const T& str)
  105. {
  106. return BKDRHash(str.c_str());
  107. }
  108. };
  109. template<class T>
  110. struct _HashFunc2
  111. {
  112. size_t operator()(const T& str)
  113. {
  114. return SDBMHash(str.c_str());
  115. }
  116. }; 
  117. template<class T>
  118. struct _HashFunc3
  119. {
  120. size_t operator()(const T& str)
  121. {
  122. return RSHash(str.c_str());
  123. }
  124. }; 
  125. template<class T>
  126. struct _HashFunc4
  127. {
  128. size_t operator()(const T& str)
  129. {
  130. return APHash(str.c_str());
  131. }
  132. }; 
  133. template<class T>
  134. struct _HashFunc5
  135. {
  136. size_t operator()(const T& str)
  137. {
  138. return JSHash(str.c_str());
  139. }
  140. };
  141. template<class T>
  142. struct _HashFunc6
  143. {
  144. size_t operator()(const T& str)
  145. {
  146. return DEKHash(str.c_str());
  147. }
  148. };

布隆过滤器具体实现如下:

  1. #define _CRT_SECURE_NO_WARNINGS 1
  2. template<class K = string, 
  3. class HashFunc1 = _HashFunc1<K>, class HashFunc2 = _HashFunc2<K>, 
  4. class HashFunc3 = _HashFunc3<K>, class HashFunc4 = _HashFunc4<K>, 
  5. class HashFunc5 = _HashFunc5<K>, class HashFunc6 = _HashFunc6<K>>
  6. class BloomFilter
  7. {
  8. public:
  9. BloomFilter(size_t size = 0)
  10. {
  11. _capacity = _GetNextPrime(size);
  12. _bm.Resize(_capacity);//调用BitMap的Resize调整大小
  13. }
  14. void Set(const K& key)
  15. {
  16. size_t index1 = HashFunc1()(key);
  17. size_t index2 = HashFunc2()(key);
  18. size_t index3 = HashFunc3()(key);
  19. size_t index4 = HashFunc4()(key);
  20. size_t index5 = HashFunc5()(key);
  21. size_t index6 = HashFunc6()(key); 
  22. _bm.Set(index1 % _capacity);//设置的位为index1 % _capacity,调用BitMap的Set
  23. _bm.Set(index2 % _capacity);
  24. _bm.Set(index3 % _capacity);
  25. _bm.Set(index4 % _capacity);
  26. _bm.Set(index5 % _capacity);
  27. _bm.Set(index6 % _capacity);
  28. }
  29. bool Test(const K& key)
  30. {
  31. //只要存在一个为0就不存在,否则存在
  32. size_t index1 = HashFunc1()(key);
  33. if (!_bm.Test(index1 % _capacity))
  34. return false;
  35. size_t index2 = HashFunc2()(key);
  36. if(!_bm.Test(index2 % _capacity))
  37. return false;
  38. size_t index3 = HashFunc3()(key);
  39. if(!_bm.Test(index3 % _capacity))
  40. return false;
  41. size_t index4 = HashFunc4()(key);
  42. if(!_bm.Test(index4 % _capacity))
  43. return false;
  44. size_t index5 = HashFunc5()(key);
  45. if(!_bm.Test(index5 % _capacity))
  46. return false;
  47. size_t index6 = HashFunc6()(key);
  48. if(!_bm.Test(index6 % _capacity))
  49. return false;
  50. return true;
  51. }
  52. private:
  53. BitMap _bm;
  54. size_t _capacity;
  55. };
  56. void TestBloomFilter()
  57. {
  58. BloomFilter<> bf(50);
  59. bf.Set("Scen");
  60. bf.Set("斯洛");
  61. bf.Set("https://blog.51cto.com/user_index.php?action=addblog_new&job=modify&tid=1773181");
  62. cout << "exist? " << bf.Test("Scen") << endl;
  63. cout << "exist? " << bf.Test("Scenluo") << endl;
  64. cout << "exist? " << bf.Test("斯洛") << endl;
  65. cout << "exist? " << bf.Test("https://blog.51cto.com/user_index.php?action=addblog_new&job=modify&tid=1773181") << endl;
  66. cout << "exist? " << bf.Test("https://blog.51cto.com/user_index.php?action=addblog_new&job=modify&tid=1773131") << endl;
  67. }

布隆过滤器的缺陷:

1、误算率(False Positive)是其中之一。

      随着存入的元素数量增加,误算率随之增加。但是如果元素数量太少,则使用散列表足矣。所以我们用多个哈希表去存储一个数据。那么问题又来了,我们是多用一些呢,还是少用一些。如果多用哈希表的话,如上面的题,一个哈希就需要500M,那么放的越多是不是越占内存啊。如果太少的话是不是误算率就高啊,所以取个适中的。我的程序实现是取了六个哈希表。

2、如果当前位置为0肯定不存在,但是为1不一定存在。

一般布隆过滤器只支持设计,不支持删除。可以通过引用计数,但空间浪费较大。