问题:如何给磁盘文件排序
输入:一个包含最多n个正整数的文件,每个数都小于n,其中n=10^7。如果在输入文件中有任何整数重复出现就是致命错误。没有其他数据与该数据相关联。
输出:按升序排列的输入整数的列表。
约束:最多有(大约)1MB内存空间可用,有充足的磁盘存储空间可用。运行时间最多几分钟,运行时间为10秒就不需要进一步优化了。
这个问题描述认真看其实是有问题的,正整数小于n最多是只有n-1个而不是n个的。方便起见我们可以把输入要求变为非负数。
在我写的程序中,a数组代表输入,b数组代表输出。文件的读写的代码就没有写了,因为每次从a,b数组读入数据其实等同于对文件的输入输出。另外python本来运行速度较慢,所以输入的数据规模缩小到了10^6。这些关系不大,主要的精力应该放在算法上面。
习题1
使用具有库的语言来实现一种排序算法以表示和排序集合。
使用python build-in 函数:sorted()
a = [3,4,5,2,6,7,5,6,7,89,4,5,6]
a = sorted(a)
习题2
使用位运算来实现位图数据结构。
位图数据结构的一般方法是使用一个数组。当需要置第i位时,判断该位属于数组中的哪个数,以及该数的哪一位
Python中的位运算有:
与 &
或 |
异或 ^
非 ~
左移 <<
右移 >>
Python中数组是不能定义数据类型的,可以使用numpy实现。我使用uint32作为数组中的元素,该类型有32比特位。对于一个需要置位的数 i,将i写成二进制形式,有
xxxx xxxx xxxx
末尾红色的五位,有32种取值,可以用一个uint32的32位表示。前端剩余的位,则表示为位图数组的下标。
BITS_PER_WORD = 32
MASK = 0x1f
SHIFT = 5
def set(i, bitMap):
bitMap[i >> SHIFT] |= 1 << (i & MASK)
def clr(i, bitMap):
bitMap[i >> SHIFT] &= ~(1 << (i & MASK))
def test(i, bitMap):
return (bitMap[i >> SHIFT] & (1 << (i & MASK))) != 0
def create_bitmap(numberSize):
return np.zeros(math.ceil(numberSize/BITS_PER_WORD),dtype=np.uint32)
def test_2():
numberSize = 10**6
bitMap = create_bitmap(numberSize)
set(1,bitMap)
set(2,bitMap)
print(test(1,bitMap))
clr(1,bitMap)
print(test(1,bitMap))
print(test(2,bitMap))
代码输出:
True
False
True
参考答案中定义bitMap数组大小时候写的是:
int a[1 + N/BITSPERWORD]
这是不精确的,当需要的位数正好可以被BITSPERWORD整除时,将会多出一个数组元素。
习题3
使用位图结构排序。
依次将所有数写入位图,然后依次读取位图中的数。
def test_3():
numberRange = 10**6
a = list(range(0,numberRange))
random.shuffle(a)
bitMap = create_bitmap(numberRange)
for i in a:
set(i,bitMap)
b = []
for i in range(0,numberRange):
if test(i,bitMap):
b.append(i)
print(b[:10])
输出为:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
习题4
生成小于n且不重复的k个整数的方法。
Python中使用random.shuffle()函数可以很方便完成:
def shuffle_rand(numberRange,numberCount):
a = list(range(0, numberRange))
random.shuffle(a)
a = a[:numberCount]
return a
这里有一个意外收获,range()函数实际上生成一个range对象而不是list对象,所以不能将range直接放入shuffle中。
习题5
题目要求最多10^7次方个数,这么多比特位最后需要多于1MB的内存,如果严格限制内存在1MB之内,就无法用一个位图统计所有数的出现情况。
这时候可以使用两趟算法,分为两步,首先用位图排序[0,5000000)的数,再用一次同样的算法排序[5000000,10000000)之间的数。推而广之,可以得到n趟算法的代码:
def test_5():
# 推广到n趟算法
runTime = 2
numberCount = 8*10**5
numberRange = 10**6
a = shuffle_rand(numberRange,numberCount)
b = []
for n in range(0,runTime):
r = [int(numberRange*n/runTime),int(numberRange*(n+1)/runTime)] # 左闭右开
bitMap = create_bitmap(r[1]-r[0])
for i in a:
if i >=r[0] and i < r[1]:
set(i-r[0],bitMap)
for i in range(0,r[1]-r[0]):
if test(i,bitMap):
b.append(i+r[0])
print(b[:10],b[-10:],len(b))
这里r表示当前处理的数的范围。注意这个范围是左闭右开的,应为第一个范围需要包括0,而最后一个范围不需要包括10^7。
习题6
如果每个整数不是最多出现一次而是最多出现10次,应该如何实现该算法?
一个比特位可以表示一个数出现或者不出现,同理只要有4个比特位就可以表示该数出现的次数。
这时候就需要重写bitMap的代码,方法类似。这个时候一个uint32元素可以存放8个数。bitMap代码如下:
NUM_PER_WORD = 8
MASK = 0x7
SHIFT = 3
def create_bitmap(numberRange):
return np.zeros(math.ceil(numberRange/NUM_PER_WORD),dtype=np.uint32)
def set(i,bitMap):
bitMap[i>>SHIFT] += 1<<((i&MASK)*4)
def test(i,bitMap):
return ( bitMap[i>>SHIFT]>>(i&MASK)*4 )&0xf
生成输入数据仍然使用shuffle,只是我们可以首先连接10个range产生的数组,然后对该数组shuffle取部分元素。代码如下:
def multi_shuffle_rand(multi,numberRange,numberCount):
a = []
for i in range(0,multi):
a += list(range(0,numberRange))
random.shuffle(a)
return a[:numberCount]
最后就是算法的代码了,除了输出部分,其他代码和单比特的算法是一样的。
numberRange = 10**6
numberCount = 8*10**5
runTime = 5
a = multi_shuffle_rand(10,numberRange,numberCount)
b = []
for n in range(0,runTime):
r = [int(numberRange*n/runTime),int(numberRange*(n+1)/runTime)] # 左闭右开
bitMap = create_bitmap(r[1]-r[0])
for i in a:
if i >=r[0] and i < r[1]:
set(i-r[0],bitMap)
for i in range(0,r[1]-r[0]):
if test(i,bitMap) > 0:
c = test(i,bitMap)
for j in range(0,c):
b.append(i + r[0])
习题7
检查输入合法性。那么可能的违法输入有什么呢?第一,范围错误:小于0,大于最大值n。第二,类型错误:输入浮点数,字符串等。第三,输入重复。
1、检查类型使用Python的isinstance()函数或者type()函数,如下:
a = 1
print( isinstance(a,int) )
print(type(a) == int)
返回:
True
False
isinstance函数第二个参数可以使用类型的列表,那么该变量只要属于列表中任意一种类型都会返回True。
2、检查重复调用位图对应函数查看该位是否被占用即可。
3、错误处理
题目中说输入重复是严重错误,那么就可以在出错时抛出异常同时终止程序。
使用raise 方法抛出异常:
raise Exception("input error")
Exception中参数是抛出错误时显示的说明。
习题8
1、增加了不同的区号。
假定区号也需要参与排序。那么按顺序依次排序不同区号号码。也就是多运行几趟算法而已了。
在习题5的基础上改进代码。三个区号,每个区号运行两次程序,一共六次。代码如下:
areaCode = [800,888,887]
runTime = 2
numberCount = 8 * 10 ** 5
numberRange = 10 ** 6
a = rand_with_area_code(numberRange,numberCount,areaCode)
b = []
sorted(areaCode)
for ac in areaCode:
for n in range(0, runTime):
r = [int(numberRange * n / runTime), int(numberRange * (n + 1) / runTime)] # 左闭右开
bitMap = create_bitmap(r[1] - r[0])
for i in a:
curNumber = int(i.split('-')[1])
curAreaCode = int(i.split('-')[0])
if curNumber >= r[0] and curNumber < r[1] \
and curAreaCode == ac:
set(curNumber - r[0], bitMap)
for i in range(0, r[1] - r[0]):
if test(i, bitMap):
b.append(str(ac) + "-"+str(i + r[0]))
print(b[:10], b[-10:], len(b))
rand_with_area_code()函数生成代区号的号码,返回的号码会是字符串类型,格式类似xxx-xxxxxxx。代码:
def rand_with_area_code(numberRange,numberCount,areaCode):
a = list(range(0, numberRange))
random.shuffle(a)
a= a[:numberCount]
acSize = len(areaCode)
for i in range(0,len(a)):
ac = str(areaCode[random.randint(0,acSize-1)])
a[i] = ac + "-"+str(a[i])
return a
2、快速查询
存储生成的每个位图,每次查询根据号码范围查找对应的位图。