算法基础提升——大数据问题和位运算(C++)

大数据题目解题技巧:

1)哈希函数可以把数据按照种类均匀分流

2)布隆过滤器用于集合的建立与查询,并可以节省大量空间

3)一致性哈希解决数据服务器的负载管理问题

4)利用并查集结构做岛问题的并行计算

5)位图解决某一范围上数字的出现情况,并可以节省大量空间

6)利用分段统计思想、并进一步节省大量空间

7)利用堆、外排序来做多个处理单元的结果合并

一、某一范围上数字的出现情况

1.1 问题介绍

32位无符号整数的范围是0~4,294,967,295,现在有一个正好包含40亿个无符号整数的文件,所以在整个范围中必然存在没出现过的数。可以使用最多1GB的内存,怎么找到所有未出现过的数?

【进阶】 内存限制为 10MB,但是只用找到一个没出现过的数即可。

1.2 解题思路

基础问题很简单,使用位图表示法。我们只需要开辟一个数组,大小为2^{32}-1,当一个数出现过,就将这个数字对应的数组空间涂黑,一个字节8bit,也就是一个字节可以表示8个数,用01表示出现过没有就行;最后遍历数组,未涂黑的空间对应的数即为未出现过的数。使用这种方式大约需要500M的内存。

进阶问题需要使用到词频统计的方法。假设开辟一个512*4B大小的数组,将2^{32}-1个数分成512份,那么平均每组应该有8388608个数;接下来遍历数组,将每个数除以8388608取商,出现商为0的数时,arr[0]++,以此类推,遍历结束后若数组中某个空间的数值少于8388608,那么在该范围内肯定有未出现的数,将这个空间再次分为512份,重复这一步骤,即可找到未出现的一个数。

(不管给多少空间,用此办法都可以)首先在3KB(3000/4=700多,也就是能申请长度700多的整型数组 )上找到一个离2的次方最接近的一个数,也就是2^9 = 512 离得最近(为什么要2的次方,因为0~2^32次方个数能够按照这个数整分。也就是2^32  /  2^9  = 8388608 )。申请一个数组长度为512的。然后每一个位置上表示的含义是:40亿的数字按照512的大小均分,分成了8388608份,那么0~8388608 这些数就放在第一个位置上(8388608 ~8388608x2是第二个位置表示的值,怎么统计这个范围的数出现了几次?用这个数除以8388608,比如 1/8388608 = 0,那么1这个数就是属于这个数组中的0号位置,就把0号位置的次数加1),然后统计第一个位置上这个数出现的次数是不是8388608个。如果不是,说明一定在这个范围内有一个数字没有出现,然后再把8388608按照512的大小进行均分,重复上面的操作,一定可以找出那个没有出现的数字。

如果只给3个变量,怎么找出?把40亿进行二分,少数字那边继续二分找。同样的原理。


二、找出重复的URL

2.1 问题介绍

有一个包含100亿个URL的大文件,假设每个URL占用64B,请找出其中所有重复的URL。

【补充】 某搜索公司一天的用户搜索词汇是海量的(百亿数据量),请设计一种求出每天热门Top100 词汇的可行办法。

2.2 解题思路

很显然,基础问题可以使用布隆过滤器来解答。或者用哈希函数然后再取模,进行分类,放到一堆小文件中去,然后再从一堆小文件中继续哈希。

进阶问题采用来解决。首先将所有词用哈希函数然后取模,将所有词汇分为若干组,并且保证所有相同的词汇都在同一组;然后对于每个组进行词频统计,并且对统计数据进行大根堆调整,将出现次数多的数放在堆顶;建立一个总堆,把所有组的堆顶词放在总堆中再次比较,先找出词频最高的词汇,将其出堆;然后找到该词汇原来所在的堆,将该堆中词频第二的词汇放入总堆中比较,以此类推。


三、找到出现了N次的数

3.1 问题介绍

32位无符号整数的范围是0~4294967295,现在有40亿个无符号整数,可以使用最多1GB的内存,找出所有出现了两次的数。

【补充】 可以使用最多10MB的内存,怎么找到这40亿个整数的中位数?

3.2 解题思路 

3.2.1 基础问题

(1)哈希函数分流,然后不断二分(万能方法)

(2)位图表示法:建立一个数组,用2bit来表示一个数,当这个数出现零次时,标记为00;当这个数出现一次时,标记为01;当这个数出现两次时,标记为10;出现两次以上时,标记为11。可以将位图视为一个数组,下标表示这个数,数组空间表示出现次数,不过每个数组空间只有2bit容量。内存要求大约为0.9G。

