Поєднання двох відсортованих списків у Python


77

У мене є два списки об’єктів. Кожен список уже відсортований за властивістю об’єкта, що має тип дати та часу. Я хотів би об’єднати два списки в один відсортований список. Чи найкращий спосіб просто зробити сортування, чи це розумніший спосіб зробити це в Python?

Відповіді:


117

Люди, здається, занадто ускладнюють це .. Просто об’єднайте два списки, а потім сортуйте їх:

>>> 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
Сортування дуже короткого списку, створеного додаванням двох списків, дійсно буде дуже швидким, оскільки постійні загальновиробничі витрати будуть домінувати. Спробуйте зробити це для списків із декількома мільйонами елементів або файлів на диску з декількома мільярдами елементів, і незабаром ви з’ясуєте, чому об’єднання є кращим.
Barry Kelly

5
@Barry: Якщо у вас є "кілька мільярдів предметів" і необхідна швидкість, будь-що в Python є неправильною відповіддю.
Deestan

10
@Deestan: Я не згоден - бувають випадки, коли в швидкості будуть домінувати інші фактори. Напр. якщо ви сортуєте дані на диску (об'єднати 2 файли), швидкість введення-виведення, швидше за все, буде домінувати, і швидкість python не матиме великого значення, а лише кількість операцій, які ви виконуєте (а отже, і алгоритм).
Брайан

6
Серйозно? Бенчмаркинг функції сортування зі списком із 10 записів ??
Seun Osewa

116

чи є розумніший спосіб зробити це в Python

Про це не згадувалося, тому я продовжую - у модулі heapq python 2.6+ є функція злиття stdlib . Якщо все, що ви прагнете зробити, це щось зробити, це може бути кращою ідеєю. Звичайно, якщо ви хочете реалізувати своє, злиття merge-sort - це шлях.

>>> 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

1
Хоча це правильно, але це рішення, здається, на порядок повільніше, ніж sorted(l1+l2)рішення.
Ale

2
@ Але: Це не зовсім дивно. list.sort(що sortedреалізовано з точки зору) використовує TimSort , який оптимізований для того, щоб скористатися наявним впорядкуванням (або зворотним впорядкуванням) у базовій послідовності, тому, хоча це теоретично O(n log n), в цьому випадку набагато ближче O(n)до виконання сортування. Крім того, CPython реалізований на мові list.sortC (уникаючи накладних витрат на інтерпретатор), тоді як heapq.mergeв основному реалізований на Python, і оптимізує для випадку "багато ітерацій" таким чином, що сповільнює "два ітерабелі".
ShadowRanger

6
Пунктом продажу heapq.mergeє те, що для цього не потрібні ні входи, ні виходи list; він може споживати ітератори / генератори і виробляє генератор, тому величезні входи / виходи (не зберігаються в оперативній пам'яті одночасно) можуть бути об'єднані без обміну місцями. Він також обробляє злиття довільної кількості вхідних ітерацій із меншими накладними витратами, ніж можна було очікувати (він використовує купу для координації злиття, тому накладні витрати масштабуються з журналом кількості ітерацій, не лінійно, але, як зазначалося, це не '' це не має значення для випадку "двох ітерацій").
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 Я не розумію, про що ти говориш. Суть відповіді полягає в тому, що додавання двох списків та їх сортування може бути швидшим для невеликого введення, ніж heapq.merge () з Python 2.6 (незважаючи на merge()те, що O (n) у часі, O (1) у просторі та сортування O ( n log n) у часі, а весь алгоритм тут O (n) у просторі) ¶ Порівняння зараз має лише історичне значення.
jfs

ця відповідь не має нічого спільного heapq.merge, ви порівнюєте sortіз чиїмось поданням коду-гольфу.
OrangeDog

@OrangeDog неправильно . merge_26()є з модуля heapq Python 2.6.
jfs

Ви той, хто сказав, що "вихідний код можна знайти тут" і пов'язаний із відповіддю на код-гольф. Не звинувачуйте людей у ​​тому, що вони думають, що код, який там можна знайти - це те, що ви перевірили.
OrangeDog

25

Це просто злиття. Поводьтесь із кожним списком так, ніби це був стек, і постійно висувайте меншу з двох головок стека, додаючи елемент до списку результатів, поки один із стеків не порожній. Потім додайте всі елементи, що залишилися, до отриманого списку.


Сортування злиття справді є оптимальним рішенням.
Ігнасіо Васкес-Абрамс

3
Але чи це швидше, ніж використання вбудованого сортування Python?
akaihola


9
Це просто злиття, а не злиття.
Гленн Мейнард,

2
@akaihola: Якщо len(L1 + L2) < 1000000тоді sorted(L1 + L2)швидше stackoverflow.com/questions/464342/…
JFS

17

У розчині госебу є невелика вада , що робить його O (n ** 2), а не O (n).
Проблема в тому, що це виконує:

item = l1.pop(0)

Зі зв’язаними списками або діками це буде операцією O (1), тому це не вплине на складність, але оскільки списки python реалізовані як вектори, це копіює решту елементів l1 на один пробіл, операція O (n) . Оскільки це робиться при кожному проходженні списку, він перетворює алгоритм O (n) в O (n ** 2). Це можна виправити, використовуючи метод, який не змінює списки джерел, а просто відстежує поточну позицію.

Я спробував порівняльний аналіз виправленого алгоритму проти простого сортування (l1 + l2), як запропонував dbr

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 (), якщо ви не очікуєте дуже великих списків, хоча він має гіршу алгоритмічну складність. Точка беззбитковості становить близько мільйона предметів у кожному списку джерел (загалом 2 мільйони).

Однією з переваг підходу злиття є те, що тривіально переписати його як генератор, який використовуватиме значно менше пам'яті (не потрібно проміжного списку).

[Редагувати] Я повторив це із ситуацією, ближчою до запитання - використовуючи список об'єктів, що містять поле " date", яке є об'єктом дати та часу. Вищезазначений алгоритм було замінено на порівняння .date, а метод сортування змінено на:

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

Це трохи змінює ситуацію. Порівняння дорожче означає, що число, яке ми виконуємо, стає більш важливим щодо постійної швидкості реалізації. Це означає, що злиття складає втрачені позиції, замість цього перевершуючи метод sort () на 100 000 предметів. Порівняння на основі ще більш складного об'єкта (наприклад, великих рядків або списків), можливо, змістить цей баланс ще більше.

# 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]: Примітка: Насправді я зробив лише 10 повторень для 1 000 000 предметів і відповідно масштабував, оскільки це було досить повільно.


Дякую за виправлення. Було б чудово, якби ви могли точно вказати на недолік та виправлення :)
Baishampayan Ghose

@ghoseb: Я дав короткий опис як коментар до вашого допису, але зараз я оновив відповідь, щоб надати більше деталей - по суті, l.pop () - це операція O (n) для списків. Це можна виправити, відстежуючи положення якимось іншим способом (альтернативно, вискакуючи з хвоста і зворотно в кінці)
Брайан,

Чи можете ви порівняти ці самі тести, але порівнюючи дати, як запитання? Я припускаю, що цей додатковий метод займе досить багато часу відносно.
Джош Смітон

Я б сказав, що різниця пов'язана з тим, що sort () реалізовано в c / c ++ та скомпільовано проти нашого merge (), який інтерпретується. merge () повинен бути швидшим на рівних умовах.
Дракоша

Хороший момент Дракоша. Покажіть, що порівняльний аналіз - це справді єдиний спосіб дізнатися напевно.
Джош Смітон

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) - гірше, ніж відсортовані наївними (l1 + l2)
Брайан

