Registros de aprendizaje de C++: mapa de bits de 이십사, filtro Bloom, corte hash


esta casa rural

1. mapa de bits

Veamos primero un tema:

Proporcione 4 mil millones de enteros únicos sin signo, no ordenados. Dado un número entero sin signo, ¿cómo determinar rápidamente si un árbol se encuentra entre estos 4 mil millones de números?

Puede haber dos ideas aquí, ordenar + búsqueda binaria, ponerlas en una tabla hash o en un árbol rojo-negro. Pero no ignoremos una pregunta importante: 4 mil millones. En la conversión real, el espacio ocupado por 4 mil millones de números enteros se redondea a 15G. 15G, ¿necesito un espacio de 15G para encontrar este número? Seguramente no. También es posible pensar en un método para dividir la búsqueda en varias partes, pero esto no resuelve el problema esencial y ocupa mucho espacio: ¿Qué pasa si hay más de un número para buscar?

Esta pregunta juzga si existe, por lo que no es necesario colocar el número en una determinada estructura. Puede usar un bit binario para representar la información de si los datos existen. Si el bit binario es 1, significa que existe, y si es 0, significa que no existe, que es la operación de mapa de bits. Por ejemplo, si configura una variable de tipo int, que ocupa 32 bits, todos los resultados posibles en sus bits binarios son 2^32 - 1, que es aproximadamente 4,29 mil millones, entonces hay suficientes datos para almacenar el estado. También puedes dividir 32 bits en 4 caracteres.

Como escribí en el blog anterior de Linux, las operaciones bit a bit también son mapas de bits.

Escribimos una función set para establecerla en 1 y la reiniciamos para establecerla en 0. Después de pasar un número, ¿cómo juzgamos en qué carácter se encuentra? x/8, ¿qué bit en char? x% 8. Una vez que lo encuentres, configúralo. Pero esto se compone de varios caracteres, y también se debe considerar el método de almacenamiento de la dirección. El desplazamiento a la izquierda es el desplazamiento de orden inferior a orden superior y el desplazamiento a la derecha es el desplazamiento de orden superior a orden inferior, pero No necesita preocuparse demasiado por esto, el compilador tiene su propia práctica.

Define los _bits de un vector<char>.

	void set(size_t x)//设置1
	{
    
    
		size_t i = x / 8;
		size_t j = x % 8;
		_bits[i] |= (1 << j);
	}

	void reset(size_t x)//设置0
	{
    
    
		size_t i = x / 8;
		size_t j = x % 8;
		_bits[i] &= ~(1 << j);
	}

También hay una función de prueba para ver si el valor marcado está presente.

	bool test(size_t x)
	{
    
    
		size_t i = x / 8;
		size_t j = x % 8;
		return _bits[i] & (1 << j);
	}

código de prueba

void test_bitset1()
{
    
    
	bitset<100> bs;
	bs.set(10);
	bs.set(11);
	bs.set(15);
	cout << bs.test(10) << endl;
	cout << bs.test(15) << endl;

	bs.reset(10);

	cout << bs.test(10) << endl;
	cout << bs.test(15) << endl;

	bs.reset(10);
	bs.reset(15);

	cout << bs.test(10) << endl;
	cout << bs.test(15) << endl;
}

Si el número que se utilizará es muy grande, como el mayor, como 4,29 mil millones, puede escribir zydset <-1> zs o zydset <0xFFFFFFFF> zs, y el compilador realmente abrirá estos espacios.

template<size_t N>
class bitset
{
    
    
public:
	bitset()
	{
    
    
		_bits.resize(N / 8 + 1, 0);//想象一下,100 / 8就是12,实际数值是12.5,+1就包含了这个多出来的值,也就是开辟了13个char,全都初始化为0,也就是每个char的比特位全为0。实际问题中,40亿算下来就是开辟476MB的空间,这样就大大减小了空间消耗。
	}

	void set(size_t x)
	{
    
    
		size_t i = x / 8;
		size_t j = x % 8;

		_bits[i] |= (1 << j);
	}

	void reset(size_t x)
	{
    
    
		size_t i = x / 8;
		size_t j = x % 8;

		_bits[i] &= ~(1 << j);
	}

	bool test(size_t x)
	{
    
    
		size_t i = x / 8;
		size_t j = x % 8;

		return _bits[i] & (1 << j);
	}

private:
	vector<char> _bits;
};

aplicación de mapa de bits

De 10 mil millones de números, encuentra el número que aparece solo una vez.

Puede abrir dos mapas de bits o modificar el mapa de bits para convertirlo en un mapa de bits de dos bits. El mapa de bits de dos bits es N/4 y el 8 original se convierte en 4. Verifique el estado mirando dos bits a la vez, 00 Es 0 veces, 01 es 1 vez y 10 es más de 1 vez.

