Asal tahu saja, O (nlogn) adalah batas untuk urutan berbasis perbandingan. Ada jenis berbasis non-perbandingan yang dapat memberikan kinerja O (n) (misalnya urutan penghitungan), tetapi mereka memerlukan batasan tambahan pada data.
MAK
Itu adalah hari-hari ketika pertanyaan tidak seperti "mengapa kode ini tidak berfungsi ?????" dapat diterima di SO.
Abhijit Sarkar
Jawaban:
100
Masuk akal untuk mengharapkan bahwa Anda tidak dapat melakukan yang lebih baik dari O (N log N) dalam waktu berjalan .
Namun, bagian yang menarik adalah menyelidiki apakah Anda dapat mengurutkannya di tempat , secara stabil , perilaku terburuknya, dan sebagainya.
Seperti algoritme pengurutan harga diri lainnya, algoritme ini memiliki waktu berjalan O (N log N). Karena ini adalah Mergesort, waktu pengoperasian terburuk masih O (N log N); tidak ada kasus patologis.
Persyaratan penyimpanan tambahan kecil dan konstan (yaitu beberapa variabel dalam rutinitas penyortiran). Berkat perilaku yang berbeda secara inheren dari daftar tertaut dari larik, implementasi Mergesort ini menghindari biaya penyimpanan tambahan O (N) yang biasanya dikaitkan dengan algoritme.
Ada juga contoh implementasi di C yang berfungsi untuk daftar tertaut tunggal dan ganda.
Seperti yang disebutkan @ Jørgen Fogh di bawah ini, notasi big-O dapat menyembunyikan beberapa faktor konstan yang dapat menyebabkan satu algoritme bekerja lebih baik karena lokasi memori, karena jumlah item yang sedikit, dll.
Ini bukan untuk satu daftar tertaut. Kode C-nya menggunakan * prev dan * next.
LE
3
@LE Sebenarnya untuk keduanya . Jika Anda melihat tanda tangan untuk listsort, Anda akan melihat bahwa Anda dapat beralih menggunakan parameter int is_double.
O (kn) secara teoritis linier, dan dapat dicapai dengan jenis ember. Dengan asumsi k yang wajar (jumlah bit / ukuran objek yang Anda sortir), mungkin akan sedikit lebih cepat
Adam
74
Bergantung pada sejumlah faktor, sebenarnya mungkin lebih cepat untuk menyalin daftar ke larik dan kemudian menggunakan Quicksort .
Alasan ini mungkin lebih cepat adalah karena array memiliki kinerja cache yang jauh lebih baik daripada daftar tertaut. Jika node dalam daftar tersebar di memori, Anda mungkin menghasilkan cache miss di semua tempat. Kemudian lagi, jika arraynya besar Anda akan mendapatkan cache yang hilang.
Mergesort paralel dengan lebih baik, jadi ini mungkin pilihan yang lebih baik jika itu yang Anda inginkan. Ini juga jauh lebih cepat jika Anda melakukannya langsung di daftar tertaut.
Karena kedua algoritme berjalan dalam O (n * log n), membuat keputusan yang tepat akan melibatkan pembuatan profil keduanya di mesin tempat Anda ingin menjalankannya.
--- EDIT
Saya memutuskan untuk menguji hipotesis saya dan menulis program C yang mengukur waktu (menggunakan clock()) yang dibutuhkan untuk mengurutkan daftar int yang ditautkan. Saya mencoba dengan daftar tertaut di mana setiap node dialokasikan malloc()dan daftar tertaut di mana node diletakkan secara linier dalam sebuah array, sehingga kinerja cache akan lebih baik. Saya membandingkan ini dengan qsort bawaan, yang mencakup menyalin semuanya dari daftar terfragmentasi ke array dan menyalin hasilnya kembali. Setiap algoritma dijalankan pada 10 set data yang sama dan hasilnya dirata-ratakan.
Inilah hasilnya:
N = 1000:
Daftar terpecah dengan jenis gabungan: 0,000000 detik
Array dengan qsort: 0,000000 detik
Daftar yang dikemas dengan jenis gabungan: 0,000000 detik
N = 100000:
Daftar terfragmentasi dengan jenis gabungan: 0,039000 detik
Array dengan qsort: 0,025000 detik
Daftar yang dikemas dengan jenis gabungan: 0,009000 detik
N = 1000000:
Daftar terfragmentasi dengan jenis gabungan: 1,162000 detik
Array dengan qsort: 0,420000 detik
Daftar yang dikemas dengan jenis gabungan: 0,112000 detik
N = 100000000:
Daftar terfragmentasi dengan jenis gabungan: 364.797000 detik
Array dengan qsort: 61.166000 detik
Daftar yang dikemas dengan jenis gabungan: 16,525000 detik
Kesimpulan:
Setidaknya di mesin saya, menyalin ke dalam array sangat bermanfaat untuk meningkatkan kinerja cache, karena Anda jarang memiliki daftar tertaut yang sepenuhnya dikemas dalam kehidupan nyata. Perlu dicatat bahwa mesin saya memiliki Phenom II 2.8GHz, tetapi hanya RAM 0.6GHz, jadi cache sangat penting.
Komentar yang bagus, tetapi Anda harus mempertimbangkan biaya tidak konstan untuk menyalin data dari daftar ke larik (Anda harus melintasi daftar), serta waktu berjalan kasus terburuk untuk quicksort.
csl
1
O (n * log n) secara teori sama dengan O (n * log n + n), yang akan termasuk biaya penyalinan. Untuk n yang cukup besar, biaya penyalinan seharusnya tidak menjadi masalah; melintasi daftar sekali sampai akhir harus n kali.
Dean J
1
@DeanJ: Secara teoritis, ya, tapi ingat bahwa poster asli mengemukakan kasus di mana pengoptimalan mikro penting. Dan dalam hal ini, waktu yang dihabiskan untuk mengubah daftar tertaut menjadi sebuah array harus dipertimbangkan. Komentarnya berwawasan, tapi saya tidak sepenuhnya yakin itu akan memberikan keuntungan kinerja dalam kenyataan. Mungkin berhasil untuk N yang sangat kecil, mungkin.
csl
1
@csl: Sebenarnya, saya mengharapkan manfaat dari lokalitas untuk memulai N. Dengan asumsi bahwa cache miss adalah efek kinerja yang dominan, maka pendekatan copy-qsort-copy menghasilkan sekitar 2 * N cache yang hilang untuk disalin, ditambah jumlah kesalahan untuk qsort, yang merupakan pecahan kecil dari N log (N) (karena sebagian besar akses dalam qsort adalah ke elemen yang dekat dengan elemen yang baru diakses). Jumlah kesalahan untuk jenis gabungan adalah bagian yang lebih besar dari N log (N), karena proporsi perbandingan yang lebih tinggi menyebabkan cache tidak ditemukan. Jadi untuk N besar, istilah ini mendominasi dan memperlambat mergesort.
Steve Jessop
2
@Steve: Anda benar bahwa qsort bukanlah pengganti drop-in, tetapi maksud saya bukan tentang qsort vs. mergesort. Saya hanya tidak ingin menulis versi lain dari mergesort ketika qsort sudah tersedia. Perpustakaan standar jauh lebih nyaman daripada menggulung milik Anda sendiri.
Jørgen Fogh
8
Jenis perbandingan (yaitu yang didasarkan pada elemen pembanding) tidak mungkin lebih cepat dari n log n. Tidak masalah apa struktur data yang mendasarinya. Lihat Wikipedia .
Jenis lain yang memanfaatkan banyaknya elemen identik dalam daftar (seperti jenis penghitungan), atau beberapa distribusi elemen yang diharapkan dalam daftar, lebih cepat, meskipun saya tidak dapat memikirkan yang berfungsi dengan baik pada daftar tertaut.
Ini adalah makalah kecil yang bagus tentang topik ini. Kesimpulan empirisnya adalah bahwa Treesort adalah yang terbaik, diikuti oleh Quicksort dan Mergesort. Jenis sedimen, jenis gelembung, jenis pemilihan berkinerja sangat buruk.
STUDI PERBANDINGAN ALGORITMA PENYORTIRAN DAFTAR TERKAIT oleh Ching-Kuang Shene
Seperti yang dinyatakan berkali-kali, batas bawah pengurutan berbasis perbandingan untuk data umum adalah O (n log n). Untuk meringkas argumen ini secara singkat, ada n! berbagai cara memilah daftar. Semua jenis pohon pembanding yang memiliki n! (yang ada di O (n ^ n)) kemungkinan pengurutan terakhir akan membutuhkan setidaknya log (n!) sebagai tingginya: ini memberi Anda batas bawah O (log (n ^ n)), yaitu O (n log n).
Jadi, untuk data umum pada daftar tertaut, pengurutan terbaik yang akan bekerja pada data yang dapat membandingkan dua objek adalah O (n log n). Namun, jika Anda memiliki domain yang lebih terbatas untuk mengerjakan sesuatu, Anda dapat meningkatkan waktu yang dibutuhkan (setidaknya sebanding dengan n). Misalnya, jika Anda bekerja dengan bilangan bulat yang tidak lebih besar dari beberapa nilai, Anda dapat menggunakan Counting Sort atau Radix Sort , karena ini menggunakan objek spesifik yang Anda sortir untuk mengurangi kompleksitas dengan proporsi ke n. Berhati-hatilah, ini menambahkan beberapa hal lain ke kompleksitas yang mungkin tidak Anda pertimbangkan (misalnya, Counting Sort dan Radix sort keduanya menambahkan faktor yang didasarkan pada ukuran angka yang Anda sortir, O (n + k) ) di mana k adalah ukuran bilangan terbesar untuk Counting Sort, misalnya).
Selain itu, jika Anda kebetulan memiliki objek yang memiliki hash sempurna (atau setidaknya hash yang memetakan semua nilai secara berbeda), Anda dapat mencoba menggunakan penghitungan atau pengurutan radix pada fungsi hashnya.
Sebuah Radix semacam ini sangat cocok untuk sebuah linked list, karena sangat mudah untuk membuat tabel pointer kepala sesuai untuk setiap nilai yang mungkin dari digit.
Bisakah Anda menjelaskan lebih lanjut tentang topik ini atau memberikan tautan sumber daya untuk jenis radix dalam daftar tertaut.
LoveToCode
2
Merge sort tidak memerlukan akses O (1) dan merupakan O (n ln n). Tidak ada algoritma yang dikenal untuk menyortir data umum yang lebih baik dari O (n ln n).
Algoritme data khusus seperti radix sort (batas ukuran data) atau histogram sort (menghitung data diskrit) dapat mengurutkan daftar tertaut dengan fungsi pertumbuhan yang lebih rendah, selama Anda menggunakan struktur yang berbeda dengan akses O (1) sebagai penyimpanan sementara .
Kelas lain dari data khusus adalah semacam perbandingan dari daftar yang hampir diurutkan dengan elemen k rusak. Ini dapat diurutkan dalam operasi O (kn).
Menyalin daftar ke larik dan kembali akan menjadi O (N), jadi algoritme pengurutan apa pun dapat digunakan jika spasi bukan masalah.
Misalnya, diberikan daftar tertaut yang berisi uint_8, kode ini akan mengurutkannya dalam waktu O (N) menggunakan semacam histogram:
#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 );
}
Telah dibuktikan bahwa tidak ada algoritma pengurutan berbasis perbandingan yang lebih cepat dari n log n.
Artelius
9
Tidak, telah terbukti bahwa tidak ada algoritme pengurutan berbasis perbandingan pada data umum yang lebih cepat daripada n log n
Pete Kirkham
Tidak, algoritme pengurutan apa pun yang lebih cepat daripada O(n lg n)tidak akan menjadi berbasis perbandingan (misalnya, pengurutan radix). Menurut definisi, jenis perbandingan berlaku untuk domain apa pun yang memiliki urutan total (yaitu, dapat dibandingkan).
bdonlan
3
@bdonlan Inti dari "data umum" adalah bahwa ada algoritma yang lebih cepat untuk input terbatas, daripada input acak. Pada kasus yang membatasi, Anda dapat menulis algoritme O (1) sepele yang mengurutkan daftar mengingat data input dibatasi untuk sudah diurutkan
Pete Kirkham
Dan itu bukan jenis berdasarkan perbandingan. Pengubah "pada data umum" adalah redundan, karena sortir perbandingan sudah menangani data umum (dan notasi big-O adalah untuk jumlah perbandingan yang dibuat).
Steve Jessop
1
Bukan jawaban langsung untuk pertanyaan Anda, tetapi jika Anda menggunakan Daftar Lewati , itu sudah diurutkan dan memiliki waktu pencarian O (log N).
O(lg N)waktu pencarian yang diharapkan - tetapi tidak dijamin, karena daftar lewati bergantung pada keacakan. Jika Anda menerima input yang tidak tepercaya, pastikan pemasok input tidak dapat memprediksi RNG Anda, atau mereka dapat mengirimi Anda data yang memicu kinerja kasus terburuknya
bdonlan
1
Seperti yang saya tahu, algoritme pengurutan terbaik adalah O (n * log n), apa pun wadahnya - telah terbukti bahwa pengurutan dalam arti luas kata (gaya mergesort / quicksort dll) tidak bisa lebih rendah. Menggunakan daftar tertaut tidak akan memberi Anda waktu pengoperasian yang lebih baik.
Satu-satunya algoritme yang berjalan di O (n) adalah algoritme "retas" yang mengandalkan nilai penghitungan alih-alih pengurutan.
Ini bukan algoritma hack, dan tidak berjalan di O (n). Ini berjalan di O (cn), di mana c adalah nilai terbesar yang Anda sortir (yah, sebenarnya itu adalah perbedaan antara nilai tertinggi dan terendah) dan hanya berfungsi pada nilai integral. Ada perbedaan antara O (n) dan O (cn), kecuali jika Anda dapat memberikan batas atas definitif untuk nilai yang Anda sortir (dan dengan demikian mengikatnya dengan konstanta), Anda memiliki dua faktor yang memperumit kompleksitas.
DivineWolfwood
Sebenarnya, itu masuk O(n lg c). Jika semua elemen Anda unik, maka c >= ndibutuhkan waktu lebih lama dari O(n lg n).
bdonlan
1
Berikut adalah implementasi yang melintasi daftar hanya sekali, mengumpulkan proses, lalu menjadwalkan penggabungan dengan cara yang sama seperti penggabungan.
Kompleksitas adalah O (n log m) dimana n adalah jumlah item dan m adalah jumlah run. Kasus terbaik adalah O (n) (jika data sudah diurutkan) dan kasus terburuk adalah O (n log n) seperti yang diharapkan.
Ini membutuhkan memori sementara O (log m); pengurutan dilakukan di tempat pada daftar.
(diperbarui di bawah. commenter one membuat poin yang bagus bahwa saya harus menjelaskannya di sini)
Inti dari algoritma ini adalah:
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
Mengumpulkan run tidak membutuhkan banyak penjelasan, tetapi ada baiknya untuk mengambil kesempatan ini untuk mengakumulasi jalur ascending dan descending run (terbalik). Di sini ia menambahkan item yang lebih kecil dari kepala proses dan menambahkan item yang lebih besar dari atau sama dengan akhir proses. (Perhatikan bahwa prepending harus menggunakan tight less-than untuk menjaga stabilitas sortir.)
Paling mudah hanya menempelkan kode penggabungan di sini:
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);
}
Pertimbangkan untuk mengurutkan daftar (dagibecfjh) (mengabaikan proses). Status tumpukan dilanjutkan sebagai berikut:
[ ]
[ (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) ]
Kemudian, akhirnya, gabungkan semua daftar ini.
Perhatikan bahwa jumlah item (berjalan) pada tumpukan [i] adalah nol atau 2 ^ i dan ukuran tumpukan dibatasi oleh 1 + log2 (nruns). Setiap elemen digabung sekali per level tumpukan, karenanya O (n log m) perbandingan. Ada kemiripan lewat Timsort di sini, meskipun Timsort mempertahankan tumpukannya menggunakan sesuatu seperti deret Fibonacci di mana ini menggunakan pangkat dua.
Akumulasi proses memanfaatkan data yang telah diurutkan sehingga kompleksitas kasus terbaik adalah O (n) untuk daftar yang sudah diurutkan (satu proses). Karena kita mengumpulkan proses ascending dan descending, run akan selalu setidaknya panjang 2. (Ini mengurangi kedalaman tumpukan maksimum setidaknya satu, membayar biaya untuk menemukan proses di tempat pertama.) Kompleksitas kasus terburuk adalah O (n log n), seperti yang diharapkan, untuk data yang sangat diacak.
Setelah menjalankan pembuatan, bekerja dengan baik dengan "input terbalik" adalah sentuhan yang bagus. O(log m)memori tambahan tidak diperlukan - cukup tambahkan proses ke dua daftar secara bergantian sampai salah satu kosong.
greybeard
1
Anda dapat menyalinnya ke dalam array dan kemudian mengurutkannya.
Menyalin ke dalam larik O (n),
sorting O (nlgn) (jika Anda menggunakan algoritma cepat seperti merge sort),
menyalin kembali ke daftar tertaut O (n) jika perlu,
jadi itu akan menjadi O (nlgn).
Perhatikan bahwa jika Anda tidak mengetahui jumlah elemen dalam daftar tertaut, Anda tidak akan mengetahui ukuran larik. Jika Anda membuat kode di java, Anda dapat menggunakan Arraylist misalnya.
Akan menjadi jawaban yang lebih baik jika Anda menjelaskan mengapa .
csl
0
Pertanyaannya adalah LeetCode # 148 , dan ada banyak solusi yang ditawarkan dalam semua bahasa utama. Milik saya adalah sebagai berikut, tetapi saya bertanya-tanya tentang kerumitan waktu. Untuk menemukan elemen tengah, kami melintasi daftar lengkap setiap kali. nElemen pertama kali diiterasi, 2 * n/2elemen kedua diulangi, seterusnya, dan seterusnya. Sepertinya sudah O(n^2)waktunya.
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)
Jawaban:
Masuk akal untuk mengharapkan bahwa Anda tidak dapat melakukan yang lebih baik dari O (N log N) dalam waktu berjalan .
Namun, bagian yang menarik adalah menyelidiki apakah Anda dapat mengurutkannya di tempat , secara stabil , perilaku terburuknya, dan sebagainya.
Simon Tatham, dari Putty fame, menjelaskan cara mengurutkan daftar tertaut dengan jenis gabungan . Dia menyimpulkan dengan komentar berikut:
Ada juga contoh implementasi di C yang berfungsi untuk daftar tertaut tunggal dan ganda.
Seperti yang disebutkan @ Jørgen Fogh di bawah ini, notasi big-O dapat menyembunyikan beberapa faktor konstan yang dapat menyebabkan satu algoritme bekerja lebih baik karena lokasi memori, karena jumlah item yang sedikit, dll.
sumber
listsort
, Anda akan melihat bahwa Anda dapat beralih menggunakan parameterint is_double
.listsort
kode C yang hanya mendukung daftar tertaut tunggalBergantung pada sejumlah faktor, sebenarnya mungkin lebih cepat untuk menyalin daftar ke larik dan kemudian menggunakan Quicksort .
Alasan ini mungkin lebih cepat adalah karena array memiliki kinerja cache yang jauh lebih baik daripada daftar tertaut. Jika node dalam daftar tersebar di memori, Anda mungkin menghasilkan cache miss di semua tempat. Kemudian lagi, jika arraynya besar Anda akan mendapatkan cache yang hilang.
Mergesort paralel dengan lebih baik, jadi ini mungkin pilihan yang lebih baik jika itu yang Anda inginkan. Ini juga jauh lebih cepat jika Anda melakukannya langsung di daftar tertaut.
Karena kedua algoritme berjalan dalam O (n * log n), membuat keputusan yang tepat akan melibatkan pembuatan profil keduanya di mesin tempat Anda ingin menjalankannya.
--- EDIT
Saya memutuskan untuk menguji hipotesis saya dan menulis program C yang mengukur waktu (menggunakan
clock()
) yang dibutuhkan untuk mengurutkan daftar int yang ditautkan. Saya mencoba dengan daftar tertaut di mana setiap node dialokasikanmalloc()
dan daftar tertaut di mana node diletakkan secara linier dalam sebuah array, sehingga kinerja cache akan lebih baik. Saya membandingkan ini dengan qsort bawaan, yang mencakup menyalin semuanya dari daftar terfragmentasi ke array dan menyalin hasilnya kembali. Setiap algoritma dijalankan pada 10 set data yang sama dan hasilnya dirata-ratakan.Inilah hasilnya:
N = 1000:
N = 100000:
N = 1000000:
N = 100000000:
Kesimpulan:
Setidaknya di mesin saya, menyalin ke dalam array sangat bermanfaat untuk meningkatkan kinerja cache, karena Anda jarang memiliki daftar tertaut yang sepenuhnya dikemas dalam kehidupan nyata. Perlu dicatat bahwa mesin saya memiliki Phenom II 2.8GHz, tetapi hanya RAM 0.6GHz, jadi cache sangat penting.
sumber
Jenis perbandingan (yaitu yang didasarkan pada elemen pembanding) tidak mungkin lebih cepat dari
n log n
. Tidak masalah apa struktur data yang mendasarinya. Lihat Wikipedia .Jenis lain yang memanfaatkan banyaknya elemen identik dalam daftar (seperti jenis penghitungan), atau beberapa distribusi elemen yang diharapkan dalam daftar, lebih cepat, meskipun saya tidak dapat memikirkan yang berfungsi dengan baik pada daftar tertaut.
sumber
Ini adalah makalah kecil yang bagus tentang topik ini. Kesimpulan empirisnya adalah bahwa Treesort adalah yang terbaik, diikuti oleh Quicksort dan Mergesort. Jenis sedimen, jenis gelembung, jenis pemilihan berkinerja sangat buruk.
STUDI PERBANDINGAN ALGORITMA PENYORTIRAN DAFTAR TERKAIT oleh Ching-Kuang Shene
http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.31.9981
sumber
Seperti yang dinyatakan berkali-kali, batas bawah pengurutan berbasis perbandingan untuk data umum adalah O (n log n). Untuk meringkas argumen ini secara singkat, ada n! berbagai cara memilah daftar. Semua jenis pohon pembanding yang memiliki n! (yang ada di O (n ^ n)) kemungkinan pengurutan terakhir akan membutuhkan setidaknya log (n!) sebagai tingginya: ini memberi Anda batas bawah O (log (n ^ n)), yaitu O (n log n).
Jadi, untuk data umum pada daftar tertaut, pengurutan terbaik yang akan bekerja pada data yang dapat membandingkan dua objek adalah O (n log n). Namun, jika Anda memiliki domain yang lebih terbatas untuk mengerjakan sesuatu, Anda dapat meningkatkan waktu yang dibutuhkan (setidaknya sebanding dengan n). Misalnya, jika Anda bekerja dengan bilangan bulat yang tidak lebih besar dari beberapa nilai, Anda dapat menggunakan Counting Sort atau Radix Sort , karena ini menggunakan objek spesifik yang Anda sortir untuk mengurangi kompleksitas dengan proporsi ke n. Berhati-hatilah, ini menambahkan beberapa hal lain ke kompleksitas yang mungkin tidak Anda pertimbangkan (misalnya, Counting Sort dan Radix sort keduanya menambahkan faktor yang didasarkan pada ukuran angka yang Anda sortir, O (n + k) ) di mana k adalah ukuran bilangan terbesar untuk Counting Sort, misalnya).
Selain itu, jika Anda kebetulan memiliki objek yang memiliki hash sempurna (atau setidaknya hash yang memetakan semua nilai secara berbeda), Anda dapat mencoba menggunakan penghitungan atau pengurutan radix pada fungsi hashnya.
sumber
Sebuah Radix semacam ini sangat cocok untuk sebuah linked list, karena sangat mudah untuk membuat tabel pointer kepala sesuai untuk setiap nilai yang mungkin dari digit.
sumber
Merge sort tidak memerlukan akses O (1) dan merupakan O (n ln n). Tidak ada algoritma yang dikenal untuk menyortir data umum yang lebih baik dari O (n ln n).
Algoritme data khusus seperti radix sort (batas ukuran data) atau histogram sort (menghitung data diskrit) dapat mengurutkan daftar tertaut dengan fungsi pertumbuhan yang lebih rendah, selama Anda menggunakan struktur yang berbeda dengan akses O (1) sebagai penyimpanan sementara .
Kelas lain dari data khusus adalah semacam perbandingan dari daftar yang hampir diurutkan dengan elemen k rusak. Ini dapat diurutkan dalam operasi O (kn).
Menyalin daftar ke larik dan kembali akan menjadi O (N), jadi algoritme pengurutan apa pun dapat digunakan jika spasi bukan masalah.
Misalnya, diberikan daftar tertaut yang berisi
uint_8
, kode ini akan mengurutkannya dalam waktu O (N) menggunakan semacam histogram:sumber
O(n lg n)
tidak akan menjadi berbasis perbandingan (misalnya, pengurutan radix). Menurut definisi, jenis perbandingan berlaku untuk domain apa pun yang memiliki urutan total (yaitu, dapat dibandingkan).Bukan jawaban langsung untuk pertanyaan Anda, tetapi jika Anda menggunakan Daftar Lewati , itu sudah diurutkan dan memiliki waktu pencarian O (log N).
sumber
O(lg N)
waktu pencarian yang diharapkan - tetapi tidak dijamin, karena daftar lewati bergantung pada keacakan. Jika Anda menerima input yang tidak tepercaya, pastikan pemasok input tidak dapat memprediksi RNG Anda, atau mereka dapat mengirimi Anda data yang memicu kinerja kasus terburuknyaSeperti yang saya tahu, algoritme pengurutan terbaik adalah O (n * log n), apa pun wadahnya - telah terbukti bahwa pengurutan dalam arti luas kata (gaya mergesort / quicksort dll) tidak bisa lebih rendah. Menggunakan daftar tertaut tidak akan memberi Anda waktu pengoperasian yang lebih baik.
Satu-satunya algoritme yang berjalan di O (n) adalah algoritme "retas" yang mengandalkan nilai penghitungan alih-alih pengurutan.
sumber
O(n lg c)
. Jika semua elemen Anda unik, makac >= n
dibutuhkan waktu lebih lama dariO(n lg n)
.Berikut adalah implementasi yang melintasi daftar hanya sekali, mengumpulkan proses, lalu menjadwalkan penggabungan dengan cara yang sama seperti penggabungan.
Kompleksitas adalah O (n log m) dimana n adalah jumlah item dan m adalah jumlah run. Kasus terbaik adalah O (n) (jika data sudah diurutkan) dan kasus terburuk adalah O (n log n) seperti yang diharapkan.
Ini membutuhkan memori sementara O (log m); pengurutan dilakukan di tempat pada daftar.
(diperbarui di bawah. commenter one membuat poin yang bagus bahwa saya harus menjelaskannya di sini)
Inti dari algoritma ini adalah:
Mengumpulkan run tidak membutuhkan banyak penjelasan, tetapi ada baiknya untuk mengambil kesempatan ini untuk mengakumulasi jalur ascending dan descending run (terbalik). Di sini ia menambahkan item yang lebih kecil dari kepala proses dan menambahkan item yang lebih besar dari atau sama dengan akhir proses. (Perhatikan bahwa prepending harus menggunakan tight less-than untuk menjaga stabilitas sortir.)
Paling mudah hanya menempelkan kode penggabungan di sini:
Pertimbangkan untuk mengurutkan daftar (dagibecfjh) (mengabaikan proses). Status tumpukan dilanjutkan sebagai berikut:
Kemudian, akhirnya, gabungkan semua daftar ini.
Perhatikan bahwa jumlah item (berjalan) pada tumpukan [i] adalah nol atau 2 ^ i dan ukuran tumpukan dibatasi oleh 1 + log2 (nruns). Setiap elemen digabung sekali per level tumpukan, karenanya O (n log m) perbandingan. Ada kemiripan lewat Timsort di sini, meskipun Timsort mempertahankan tumpukannya menggunakan sesuatu seperti deret Fibonacci di mana ini menggunakan pangkat dua.
Akumulasi proses memanfaatkan data yang telah diurutkan sehingga kompleksitas kasus terbaik adalah O (n) untuk daftar yang sudah diurutkan (satu proses). Karena kita mengumpulkan proses ascending dan descending, run akan selalu setidaknya panjang 2. (Ini mengurangi kedalaman tumpukan maksimum setidaknya satu, membayar biaya untuk menemukan proses di tempat pertama.) Kompleksitas kasus terburuk adalah O (n log n), seperti yang diharapkan, untuk data yang sangat diacak.
(Um ... Pembaruan kedua.)
Atau lihat saja wikipedia di mergesort bottom-up .
sumber
O(log m)
memori tambahan tidak diperlukan - cukup tambahkan proses ke dua daftar secara bergantian sampai salah satu kosong.Anda dapat menyalinnya ke dalam array dan kemudian mengurutkannya.
Menyalin ke dalam larik O (n),
sorting O (nlgn) (jika Anda menggunakan algoritma cepat seperti merge sort),
menyalin kembali ke daftar tertaut O (n) jika perlu,
jadi itu akan menjadi O (nlgn).
Perhatikan bahwa jika Anda tidak mengetahui jumlah elemen dalam daftar tertaut, Anda tidak akan mengetahui ukuran larik. Jika Anda membuat kode di java, Anda dapat menggunakan Arraylist misalnya.
sumber
Mergesort adalah yang terbaik yang dapat Anda lakukan di sini.
sumber
Pertanyaannya adalah LeetCode # 148 , dan ada banyak solusi yang ditawarkan dalam semua bahasa utama. Milik saya adalah sebagai berikut, tetapi saya bertanya-tanya tentang kerumitan waktu. Untuk menemukan elemen tengah, kami melintasi daftar lengkap setiap kali.
n
Elemen pertama kali diiterasi,2 * n/2
elemen kedua diulangi, seterusnya, dan seterusnya. Sepertinya sudahO(n^2)
waktunya.sumber