Bağlantılı bir listeyi sıralamak için en hızlı algoritma nedir?


98

O (n log n) 'nin bağlantılı bir listenin yapabileceği en iyi şey olup olmadığını merak ediyorum.


32
Bilmeniz için, O (nlogn), karşılaştırmaya dayalı sıralama için sınırdır. O (n) performansı verebilecek karşılaştırmaya dayalı olmayan sıralamalar vardır (örneğin, sayma sıralaması), ancak veriler üzerinde ek kısıtlamalar gerektirirler.
MAK

1
"Bu kod neden çalışmıyor ?????" sorusunun aksine soruların olduğu günlerdi. SO'da kabul edilebilirdi.
Abhijit Sarkar

Yanıtlar:


102

Çalışma süresinde O (N log N) den daha iyisini yapamayacağınızı beklemek mantıklıdır .

Bununla birlikte, ilginç olan kısım, onu yerinde , istikrarlı bir şekilde , en kötü durum davranışını vb. Sıralayıp sıralayamayacağınızı araştırmaktır .

Putty ile ünlü Simon Tatham, bağlantılı bir listenin birleştirme sıralamasıyla nasıl sıralanacağını açıklıyor . Aşağıdaki yorumlarla bitiriyor:

Kendine saygılı herhangi bir sıralama algoritması gibi, bunun da çalışma süresi O (N log N) vardır. Bu Mergesort olduğu için, en kötü durum çalışma süresi hala O (N log N); patolojik vaka yok.

Yardımcı depolama gereksinimi küçük ve sabittir (yani, sıralama rutini içinde birkaç değişken). Dizilerden bağlantılı listelerin doğası gereği farklı davranışı sayesinde, bu Mergesort uygulaması normalde algoritma ile ilişkili O (N) yardımcı depolama maliyetini ortadan kaldırır.

C de hem tekil hem de çift bağlantılı listeler için çalışan bir örnek uygulama vardır.

@ Jørgen Fogh'un aşağıda bahsettiği gibi, büyük-O notasyonu, düşük sayıda öğe vb. Nedeniyle bellek konumu nedeniyle bir algoritmanın daha iyi performans göstermesine neden olabilecek bazı sabit faktörleri gizleyebilir.


3
Bu, tek bağlantılı liste için değildir. C kodu * prev ve * next kullanıyor.
LE

3
@LE Aslında ikisi için de . İmzasını listsortgörürseniz, parametreyi kullanarak geçiş yapabileceğinizi görürsünüz int is_double.
csl

1
@LE: işte yalnızca tek bağlantılı listeleri destekleyen C kodunun bir Python sürümülistsort
jfs

O (kn) teorik olarak doğrusaldır ve kovalı sıralama ile elde edilebilir. Makul bir k varsayarsak (sıraladığınız bit sayısı / nesnenin boyutu), biraz daha hızlı olabilir
Adam

74

Bir dizi faktöre bağlı olarak, listeyi bir diziye kopyalamak ve ardından bir Hızlı Sıralama kullanmak aslında daha hızlı olabilir .

Bunun daha hızlı olmasının nedeni, bir dizinin bağlantılı bir listeden çok daha iyi önbellek performansına sahip olmasıdır. Listedeki düğümler belleğe dağılmışsa, her yerde önbellek eksiklikleri oluşturuyor olabilirsiniz. Sonra tekrar, dizi büyükse yine de önbellekte eksiklikler yaşarsınız.

Mergesort daha iyi paralellik gösterir, bu nedenle istediğiniz buysa daha iyi bir seçim olabilir. Doğrudan bağlantılı liste üzerinde gerçekleştirirseniz, çok daha hızlıdır.

Her iki algoritma da O (n * log n) 'de çalıştığından, bilinçli bir karar vermek, onları çalıştırmak istediğiniz makinede ikisinin de profilinin çıkarılmasını içerir.

--- DÜZENLE