Con la ayuda de la clase escrita arriba, escribe una clase que resuelva este problema.

template<size_t N>
class twobitset
{
    
    
public:
	void set(size_t x)
	{
    
    
		// 00 -> 01
		if (_bs1.test(x) == false
		&& _bs2.test(x) == false)
		{
    
    
			_bs2.set(x);
		}
		// 01 -> 10
		else if (_bs1.test(x) == false
			&& _bs2.test(x) == true)
		{
    
    
			_bs1.set(x);
			_bs2.reset(x);
		}
		// 10
	}

	void Print()
	{
    
    
		for (size_t i = 0; i < N; ++i)
		{
    
    
			if (_bs2.test(i))
			{
    
    
				cout << i << endl;
			}
		}
	}

public:
	bitset<N> _bs1;
	bitset<N> _bs2;
};

Dados dos archivos con 10 mil millones de enteros y solo 1G de memoria, ¿cómo encontrar la intersección de los dos archivos?

Puede crear dos mapas de bits, luego realizar un bucle, && cada carácter, y los restantes aparecen una vez.

También es posible leer el valor de un archivo en un mapa de bits y luego leer otro archivo para juzgar si está en el mapa de bits anterior, que es la intersección, pero el resultado obtenido de esta manera debe deduplicarse nuevamente. El método aquí es cuando se encuentre el valor de intersección por primera vez, establezca el valor correspondiente al mapa de bits anterior en 0.

Si la cantidad de datos es grande, es mejor utilizar el primer método. Por ejemplo, cuando sean 10 mil millones, use el primero, y cuando sean 100 millones, use el segundo, porque el primero crea un número fijo y el segundo crea tantos como haya.

10 mil millones de enteros, memoria 1G, encuentre números enteros que no aparezcan más de 2 veces. Luego puede usar un mapa de bits de dos bits, 00 es 0 veces, 01 es 1 vez, 10 es 2 veces y 11 es 3 veces.

Ventajas y desventajas

Ventajas: rápido, ahorra espacio.

Desventajas: solo se pueden asignar números enteros y otros tipos de números de punto flotante, cadenas, etc. no pueden almacenar asignaciones

2. Filtro de floración

Para una cadena, suponiendo que hay 10 caracteres, cada uno de acuerdo con la tabla de códigos ANSII encontrará que hay 256, por lo que es 256 elevado a la décima potencia, y no funcionará utilizar el mapa de bits anterior en este momento. Debe haber mucho conflicto en este momento.

La idea del filtro Bloom es que es difícil resolver todos los conflictos y no existe una buena manera. El enfoque de Bloom es reducir los conflictos. El método anterior era uno a uno, un mapeo a una ubicación, por lo que Bloom puede reducir la tasa de errores de juicio al mapear múltiples ubicaciones una por una. Aquí es uno a tres.

template<size_t N, class K, class Hash1, class Hash2, class Hash3>
class BloomFilter
{
    
    
public:
	void set(const K& key)
	{
    
    
		size_t hash1 = Hash1()(key) % N;
		_bs.set(hash1);
		size_t hash2 = Hash2()(key) % N;
		_bs.set(hash2);
		size_t hash3 = Hash3()(key) % N;
		_bs.set(hash3);
	}

	bool test(const K& key)
	{
    
    
		size_t hash1 = Hash1()(key) % N;
		if (!_bs.test(hash1))
		{
    
    
			return false;
		}
		size_t hash2 = Hash2()(key) % N;
		if (!_bs.test(hash2))
		{
    
    
			return false;
		}
		size_t hash3 = Hash3()(key) % N;
		if (!_bs.test(hash3))
		{
    
    
			return false;
		}
	}
private:
	bitset<N> _bs;
};

Se seguirá utilizando la clase de mapa de bits anterior.

El filtro Bloom es preciso si el juicio no existe, pero el juicio es inexacto. Porque si no está allí, la posición debe ser 0 y no hay rastro de mapeo, por lo que no es muy preciso, pero no es seguro porque puede haber otras cadenas asignadas a esta posición.

Los filtros Bloom se utilizan en escenarios donde se pueden tolerar errores de juicio. Por ejemplo: al registrarse determine rápidamente si se ha utilizado el apodo, en cuanto a la presencia o ausencia del teléfono móvil, primero puede utilizar el filtro Bloom para determinar la ausencia, y si es así, luego ingresar a la base de datos para buscar.

Ventajas: rápido, ahorra memoria
Desventajas: juicio erróneo

1. Función hash

