ThoughtWorks代码挑战——FizzBuzzWhizz游戏 通用高速版(C/C++ & C#)

  最早看到这个题目是从@ 程序媛想事儿(Alexia)最难面试的IT公司之ThoughtWorks代码挑战——FizzBuzzWhizz游戏
开始的,然后这几天陆陆续续有N个小伙伴发表了自己的文章和代码,本来不想做些什么,但是看了这么多代码,总有点想写(射)点什么的欲望(你玩英雄联盟吗?玩的话,应该知道我说的是什么)。

  我说说我对这个题目的看法,当初看Alexia的文章时,也没有看得很仔细,甚至没有看这个题目的原出处,一边在玩英雄联盟,一边看了一下题目,Alexia并没有贴出相应的代码要求(我是后来看了大家的文章才看到,偶对什么拉勾网不怎么感冒),不过就我的尿性来讲,也不太会往设计模式方向走。就我一贯的思路和作风,还是会往算法和代码优化的方向走(这个题目的确没什么算法深度可言)。所以现在在我看来,这个题目可以有两个方向,一个是走算法以及优化的路线,另一个是设计模式的路线。用设计模式实现必然会损失一些性能。题目里的要求是最好有单元测试,也是很不错的idea。

  还有一点,看了这么多园子里的代码,没有一个实现可扩展的代码,即Fizz,Buzz,Whizz可扩展,特殊数(3,5,7)可扩展(很多人没实现扩展,但实现了改变特殊数的值),最大数(默认为100)可扩展(实现这个难度不大,但基本没人考虑,如果考虑最大数比较大的时候,则还是有一定技巧的,比如一些位操作,只不过看你追求的是什么,在规模比较小的时候,意义不大)。

  我们的思路是:实现高可扩展性,尽量简化逻辑,避免使用\(除法)、%(取余)(当前各式主流CPU整数除法都是比较慢的指令),避免使用高等函数(浮点),能够使用加法绝不使用乘法,用加法代替乘法(其实这个思想跟筛法求素数是一个道理,实现筛法的时候你会用乘法吗?)。唯一思路跟我比较接近的是
@黑耗子
FizzBuzzWhizz游戏的高效解法 ,其评论里的代码更是跟我的想法几乎一致。

  后记:后来我发现,编译器在处理固定数的除法上是有优化的,比如/10,
/3, /5等,可以转化成一次乘法和一次位移,所有除以固定数或对固定数取余还是可以接受的,但如果是除以一个变量或对一个变量取余,是不可取的。除以10的商可以由
Value / 10 ~= [ (Value * 0x66666667) / 2^32 ] / 2^2 ] 获得,可以参考 http://bbs.csdn.net/topics/320096074http://bbs.emath.ac.cn/thread-521-3-1.html (有原理解释)。

  下面谈谈我的代码的优势,或者说特点:高扩展性,我尽量考虑了扩展的可能性,不过由于内置数据类型的限制,目前最多只能支持64个特殊数,如果还想扩展,也不存在难度。几乎没有使用乘法,全部都是加减法,或者查表法代替,用空间换时间,没有使用高等(数学)函数,只有
left_start_num = special_num * integer_base10[digital];
这一个地方使用了乘法,当然把数字变成字符串时,fast_itoa_radix_10()函数里还是使用了/10,但是这个是所有方法不可避免的,而且我尝试用减法代替,效率很低。其实现代CPU的整数乘法已经很快了,还是可以值得考虑的。如果你的目标CPU乘法很慢的话,我的方法更有优势。

  好了,谈谈我的思路,其实很简单,100个学生报数,从1报到100,根据规则3、4,遇到3个特殊数的倍数的时候需报出该特殊数对应的字符(串),可叠加。那么这些报出来的字符串是这样的(跟黑耗子的很类似,不过后面还是有点区别的):

  1. Fizz

  2. Buzz

  3. FizzBuzz

  4. Whizz

  5. FizzWhizz

  6. BuzzWhizz

  7. FizzBuzzWhizz

  我们在最前面加一个0.