Hipotezimi test etmeye karar verdim ve clock()bağlantılı bir bilgi listesini sıralamak için geçen zamanı (kullanarak ) ölçen bir C programı yazdım . Her düğümün tahsis edildiği malloc()bağlantılı bir liste ve düğümlerin bir dizide doğrusal olarak yerleştirildiği bağlantılı bir liste ile denedim , böylece önbellek performansı daha iyi olurdu. Bunları, her şeyi parçalanmış bir listeden bir diziye kopyalamayı ve sonucu tekrar geri kopyalamayı içeren yerleşik qsort ile karşılaştırdım. Her algoritma aynı 10 veri seti üzerinde çalıştırıldı ve sonuçların ortalaması alındı.

Sonuçlar şunlardır:

N = 1000:

Birleştirme sıralamasıyla parçalanmış liste: 0.000000 saniye

Qsort ile dizi: 0.000000 saniye

Birleştirme sıralamasıyla paketli liste: 0.000000 saniye

N = 100000:

Birleştirme sıralamasıyla parçalanmış liste: 0,039000 saniye

Qsort ile dizi: 0,025000 saniye

Birleştirme sıralaması içeren paket liste: 0,009000 saniye

N = 1000000:

Birleştirme sıralamasıyla parçalanmış liste: 1.162000 saniye

Qsort ile dizi: 0.420000 saniye

Birleştirme sıralamasıyla paketli liste: 0.112000 saniye

N = 100000000:

Birleştirme sıralamasıyla parçalanmış liste: 364.797000 saniye

Qsort ile dizi: 61.166000 saniye

Birleştirme sıralaması içeren paket liste: 16.525000 saniye

Sonuç:

En azından benim makinemde, bir diziye kopyalamak, önbellek performansını artırmak için buna değer, çünkü gerçek hayatta nadiren tamamen paketlenmiş bir bağlantılı listeye sahip olursunuz. Makinemin 2.8GHz Phenom II'ye sahip olduğu ancak sadece 0.6GHz RAM'e sahip olduğu unutulmamalıdır, bu yüzden önbellek çok önemlidir.


2
İyi yorumlar, ancak verileri bir listeden bir diziye kopyalamanın sabit olmayan maliyetini (listeyi taramanız gerekir) ve hızlı sıralama için en kötü durum çalışma süresini göz önünde bulundurmalısınız.
csl

1
O (n * log n) teorik olarak O (n * log n + n) ile aynıdır ve kopyanın maliyeti de buna dahildir. Yeterince büyük herhangi bir n için, kopyanın maliyeti gerçekten önemli olmamalıdır; Bir listeyi bir kez sonuna kadar geçmek n kez olmalıdır.
Dean J

1
@DeanJ: Teorik olarak, evet, ancak orijinal posterin mikro optimizasyonların önemli olduğu durumu ortaya koyduğunu unutmayın. Ve bu durumda, bağlantılı bir listeyi bir diziye dönüştürmek için harcanan zaman dikkate alınmalıdır. Yorumlar anlayışlı, ancak gerçekte performans artışı sağlayacağına tamamen ikna olmadım. Belki çok küçük bir N için işe yarayabilir.
csl

1
@csl: Aslında, büyük N için yerelliğin faydalarının devreye girmesini bekliyorum. Önbellek eksikliklerinin baskın performans etkisi olduğunu varsayarsak, kopya-qsort-kopyalama yaklaşımı, kopyalama için yaklaşık 2 * N önbellek kaybına neden olur, artı qsort için kayıp sayısı, ki bu, N log (N) ' nin küçük bir kısmı olacaktır (çünkü qsort'taki çoğu erişim, son erişilen bir öğeye yakın bir öğeye yöneliktir). Birleştirme sıralaması için kaçırma sayısı, daha yüksek bir karşılaştırma oranı önbellek kaybına neden olduğundan , N log (N) ' nin daha büyük bir bölümüdür . Yani büyük N için, bu terim birleşme sırasına hakimdir ve onu yavaşlatır.
Steve Jessop