3.2.2 进阶问题

  1. 首先计算10kb内存可以申请的unsigned int类型数组的大小,10KB/4B = 2500,取2^11=2048,建立arr[2048];
  2. 然后将整个范围0~4294967295均匀分为2048组,当该组中的数字出现时,数组对应空间加一;
  3. 将arr[0],arr[1]……累加起来,找到累加和为20亿时所在的数组空间,此时锁定了中位数出现的范围;
  4. 将该范围再分为2048份,重复上面的步骤,找到中位数。

例如:arr[1]+……+arr[499]=18亿,arr[500] = 3亿;我们知道中位数在arr[500]里面,我们只要找到arr[500]里面的第2亿个数即可;重复这一步骤,最后能锁定一个确切的数。


四、大量数据的排列问题

4.1 问题介绍

有10GB大文件里面存放着无序有符号的一些整数,范围为0 ~ 2^{32}-1,给你5GB空间,如何把无序数序列变成有序序列。

4.2 解题思路

思路:(先分桶再排序)

1)用小根堆(每一个元素有数值,次数两个属性)来作为媒介遍历每一个文件段(5GB可以申请可以存储多大的小根堆),每一个小段进入小根堆后,输出小根堆就可以了,然后总结果就是依次排序。简单来说,先将所有数分为n组,那么首先我遍历大文件,找到所有属于第一组的数,放入小根堆,排序后输出;再找第二组的数,放入小根堆,排序后输出……(不会某一组中的数过多,因为我们记录了每个数的出现次数,所以实际上堆中的数字是范围内的所有数,理论上数的数量可以无限多)。

2)用大根堆。简单例子:总共有10个数,创建一个只存放3个数的大根堆。然后依次遍历这十个数,这个大根堆里面存放的是10个数中最小的3个数。然后输出有序的最小的3个数。此时记录排完序的3个数中最大的那个数Y,清空大根堆后,继续去遍历这10个数,但是小于Y的就不要遍历了,然后找出最小的3个数,然后输出,一直重复就可以了。


 五、位运算实现大小比较

5.1 问题介绍

给定两个有符号32位整数 a和b,返回a和b中较大的。

【要求】 不用做任何比较判断。

5.2 解题思路

  1. 将两个数相减,判断正负,然后使用互斥条件输出较大数。
  2. 因为可能发生溢出情况,所以需要额外判断两个数是是否同符号。

5.3 代码实现

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>

using namespace std;

//与1异或函数:1->0 ; 0->1
int flip(int n)
{
	return n ^ 1;
}

//判断正负:n非负,返回1;n为负数,返回0
int sign(int n)
{
	return flip((n >> 31) & 1);//取出符号位
}

//输出较大的值(可能溢出)
int getMax1(int a, int b)
{
	int c = a - b;
	int scA = sign(c);//a>=b,scA=1;a<b,scA=0
	int scB = flip(scA);//a>=b,scB=0;a<b,scB=1
	return a * scA + b * scB;
}

int getMax2(int a, int b)
{
	int c = a - b;
	int sa = sign(a);
	int sb = sign(b);
	int sc = sign(c);
	int difSab = sa ^ sb;//a和b符号不一样为1,一样为0
	int sameSab = flip(difSab);//a和b符号不一样为0,一样为1
	int returnA = difSab * sa + sameSab * sc;//符合不一样,取决于a的正负;否则取决于c的正负
	int returnB = flip(returnA);
	return a * returnA + b * returnB;
}

int main()
{
	int a = 56874;
	int b = 73814;
	cout << getMax1(a, b) << endl;
	cout << getMax2(a, b) << endl;
}

 六、位运算判断是否为幂函数

6.1 问题介绍

判断一个32位正数是不是2的幂、4的幂。

6.2 解题思路

6.2.1 是否是2的幂

  1. 如果是2的幂,那么这个数的二进制形式中应该只有一个1,例如4->10;16->10000等;
  2. 如果这个数是2的幂,那么这个数减1以后应该会连续出现多个1,例如16-1 -> 01111;
  3. 那么如果数n是2的幂,n&n-1的值应该为0,例如 10000 & 01111 = 0。

6.2.2 是否是4的幂

  1. 如果是4的幂,那么这个数的二进制形式中应该只有一个1,并且1的右边只能有偶数个0;
  2. 将这个数与0101 0101 0101 0101 0101 0101 0101 0101 0101 做与运算,如果结果为1,那么表示1出现在了对应的位置上,是4的幂;反之则不是。

6.3 代码实现

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>

