在億級數據處理場景中,「快速判重」始終是繞不開的核心痛點——傳統數據結構要麼空間開銷爆炸,要麼查詢效率低下,難以平衡性能與資源成本。而位圖(BitMap)與布隆過濾器(Bloom Filter),正是解決這一難題的高效方案:位圖以比特級存儲實現精準判重,布隆過濾器則基於位圖優化,用概率模型突破數據範圍限制。

本文將從底層原理出發,拆解二者的關聯邏輯、核心差異,結合實戰場景講清如何用這兩個工具高效搞定億級數據判重,助力開發者優化系統性能、降低資源損耗。

一.位圖原理

位圖是一種基於比特位(Bit)存儲數據的高效數據結構,核心邏輯是用「單個比特位的0/1狀態」映射「數據是否存在」——將目標數據作為比特數組(BitArray)的索引,索引對應比特位為1表示數據存在,為0則表示不存在,本質是“用空間極致壓縮實現高效判重/存在性校驗”。

看一個問題:如果此時有40億個無序的無符號整數;然後,任意給出一個無符號整數,如何快速判斷這個數在40億數據當中存不存在?

問題分析

一般判斷數據存不存在的問題,我們可以使用"set"或者"排序+二分"[O(logN)]的方式,但是對於億級數據——以40億無符號整數數據為例: 需要佔160億Byte,大約16G;也就是説單單存儲數據就要佔16G內存!這顯然不是我們想要的。

換一種方式,如果只是查找一個數據存不存在,像哈希直接定址法一樣直接標記?但我們用去bit位標記

對於一個整型: 32個bit位,如果每個bit位都代表一個數據,一個整型能表示32個數據,那麼40億數據需要125,000,000個整數(看上去還是很多,但只需要大約0.46G);

如何實現比特數組

比特數組:以「比特(bit,0/1)」為最小單位的數組,1位存1個二元狀態(是/否、有/無),極致省內存,也就是常説的「位圖」。

template<size_t N>//非類型模板參數 -- 一開始就確定好開多大空間
class bitset
{
public:
	bitset()//開闢_a的空間
	{			
		_a.resize(N / 32 + 1);//除會把餘數捨去,多開一個
	}

	//把x映射的那個位映射成1
	void set(size_t x)
	{
		size_t i = x / 32;
		size_t j = x % 32;

		//就可以用位運算操作 -- 按位與/或/異或/反
		_a[i] |= (1 << j);//從0開始的所以是左移j位
	}

	//把x映射的那個位映射成0
	void reset(size_t x)
	{
		size_t i = x / 32;
		size_t j = x % 32;

		//!是邏輯取反
		_a[i] &= (~(1 << j));//按位取反 在與
	}

	bool Test(size_t x)//在不在 判斷是0是1
	{
		size_t i = x / 32;
		size_t j = x % 32;

		return _a[i] & (1 << j);//在 返回真
	}
private:
	//關於對bit的操作
	//位段 -- 找到一個bit的數據
	//位運算
	vector<int> _a;//vector默認構造不開空間 -- 自己再寫個構造
	//我們需要用的是bit位,但實際操作的是整型
	//若果映射 x = 80, i = x / 32 則在vector的第i個整型上面(注意下標從0開始)
	//                  j = x % 32 得到在這個第i整型的第幾個bit位上面
};

原理:用bit位去映射(直接映射的方式),有為1,無為0; 通過x/32 與 x%32找到映射位置;

[C++]億級數據的高效判重_判重

數據測試

int main()
{
	fx::bitset<1000> bs;
	bs.set(1);
	bs.set(5);
	bs.set(10);
	bs.set(100);
	//測試某數據是否在位圖中:
	cout << bs.Test(1) << endl;
	cout << bs.Test(10) << endl;
	cout << bs.Test(100) << endl;
	cout << bs.Test(88) << endl << endl;

	bs.reset(100);
	bs.reset(99);

	cout << bs.Test(10) << endl;
	cout << bs.Test(100) << endl;
	cout << bs.Test(88) << endl;
	return 0;
}

為便於講解,只檢測了幾個數據,但億級數據的處理也是一樣;唯一要注意,在實際應用中,位圖數據源幾乎都來自文件/數據庫,需要我們掌握文件操作等內容。