2
@Steve: qsort'un bir drop-in ikamesi olmadığı konusunda haklısın, ama benim açımdan gerçekten qsort ve mergesort hakkında değil. Qsort hazır olduğunda, birleşme sırasının başka bir versiyonunu yazmak istemedim. Standart kütüphane yolu kendi haddeleme daha uygun.
Jørgen Fogh

10

Bu, bu konuyla ilgili güzel bir küçük makale. Onun ampirik sonucu, Treesort'un en iyisi olduğu ve bunu Quicksort ve Mergesort'un takip ettiği. Tortu ayırma, kabarcık sıralama, seçme sıralama çok kötü performans gösterir.

BAĞLANTILI LİSTE AYIKLAMA ALGORİTMALARININ KARŞILAŞTIRMALI ÇALIŞMASI Ching-Kuang Shene tarafından

http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.31.9981


8

Karşılaştırma sıraları (yani, öğeleri karşılaştırmaya dayalı olanlar) bundan daha hızlı olamaz n log n. Temel veri yapısının ne olduğu önemli değildir. Wikipedia'ya bakın .

Listede birçok özdeş öğenin (sayma sıralaması gibi) veya listedeki öğelerin beklenen dağılımının avantajlarından yararlanan diğer türler, daha hızlıdır, ancak özellikle iyi çalışan herhangi birini düşünemiyorum bağlantılı bir listede.


5

Birçok kez belirtildiği gibi, genel veriler için karşılaştırmaya dayalı sıralamaya dayalı alt sınır O (n log n) olacaktır. Bu argümanları kısaca özetlemek gerekirse, n var! bir liste sıralanabilir farklı yollar. N! İçeren herhangi bir tür karşılaştırma ağacı! (O (n ^ n) içindedir) olası son sıralar, yüksekliği olarak en azından log (n!) log n).

Dolayısıyla, bağlantılı bir listedeki genel veriler için, iki nesneyi karşılaştırabilen herhangi bir veri üzerinde çalışacak olası en iyi sıralama O (n log n) olacaktır. Bununla birlikte, çalışmanız gereken daha sınırlı bir alanınız varsa, gereken süreyi iyileştirebilirsiniz (en azından n ile orantılı). Örneğin, belirli bir değerden büyük olmayan tamsayılarla çalışıyorsanız , karmaşıklığı n oranına göre azaltmak için sıraladığınız belirli nesneleri kullandıklarından Sayma Sıralaması veya Taban Tablosu Sıralaması kullanabilirsiniz. Ancak dikkatli olun, bunlar karmaşıklığa sizin göz önünde bulundurmayabileceğiniz başka şeyler de ekler (örneğin, Sayma Sıralaması ve Tabla sıralaması, sıraladığınız sayıların boyutuna dayalı faktörleri ekler, O (n + k ) burada k, Sayım Sıralaması için en büyük sayının boyutudur).

Ayrıca, mükemmel bir hash'e (veya en azından tüm değerleri farklı şekilde eşleyen bir hash'e) sahip nesneleriniz varsa, hash fonksiyonlarında bir sayma veya taban sıralaması kullanmayı deneyebilirsiniz.


3

Bir Basix sıralaması özellikle bağlantılı bir listeye uygundur, çünkü bir basamağın olası her değerine karşılık gelen baş işaretçileri tablosu oluşturmak kolaydır.


1
Bu konu hakkında daha fazla açıklama yapabilir misiniz veya bağlantılı listede radix sıralama için herhangi bir kaynak bağlantısı verebilir misiniz?
LoveToCode

2

Birleştirme sıralaması O (1) erişimi gerektirmez ve O (n ln n) 'dir. Genel verileri sıralamak için bilinen hiçbir algoritma O (n ln n) 'den daha iyi değildir.

