题目介绍
这是题目原文。
注意:在拉取项目的时候需要注意拉取2024fall
的Tag
,本人血泪教训!
本题是关于HyperLogLog
的具体实现,其介绍可以看这个视频进行了解。简单来说HyperLogLog
可以在一个非常小的固定内存下(一般为十几KB)非常快速地估算出大量数据的唯一元素数量,并且其可以分别对多个数据库独立估算,还不会损失精度。
比如要统计访问网站的唯一用户数,如果用精确算法,需要对数据库中的所有数据进行比对,这会占用非常大的内存且非常耗时,尤其是在分布式存储的情况下。但我们在绝大多数的时候并不需要一个精确的值,因此可以使用HLL
对其进行估算(虽说是估算,但其误差也非常小)。
这道题主要检查了学生的C++
基础,以及对C++
项目的开发和调试能力。
文章末尾有本人的项目代码地址,文章内容主要讲述思路和一些细节不会出现代码,大家各取所需。
Task#1
Task#1
是实现基本的HLL
算法。
所需修改的文件目录为:
src/include/primer/hyperloglog.h
src/primer/hyperloglog.cpp
测试文件的目录为:
test/primer/hyperloglog_test.cpp
原始代码中已经给出了一些变量和方法的定义及实现,需要我们进行完善和补全。
hyperloglog.h
中的变量部分给出了最终的结果cardinality_
和计算常量CONSTANT
,缺少了题目中所提及的b
、m
、p
,b
和p
可以设置为uint8_t
类型的参数,因为其值的范围在0到63之间,m
可以设置为uint8_t
的数组,原因同上。
CalculateHash()
:将传入的值转换为哈希值的方法。GetCardinality()
:返回最终结果。
hyperloglog.cpp
是该算法主要的函数实现。以下是各个方法的简介:
HyperLogLog(inital_bits)
:构造函数,需要我们根据传入的b
初始化我们内部的b
和m
。AddElem(val)
:计算出传入值的p
并将其存在m
中(最主要方法)。ComputeCardinality()
:根据公式计算最终结果。ComputeBinary()
:将哈希值转换为64位二进制流。PositionOfLeftmostOne()
:计算出p
值(二进制流中最左侧 1 的位置)。
我这里添加了一个函数GetBucketValue()
,用来计算p
应该存在m
的哪个位置中。
接下来我会大概说一下我的解答中每个方法的实现。
HyperLogLog(inital_bits)
根据传入的b
初始化我们内部的b
和m
。需要对b
的值进行判断,看是否是小于0的值,如果小于0则直接return
,否则对我们内部的b
进行赋值。同时需要根据b
重新设置我们m
的大小。
注意: 如果将b
设置为size_t
或者unit_t
类型,需要注意其数值不会小于0,容易因此产生bug。
ComputeBinary()
可以直接使用std::bitsize<64>()
方法,将传入的哈希值转换为64位二进制流。需注意,这里转换后的值第0位是低位,第63位是高位,不要搞反了。
GetBucketValue()
我们需要从后往前获取b
位二进制流的内容,计算这个二进制数在十进制下是几。可以通过位操作符<<
进行计算。
PositionOfLeftmostOne()
由于Task#1
是计算高位1
的位置,高位的前b
位又被用来计算插入位置。所以我们需要从第63 - b
位开始,从高到低查找二进制流第一个1
的位置。
AddElem(val)
有了前面几个方法,这个方法实现起来就比较简单了。分别调用前面的几个方法,得到参数的哈希值、64位二进制流、插入m
的位置、高位1
的位置p
,就只需将当前p
和m
上的值比较一下大小,将大的存入就可以了。
ComputeCardinality()
根据公式计算最终结果,可以先算出分母的总数,再计算整个公式,可以使用static_cast<size_t>()
方法保留结果的整数部分。进行幂计算时可以使用1.0 / std::pow(2, num)
进行计算。
Task#2
要求我们实现HyperLogLog
算法的密集布局,整体与Task#1
类似,不过这里是从低位开始计算0
的数量,并且如果数量超过15
,需要将其拆分成高3位和低4位分别进行存储。
所需修改的文件目录为:
src/include/primer/hyperloglog_presto.h
src/primer/hyperloglog_presto.cpp
hyperloglog_presto.h
文件多出了dense_bucket_
和overflow_bucket_
两个参数,前者和前面的m
基本相同,后者是在发生溢出时存储高3位的桶。
整体思路没有什么变化,只有以下几点需要变化:
PositionOfLeftmostOne()
方法需要改为从低到高计算0的个数。- 获取到0的个数
p
后,需将其拆分为高3位和低4位。可以使用与操作符&
进行处理。 - 在对比
p
和插入位置的值,以及计算最终结果时,要将dense_bucket_
和overflow_bucket_
中相应位置的值进行合并,转换为十进制进行比较。转换为十进制可以使用to_ulong()
方法,合并操作可以使用或操作符|
。
测试
先将test/primer/hyperloglog_test.cpp
中的测试函数第二个参数的DIABLE_
去掉。
mkdir build && cd build
cmake -DCMAKE_BUILD_TYPE=Debug ..
make -j$(nproc) hyperloglog_test
./test/hyperloglog_test
正常应该如图所示:
提交
cd ..
mkdir build_rel && cd build_rel
cmake -DCMAKE_BUILD_TYPE=Release ..
make -j$(nproc)
make format
make check-clang-tidy-p0
make submit-p0
如果有格式问题clang-tidy
会报错。我这里本地没有报错,但是提交到平台上就会报格式错误,可能是工具版本的问题。
执行完成后会在根目录生成一个压缩包,上传该压缩包等待平台打分。
正常如图所示:
代码地址:https://github.com/GCW-kuku/bustub