[空],把[空]看成000,把Fizz看成二进制的从右边数的第一位,把Buzz看成是二进制从右数的第二位,把Whizz看成是从右数的第三位,则该列表变为:

  • 0. [空] = 000

  • 1. 001

  • 2. 010

  • 3. 011

  • 4. 100

  • 5. 101

  • 6. 110

  • 7. 111

  这不是刚好是三位的二进制吗,如果特殊数有N个,则这里列表的长度为2^N,对于本题默认参数,即为2^3 =
8。所以所有的报数除了这8种(其中000这种是不会被报的)中的7种,再加上不符合规则3、4、5的报自己的顺序的数字本身(这个我们认为是同一种规则),归纳起来共8种不同的规则,我们对每个要报的数,给它一个这个规则的索引,即完成FizzBuzzWhizz游戏的处理(因0.
[空] 000这个规则用不到,故我们把它用在表示报数字本身这一规则),当要具体报数的时候,我们再去这8种规则里取具体要报的字符串或数字即可。

  我们为什么不直接输出字符串,而输出索引值?这个嘛,是因为我们这样做,可以更好的利用二进制的特性,再者可以实现先预计算,到显示的时候,你要哪个我给你显示哪个(虽然题目本身没有要求我们这么做),因为如果列表所有元素都转换成字符串,还是要花不少时间的。虽然也许理论上是多了一个从索引转换为字符串的步骤,但逻辑更清晰,所以很多人也是这么干的。而且这么做还会增加一定量的内存消耗,如果最大数很大的话,也是要值得考虑的问题。

  构造这个索引字符串列表很简单,把二进制和特殊数列表一一对应即可。具体值,参考以上两个列表。我们需要的是两个参数,特殊数类型的最大种类max_word_type,即可构造这个2^max_word_type的列表。

  然后,我们构造(设置)索引值列表,根据规则3、4、5,假设3个特殊数分别为a、b、c,得到下列优先级:

  1. Fizz      (数字里含有第一个特殊数a的);

  2. FizzBuzzWhizz  (同时是a,b,c倍数的数);

  3. FizzBuzz      (同时是a,b的倍数的数);

  4. FizzWhizz    (同时是a,c的倍数的数);

  5. BuzzWhizz   (同时是b,c的倍数的数);

  6. Fizz          
      (是a的倍数的数);

  7. Buzz         
      (是b的倍数的数);

  8. Whizz          (是c的倍数的数);

  9. 报自己的数字

  跟 @黑耗子
的方法相反,由于我们使用了二进制合并(或操作,也可以用加法),所以我们的顺序是从下而上的(跟黑耗子的文章评论里的代码方法一致,如果你不能理解我说的可以去看看那个代码),即优先处理优先级9,最后处理优先级1(后来我用代码测试了一下,一开始我也认为黑耗子的从上而下的顺序可能更好一点,但后来的实践表明,可能从下而上还稍快一点点,虽然后者重复写入的次数更多,前者每次写入都要检查一遍是否为空,不为空才写入,而后者完全是覆盖式的,不用先查询再写入)。由于我们用二进制合并的机制,所以优先级2-8可能算得上是同一个步骤处理的,不分先后,而且互相不受优先级影响,具体实现请看下面代码:


    // 所有sayword_index_list的默认值均为NORMAL_NUM_INDEX(0), 即默认是非特殊报数
for (num = 0; num <= max_number; ++num) {
sayword_index_list[num] = NORMAL_NUM_INDEX;
}

// 规则3, 4: 计算(合并)和设置所有特殊数的mask值
for (index = 0; index < max_word_type; ++index) {
// 取一个特殊数, 从后往前读
special_num = special_num_list[index];
// 如果特殊数不在[1, 9]范围内, 则认为是无效特殊数, 跳过
if (special_num >= 1 && special_num <= 9) {
// 该特殊数的mask值
mask = 1 << index;
for (num = special_num; num <= max_number; num += special_num) {
sayword_index_list[num] |= (index_mask_t)mask;
}
}
else {
special_num_list[index] = INVALID_SPECIAL_NUM;
}
}

  下面是优先级1的处理,这里可能是稍微复杂那么一点的地方,其实也很简单。规则5:学生报数时,如果所报数字包含了第一个特殊数,那么也不能说该数字,而是要说相应的单词,比如本例中第一个特殊数是3,那么要报13的同学应该说Fizz。