using namespace std;

bool is2Power(int n)
{
	return (n & (n - 1)) == 0;
}

bool is4Power(int n)
{
	return (n & (n - 1)) == 0 && (n & 0x55555555) != 0;
}

int main()
{
	int a = 32;
	int b = 1035;
	int c = 256;
	cout << is2Power(a) << endl;
	cout << is2Power(b) << endl;
	cout << is2Power(c) << endl;
	cout << is4Power(a) << endl;
	cout << is4Power(b) << endl;
	cout << is4Power(c) << endl;
}

七、位运算表示加减乘除

7.1 问题介绍

给定两个有符号32位整数a和b,不能使用算术运算符,分别实现a和b的加、减、乘、除运算

【要求】 如果给定a、b执行加减乘除的运算结果就会导致数据的溢出,那么你实现的函数不必对此负责,除此之外请保证计算过程不发生溢出

7.2 解题思路

例如:a=01101,b=00111,a+b=10100

7.2.1 加法

首先要明确的是a+b等同于重复执行  a^b+(a&b)<<1 ,因为a^b意为无符号相加,(a&b)<<1表示的是进位结果;我们需要通过不断重复 a^b+(a&b)<<1 直到消除进位。

例如a+b = 01101 + 01101 = 00000 + 10100 = 10100 + 00000 = 10100,得到结果。

uint add(uint a, uint b)
{
	int Sum = a;
	while (b != 0)//进位信息为0时结束
	{
		Sum = a ^ b;//无进位相加
		b = (a & b) << 1;//进位信息
		a = Sum;
	}
	return Sum;
}

7.2.2 减法

减法运算 a-b 可以理解为 a+(-b)。由于一个数的相反数等于这个数取反后加1,所以 a-b 可以表示为 a + (~b + 1)。

uint negNum(uint n)//相反数
{
	return add(~n, 1);
}

uint minus(uint a, uint b)//减法
{
	return add(a, negNum(b));
}

 7.2.3 乘法

模拟竖式计算即可。将b的每一位和a相乘,然后移位相加。

uint multi(int a, int b)
{
	int res = 0;
	while (b != 0)
	{
		if ((b & 1) != 0)
			res = add(res, a);
		a <<= 1;
		b >>= 1;
	}
	return res;
}

  7.2.4 除法

跟加法、减法一样,除法是乘法的逆运算,那么除法就是做有限次的减法,直到被减数小于减数为止。这种思路类似于乘法中的简单思路,问题同样是在被除数远大于除数时,需要做多次除法。
改良的除法思路如下,考虑到int型整数占32位,那么用i = 31开始比较被除数与除数的2i的大小,若被除数大,则在商处直接加上i从而减小减法的次数。

int Divide(int a, int b)
{
	int x = isNeg(a) ? negNum(a) : a;
	int y = isNeg(b) ? negNum(b) : b;
	int res = 0;
	for (int i = 31; i > -1; i = Minus(i, 1))
	{
		if ((x >> i) >= y)
		{
			res |= (1 << i);
			x = Minus(x, y << i);
		}
	}
	return isNeg(a) ^ isNeg(b) ? negNum(res) : res;
}

7.3 代码实现

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
using namespace std;

int add(int a, int b)//加法
{
	int Sum = a;
	while (b != 0)//进位信息为0时结束
	{
		Sum = a ^ b;//无进位相加
		b = (a & b) << 1;//进位信息
		a = Sum;
	}
	return Sum;
}

int negNum(int n)//相反数
{
	return add(~n, 1);
}

int Minus(int a, int b)//减法
{
	return add(a, negNum(b));
}

int multi(int a, int b)
{
	int res = 0;
	while (b != 0)
	{
		if ((b & 1) != 0)
			res = add(res, a);
		a <<= 1;
		b >>= 1;
	}
	return res;
}

bool isNeg(int n)
{
	return n < 0;
}

int Divide(int a, int b)
{
	int x = isNeg(a) ? negNum(a) : a;
	int y = isNeg(b) ? negNum(b) : b;
	int res = 0;
	for (int i = 31; i > -1; i = Minus(i, 1))
	{
		if ((x >> i) >= y)
		{
			res |= (1 << i);
			x = Minus(x, y << i);
		}
	}
	return isNeg(a) ^ isNeg(b) ? negNum(res) : res;
}

int main()
{
	int a = 77, b = 7;
	cout << add(a, b) << endl;
	cout << Minus(a, b) << endl;
	cout << multi(a, b) << endl;
	cout << Divide(a, b) << endl;
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值