Geçici depolama olarak O (1) erişimiyle farklı bir yapı kullandığınız sürece, taban sıralaması (verilerin boyutunu sınırlar) veya histogram sıralama (ayrık verileri sayar) gibi özel veri algoritmaları, bağlantılı bir listeyi daha düşük bir büyüme işleviyle sıralayabilir. .

Diğer bir özel veri sınıfı, sıra dışı k öğeli neredeyse sıralanmış bir listenin karşılaştırmalı türüdür. Bu, O (kn) işlemlerinde sıralanabilir.

Listenin bir diziye kopyalanması ve geri alınması O (N) olacaktır, bu nedenle alan bir sorun değilse herhangi bir sıralama algoritması kullanılabilir.

Örneğin, aşağıdakileri içeren bağlantılı bir liste verildiğinde uint_8, bu kod bir histogram sıralaması kullanarak O (N) zamanında sıralayacaktır:

#include <stdio.h>
#include <stdint.h>
#include <malloc.h>

typedef struct _list list_t;
struct _list {
    uint8_t value;
    list_t  *next;
};


list_t* sort_list ( list_t* list )
{
    list_t* heads[257] = {0};
    list_t* tails[257] = {0};

    // O(N) loop
    for ( list_t* it = list; it != 0; it = it -> next ) {
        list_t* next = it -> next;

        if ( heads[ it -> value ] == 0 ) {
            heads[ it -> value ] = it;
        } else {
            tails[ it -> value ] -> next = it;
        }

        tails[ it -> value ] = it;
    }

    list_t* result = 0;

    // constant time loop
    for ( size_t i = 255; i-- > 0; ) {
        if ( tails[i] ) {
            tails[i] -> next = result;
            result = heads[i];
        }
    }

    return result;
}

list_t* make_list ( char* string )
{
    list_t head;

    for ( list_t* it = &head; *string; it = it -> next, ++string ) {
        it -> next = malloc ( sizeof ( list_t ) );
        it -> next -> value = ( uint8_t ) * string;
        it -> next -> next = 0;
    }

    return head.next;
}

void free_list ( list_t* list )
{
    for ( list_t* it = list; it != 0; ) {
        list_t* next = it -> next;
        free ( it );
        it = next;
    }
}

void print_list ( list_t* list )
{
    printf ( "[ " );

    if ( list ) {
        printf ( "%c", list -> value );

        for ( list_t* it = list -> next; it != 0; it = it -> next )
            printf ( ", %c", it -> value );
    }

    printf ( " ]\n" );
}


int main ( int nargs, char** args )
{
    list_t* list = make_list ( nargs > 1 ? args[1] : "wibble" );


    print_list ( list );

    list_t* sorted = sort_list ( list );


    print_list ( sorted );

    free_list ( list );
}

5
Oldu kanıtlanmış hiçbir karşılaştırma tabanlı sıralama algorthms daha hızlı olan n kaydettiğini var olduğunu.
Artelius

9
Hayır, genel verilerdeki karşılaştırmaya dayalı sıralama algoritmalarının n log n'den daha hızlı olmadığı kanıtlandı
Pete Kirkham

Hayır, O(n lg n)karşılaştırmaya dayalı olmayandan daha hızlı herhangi bir sıralama algoritması (örneğin, taban sıralaması). Tanım olarak, karşılaştırma sıralaması, toplam sıralaması olan (yani karşılaştırılabilen) herhangi bir alan için geçerlidir.
bdonlan

3
@bdonlan "genel veri" nin amacı, kısıtlı girdi için rasgele girdi yerine daha hızlı olan algoritmaların olmasıdır. Sınırlayıcı durumda, giriş verilerinin halihazırda sıralanacak şekilde sınırlandırılması durumunda bir listeyi sıralayan önemsiz bir O (1) algoritması yazabilirsiniz
Pete Kirkham

Ve bu karşılaştırmaya dayalı bir sıralama olmaz. Karşılaştırma sıralamaları zaten genel verileri işlediğinden (ve büyük-O gösterimi yapılan karşılaştırmaların sayısı içindir) "genel verilerde" değiştiricisi gereksizdir.
Steve Jessop

