剑指offer 6.4 抽象建模能力
参考:
面试题43:n个骰子的点数
题目:把n个骰子扔在地上,所有骰子朝上一面的点数之和为s。输入n,打印出s的所有可能的值出现的概率。
思路梳理
n个骰子的点数和的最小值为 n,最大值为 6n。
先统计出每一个点数出现的次数,然后把每一个点数出现的次数除以6^n,就能求出每个点数出现的概率。
本质是求数列f(n)=f(n-1)+f(n-2)+f(n-3)+f(n-4)+f(n-5)+f(n-6)
- 先把n个骰子分为两堆:第一堆只有一个,另一个有n-1个。
- 接下来把剩下的n-1个骰子还是分成两堆,第一堆只有一个,第二堆有n-2个。
- 定义一个长度为 6n 的数组,保存所有 s 可能出现的情况,和为 s 的点数出现的次数保存到数组第 s 个元素里。
**解法一:**基于递归求骰子点数,时间效率不够高。
- 递归结束的条件就是最后只剩下一个骰子。
**解法二:**基于循环求骰子点数,时间性能好。
- 定义两个数组,来分别保存两次循环求得的点数之和 s 对应的出现次数;
- 在一次循环中,第一个数组中的第n个数字表示骰子和为n出现的次数。
- 在下一循环中,我们加上一个新的骰子,此时和为n的骰子出现的次数应该等于上一次循环中骰子点数和为 n-1、n-2、n-3、n-4、n-5与n-6的次数的总和,
- 所以我们把另一个数组的第n个数字设为前一个数组对应的第n-1、n-2、n-3、n-4、n-5与n-6之和。
- 需要注意的是,每次使用新数组的时候,需要把数组所有位置清零,因为我们对于第n位进行的累加操作,如果之前第n位有数字但不清零的话,会导致结果偏大。
def PrintProbability(number):
maxValue = 6
prob = [[], []]
# 标记哪个数组为当前参照的数组
flag = 0
# 初始化第一个数组
prob[0] = [0]*(maxValue*number+1)
for i in range(1, maxValue+1):
prob[0][i] = 1
for time in range(2, number+1):
# 更新数组之前将本数组清零
prob[1-flag] = [0]*(maxValue*number+1)
# 更新数组
for pCur in range(time, time*maxValue+1):
diceNum = 1
# 累加另一个数组在当前索引的前六个元素
while pCur > diceNum and diceNum <= maxValue:
prob[1 - flag][pCur] += prob[flag][pCur-diceNum]
diceNum += 1
# 改变数组标记
flag = 1-flag
total = maxValue ** number
for i in range(number, maxValue*number+1):
# 计算每一种次数出现的概率,并且按照科学计数法的格式输出
ratio = prob[flag][i] / float(total)
print("{}: {:e}".format(i, ratio))
return
s = PrintProbability(5)
面试题44:扑克牌的顺子
题目:从扑克牌中随机抽5张牌,判断是不是一个顺子,即这5张牌是不是连续的。2~10为数字本身,A为1,J为11,Q为12,K为13,而大、小王可以看成任意数字。
思路梳理
- 可以把5张牌看成由5个数字组成的数组。大、小王是特殊的数字,我们不妨把它们都定义为0,这样就能和其他扑克牌区分开来了。
- 怎样判断5个数字是不是连续的,最直观的方法是把数组排序。
- 由于 0 可以当成任意数字,我们可以用0去补满数组中的空缺。如果排序之后的数组不是连续的,即相邻的两个数字相隔若干个数字,但只要我们有足够的 0 可以补满这两个数字的空缺,这个数组实际上还是连续的。
总结:
- 首先把数组排序,再统计数组中0的个数,最后统计排序之后的数组中相邻数字之间的空缺总数。
- 如果空缺的总数小于或者等于0的个数,那么这个数组就是连续的;反之则不连续。
- 最后,我们还需要注意一点:如果数组中的非0数字重复出现,则该数组不是连续的。换成扑克牌的描述方式就是如果一副牌里含有对子,则不可能是顺子。
由于扑克牌的值出现在0~13之间,我们可以定义一个长度为14的哈希表,这样在O(n)时间就能完成排序。
# -*- coding:utf-8 -*-
class Solution:
def IsContinuous(self, numbers):
if numbers == [] or len(numbers)<5:
return False
# 调用 python 自带的方法排序
numbers.sort()
cnt_0 = 0
cnt_a = 0
for i in range(len(numbers)):
if numbers[i] == 0:
cnt_0 += 1
elif i > cnt_0:
# 出现了对子
if numbers[i] == numbers[i-1]:
return False
# 记录差值
if numbers[i] != numbers[i-1] + 1:
cnt_a += numbers[i] - numbers[i-1] - 1
if cnt_a > cnt_0:
return False
return cnt_a <= cnt_0
面试题45:圆圈中最后剩下的数字
题目:0,1,…,n-1这n个数字排成一个圆圈,从数字0开始每次从这个圆圈里删除第m个数字。求出这个圆圈里剩下的最后一个数字。
典型的约瑟夫(Josephuse)环问题:
例如,0、1、2、3、4这5个数字组成一个圆圈(如图6.2所示),从数字0开始每次删除第3个数字,则删除的前四个数字依次是2、0、4、1,因此最后剩下的数字是3。
题目梳理
解法一:创建环形链表
将原始数组转换成一个环形链表;依次删除节点即可。
每删除一个数字需要m步运算,总共有n个数字,因此总的时间复杂度是O(mn)。同时这种思路还需要一个辅助链表来模拟圆圈,其空间复杂度是O(n)
解法二:寻找输出数字的规律
递推公式:f[i] = (f[i-1]+m)%i (i>1),f[1] = 0
下一轮的输出是在上一轮输出中加上m之后取余。
假设有10个人,即n = 10,0 1 2 3 4 5 6 7 8 9 选择 m = 3:
那么第一个人(2)出列后的序列为:0 1 3 4 5 6 7 8 9 ,
下一次从3开始删除,即:3 4 5 6 7 8 9 0 1 (i)
可以将该式转换为:0 1 2 3 4 5 6 7 8 (ii)
寻找规律:那么则有 ((ii)+ 3 )% 10 = (i)
时间复杂度是O(n),空间复杂度是O(1)
class Solution:
def LastRemaining_Solution(self, n, m):
if n < 1 or m < 1:
return -1
remainIndex = 0
for i in range(1, n+1):
remainIndex = (remainIndex + m) % i
return remainIndex