Complemente la función hash de la cadena completa, aquí hay algunas funciones con forma, que han sido sometidas a algunos cálculos matemáticos.

struct BKDRHash
{
    
    
	size_t operator()(const string& s)
	{
    
    
		register size_t hash = 0;
		for (auto ch : s)
		{
    
    
			hash = hash * 131 + ch;
		}
		return hash;
	}
};

struct APHash
{
    
    
	size_t operator()(const string& s)
	{
    
    
		register size_t hash = 0;
		size_t ch;
		for (long i = 0; i < s.size(); i++)
		{
    
    
			size_t ch = s[i];
			if ((i & 1) == 0)
			{
    
    
				hash ^= ((hash << 7) ^ ch ^ (hash >> 3));
			}
			else
			{
    
    
				hash ^= (~((hash << 11) ^ ch ^ (hash >> 5)));
			}
		}
		return hash;
	}
};

struct DJBHash
{
    
    
	size_t operator()(const string& s)
	{
    
    
		register size_t hash = 5381;
		for(auto ch : s)
		{
    
    
			hash += (hash << 5) + ch;
		}
		return hash;
	}
};

template<size_t N, class K = string, class Hash1 = BKDRHash, class Hash2 = APHash, class Hash3 = DJBHash>
class BloomFilter
{
    
    
public:
	void set(const K& key)
	{
    
    
		size_t hash1 = Hash1()(key) % N;
		_bs.set(hash1);
		size_t hash2 = Hash2()(key) % N;
		_bs.set(hash2);
		size_t hash3 = Hash3()(key) % N;
		_bs.set(hash3);
	}

	bool test(const K& key)
	{
    
    
		size_t hash1 = Hash1()(key) % N;
		if (!_bs.test(hash1))
		{
    
    
			return false;
		}
		size_t hash2 = Hash2()(key) % N;
		if (!_bs.test(hash2))
		{
    
    
			return false;
		}
		size_t hash3 = Hash3()(key) % N;
		if (!_bs.test(hash3))
		{
    
    
			return false;
		}
	}
private:
	bitset<N> _bs;
};

void test_bloomfilter()
{
    
    
	BloomFilter<100>;
}

inserte la descripción de la imagen aquí

El número de funciones hash representa cuántos bits se asignan a un valor. Cuantas más funciones, menor será la tasa de falsos positivos y consumirá más espacio. El equilibrio de k y m se muestra en la siguiente figura.

inserte la descripción de la imagen aquí

Según nuestro código, se utilizan tres funciones hash, por lo que k == 3, y el m/n calculado es más de 4 puntos, así que modifique el código para abrir 4 veces el espacio a la vez.

class BloomFilter
{
    
    
public:
	void set(const K& key)
	{
    
    
		size_t len = N * _X;
		size_t hash1 = Hash1()(key) % len;
		_bs.set(hash1);
		size_t hash2 = Hash2()(key) % len;
		_bs.set(hash2);
		size_t hash3 = Hash3()(key) % len;
		_bs.set(hash3);

		cout << hash1 << " " << hash2 << " " << hash3 << endl;
	}

	bool test(const K& key)
	{
    
    
		size_t len = N * _X;
		size_t hash1 = Hash1()(key) % len;
		if (!_bs.test(hash1))
		{
    
    
			return false;
		}
		size_t hash2 = Hash2()(key) % len;
		if (!_bs.test(hash2))
		{
    
    
			return false;
		}
		size_t hash3 = Hash3()(key) % len;
		if (!_bs.test(hash3))
		{
    
    
			return false;
		}
	}
private:
	static const size_t _X = 4;
	bitset<N * _X> _bs;
};

código de prueba