1

Sorunuza doğrudan bir cevap değil, ancak bir Atlama Listesi kullanıyorsanız , zaten sıralanmıştır ve O (log N) arama süresine sahiptir.


1
beklenen O(lg N) arama süresi - ancak garanti edilmez, çünkü atlama listeleri rastgeleliğe dayalıdır. Güvenilir olmayan bir girdi alıyorsanız, girdinin tedarikçisinin
RNG'nizi

1

Bildiğim gibi, en iyi sıralama algoritması, konteyner ne olursa olsun O (n * log n) 'dir - kelimenin geniş anlamıyla (birleştirme / hızlı sıralama vb. Stil) sıralamanın daha düşük olamayacağı kanıtlanmıştır. Bağlantılı bir liste kullanmak size daha iyi bir çalışma süresi sağlamaz.

O (n) 'de çalışan tek algoritma, gerçekte sıralama yerine sayma değerlerine dayanan bir "hack" algoritmasıdır.


3
Bu bir hack algoritması değildir ve O (n) ile çalışmaz. O (cn) ile çalışır, burada c sıraladığınız en büyük değerdir (aslında en yüksek ve en düşük değerler arasındaki farktır) ve yalnızca integral değerler üzerinde çalışır. O (n) ve O (cn) arasında bir fark vardır, çünkü sıraladığınız değerler için kesin bir üst sınır veremezseniz (ve dolayısıyla onu bir sabitle sınırlayamazsanız), karmaşıklığı karmaşıklaştıran iki faktörünüz vardır.
DivineWolfwood

Kesinlikle, içeri giriyor O(n lg c). Tüm öğeleriniz benzersizse, o zaman c >= nve bu nedenle daha uzun sürer O(n lg n).
bdonlan

1

İşte bir uygulama var ishal toplayarak, sadece bir kez listesini erişir, mergesort yok aynı şekilde o programları birleştirir.

Karmaşıklık O (n log m) olup, burada n öğe sayısıdır ve m çalıştırma sayısıdır. En iyi durum, beklendiği gibi O (n) (veriler zaten sıralanmışsa) ve en kötü durum O (n log n) 'dir.

O (log m) geçici hafıza gerektirir; sıralama, listelerde yerinde yapılır.

(aşağıda güncellendi. Yorumcu biri, onu burada açıklamam gerektiği konusunda iyi bir noktaya işaret ediyor)

Algoritmanın özü şudur:

    while list not empty
        accumulate a run from the start of the list
        merge the run with a stack of merges that simulate mergesort's recursion
    merge all remaining items on the stack

Koşular biriktirmek çok fazla açıklama gerektirmez, ancak hem artan hem de alçalan koşuları (tersine çevrilmiş) biriktirme fırsatını kullanmak iyidir. Burada, çalışmanın başından daha küçük öğelerin başına ve çalışmanın sonuna eşit veya daha büyük olan öğeleri ekler. (Önceden eklerken, sıralama kararlılığını korumak için daha az-değerinin kullanılması gerektiğini unutmayın.)

Birleştirme kodunu buraya yapıştırmak en kolay yoldur:

    int i = 0;
    for ( ; i < stack.size(); ++i) {
        if (!stack[i])
            break;
        run = merge(run, stack[i], comp);
        stack[i] = nullptr;
    }
    if (i < stack.size()) {
        stack[i] = run;
    } else {
        stack.push_back(run);
    }

Listeyi sıralamayı düşünün (dagibecfjh) (çalıştırmaları yok sayarak). Yığın durumları şu şekilde ilerler:

    [ ]
    [ (d) ]
    [ () (a d) ]
    [ (g), (a d) ]
    [ () () (a d g i) ]
    [ (b) () (a d g i) ]
    [ () (b e) (a d g i) ]
    [ (c) (b e) (a d g i ) ]
    [ () () () (a b c d e f g i) ]
    [ (j) () () (a b c d e f g i) ]
    [ () (h j) () (a b c d e f g i) ]

