在Python中合并两个排序的列表


77

我有两个对象列表。每个列表已经按日期时间类型的对象的属性排序。我想将两个列表合并为一个排序的列表。最好的方法是仅执行排序,还是在Python中有更聪明的方法来执行此操作?

Answers:


116

人们似乎对此过于复杂了。只需组合两个列表,然后对它们进行排序:

>>> l1 = [1, 3, 4, 7]
>>> l2 = [0, 2, 5, 6, 8, 9]
>>> l1.extend(l2)
>>> sorted(l1)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

..或更短(且无需修改l1):

>>> sorted(l1 + l2)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

..简单!另外,它仅使用两个内置函数,因此,假设列表的大小合理,则比在循环中实现排序/合并要快。更重要的是,以上代码少得多,而且可读性强。

如果您的列表很大(我猜可能超过几十万),则使用替代/自定义排序方法可能会更快,但是可能首先要进行其他优化(例如,不存储数百万个datetime对象)

使用timeit.Timer().repeat()(将功能重复1000000次),我根据ghoseb的解决方案对它进行了宽松的基准测试,并且sorted(l1+l2)速度更快:

merge_sorted_lists 拿..

[9.7439379692077637, 9.8844599723815918, 9.552299976348877]

sorted(l1+l2) 拿..

[2.860386848449707, 2.7589840888977051, 2.7682540416717529]

5
最后是一个合理的答案,考虑了实际的基准测试。:-) ---而且,维持1行而不是15-20是更可取的。
Deestan

19
排序一个非常短的列表(通过添加两个列表创建)确实非常快,因为持续的开销将占主导地位。尝试对具有几百万个项目的列表或磁盘上具有数十亿个项目的文件执行此操作,您很快就会发现为什么合并是更可取的。
巴里·凯利

5
@Barry:如果您有“数十亿个项目”和速度要求,那么Python中的任何内容都是错误的答案。
Deestan

10
@Deestan:我不同意-有时候速度会受到其他因素的控制。例如。如果您要对磁盘上的数据进行排序(合并2个文件),则IO时间将很可能占主导地位,而python的速度将无关紧要,仅取决于您执行的操作数(以及算法)。
布赖恩

5
认真吗 用10个条目列表对排序功能进行基准测试?
Seen Osewa 2010年

111

有没有更聪明的方式在Python中做到这一点

这没有被提及,所以我继续-python 2.6+的heapq模块中有一个合并stdlib函数。如果您只是想把事情做好,那么这可能是一个更好的主意。当然,如果您想实现自己的方法,则合并合并排序是一种方法。

>>> list1 = [1, 5, 8, 10, 50]
>>> list2 = [3, 4, 29, 41, 45, 49]
>>> from heapq import merge
>>> list(merge(list1, list2))
[1, 3, 4, 5, 8, 10, 29, 41, 45, 49, 50]

这是文档


4
我已将链接添加到heapq.py。merge()实现为纯python函数,因此很容易将其移植到较早的Python版本。
jfs

尽管正确,但该解决方案似乎比该sorted(l1+l2)解决方案慢一个数量级。
Ale

1
@Ale:这并不完全令人惊讶。list.sort(根据sorted实现)使用TimSort,后者经过优化以利用基础序列中的现有排序(或反向排序),因此,即使从理论上讲O(n log n),在这种情况下,它也更接近O(n)执行排序。除此之外,CPythonlist.sort是用C实现的(避免了解释器的开销),而heapq.merge大多数是用Python实现的,并且以“许多可迭代”情况优化的方式进行了优化,从而减慢了“两个可迭代”情况。
ShadowRanger

6
卖点heapq.merge是它不需要输入或输出list; 它可以消耗迭代器/生成器并生成一个生成器,因此可以合并大量输入/输出(不立即存储在RAM中)而不会发生交换颠簸。它还可以以比预期低的开销来处理任意数量的输入可迭代项的合并(它使用堆来协调合并,因此开销与可迭代项数的对数成比例地扩展,而不是线性地缩放,但是正如所指出的那样,与“两个可迭代”情况无关)。
ShadowRanger

54

长话短说,除非len(l1 + l2) ~ 1000000使用:

L = l1 + l2
L.sort()

合并与排序比较

该图的说明和源代码可在此处找到。

该图是通过以下命令生成的:

