1. 算法介绍
位图算法享有速度之巅(Speed Zenith)的名号,其采用指针标记和位运算的存储方式将数据压缩装载,提高运算效率同时还能节省空间,这样的功能无不令算法界瞠目结舌。使用位图算法可以用一个long long类型的变量存下一整副扑克牌并绰绰有余。位图算法在读取数据时可以通过指针快速定位到指定的位置,判断当前位的二进制状况,从而立刻获取数据在数据集中的存在状态。位图算法的入门要求使用者对数据结构和计算机组成原理这两门课程有一定程度的理解。
2. 算法题目
题目:一棋牌室举行斗地主比赛,为了比赛快速进行,现有8600桌斗地主牌局同时进行,所有牌桌使用的牌类型都相同,每张牌内部都镶嵌了专属的id,比赛期间,裁判接到紧急暂停,收取了所有比赛选手手里和牌桌上的牌,经机器统计,本次比赛过程中共计丢失了3668张牌,相关人员搜查在场所有比赛人员后,找到若干名参赛选手身上额外携带镶嵌了专属id的扑克牌。请设计算法,以最快的速度判断这些选手身上携带的牌是否是本场比赛中途丢失的。【注:每场比赛的扑克牌id都是从0开始数,然后依次累加1,直到最后一张】【注:比赛规定选手中途不允许偷偷带走扑克牌,但在赛后可以带走收藏】
3. 题目分析
本题是一道经典的查找类型的题目,难点在于并没有给出丢失的牌是什么,而且当前手中持有的未丢失牌数量巨大,逐个判断起来非常吃力,贸然使用malloc或new分配大空间,想要以空间换时间的做法很容易导致一些低性能的机器崩溃,此时就需要将思路转移到既能提高判断效率,又不至于过度浪费空间的算法上,所以本题最适合采用经典的位图算法。
4. 算法原理
位图算法需要使用指针定位到具体的字节上,一般为了便于计算,都采用的是char类型的指针进行定位,因为char类型只有一个字节,不容易混淆。通过malloc函数或者new操作符为char类型的指针分配足够大的空间后,就可以将这些空间从字节拆解成位,原本只能存放1个数据的int类型经过拆解后足足可以存放32个数据,同样原本只能存放一个数据的char类型现在可以存放8个数据。拆分完成后,再将需要存储的数据集通过位运算的方式记录到分配好的每一位上,就形成了最初级的位图算法,如下图所示:
此时想必各位读者已经看出位图算法的端倪了,无非是将之前没有充分利用到的字节空间重新分割利用罢了,问题的难点就在于如何使用指针进行每个数据的定位,并且写入或判断呢。方法和拓展功能有很多,本次介绍最入门的一种方式,定位:指针头 + 数据 / 8 、写入:指针解引 | (数据 % 8) 、读取:指针解引 & (数据 % 8),可能使用语言叙述比较抽象,这里提供一个demo供各位尽量参考。代码如下:
void init(char* data, int len) {
// 假设:能够被3整除的数,都在这个集合中.
int n = len * 8;
for (int i = 0; i < n; i++) {
if (i % 3 == 0) {
// 计算这个位对应哪个字节
char* p = data + i / 8;
*p = *p | (1 << (i % 8));
}
}
}
这里的位运算究竟是左移还是右移取决于机器的存储方式,正常电脑左移即可。 我这里补充一张图,供大家再理解。
5. 解题思路
明白了位图算法的原理后,这道算法题解决起来就势如破竹了,首先是扑克牌的装载,设计一个loading函数, 记得加上头文件<fstream>,代码如下:
void loading(char* poke) {
// 准备装载变量
int max = 8600 * 8 - 3668;
int temper = 0;
// 打开准备好的扑克牌数据文件
ifstream file;
string filename = "poke.txt";
file.open(filename);
if (file.fail()) {
cout << "打开扑克牌数据文件出错" << endl;
system("pause");
exit;
}
//利用指针和位运算装载扑克牌
for (int i = 0; i < max; i++) {
file >> temper;
char* ptr = poke + temper / 8;
*ptr = *ptr | (1 << (temper % 8));
}
//关闭文件
file.close();
}
然后是判断函数,同样采用位运算,不过要注意返回类型,判断函数checking代码如下:
bool checking(char* poke, int id) {
// 定位到指定的字节
char* ptr = poke + id / 8;
// 判断该字节中指定的位是否为1
bool temper = *ptr & (1 << (id % 8));
return temper;
}
最后完成主函数调用即可,代码如下:
int main(void) {
// 开辟扑克牌存储空间
int n = 8600 * 54;
int length = n / 8 + 1;
char* poke = new char[length];
memset(poke, 0, length);
// 装载扑克牌数据
loading(poke);
// 开始监测
int pokeId;
while (1) {
cout << "这张牌内部镶嵌的Id是:";
cin >> pokeId;
if (checking(poke, pokeId)) {
cout << "这张牌不是本场比赛丢失的扑克牌" << endl;
}
else {
cout << "这张牌是本场比赛丢失的扑克牌!!" << endl;
}
}
return 0;
}
6. 总结
位图算法有非常多的扩展,本次内容只是介绍了最入门的一种用法,用来解决一些基本的算法题面试题足够了,当然C/C++中还包含着无数其他的奇技淫巧供我们发掘和惊叹,这正是C++的魅力所在。借用C++之父的一句话:世界上只有两门编程语言,一门整天被骂的语言,和一门没人用的语言。