如果数字中包含了第一个特殊数,那么忽略规则3和规则4,比如要报35的同学只报Fizz,不报BuzzWhizz。也就是说,只要你的数字里面包含第一个特殊数3,那么就只报Fizz,Fizz在索引列表里的索引永远都是001,因为它永远都是第一个特殊数对应的字符串。下面就是怎么把所有含有第一个特殊数3的数都找出来,小伙伴们的代码里都有,而且很简洁,但不是通用的方法,我们来研究一下通用的算法:

  首先,我们先假设要报的最大数max_number 有 N 位数字,则这 N
位数字里,可能包含个位,十位,百位,千位,万位……等,我们以所有十位数包含3的数为例,从十位数3的右边来看,分别有30, 31, 32, 33, 34, 35,
36, 37, 38, 39, 十位数3的左边来看,有130, 131, 132, 133, , , 230, 231, 232, 233 , , , 330,
331, 332, 333, , ,
。我们看到,十位数3的右边一共只有30到39共10种数字,它是比十位数低的位数,从0开始循环,一直循环到9,然后跟3组合而成;然后看十位数3的左边,则是右边循环所得到的结果30到39的基础上,叠加上1,
2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, , , , ,
如此循环,两者再拼接而成,让组合而成的数不超过max_number即可,依此类推到百位,千位,万位等。我们总结出两条规律:1、对于某位数D(设D为从右边数的第P位数字),先搜索某位数上(个位,十位,百位等)的右边,去掉该位数以后,从0开始循环,一直循环到(10^P
- 1)为止,再与该D位数字组合而成,记做 D + 右边循环(低位循环)。2、在每一位 D + 右边循环 的基础上(上例中即为30, 31, 32, 33,
34, 35, 36, 37, 38, 39),再扩展左边循环,左边循环一样简单,从1开始循环,把左边循环的数跟D + 右边循环组合起来得到:左边循环 + D
+ 右边循环,让这个组合起来的数不超过max_number即可,超过的话,左边循环(高位循环)结束。具体实现请看下面代码:


    // 规则5: 根据此规则, 设置所有所报数字包含了第一个特殊数(first)的数,
// 先筛选所有个位数包含first的数, 再筛选所有十位数包含first的数,
// 依此类推, 百位, 千位..., 直接达到max_number
// FIRST_SPECIAL_NUM_FIXED_INDEX的值固定为1, 因为第一个特殊数(仅第一个)的mask就是1

// 第一个特殊数
special_num = special_num_list[0];

// 检查特殊数是否在[1, 9]范围内
if (special_num >= 1 && special_num <= 9) {
// 筛选所有个,十,百,千,万,十万,百万位数等包含first_special_num的数
for (digital = 0; digital < max_digital; ++digital) {
right_start_num = special_num * integer_base10[digital];
right_max_num = MIN(right_start_num + integer_base10[digital] - 1, max_number);
// 右边的步长恒为1
right_num_step = 1;
// 这里right_start_num虽然已经是first_special_num的倍数,
// 但是因为还要进行左边(高位)的循环, 所以不能省略
// 右边循环(该个,十,百,千,万位的右边, 即低位循环)
for (right_num = right_start_num; right_num <= right_max_num; right_num += right_num_step) {
sayword_index_list[right_num] = FIRST_SPECIAL_NUM_FIXED_INDEX;

if (digital < integer_base10_length) {
left_num_step = integer_base10[digital + 1];
left_start_num = right_num + left_num_step;
// 左边循环(该个,十,百,千,万位的左边, 即高位循环)
for (left_num = left_start_num; left_num <= max_number; left_num += left_num_step) {
sayword_index_list[left_num] = FIRST_SPECIAL_NUM_FIXED_INDEX;
}
}
}
}
}