@Brian Я насправді вважаю, що це рішення є найчистішим із усіх, і я вважаю, що ти маєш рацію щодо O (n) складності вискакування першого елемента зі списку. Ви можете усунути цю проблему, використовуючи deque з колекцій, який дає вам O (1) при вискакуванні предмета з будь-якої сторони. docs.python.org/2/library/collections.html#collections.deque
mohi666

@Brian, head, tail = l[0], l[1:]також матиме складність O (n ** 2)?
Микола Фоміних

2
@ Брайан: У якості альтернативи collections.deque, вона також могла б бути вирішена шляхом створення l1і l2в зворотному порядку ( 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

Як люди вже згадували, він може викликати функцію порівняння більше разів за допомогою якогось постійного коефіцієнта (але, можливо, у багатьох випадках він викликає її більше разів за коротший період!).

Однак я ніколи б на це не покладався. - Даніель Надасі

Я вважаю, що розробники Python прагнуть дотримуватися тимчасового сортування або, принаймні, дотримуватися сортування, яке в цьому випадку має значення O (n).

Узагальнене сортування (тобто залишення окремих сортувань radix із доменів з обмеженими значеннями)
не може бути здійснено менш ніж за O (n log n) на послідовній машині. - Баррі Келлі

Правильно, сортування в загальному випадку не може бути швидшим за це. Але оскільки O () є верхньою межею, тимчасове сортування, яке є O (n log n) при довільному введенні, не суперечить його O (n) із сортуванням (L1) + сортуванням (L2).


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

Реалізація кроку злиття в Merge Sort, який переглядає обидва списки:

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 libs для цього, але я не фахівець), складність буде O (N), що явно швидше. Ідея дуже добре описана у дописі Баррі Келлі.


Як цікавий момент, алгоритм сортування python є дуже хорошим, тому продуктивність, швидше за все, буде кращою, ніж O (n log n), оскільки алгоритм часто використовує переваги закономірностей у вхідних даних. Однак я ніколи б на це не покладався.
Даніель Надасі

Узагальнене сортування (тобто залишення окремих сортувань radix із доменів з обмеженими значеннями) не може бути здійснено менш ніж за O (n log n) на послідовній машині.
Barry Kelly

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) є лінійним, тому ця версія випадково квадратична
Аллен Дауні

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)

Незрозуміло, чи це відповідь на питання, не кажучи вже про те, чи насправді? Чи можете ви дати якесь пояснення?
Бен,

Сорі, але я не зрозумів, чого ти хочеш!
Oussama Ďj Sbaa

Ви зауважите, що відповіді з більшою кількістю голосів (і більшість інших) містять текст, який пояснює, що відбувається у відповіді, і чому ця відповідь є відповіддю на питання.
Бен,

тому що він поєднує два списки в одному відсортованому списку, і це відповідь на питання
Oussama Ďj Sbaa

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)

1

Цей код має часову складність 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

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)

Відсортує список на місці. Визначте власну функцію для порівняння двох об’єктів і передайте цю функцію у вбудовану функцію сортування.

НЕ використовуйте сортування міхурів, він має жахливі результати.


Сортування злиття, безумовно, буде швидшим, але трохи складнішим, якщо вам доведеться його реалізовувати самостійно. Я думаю, що python використовує швидке сортування.
Джош Смітон

-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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.