一样的生日

生日悖论,如果房间内有23人以上,那么至少两人生日相同的概率要大于50%。

我们来写个代码,模拟下结果。当然,可以从概率学上,写数学公式进行计算,但是,先假设我们不知道概率学,纯粹就是用计算机代码去模拟。

我们先看参考代码:

def create_birthday_list(people_count=23):
    birthday_list = []
    for i in range(people_count):
        birthday_list.append(random.randint(1, 365))
    return birthday_list

def simulate(total_times=100000, people_count=23):
    hit_times = 0
    for i in xrange(total_times):
        birthday_list = create_birthday_list(people_count)
        if has_same_birthday(birthday_list):
            hit_times += 1
    percent = round(100 * hit_times/float(total_times), 2)
    return percent

if __name__ == '__main__':
    t1 = time.time()
    percent = simulate()
    time_cost = time.time() - t1
    print('percent: {0}%, time cost: {1}'.format(percent, time_cost))

其中 has_same_birthday 还没有定义,我们先从比较自然的方式去构建这个函数:

def has_same_birthday(birthday_list):
    for day in birthday_list:
        if birthday_list.count(day) > 1:
            return True
    return False

本质上就是一个 list 内有没有重复的元素,所以我们还可以使用 set 这个数据类型自动去重,然后比对 length 是否变化了,变化了自然就是有重复的生日:

def has_same_birthday(birthday_list):
    if len(birthday_list) != len(set(birthday_list)):
        return True
    else:
        return False

上面两种 has_same_birthday,性能相差并不明显,当然使用 set 的会快一些,而且性能是稳定的,通过 for 循环,再 count 一个元素是否大于 1,性能是不稳定的,但是总体上又相差不大。
两种方式,都挺值得参考,一个是自然的逻辑,一个则是换下角度去看待并利用 Python 自带的数据类型来完成。


也有人或许会写成类似下面的代码:

def has_same_birthday(birthday_list):
    for i, i_day in enumerate(birthday_list):
        for j, j_day in enumerate(birthday_list):
            if i_day == j_day and i != j:
                return True
    return False

如何判定一个 list 内有无重复元素? 给这个 list 做两次循环,相当于两个一模一样的 list 进行逐个比对。这种双重遍历,其实是比较高级的用法,但相对而言,大多数时候是用不到的。
新手也会自然地用到高级一些的写法,可能纯属巧合,巧合之后,又很可能是滥用,显得逻辑奇葩。比如此处多重遍历在一起,它的性能明显会低很多,除了增加费解之外,又没有其它好处。但另外一方面,性能 低很多 也未必真的会低多少,你可以自己跑一跑代码,看最终耗费的时间,就明白了。
这也就是我们不建议把代码写成 倒金字塔 的一个重要原因,对于初学者来说,倒金字塔的代码,基本上意味着这里可能存在问题。
但是,事无绝对,把它当做一件事,但也不要因此就用这条规则束缚了自己。