O (n log n) 'nin bağlantılı bir listenin yapabileceği en iyi şey olup olmadığını merak ediyorum.
O (n log n) 'nin bağlantılı bir listenin yapabileceği en iyi şey olup olmadığını merak ediyorum.
Yanıtlar:
Ç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.
listsort
görürseniz, parametreyi kullanarak geçiş yapabileceğinizi görürsünüz int is_double
.
listsort
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.
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
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.
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.
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.
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 );
}
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.
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.
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
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.
O(n lg c)
. Tüm öğeleriniz benzersizse, o zaman c >= n
ve bu nedenle daha uzun sürer O(n lg n)
.
İş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 .
O(log m)
ek belleğe ihtiyaç duyulmamalıdır - sadece biri boşalana kadar sırayla iki listeye çalışma ekleyin.
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.
Mergesort, burada yapabileceğinizin en iyisidir.
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)