后记:后来,我改进了一下这个代码,“if (digital < integer_base10_length) {”
这一句其实是可以拿出来单独处理的,因为大多数情况下是不会出现这种情况的,即当前位数超过int32范围内最大的10的幂次方数1000000000,此时左边是不用循环的。虽然这种情况出现的可能性非常低,但考虑完整性,还是保留了这个判断。而且分开处理后(拿到外层循环只判断一次,具体可以看GitHub的代码,为什么不贴在这里,因为我觉得这里贴的应该尽量简洁,能说明问题即可)也只需要一次if判断即可,而不必像原来那样在每次循环时都判断一次,耗时比原来略微减少了一点。

再后来,我重新研究了一下这段代码,你可以发现右边循环的数都是连续的,左边循环的数的间隔至少是10或10以上,所有我们何不先循环左边循环,内部再嵌套右边循环,这样写入时,右边循环的地址是连续地址,对写缓存比较有利;而且对于个位数来说,它是没有右边循环的,可以特别处理。遗憾的是,测试结果好像还比先右边循环再循环左边稍慢一点,由于我们测试的仅仅是max_number=100的情况,也许max_number更大的时候,先左后右的方法才能体现出优势。不过对于为什么会出现这种情况,我也是不太能够理解,也许还是跟缓存或编译器代码优化有关(编译器不是万能的,有时候可能编译出来的代码不一定是最优的),也可能是因为max_number=100的原因,因为即使先循环左边,间隔也不过才10而已,由于我们是重复循环10000次,数据基本上已经在缓存里了,所有缓存优化已经变得没有差别了,主要差别还是在代码的执行过程上。

具体的代码请看src\FizzBuzzWhizz\FizzBuzzWhizz_fast.cpp,里面两种方法都有写。

GitHub地址:https://github.com/shines77/FizzBuzzWhizz

代码分别有C/C++版本和C#版本,C#版本是和@黑耗子的版本一起测试的。

下面介绍一下C\C++的三个版本:

1.
FizzBuzzWhizz_stl():STL版本,使用std::string和std::vector,一开始没有优化的时候,更是用时500多ms,比C#版本还慢!后来改了一下变成现在的版本,但依然效率不够高;

2. FizzBuzzWhizz_sys():这个版本使用自己分配内存处理字符串数组,并且调用的系统自带的strcat(), strcpy(),
itoa()函数,效率依然不够高;

3. FizzBuzzWhizz_fast():这个版本重写了strcat(),
strcpy()以及itoa()函数,对这些函数做了一些优化,速度得到一定提升。

代码里有用到_aligned_malloc()和_aligned_free(),这个是微软自己写的对齐内存分配函数(其实自己实现也很简单,我有写过,懒得拿出来用了),如果你的系统不支持,可以在common.h找到_aligned_malloc的宏,默认的处理方式是,如果不是微软的编译器则会重定义到malloc和free。本来早期是想让内存地址对齐到16字节的,因为我考虑可能会用sse2来优化,后来发现对齐到4字节就够了(如果不用SSE的话),而malloc默认分配的的地址本来就是4字节对齐的(默认情况下,不另外设置),所以_aligned_malloc其实是多余的,不过如果是Visual
Studio,默认应该是支持的。如果不支持,也可以修改common.h里的代码,让宏生效。

从这个程序得到一个启示,就是不管是C++还是C#,很有可能在默认的情况,会有一些性能瓶颈,而你不知情,比如以上的STL就是,没想到效率差这么多。C#也没有那么慢,虽然归根结底是要比C++慢的,托管的代码有这样的效率还可以接受,对于这个算法,我没对Java做同样的测试。不过之前我做过一些测试,Java的HotSpot大多数情况下要比C#优秀,甚至一个简单的递归版fibonacci()比C++还要快,如果你想知道详情,可以私下交流。

另外,我还发现一个问题,就是每个算法单独运行比和其他方法一起运行的时候,耗时要多一些。后来想想,可能是CPU没有预热的原因,因为这种现象我在C++和C#里都发现了,现代的CPU和主板都具备在空闲的时间自己把CPU频率降下来,以节省能源,而我们的测试代码虽然循环了10000次,但耗时还是很短的,所以一开始执行代码的时候CPU并没有全速运行,所以我们写个无用的代码,让CPU唤醒一下,后面的测试就准确了,从此腿也不酸了,腰也不疼了。CPU预热我只在C++版本里写了,C#版本没弄,如果你有兴趣,可以自己加上去。

下面是程序运行的截图:

C\C++版本:

这个是C#版本的运行结果:

我的代码是那个“失业青年”,解释一下为什么要比“屌丝青年”的代码要慢,他们都是写死的版本,我的是通用版本,效率自然会低一些(因为做的工作要多一些),而且这是我比较早的版本转成C#的,后来的逻辑也没太大改动,我也就懒得改了。但是如果都写成通用版本,两者估计会更接近(他的代码求最大公约数的时候使用了除法),两者从逻辑上差别不算很大,所以也不会有很大出入。

关于怎么实现可扩展,为了方便跟黑耗子的代码做比较,默认只测试了跟他一样的数据,也没有写数据输入部分,为什么没写,首先,写这个不存在难度,只是没必要在这上面浪费时间。

那我们怎么实现扩展性?请看如下代码:


void FizzBuzzWhizz_Test_Wrapper_4(const int max_number, bool display_to_screen)
{
int index, max_num_list;
const string say_word_list[] = { "Fizz", "Buzz", "Whizz", "Zoozz" };
int special_num_list[][4] = {
{ 3, 5, 7, 9 },
{ 2, 4, 8, 9 },
{ 3, 6, 8, 9 },
{ 2, 2, 2, 2 },
};

// 最大say_word类型
int max_word_type = _countof(say_word_list);

// say_word组合后的最大字符长度
int max_word_length = 1;
for (index = 0; index < max_word_type; ++index)
max_word_length += say_word_list[index].length();

// 对齐到STRING_ADDR_ALIGNMENT(4字节)
max_word_length = (max_word_length + STRING_ADDR_MASK) & (~STRING_ADDR_MASK);

// 测试参数总组数
max_num_list = sizeof(special_num_list) / sizeof(special_num_list[0]);

if (display_to_screen)
max_num_list = 1;

for (index = 0; index < max_num_list; ++index) {

display_special_num_list(max_word_type, special_num_list[index]);

// 使用stl的std::string, 速度慢
FizzBuzzWhizz_stl_Test(max_number, max_word_type, max_word_length, say_word_list, special_num_list[index], display_to_screen);

// 使用自定义的string array, 采用系统自带的字符串处理函数, 中等速度
FizzBuzzWhizz_sys_Test(max_number, max_word_type, max_word_length, say_word_list, special_num_list[index], display_to_screen);

// 使用自己编写的字符串处理函数, 较快
FizzBuzzWhizz_fast_Test(max_number, max_word_type, max_word_length, say_word_list, special_num_list[index], display_to_screen);
}

printf("===============================================\n\n");
}

可以自行修改say_word_list和special_num_list,但要保证special_num_list每一行的长度必需跟say_word_list的个数相同。当然,say_word_list的长度在当前的代码里不是无限的,最多支持32个。如果需要,可以扩展为64个(通过修改common.h里的index_mask_t类型即可实现),如果超过64个,则需要写新的代码以实现mask的位操作。

时间: 2024-08-04 01:35:47

ThoughtWorks代码挑战——FizzBuzzWhizz游戏 通用高速版(C/C++ & C#)的相关文章

最难面试的IT公司之ThoughtWorks代码挑战——FizzBuzzWhizz游戏(C#解法)

原题 一开始楼主傻BB直接求解,思路惨不忍睹,各种循环啊有木有?还写了个计算是否整除扩展方法以为重复复用很了不起啊(其实是傻...) 二逼青年的代码 1   #region so1 2             int[] count = new int[100]; 3             string[] s4 = new string[] { "Fizz", "Buzz", "Whizz" }; 4             for (int

[转] AOJ 0525 Osenbei《挑战程序设计竞赛(第2版)》练习题答案

来自 码农场 ? AOJ 0525 Osenbei<挑战程序设计竞赛(第2版)>练习题答案 只把代码复制过来,原博的其他分析请看链接. 1 #include <iostream> 2 #include <bitset> 3 #include <algorithm> 4 5 using namespace std; 6 7 bitset<10000> cookie[10]; 8 9 ///////////////////////////SubMai

FizzBuzzWhizz游戏的js解法

5.6休假回来在园子里看到一道比较火的面试题:(链接地址),js解法代码: function test(a, b, c) { var s = [false, "Fizz", "Buzz", "FizzBuzz", "Whizz", "FizzWhizz", "BuzzWhizz", "FizzBuzzWhizz"], mod = function(n, a, rv)

C#游戏开发高速新手教程Unity5.5教程

C#游戏开发高速新手教程Unity5.5教程 试读文档下载地址:http://pan.baidu.com/s/1slwBHoD C#是微软公布的高级程序设计语言.这门语言和C语言一样,已经成为了大学计算机相关专业必学的一门语言.非常多初学这门语言的同学,还以为C#仅仅能开发Windows桌面应用程序了,写写ASP.NET站点. 本书将为读者纠正这样的观念,使用C#一样能够开发出精彩的游戏! 目  录 第1章  使用C#编写游戏前的准备 1 1.1  Unity概述 1 1.1.1  Unity简

坦克大战游戏1.0版

这是坦克大战游戏1.0版,以后会陆陆续续的完成... 准备素材(itank.jpg): 代码如下: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>坦克游戏1.0版</title> </head> <body onkeydown="doSomething(event)"

像写诗一样写代码,玩游戏一样的开心心情,还能领工资!

[软]上海-Qt侠 2017/7/12 16:11:20我完全是兴趣主导,老板不给我钱,我也要写好代码!白天干,晚上干,周一周五干,周末继续干!编程已经深入我的基因,深入我的骨髓,深入我的灵魂!当我解决一个程序问题,比别人玩王者荣耀拿了第一名还开心!你要想想,能干自己感兴趣的事情,像写诗一样写代码,玩游戏一样的开心心情,还能领工资!真他妈爽!没有比这个更爽的事情!

Pascal小游戏 俄罗斯方块怀旧版

俄罗斯方块怀旧版(注释版) {$APPTYPE GUI}{$MODE DELPHI}program WinPiece; usesWindows; constAppName = 'WinPiece';pm = 25; vardc : hdc;AMessage : Msg;hWindow: HWnd;hPen ,hBrush : longword;intNextPiece, intCurPiece,intTempPiece : longint;BigMap : array [0..11,-4..20

字符串HASH模板 取自挑战程序设计竞赛(第2版)

/*===================================================* 从b串中寻找和a串长度相同的子串,返回开始位置 不保证绝对正确,发生冲突概率为O(sqrt(n)), n为哈希函数的最大值 \*===================================================*/ #define ull unsigned long long const ull B = 1e8+7; /*according to the book*/

FizzBuzzWhizz 游戏

import java.util.Scanner; public class FizzBuzzWhizz { /** * 你是一名体育老师,在某次课距离下课还有五分钟时,你决定搞一个游戏.此时有100名学生在上课.游戏的规则是: * * 1 )你首先说出三个不同的特殊数,要求必须是个位数,比如3.5.7. * 2 )让所有学生拍成一队,然后按顺序报数. * * 3 )学生报数时,如果所报数字是第一个特殊数 * (3)的倍数,那么不能说该数字,而要说Fizz:如果所报数字是第二个特殊数 * (5)