$ python make-figures.py --nsublists 2 --maxn=0x100000 -s merge_funcs.merge_26 -s merge_funcs.sort_builtin

您正在将其与高尔夫解决方案进行比较,而不是实际上试图提高效率的解决方案。
OrangeDog

@OrangeDog我不明白你在说什么。答案是要添加两个列表并对其进行排序,对于较小的输入可能比Python 2.6中的heapq.merge()更快(尽管时间merge()为O(n),空间为O(1),并且排序为O( (n log n)时间,整个算法在空间中为O(n)。¶比较现在仅具有历史价值。
jfs

该答案与无关heapq.merge,您正在将其sort与某人的代码高尔夫球提交进行比较。
OrangeDog

@OrangeDog错误merge_26()来自Python 2.6 heapq模块。
jfs

您就是说“可以在这里找到源代码”并链接到代码高尔夫球答案的人。不要怪别人以为可以找到的代码就是您所测试的代码。
OrangeDog

25

这只是合并。将每个列表视为一个堆栈,并连续弹出两个堆栈头中较小的一个,将该项添加到结果列表中,直到其中一个堆栈为空。然后将所有剩余的项目添加到结果列表中。


合并排序确实是最佳解决方案。
Ignacio Vazquez-Abrams,2009年

3
但是,它比使用Python的内置排序还快吗?
akaihola


9
这仅仅是合并,而不是合并排序。
格伦·梅纳德

2
@akaihola:如果len(L1 + L2) < 1000000那么sorted(L1 + L2)stackoverflow.com/questions/464342/...
JFS

17

ghoseb解决方案中存在一点缺陷,使其为O(n ** 2),而不是O(n)。
问题是这正在执行:

item = l1.pop(0)

使用链接列表或双端队列,这将是O(1)操作,因此不会影响复杂性,但是由于python列表是作为矢量实现的,因此将l1的其余元素复制了一个空格,即O(n)操作。由于每次遍历列表都会完成此操作,因此会将O(n)算法转换为O(n ** 2)算法。可以使用不更改源列表但仅跟踪当前位置的方法来纠正此问题。

我已经尝试过测试dbr建议的校正算法与简单sorted(l1 + l2)的比较

def merge(l1,l2):
    if not l1:  return list(l2)
    if not l2:  return list(l1)

    # l2 will contain last element.
    if l1[-1] > l2[-1]:
        l1,l2 = l2,l1

    it = iter(l2)
    y = it.next()
    result = []

    for x in l1:
        while y < x:
            result.append(y)
            y = it.next()
        result.append(x)
    result.append(y)
    result.extend(it)
    return result

我已经用生成的列表测试了这些

l1 = sorted([random.random() for i in range(NITEMS)])
l2 = sorted([random.random() for i in range(NITEMS)])

对于各种大小的列表,我得到以下计时(重复100次):

# items:  1000   10000 100000 1000000
merge  :  0.079  0.798 9.763  109.044 
sort   :  0.020  0.217 5.948  106.882

因此,实际上,看起来dbr是正确的,除非您期望有非常大的列表,否则最好使用sorted(),尽管它确实具有更差的算法复杂度。收支平衡点在每个来源列表中大约有100万个项目(总计200万个)。

合并方法的一个优点是,将其重写为生成器非常简单,它将使用更少的内存(不需要中间列表)。

[编辑] 我在更接近问题的情况下重试了此方法-使用包含字段“ date”(日期时间对象)的对象列表。更改了上述算法以进行比较.date,并将sort方法更改为:

return sorted(l1 + l2, key=operator.attrgetter('date'))

这确实改变了一点。比较起来比较昂贵,这意味着相对于实现的恒定速度,我们执行的数量变得更加重要。这意味着合并弥补了损失,在100,000个项目上超过了sort()方法。基于更复杂的对象(例如较大的字符串或列表)进行比较可能会使这种平衡发生更多的变化。

# items:  1000   10000 100000  1000000[1]
merge  :  0.161  2.034 23.370  253.68
sort   :  0.111  1.523 25.223  313.20

[1]:注意:对于1,000,000个项目,我实际上只重复了10次,并且由于速度很慢而相应地按比例放大。


感谢您的修复。如果您能准确指出缺陷和解决方法
那就太好了

@ghoseb:我对您的帖子发表了简短的说明,但我现在更新了答案以提供更多详细信息-本质上l.pop()是列表的O(n)操作。这是可以解决的,通过跟踪位置以其他方式(或者通过从尾部,而不是弹出,并在年底扭转)
布赖恩

您可以对这些相同的测试进行基准测试,但可以像问题一样比较日期吗?我猜想这种额外的方法相对会占用相当多的时间。
乔什·史密顿

我要说的区别是由于在C / C ++中实现了sort()并与我们正在解释的merge()进行了比较。在相等条件下,merge()应该更快。
2009年

好点Drakosha。证明基准测试确实是唯一可以肯定的方法。
乔什·史密顿

6

这是两个排序列表的简单合并。看一下下面的示例代码,其中合并了两个排序的整数列表。

#!/usr/bin/env python
## merge.py -- Merge two sorted lists -*- Python -*-
## Time-stamp: "2009-01-21 14:02:57 ghoseb"

l1 = [1, 3, 4, 7]
l2 = [0, 2, 5, 6, 8, 9]

def merge_sorted_lists(l1, l2):
    """Merge sort two sorted lists

    Arguments:
    - `l1`: First sorted list
    - `l2`: Second sorted list
    """
    sorted_list = []

    # Copy both the args to make sure the original lists are not
    # modified
    l1 = l1[:]
    l2 = l2[:]

    while (l1 and l2):
        if (l1[0] <= l2[0]): # Compare both heads
            item = l1.pop(0) # Pop from the head
            sorted_list.append(item)
        else:
            item = l2.pop(0)
            sorted_list.append(item)

    # Add the remaining of the lists
    sorted_list.extend(l1 if l1 else l2)

    return sorted_list

if __name__ == '__main__':
    print merge_sorted_lists(l1, l2)

这对于datetime对象应该可以正常工作。希望这可以帮助。


3
不幸的是,这适得其反-通常合并将是O(n),但是由于您是从每个列表的左侧弹出(一个O(n)操作),因此实际上是将其设为O(n ** 2)进程-比天真的sorted(l1 + l2)差
布赖恩(Brian

@Brian我实际上认为此解决方案是所有解决方案中最干净的,并且我相信从列表中弹出第一个元素的O(n)复杂性是正确的。您可以通过使用集合中的双端队列来消除该问题,当从任一侧弹出一个项目时,都会给您O(1)。docs.python.org/2/library/collections.html#collections.deque
mohi666

@Brian,head, tail = l[0], l[1:]也将具有O(n ** 2)复杂度吗?
Nikolay Fominyh,2016年

2
@布赖恩:作为替代collections.deque,它也可以通过创建解决l1l2以相反的顺序(l1 = l1[::-1]l2 = l2[::-1]),然后从右手侧,而不是左侧工作,替换if l1[0] <= l2[0]:if l1[-1] <= l2[-1]:,替换pop(0)pop()并改变sorted_list.extend(l1 if l1 else l2)sorted_list.extend(reversed(l1 if l1 else l2))
ShadowRanger

4
from datetime import datetime
from itertools import chain
from operator import attrgetter

class DT:
    def __init__(self, dt):
        self.dt = dt

list1 = [DT(datetime(2008, 12, 5, 2)),
         DT(datetime(2009, 1, 1, 13)),
         DT(datetime(2009, 1, 3, 5))]

list2 = [DT(datetime(2008, 12, 31, 23)),
         DT(datetime(2009, 1, 2, 12)),
         DT(datetime(2009, 1, 4, 15))]

list3 = sorted(chain(list1, list2), key=attrgetter('dt'))
for item in list3:
    print item.dt

输出:

2008-12-05 02:00:00
2008-12-31 23:00:00
2009-01-01 13:00:00
2009-01-02 12:00:00
2009-01-03 05:00:00
2009-01-04 15:00:00

我敢打赌,这比任何花哨的纯Python合并算法都要快,即使对于大数据也是如此。Python 2.6完全heapq.merge是另外一回事了。


3

Python的排序实现“ timsort”专门针对包含有序部分的列表进行了优化。另外,它是用C编写的。

http://bugs.python.org/file4451/timsort.txt
http://en.wikipedia.org/wiki/Timsort

正如人们所提到的,它可能会以某个常数因子多次调用比较函数(但在许多情况下可能会在较短的时间内多次调用比较函数!)。

但是,我永远不会依靠它。–丹尼尔·纳达斯(Daniel Nadasi)

我相信Python开发人员致力于保持最小排序,或者在这种情况下至少保持O(n)排序。


不能在串行计算机上以小于O(n log n)的方式进行广义排序(即将基数排序与有限值域分开)。–巴里·凯利

是的,一般情况下排序不能比这快。但是,由于O()是一个上限,因此在sorted(L1)+ sorted(L2)的情况下,任意输入上的timsort为O(n log n)并不矛盾。


3
def merge_sort(a,b):

    pa = 0
    pb = 0
    result = []

    while pa < len(a) and pb < len(b):
        if a[pa] <= b[pb]:
            result.append(a[pa])
            pa += 1
        else:
            result.append(b[pb])
            pb += 1

    remained = a[pa:] + b[pb:]
    result.extend(remained)


return result

3

合并排序中合并步骤的实现,该步骤遍历两个列表:

def merge_lists(L1, L2):
    """
    L1, L2: sorted lists of numbers, one of them could be empty.

    returns a merged and sorted list of L1 and L2.
    """

    # When one of them is an empty list, returns the other list
    if not L1:
        return L2
    elif not L2:
        return L1

    result = []
    i = 0
    j = 0

    for k in range(len(L1) + len(L2)):
        if L1[i] <= L2[j]:
            result.append(L1[i])
            if i < len(L1) - 1:
                i += 1
            else:
                result += L2[j:]  # When the last element in L1 is reached,
                break             # append the rest of L2 to result.
        else:
            result.append(L2[j])
            if j < len(L2) - 1:
                j += 1
            else:
                result += L1[i:]  # When the last element in L2 is reached,
                break             # append the rest of L1 to result.

    return result

L1 = [1, 3, 5]
L2 = [2, 4, 6, 8]
merge_lists(L1, L2)               # Should return [1, 2, 3, 4, 5, 6, 8]
merge_lists([], L1)               # Should return [1, 3, 5]

我仍在学习算法,请告诉我代码是否可以在任何方面进行改进,感谢您的反馈,谢谢!


2

使用合并排序的“合并”步骤,它在O(n)时间内运行。

来自维基百科(伪代码):

function merge(left,right)
    var list result
    while length(left) > 0 and length(right) > 0
        if first(left) ≤ first(right)
            append first(left) to result
            left = rest(left)
        else
            append first(right) to result
            right = rest(right)
    end while
    while length(left) > 0 
        append left to result
    while length(right) > 0 
        append right to result
    return result

2

递归实现如下。平均性能为O(n)。

def merge_sorted_lists(A, B, sorted_list = None):
    if sorted_list == None:
        sorted_list = []

    slice_index = 0
    for element in A:
        if element <= B[0]:
            sorted_list.append(element)
            slice_index += 1
        else:
            return merge_sorted_lists(B, A[slice_index:], sorted_list)

    return sorted_list + B

或具有更高空间复杂度的发电机:

def merge_sorted_lists_as_generator(A, B):
    slice_index = 0
    for element in A:
        if element <= B[0]:
            slice_index += 1
            yield element       
        else:
            for sorted_element in merge_sorted_lists_as_generator(B, A[slice_index:]):
                yield sorted_element
            return        

    for element in B:
        yield element

2

这是我在线性时间内不编辑l1和l2的解决方案:

def merge(l1, l2):
  m, m2 = len(l1), len(l2)
  newList = []
  l, r = 0, 0
  while l < m and r < m2:
    if l1[l] < l2[r]:
      newList.append(l1[l])
      l += 1
    else:
      newList.append(l2[r])
      r += 1
  return newList + l1[l:] + l2[r:]

1

好吧,天真的方法(将2个列表组合成一个大的列表并进行排序)将是O(N * log(N))复杂性。另一方面,如果您手动实现合并(我不知道为此在python库中有任何现成的代码,但我不是专家),复杂度将为O(N),这显然更快。巴里·凯利(Barry Kelly)在帖子中很好地描述了这个想法。


有趣的是,python排序算法非常好,因此性能可能会优于O(n log n),因为该算法通常会利用输入数据中的规则性。但是,我永远不会依靠它。
Daniel Nadasi 09年

不能在串行计算机上以小于O(n log n)的方式进行广义排序(即将基数排序与有限值域分开)。
巴里·凯利

1

如果您想以与学习迭代过程更一致的方式进行操作,请尝试以下操作

def merge_arrays(a, b):
    l= []

    while len(a) > 0 and len(b)>0:
        if a[0] < b[0]: l.append(a.pop(0))    
        else:l.append(b.pop(0))

    l.extend(a+b)
    print( l )

pop(0)是线性的,因此此版本是偶然的二次方
Allen Downey

1
import random

    n=int(input("Enter size of table 1")); #size of list 1
    m=int(input("Enter size of table 2")); # size of list 2
    tb1=[random.randrange(1,101,1) for _ in range(n)] # filling the list with random
    tb2=[random.randrange(1,101,1) for _ in range(m)] # numbers between 1 and 100
    tb1.sort(); #sort the list 1 
    tb2.sort(); # sort the list 2
    fus=[]; # creat an empty list
    print(tb1); # print the list 1
    print('------------------------------------');
    print(tb2); # print the list 2
    print('------------------------------------');
    i=0;j=0;  # varialbles to cross the list
    while(i<n and j<m):
        if(tb1[i]<tb2[j]):
            fus.append(tb1[i]); 
            i+=1;
        else:
            fus.append(tb2[j]);
            j+=1;

    if(i<n):
        fus+=tb1[i:n];
    if(j<m):
        fus+=tb2[j:m];

    print(fus);

  # this code is used to merge two sorted lists in one sorted list (FUS) without
  #sorting the (FUS)

目前尚不清楚这是否是问题的答案,更不用说它是否确实可以解决?您能提供某种解释吗?
2014年

抱歉,我不明白你在想什么!
OussamaĎjSbaa 2014年

你会注意到,较高的投票答案(和其他大部分)有一些文字,解释正在发生的事情的答案,为什么这个问题的答案是一个问题的答案..

因为它将两个列表合并到一个排序的列表中,所以这就是问题的答案
OussamaĎjSbaa 2014年

1

使用了合并排序的合并步骤。但是我用过发电机时间复杂度 O(n)

def merge(lst1,lst2):
    len1=len(lst1)
    len2=len(lst2)
    i,j=0,0
    while(i<len1 and j<len2):
        if(lst1[i]<lst2[j]):
                yield lst1[i]
                i+=1
        else:
                yield lst2[j]
                j+=1
    if(i==len1):
        while(j<len2):
                yield lst2[j]
                j+=1
    elif(j==len2):
        while(i<len1):
                yield lst1[i]
                i+=1
l1=[1,3,5,7]
l2=[2,4,6,8,9]
mergelst=(val for val in merge(l1,l2))
print(*mergelst)

0
def compareDate(obj1, obj2):
    if obj1.getDate() < obj2.getDate():
        return -1
    elif obj1.getDate() > obj2.getDate():
        return 1
    else:
        return 0



list = list1 + list2
list.sort(compareDate)

将列表排序到位。定义您自己的用于比较两个对象的函数,然后将该函数传递给内置的sort函数。

不要使用冒泡排序,它具有可怕的性能。


合并排序肯定会更快,但是如果您必须自己实现,则稍微复杂一些。我认为python使用quicksort。
乔什·史密顿

3
不,Python使用timsort。
Ignacio Vazquez-Abrams,2009年

0

该代码具有时间复杂度O(n),并且可以将任何数据类型的列表合并,并以量化函数为参数func。它产生一个新的合并列表,并且不修改作为参数传递的两个列表。

def merge_sorted_lists(listA,listB,func):
    merged = list()
    iA = 0
    iB = 0
    while True:
        hasA = iA < len(listA)
        hasB = iB < len(listB)
        if not hasA and not hasB:
            break
        valA = None if not hasA else listA[iA]
        valB = None if not hasB else listB[iB]
        a = None if not hasA else func(valA)
        b = None if not hasB else func(valB)
        if (not hasB or a<b) and hasA:
            merged.append(valA)
            iA += 1
        elif hasB:
            merged.append(valB)
            iB += 1
    return merged

-1

希望这可以帮助。非常简单直接:

l1 = [1、3、4、7]

l2 = [0,2,5,6,8,9]

l3 = l1 + l2

l3.sort()

列印(l3)

[0、1、2、3、4、5、6、7、8、9]


1
OP没有询问如何添加和排序列表,而是询问在上下文中是否存在更好或更多的“ Python”方式。
ForeverZer0
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.