void test_bloomfilter()
{
    
    
	BloomFilter<100> zs;
	zs.set("sort");
	zs.set("bloom");
	zs.set("string");
	zs.set("test");
	zs.set("etst");
	zs.set("estt");

	cout << zs.test("sort") << endl;
	cout << zs.test("bloom") << endl;
	cout << zs.test("string") << endl;
	cout << zs.test("test") << endl;
	cout << zs.test("etst") << endl;
	cout << zs.test("estt") << endl;
	cout << zs.test("zyd") << endl;
	cout << zs.test("int") << endl;
	cout << zs.test("float") << endl;
}
//测试误判率
void test_bloomfilter2()
{
    
    
	srand(time(0));
	const size_t N = 10000;
	BloomFilter<N> bf;
	vector<string> v1;
	string url = "https://blog.csdn.net/kongqizyd146?spm=1011.2415.3001.5343";
	for (size_t i = 0; i < N; ++i)
	{
    
    
		v1.push_back(url + to_string(i));
	}
	for (auto& str : v1)
	{
    
    
		bf.set(str);
	}
	vector<string> v2;//v2和v1相似,但不一样
	for (size_t i = 0; i < N; ++i)
	{
    
    
		string url = "https://blog.csdn.net/kongqi146?spm=1011.2415.3001.5343";
		url += to_string(999999 + i);
		v2.push_back(url);
	}
	size_t n2 = 0;
	for (auto& str : v2)
	{
    
    
		if (bf.test(str))
		{
    
    
			++n2;
		}
	}
	cout << "相似字符误判率: " << (double)n2 / (double)N << endl;
	vector<string> v3;
	for (size_t i = 0; i < N; ++i)
	{
    
    
		string url = "zhihu.com";
		url += to_string(i + rand());
		v3.push_back(url);
	}
	size_t n3 = 0;
	for (auto& str : v3)
	{
    
    
		if (bf.test(str))
		{
    
    
			++n3;
		}
	}
	cout << "不相似字符串误判率: " << (double)n3 / (double)N << endl;
}

inserte la descripción de la imagen aquí

100.000 datos

inserte la descripción de la imagen aquí

Puede controlar cuántas posiciones se asignan para controlar la tasa de falsos positivos.

2. Eliminar

La función de eliminación no se puede eliminar directamente. Puede utilizar la idea de contar y utilizar bits para contar. 1 bit representa 2 y 2 bits representan 4. Sin embargo, de hecho, el filtro Bloom no admite la eliminación, es decir, No consideres eliminarlo.

3. Corte de hachís

Dos archivos, cada uno con 10 mil millones de consultas, y ahora hay memoria 1G, ¿cómo encontrar la intersección de archivos? Dar algoritmos exactos y aproximados. consulta como una cadena.

Suponiendo que se necesitan 50 bytes para encontrar una cadena, 10 mil millones son 500 mil millones de bytes, que son aproximadamente 466 G, y dos archivos son 932 G.

La solución aquí es una segmentación hash i = HashFunc(consulta) % 1000; HashFunc puede usar una función hash. El número i calculado por cada consulta se ingresa en el archivo pequeño de Ai y el otro archivo se coloca en el archivo pequeño de Bi. La premisa aquí es dividir los dos archivos en varios archivos pequeños, luego encontrar la intersección de A1 y B1, encontrar la intersección de A2 y B2 y finalmente obtener la intersección general. La misma consulta en los archivos A y B irá a archivos pequeños con el mismo número.

Este método también tiene algunos defectos. Debido a que la longitud de cada cadena es diferente, no se puede controlar que el tamaño de cada archivo pequeño sea el mismo, es decir, se produce un conflicto y es posible que el problema no se resuelva cambiando la función hash.

En un solo archivo hay una gran cantidad de consultas repetidas
En un solo archivo hay una gran cantidad de consultas diferentes

En el primer caso, los que se repiten no necesitan almacenarse nuevamente, por lo que puede usar /unordered_set/set para leer la consulta del archivo en secuencia e insertarla en el conjunto.

Si todo el archivo pequeño se puede insertar con éxito después de leerlo, es el caso 1; si se produce una excepción durante el proceso de inserción, es el caso 2, cambie a otra función hash, divida nuevamente y luego encuentre la intersección.

Si la inserción establecida ya existe, devuelve falso. Si no hay memoria, se generará una excepción bad_alloc y el resto estará vacío.

solicitud

Dado un archivo de registro con un tamaño de más de 100G, la dirección IP se almacena en el registro y se diseña un algoritmo para encontrar la dirección IP con más apariciones y la K IP superior.

Aquí está el corte hash, dividido en 500 archivos pequeños, lea los datos uno por uno, i = HashFunc (ip)% 500, esta ip es el i-ésimo archivo pequeño; inserte archivos pequeños uno por uno, use el mapa para contar el número de Ocurrencias de IP; proceso estadístico, se produce una excepción de memoria, lo que indica que un solo archivo pequeño es demasiado grande y hay demasiados conflictos, y es necesario cambiar la función hash y volver a aplicar hash al archivo; si no se lanza ninguna excepción, el Las estadísticas son normales y la más grande se registra una vez completadas las estadísticas. Limpie el mapa y luego vaya al siguiente archivo pequeño. Para encontrar k superior, coloque cada valor máximo obtenido en el montón.

La misma IP debe ingresar al mismo archivo pequeño y, al leer un solo archivo pequeño, puede contar la cantidad de apariciones de IP.

Finalizar.

Supongo que te gusta

Origin blog.csdn.net/kongqizyd146/article/details/130792046
Recomendado
Clasificación