开篇三个问题的思路
问题1:
给定最多40亿随机排列的32位整数的文件,找出一个不存在该文件中的32位整数。在内存足够和仅有几百字节的内存的情况下如何解决该问题?
内存足够:使用第一章的位图法,时间复杂度O(n)。
内存不够:那么可以使用一种特殊的二分法。首先我们知道32位整数(非负整数)的取值范围是[0,2^32),一共存在40亿多个不同的整数。只要给定的数的个数小于范围可以取的数的个数,说明该范围中一定有一个数是没有给定的。等分取值区间为两个区间[0,2^31)和[2^31,2^32)。分别统计40亿个数中落在两个区间的次数。对次数小于范围可取数的区间(一定存在)再次等分。一次进行知道找到该数。这个方法时间复杂度O(nlogn)。
答案给出的方法中提到每次搜索将对应区间数复制到新的文件,这样每一轮搜索数的数量会减少一半,时间复杂度会变为O(n)
问题2:
数组的循环位移。有三种方法。
第一种:“杂耍法”
对于数组a,如果向左位移3。那么a[0]处移动之后应该是a[3]。取出a[0]元素,将a[3]移动到a[0],紧接着可以将a[6]移动到a[3],到达末尾时取数组开头对应元素继续。当所有元素都移动了一遍之后,循环位移就成功了。
第二种:“递归法”
对于向左位移3的数组a。将a[:3]和a[3:]的开头3个元素交换。对a[3:]做同样的递归操作。直到开头3个元素移动到最末尾。
第三种:“转置法”
对于向左位移3的数组a。分别翻转数组的两个部分,之后翻转整个数组。数组的两个部分就被交换了。
问题3:
给一个英文词典,找出所有的变位词。例如“pots”,”stop”和“tops”是变位词。
将一个单词的字母排序,相同排序的单词是变位词。
习题1
给定一个输入单词,找出在字典中的所有变位词。仅给定单词和字典时,如何解决?如果有一些时间和空间对字典进行预处理,又会如何?
1、生成字典
第一个问题是如何得到字典。我考虑生成一个假的字典。随机生成的方法本质是只能生成随机数的,如何将数转换为字符串呢?
如果把仅有小写字母的单词看做是26进制的数,a-z分别表示0-25。那么随机生成一个数,就可以通过进制变换(也就是多次整除和取余)来获得单词了。
代码如下:
def create_dic(wordCount=10**6):
minLetterCount = 5
reqLetterCount = math.ceil(math.log(wordCount*2,26))
letterCount = max(minLetterCount,reqLetterCount)
numDict = rand_uint(letterCount,wordCount)
wordDict = []
while(len(numDict)>0):
n = numDict.pop()
word = []
while(n != 0):
c = chr(n % 26 + ord('a'))
n = n//26
word.append(c)
wordDict.append(''.join(word))
return wordDict
首先根据输入参数需要的单词个数,计算生成的单词最多多个少字母。rand_uint()为根据最大字母数和单词个数生成不重复随机数序列的函数。
2、仅给定输入单词和字典
将变位词的字母排序,得到的新序列应该是相同的。根据这个原理,只需要将输入单词和字典中单词全部排序,选出排序序列相同的即可。代码如下:
def test_1_1():
given_word = "given"
dic = create_dic()
given_sig = sort_word(given_word)
ans = []
for word in dic:
sig = sort_word(word)
if(sig == given_sig):
ans.append(word)
print(ans[:10],len(ans))
3、允许对词典预处理
方法也是一样,首先对词典中单词进行一次排序。将单词存储到字典中,使用排序后的序列作为键。代码如下:
def test_1_2():
given_word = "given"
words = create_dic()
given_sig = sort_word(given_word)
dic = {}
while(len(words)>0):
word = words.pop()
sig = sort_word(word)
if dic.get(sig) == None:
dic[sig] = [word]
else:
dic[sig].append(word)
ans = dic[given_sig]
print(ans[:10],len(ans))
输出
['ivgne', 'gvnie', 'egnvi', 'gevin', 'vegni', 'vngei', 'iegnv', 'gneiv', 'iengv', 'ginev'] 13
对于100万个单词,数组占用8Mb内存,字典占用12Mb内存。内存使用可以接受。
习题2
给定包含43亿个32位整数的顺序文件,如何找出至少出现两次的整数?
计算器计算2^32=4,294,967,296。给定数的个数大于32位整数取值范围,说明一定有重复。思路同这一章的问题1。代码如下:
def test_2():
rng = 10**5
num,rand = create_data(rng)# 返回生成的数组,和重复的那个数
l = 0
r = rng
while(r-l>1):
m = (r+l)//2
if count_num(l,m,num)>m-l: # 统计范围左闭右开
r = m
continue
elif count_num(m,r,num)>r-m:
l = m
continue
else:
raise Exception("input error")
print(l,rand)
输出的两个数相同,说明程序实现成功。
习题3.1
“杂耍法”计算数组的循环位移。
杂耍法思路很简单,但是其中有一个不容易发现的陷阱。举两个例子说明。
1、数组长度7,左移3个单位。
移动的顺序应该是
0 <- 3 <- 6 <- 2 <- 5 <- 1 <- 4 <- 0
这是一般的预想情况。
2、数组长度9,左移3个单位。
使用一样的思路,移动顺序应该是
0 <- 3 <- 6 <- 0
移动三个元素之后移动就终止了。数组长度正好是移动距离的倍数时,需要移动多轮。
代码如下:
def move_juggle(word,step):
l = len(word)
si = 0 # 一轮的起始位置 start index
c = 0 # 移动次数 count
while(1):
tmp = word[si]
i = si # 一轮中,数组空位下标 index
while(1):
ni = (i+step)%l # 待移动元素下标 next index
if ni == si:
word[i] = tmp
c+=1
break
word[i] = word[ni]
i = ni
c+=1
if c < l: # 当移动次数和数组长度相同时终止
si+=1
continue
else:
break
return "".join(word)
习题3.2
递归法计算数组的循环位移。
递归函数:
def move_word(word,strt,step):
l = len(word)-strt
if l > 2*step:
tmp = word[strt:strt+step]
word[strt:strt+step]=word[strt+step:strt+step*2]
word[strt + step:strt + step * 2] = tmp
return move_word(word,strt+step,step)
else:
tmp = word[strt:strt+step]
left = word[strt+step:]
word[strt:strt+len(left)] = left
word[strt+len(left):strt+len(left)+step] = tmp
return word
习题4.1
实现求逆算法的数组循环位移。
比较简单,代码如下:
def move_reverse(word,step):
def reverse(word):
start = 0
end = len(word)-1
while(start<end):
tmp = word[start]
word[start] = word[end]
word[end] = tmp
start += 1
end -= 1
return word
ans = reverse(reverse(word[:step])+reverse(word[step:]))
return "".join(ans)
习题4.2
比较三种求循环位移的方法的速度。
在固定数组距离,变换交换距离的情况下。作者给出的结果如下:
“杂技算法”在交换距离增加之后,所需时间会急剧增加。这是因为该算法的高速缓存性能差,高速缓存大小很小,在交换距离增加的时候,“杂技算法”每次只从高速缓存读取一个元素,导致高速缓存需要读取的次数大大增加。
然而,在Python实现的算法中,没有出现这个情况。杂技算法一直是最快的。这应该与Python底层实现有关系。
习题5
(aTbTcT)T = cba
习题6
电话上的号码簿搜索程序。标准电话按键如下:
问题是,如果给定一个号码簿,求拨号时遇到重名的概率?另外,如何实现以名字的按键编码为参数,并返回所有可能的匹配名字的函数。
这题思路和变位词一样。对于寻找具有共性的元素的问题,我们变化元素表示的方式,使不同的但具有共性的元素拥有同样的内容。这里可以将人名转换为对应的号码字符串,或者可以直接表示为整数。将这个号码作为键把人名存储在字典中。代码如下:
def test_6():
def cal_num(word):
num = []
for c in word:
asc = ord(c)-ord('a')
n = asc//3+2
if n>9:
n = 0
num.append(str(n))
return ''.join(num)
nameCount = 10**5
name_list = create_dic(nameCount)
dic = {}
while(len(name_list)>0):
name = name_list.pop()
num = cal_num(name)
if dic.get(num) == None:
dic[num] = [name]
else:
dic[num].append(name)
# 重复率
print((nameCount-len(dic.keys()))/nameCount)
# 查询
given_name = "vince"
ans = dic.get(cal_num(given_name))
print(ans)
输出结果为:
0.50736
['xhncf']
习题8
给定一个n元实数集合,一个实数t和一个整数k,如何快速确定n是否存在一个k元子集,其元素之和不超过t?
方案1:
对整个数组排序,取最小k个数求和。时间复杂度O(nlogn)。
方案2:
顺序读取所有数,保存所遇到的数中最小的k个。可以使用堆保存k个数,整体时间复杂度为O(nlogk)。整体来说方案2更好,不仅速度更快,而且所需要的内存也更少。首先写一个大顶堆:
# 大顶堆
def swap(a, i, j):
temp = a[i]
a[i] = a[j]
a[j] = temp
def insert_full_heap(a,n):# 新元素替换根节点,向下移动
def sort_heap(a,ind):
l = ind*2+1
r = ind*2+2
if l>=len(a) or (a[ind]>=a[l] and (r>=len(a) or a[ind]>=a[r])):# 左右节点均走不通
return
elif r>=len(a) or a[l] >a[r]:# 与左节点交换的情况
swap(a, l, ind)
sort_heap(a, l)
else:# 剩下只能和右节点交换
swap(a, r, ind)
sort_heap(a, r)
if n>a[0]:
return
a[0] = n
sort_heap(a,0)
def insert_empty_heap(a,n):# 新元素放在末尾,往上移动
def up(a,ind):
if ind==0:
return
p = (ind-1)//2
if a[p] < a[ind]:
swap(a,p,ind)
up(a,p)
ind = len(a)
a.append(n)
up(a,ind)
整个程序:
num = rand_uint(2,100)
k = 5
t = 200
s = []
for n in num:
if len(s) < k:
insert_empty_heap(s,n)
else:
insert_full_heap(s,n)
print(sum(s)<t,s)
输出结果为:
True [20, 17, 6, 5, 7]