二.位圖的應用

問題一

給定100億整型數據,設計算法找到只出現一次的整數。

在面對大量數據時(查找/去重/計數),首先考慮位圖的方法;但是上文只介紹了面對數據"存不存在",也就是説只能判斷一個數據出現了,並不能體現出現幾次,面對“計數”情況,應該如何改進?

思路
  • 用兩個bit位表示一個數據
  • 用兩個bitset處理

[C++]億級數據的高效判重_布隆過濾器_02

以法②為例:

	template<size_t N>
	class twobitset
	{
	public:
		void set(size_t x)
		{
			//00 -> 01
			if (!_bs1.Test(x) && !_bs2.Test(x))
			{
				_bs2.set(x);
			}//01->10
			else if (!_bs1.Test(x) && _bs2.Test(x))
			{
				_bs1.set(x);
				_bs2.reset(x);
			}
			//如果要統計出現3次的數據
			/*else
			{
				_bs2.set(x);
			}*/
		}
		bool is_once(size_t x)
		{
			return !_bs1.Test(x) && _bs2.Test(x);//找01
		}
	private:
		bitset<N> _bs1;//會調用bitset的構造初始化
		bitset<N> _bs2;
	};

如此,我們需要統計出現"3次"、"4次"的數據,我們都可藉助位圖統計。

問題二-練習

給兩個文件各有100億數據,找交集。

思路提示
  • 位圖標記一個文件內數據
  • 遍歷第二個文件,尋找交集。
  • (如果要解決重複數據而導致多次輸出的問題,可以增加第二個位圖,記錄已輸出的交集元素)

總結

位圖適用場景
  •  海量整數存在性判斷
  • 1bit標記存在狀態,極致省內存,查詢O(1)。
  •  兩/多集合高效運算(交、並、差集)
  • 位圖標記集合元素,通過位運算(&交集、|並集、^差集)快速計算,效率遠超哈希/排序。
  •  整數排序(海量無重複整數)
  • 位圖標記元素,按bit位順序遍歷輸出,時間O(n),內存佔用極低。
  • 統計元素次數
  • 多個位圖/多個bit位協作計數
  • 操作系統磁盤塊標記(標記空閒/佔用狀態)

 對於非整數且難唯一映射的數據(如複雜對象)或是數據範圍極大且不規則(如10¹⁸級整數,內存無法承載),不適合使用位圖。

三.布隆過濾器

布隆過濾器: 是一種基於位圖多哈希函數的概率型數據結構,用於高效判斷元素是否存在於集合中,以極小誤判率換取極致內存佔用與查詢效率,無漏判、不支持刪除。

誤判

誤判的唯一來源的是:「不同元素經哈希函數計算後,映射到了同一個存儲位置,導致系統無法區分這兩個元素,進而把“不存在的元素”誤判為“存在”」,本質是「哈希碰撞」引發的“位置佔用衝突”。

舉個例子:當多個字符串映射到某個位置時,不同字符映射可能用了相同的“數字”( 比如"abc"和"abd"映射方式是用首字母映射,這兩個字符串一定會映射到相同bit位上 ),本來不存在數數據被標記成了存在,出現誤判。

解決

在1970年:美國科學家伯頓·霍華德·布隆(Burton Howard Bloom)發表論文,首次提出布隆過濾器概念,核心是用位圖+多哈希實現高效存在性判斷—— 一個數據用多種方式映射到多個位置,這樣只有當多個位置都被標記時,才能證明該數據存在。

[C++]億級數據的高效判重_布隆過濾器_03

布隆過濾器應用

對於用位圖判斷字符串等數據存在性而言,它判斷不存在一定是準確的(為0肯定不存在),而存在卻可能受到已有數據干擾,即使用布隆過濾器也無法完全保證不會出現誤判。所以布隆過濾器僅僅適用於"精確要求不高"的場景:

  • 不需要精確的場景:如快速判斷暱稱是否註冊過(如果要精確:位圖也可作為第一層判斷,不在時直接返回(不存在判斷一定準確),存在則去數據庫查找準確結果)。
  • 過濾效果:和第一點"需精確"情況一樣,布隆過濾器用於快速將“不存在”情況過濾出去,提高效率。