2.1 三个问题
A. 找出不在文件中的32位整数
初始想法
当内存足够时可以直接在内存中维护位向量,有对应位是否为1表示是否出现了对应整数。当内存只有几百字节时可以考虑将位向量分段,每次读一遍顺序文件正确设置对应位,位向量的每个段都存入外部文件。
经典解法
提出的第一种解法实际上超出了题目要求的范围,因为仔细读题目会发现只是要找出一个不在给定文件中的32位整数,而不需要找出所有,可以用二分查找的思想快速解决这个问题,每次给定一个缺少一个整数的范围都寻找合适的分割方法将包含一个缺失整数的范围缩小,再在缩小的范围内继续查找,直到范围足够小使得这个范围内的整数都可以放入内存中,然后就是用位向量方法找出缺失整数。
// 以下方法针对32位无符号整数,如果是一般整数就需要先用0分割
mask = 1 << 31
while cnt > 5
for each num in inputfile // 根据输入文件中数字的最高位为0还是1放入不同的输出文件
if num & mask
cnt1++
write num to outfile1
else
cnt2++
write num to outfile2
mask >> 1 //逻辑右移一位
inputfile = cnt1 < cnt2 ? outfile1: outfile2
cnt = cnt1 < cnt2 ? cnt1: cnt2
// 根据位向量找出缺失整数,难在确定当前范围,之后得到当前范围的位向量即可
mask = mask-1
if any num in inputfile bigger than mask
range = [mask+1, mask+1 | mask]
else
range = [0, mask]
B. 循环左移
在只有几十字节内存的情况下,实现循环左移i
位的经典方法有三种
a. 链式移位
1. 初始想法
假设原向量为a
,循环左移前后元素的索引具有以下对应关系:
0 | 1 | … | i-1 | i | i+1 | … | n-1 |
---|---|---|---|---|---|---|---|
n-i | n-i+1 | … | n-1 | 0 | 1 | … | n-i-1 |
上面的对应关系可以翻译成:原向量的a[k]
将变为左移后向量的a[(k+n-i)%n]
,这样写不够优美,可以借助模的性质改写为a[(k-i)%n]
。以n=10,i=3
为例,转移关系为x[0]<-x[3]<-x[6]<-x[9]<-x[2]<-x[5]<-x[8]<-x[1]<-x[4]<-x[7]<-x[0]
,这可以看成是转移链,也就是这个方法被我称为链式移位的原因。
伪码如下:
prev = a[i]
k = 0
do
temp = a[k]
a[k] = prev
k = (k+i) % n
prev = temp
while k > 0
2. 完善后
但是无法确定是不是所有元素都在一条转移链上,实际上所有向量项可以分为d
条转移链,其中d
为n,i
的最大公约数,这些链的代表元素为a[0],a[1],...,a[d-1]
,对上面伪码修改如下:
for t from 0 to d-1
k = t
prev = a[k+i]
do
temp = a[k]
a[k] = prev
k = (k+i) % n
prev = temp
while k != t
为什么链的代表元素为
a[0],a[1],...,a[d-1]
?
b. 递归
待循环左移的数组ab
,根据a
,b
的大小分为以下两种情况
b
的大小比a
大,将b
分为bl,br两部分,也就是ablbr,将a与br交换得到brbla,这时a已经放到了合适的位置上,考虑子数组brbl,只需要将该子数组循环左移i
位,末尾拼接上a就可以得到原数组循环左移i
位的结果。b
的大小比a
小,将a
写为alar,其中 ∣ a l ∣ = ∣ b ∣ , 长 度 相 等 |a_l|=|b|,长度相等 ∣al∣=∣b∣,长度相等,将b
与al交换,得到baral,只要把aral循环左移 i − ∣ b ∣ i-|b| i−∣b∣位就可以得到balar,也就是原数组循环左移i
位的结果。
伪码如下:
shift(v, bg, i, n) //将数组v[bg...bg+n-1]循环左移i位
if i == 0 || n == 0
return
if i < n-i
swap(v[bg...bg+i-1], v[bg+n-i...bg+n-1])
shift(v, bg, i, n-i)
else
swap(v[bg...bg+i-1], v[bg+n-i...bg+n-1])
shift(v,bg+i, i-(n-i), n-(n-i))
return
简单分析
分析递归的时间复杂度关注的是递归层数与每层开销,也可以使用聚合分析,分析每个元素到达数组中合适位置需要多少次移动。
考虑情况1,交换后得到brbla,a部分的每个元素到达这个位置涉及3次移动(交换需要3次移动),且这部分的元素之后不会再改动位置。同样的分析可以用于情况2,交换后得到baral,b部分每个元素到达这个位置涉及3次移动(交换需要3次移动),且这部分的元素之后不会再改动位置。因此每个元素要达到最终的位置都只需要3次移动的开销,整个递归的复杂度为
O
(
n
)
O(n)
O(n)。
c. 逐段转置
待循环左移的数组ab
,整个转置得到b'a'
,其中b'
为b
的转置,a'
为a
的转置,因此只要再分别对a',b'
转置就可以得到数组ba
,也就是循环左移的结果。
d. 三种程序用时比较
C. 寻找变位词
给定字典找出某个单词的所有变位词,只要两个单词的字母种类和个数相等就认为两个单词是变位词
1. 初始想法
构造一棵深度为26的树,一个非叶子节点表示一个字母,该非叶子节点出发的一条边表示单词中有多少个该字母,叶子节点表示变位词等价类,变位词用链表连接,先遍历一遍字典构建这棵树,然后给定一个单词就沿着这棵树直到达到某个叶子节点找到所有变位词。
2. 经典解法
给每个单词一个标识(单词种类和个数),以此为键排序可以将同一个标识的单词集合在一起,也就将所有变位词集中到了一起。
3. 简单分析
方法1的时间复杂度为 O ( n ) O(n) O(n),方法2的时间复杂度为 O ( n l g n ) O(nlgn) O(nlgn)。尽管方法1时间复杂度低,但是使用快排时可以很好地利用缓存,而方法1中每次加载树上不同的节点用于查找可能不能很好地利用缓存。