在Python3中合并k个排序的列表,内存和时间之间的权衡问题

时间:2019-02-09 10:21:31

标签: python algorithm sorting merge tradeoff

输入为: 第一行-多个数组(k); 每隔一行-第一个数字是数组大小,下一个数字是元素。

最大k为1024。最大数组大小为10 * k。所有数字介于0到100之间。内存限制-10MB,时间限制-1s。 推荐的复杂度为k⋅log(k)⋅n,其中n为数组长度。

示例输入:

4            
6 2 26 64 88 96 96
4 8 20 65 86
7 1 4 16 42 58 61 69
1 84

示例输出:

1 2 4 8 16 20 26 42 58 61 64 65 69 84 86 88 96 96 

我有4个解决方案。一种使用heapq并按块读取输入行,一种使用heapq,一种使用Counter,另一种不使用。

这使用了heapq(时间长,但对内存不利,我认为堆是正确的方式,但是如果我可以逐行读取行,也许可以对其进行优化,这样我就不需要为整个输入存储空间):

from heapq import merge


if __name__ == '__main__':
    print(*merge(*[[int(el) for el in input().split(' ')[1:]] for _ in range(int(input()))]), sep=' ')

这是上一个的高级版本。它按块读取行,但是这是一个非常复杂的解决方案,我不知道如何优化这些读取:

from heapq import merge
from functools import reduce


def read_block(n, fd, cursors, offset, has_unused_items):
    MEMORY_LIMIT = 10240000
    block_size = MEMORY_LIMIT / n
    result = []

    for i in range(n):
        if has_unused_items[i]:
            if i == 0:
                fd.seek(cursors[i] + offset)
            else:
                fd.read(cursors[i])

            block = ''
            c = 0
            char = ''

            while c < block_size or char != ' ':
                if cursors[i] == 0:
                    while char != ' ':
                        char = fd.read(1)
                        cursors[i] += 1

                char = fd.read(1)

                if char != '\n':
                    block += char
                    cursors[i] += 1
                    c += 1
                else:
                    has_unused_items[i] = False
                    break

            result.append([int(i) for i in block.split(' ')])

            while char != '\n':
                char = fd.read(1)

    return result


def to_output(fd, iter):
    fd.write(' '.join([str(el) for el in iter]))


if __name__ == '__main__':
    with open('input.txt') as fd_input:
        with open('output.txt', 'w') as fd_output:
            n = int(fd_input.readline())
            offset = fd_input.tell()
            cursors = [0] * n
            has_unused_items = [True] * n
            result = []

            while reduce(lambda x, p: x or p, has_unused_items):
                result = merge(
                    result,
                    *read_block(n, fd_input, cursors, offset, has_unused_items)
                )

            to_output(fd_output, result)

这对内存有好处(使用带有计数器的排序,但是我没有使用所有数组都已排序的信息):

from collections import Counter


def solution():
    A = Counter()

    for _ in range(int(input())):
        A.update(input().split(' ')[1:])

    for k in sorted([int(el) for el in A]):
        for _ in range(A[str(k)]):
            yield k

这很适合时间(但可能还不够好):

def solution():
    A = tuple(tuple(int(el) for el in input().split(' ')[1:]) for _ in range(int(input())) # input data
    c = [0] * len(A) # cursors for each array

    for i in range(101):
        for j, a in enumerate(A):
            for item in a[c[j]:]:
                if item == i:
                    yield i
                    c[j] += 1
                else:
                    break 

完美地,如果在第一个示例中我将按部分排列数组,那么我将不需要为整个输入存储空间,但是我不知道如何正确地按行读取行。

可以请您提出一些解决问题的方法吗?

2 个答案:

答案 0 :(得分:1)

深度思考计算机,宇宙和一切生命的答案是什么

这是我用于测试的代码

"""4
6 2 26 64 88 96 96
4 8 20 65 86
7 1 4 16 42 58 61 69
1 84"""

from heapq import merge
from io import StringIO
from timeit import timeit

def solution():
    pass

times = []
for i in range(5000):
    f = StringIO(__doc__)
    times.append(timeit(solution, number=1))

print(min(times))

这是结果,我测试了注释中提出的解决方案:

6.5e-06秒

def solution():
    A = []
    A = merge(A, *((int(i)
                    for i in line.split(' ')[1:])
                    for line in f.readlines()))
    return A

7.1e-06秒

def solution():
    A = []
    for _ in range(int(f.readline())):
        A = merge(A, (int(i) for i in f.readline().split(' ')[1:]))
    return A

7.9e-07秒

def solution():
    A = Counter()
    for _ in range(int(f.readline())):
        A.update(f.readline().split(' ')[1:])
    for k in sorted([int(el) for el in A]):
        for _ in range(A[str(k)]):
            yield k

8.3e-06秒

def solution():
    A = []
    for _ in range(int(f.readline())):
        for i in f.readline().split(' ')[1:]:
            insort(A, i)
    return A

6.2e-07秒

def solution():
    A = Counter()
    for _ in range(int(f.readline())):
        A.update(f.readline().split(' ')[1:])
    l = [int(el) for el in A]
    l.sort()
    for k in l:
        for _ in range(A[str(k)]):
            yield k

您的代码很棒,请不要使用排序(对于更大的数组,影响会变得更大)。您应该使用更大的输入来测试它(我用了您提供的)。 enter image description here

只有前一个的获奖者(加上解决方案6,这是您给的第二个获奖者)。看来速度限制是由程序的I / O而不是排序本身给定的。 enter image description here

请注意,我会生成正方形(行数==每行数)

答案 1 :(得分:1)

如果整数行已经排序,那么您只需要关注如何将各部分缝合在一起。

为此,我的解决方案在元组列表中跟踪问题的state

每个元组记录行的offsetnum_elements是行中仍要处理的元素数,next_elem是要处理的下一个元素的值,last_elem是该行中最后一个元素的值。

该算法循环遍历state元组的列表,这些元组基于next_elemlast_elem的值进行排序,并在A列表中添加下一个最小值。 state已更新,列表已排序,冲洗并重复,直到列表为空。

我很好奇它相对于其他解决方案的表现。

from operator import itemgetter

def solution():
    state = []
    A = []
    k = int(f.readline())
    for _ in range(k):
        offset = f.tell()
        line = f.readline().split()
        # Store the state data for processing each line in a tuple
        # Append tuple to the state list: (offset, num_elements, next_elem, last_elem)
        state.append((offset, int(line[0]), int(line[1]), int(line[-1])))
    # Sort the list of stat tuples by value of next and last elements
    state.sort(key=itemgetter(2, 3))
    # [
    #    (34, 7, 1, 69),
    #    (2, 6, 2, 96),
    #    (21, 4, 8, 86),
    #    (55, 1, 84, 84)
    # ]
    while (len(state) > 0):
        offset, num_elements, _, last = state[0]
        _ = f.seek(offset)
        line = f.readline().split()
        if ((len(state) == 1) or (last <= state[1][2])):
            # Add the remaining line elements to the `result`
            A += line[-(num_elements):]
            # Delete the line from state
            del state[0]
        else:
            while (int(line[-(num_elements)]) <= state[1][2]):
                # Append the element to the `result`
                A.append(line[-(num_elements)])
                # Decrement the number of elements in the line to be processed
                num_elements -= 1
            if (num_elements > 0):
                # Update the tuple
                state[0] = (offset, (num_elements), int(
                    line[-(num_elements)]), int(line[-1]))
                # Sort the list of tuples
                state.sort(key=itemgetter(2, 3))
            else:
                # Delete the depleted line from state
                del state[0]
    # Return the result
    return A