Son olarak, tüm bu listeleri birleştirin.

[İ] yığınındaki öğe sayısının (çalıştırma) sıfır veya 2 ^ i olduğuna ve yığın boyutunun 1 + log2 (sayı) ile sınırlandığına dikkat edin. Her eleman yığın seviyesi başına bir kez birleştirilir, dolayısıyla O (n log m) karşılaştırmaları yapılır. Burada Timsort'a geçici bir benzerlik var, ancak Timsort yığınını Fibonacci dizisi gibi bir şey kullanarak sürdürüyor, burada ikinin güçleri kullanıyor.

İşlemlerin biriktirilmesi, önceden sıralanmış verilerin avantajlarından yararlanır, böylece en iyi durum karmaşıklığı, önceden sıralanmış bir liste (tek çalıştırma) için O (n) olur. Hem yükselen hem de alçalan serileri biriktirdiğimiz için, çalışmalar her zaman en az 2 uzunluğunda olacaktır. (Bu, maksimum yığın derinliğini en az bir azaltır ve ilk etapta serileri bulma maliyetini öder.) En kötü durum karmaşıklığıdır. O (n log n), yüksek oranda rasgele hale getirilmiş veriler için beklendiği gibi.

(Um ... İkinci güncelleme.)

Ya da aşağıdan yukarıya birleştirme sıralamasında wikipedia'ya bakın .


Çalıştırma oluşturmanın "tersine girdi" ile iyi performans göstermesi hoş bir dokunuş. O(log m)ek belleğe ihtiyaç duyulmamalıdır - sadece biri boşalana kadar sırayla iki listeye çalışma ekleyin.
greybeard

1

Bunu bir diziye kopyalayabilir ve ardından sıralayabilirsiniz.

  • O (n) dizisine kopyalama,

  • sıralama O (nlgn) (birleştirme sıralaması gibi hızlı bir algoritma kullanıyorsanız),

  • Gerekirse bağlantılı liste O (n) 'ye geri kopyalamak,

yani O (nlgn) olacak.

Bağlantılı listedeki elemanların sayısını bilmiyorsanız, dizinin boyutunu bilmeyeceğinizi unutmayın. Java ile kodlama yapıyorsanız, örneğin bir Dizi Listesi kullanabilirsiniz.




0

Soru LeetCode # 148 ve tüm büyük dillerde sunulan birçok çözüm var. Benimki aşağıdaki gibi ama zamanın karmaşıklığını merak ediyorum. Ortadaki öğeyi bulmak için her seferinde tam listeyi tararız. İlk kez nöğeler yinelenir, ikinci kez 2 * n/2öğeler yinelenir, vb. Zaman geldi gibi görünüyor O(n^2).

def sort(linked_list: LinkedList[int]) -> LinkedList[int]:
    # Return n // 2 element
    def middle(head: LinkedList[int]) -> LinkedList[int]:
        if not head or not head.next:
            return head
        slow = head
        fast = head.next

        while fast and fast.next:
            slow = slow.next
            fast = fast.next.next

        return slow

    def merge(head1: LinkedList[int], head2: LinkedList[int]) -> LinkedList[int]:
        p1 = head1
        p2 = head2
        prev = head = None

        while p1 and p2:
            smaller = p1 if p1.val < p2.val else p2
            if not head:
                head = smaller
            if prev:
                prev.next = smaller
            prev = smaller

            if smaller == p1:
                p1 = p1.next
            else:
                p2 = p2.next

        if prev:
            prev.next = p1 or p2
        else:
            head = p1 or p2

        return head

    def merge_sort(head: LinkedList[int]) -> LinkedList[int]:
        if head and head.next:
            mid = middle(head)
            mid_next = mid.next
            # Makes it easier to stop
            mid.next = None

            return merge(merge_sort(head), merge_sort(mid_next))
        else:
            return head

    return merge_sort(linked_list)
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.