Tôi tò mò nếu O (n log n) là tốt nhất mà một danh sách liên kết có thể làm.
Tôi tò mò nếu O (n log n) là tốt nhất mà một danh sách liên kết có thể làm.
Câu trả lời:
Có thể kỳ vọng rằng bạn không thể làm tốt hơn O (N log N) trong thời gian chạy .
Tuy nhiên, phần thú vị là điều tra xem bạn có thể sắp xếp nó đúng vị trí , ổn định , hành vi trong trường hợp xấu nhất của nó hay không, v.v.
Simon Tatham, người nổi tiếng Putty, giải thích cách sắp xếp một danh sách được liên kết với sắp xếp hợp nhất . Ông kết luận bằng những nhận xét sau:
Giống như bất kỳ thuật toán sắp xếp tự trọng nào, điều này có thời gian chạy O (N log N). Bởi vì đây là Mergesort, thời gian chạy trong trường hợp xấu nhất vẫn là O (N log N); không có trường hợp bệnh lý.
Yêu cầu lưu trữ phụ trợ là nhỏ và không đổi (tức là một vài biến trong quy trình sắp xếp). Nhờ hành vi vốn có khác nhau của danh sách được liên kết từ mảng, việc triển khai Hợp nhất này tránh được chi phí lưu trữ phụ trợ O (N) thường được liên kết với thuật toán.
Ngoài ra còn có một ví dụ triển khai trong C hoạt động cho cả danh sách liên kết đơn và kép.
Như @ Jørgen Fogh đề cập bên dưới, ký hiệu big-O có thể ẩn một số yếu tố không đổi có thể khiến một thuật toán hoạt động tốt hơn vì vị trí bộ nhớ, vì số lượng mục thấp, v.v.
listsort
, bạn sẽ thấy bạn có thể chuyển đổi bằng cách sử dụng tham số int is_double
.
listsort
mã C chỉ hỗ trợ các danh sách được liên kết đơn lẻ
Tùy thuộc vào một số yếu tố, việc sao chép danh sách vào một mảng và sau đó sử dụng Quicksort thực sự có thể nhanh hơn .
Lý do điều này có thể nhanh hơn là một mảng có hiệu suất bộ nhớ cache tốt hơn nhiều so với một danh sách được liên kết. Nếu các nút trong danh sách bị phân tán trong bộ nhớ, bạn có thể tạo ra các lỗi bộ nhớ cache ở khắp nơi. Sau đó, một lần nữa, nếu mảng lớn, bạn sẽ bị thiếu bộ nhớ cache.
Hợp nhất các song song tốt hơn, vì vậy nó có thể là một lựa chọn tốt hơn nếu đó là những gì bạn muốn. Nó cũng nhanh hơn nhiều nếu bạn thực hiện nó trực tiếp trên danh sách liên kết.
Vì cả hai thuật toán đều chạy trong O (n * log n), nên việc đưa ra quyết định sáng suốt sẽ liên quan đến việc lập hồ sơ cả hai thuật toán trên máy mà bạn muốn chạy chúng.
--- BIÊN TẬP
Tôi quyết định kiểm tra giả thuyết của mình và viết một chương trình C đo thời gian (sử dụng clock()
) để sắp xếp một danh sách liên kết các int. Tôi đã thử với một danh sách được liên kết trong đó mỗi nút được cấp phát malloc()
và một danh sách được liên kết trong đó các nút được bố trí tuyến tính trong một mảng, vì vậy hiệu suất bộ nhớ cache sẽ tốt hơn. Tôi đã so sánh những thứ này với qsort tích hợp sẵn, bao gồm sao chép mọi thứ từ danh sách bị phân mảnh sang một mảng và sao chép lại kết quả. Mỗi thuật toán được chạy trên 10 tập dữ liệu giống nhau và kết quả được tính trung bình.
Đây là những kết quả:
N = 1000:
Danh sách bị phân mảnh với sắp xếp hợp nhất: 0,000000 giây
Mảng có qsort: 0,000000 giây
Danh sách được đóng gói với sắp xếp hợp nhất: 0,000000 giây
N = 100000:
Danh sách bị phân mảnh với sắp xếp hợp nhất: 0,039000 giây
Mảng có qsort: 0,025000 giây
Danh sách được đóng gói với sắp xếp hợp nhất: 0,009000 giây
N = 1000000:
Danh sách bị phân mảnh với sắp xếp hợp nhất: 1.162000 giây
Mảng với qsort: 0,420000 giây
Danh sách được đóng gói với sắp xếp hợp nhất: 0,112000 giây
N = 100000000:
Danh sách bị phân mảnh với sắp xếp hợp nhất: 364,797000 giây
Mảng với qsort: 61.166000 giây
Danh sách được đóng gói với sắp xếp hợp nhất: 16.525000 giây
Phần kết luận:
Ít nhất là trên máy tính của tôi, sao chép vào một mảng rất đáng để cải thiện hiệu suất bộ nhớ cache, vì bạn hiếm khi có một danh sách liên kết được đóng gói hoàn chỉnh trong cuộc sống thực. Cần lưu ý rằng máy của tôi có Phenom II 2,8GHz nhưng RAM chỉ có 0,6GHz nên bộ nhớ đệm rất quan trọng.
Các loại so sánh (tức là các loại dựa trên việc so sánh các phần tử) không thể nhanh hơn n log n
. Không quan trọng cấu trúc dữ liệu cơ bản là gì. Xem Wikipedia .
Các kiểu sắp xếp khác tận dụng lợi thế của việc có nhiều phần tử giống nhau trong danh sách (chẳng hạn như kiểu đếm) hoặc một số phân phối dự kiến của các phần tử trong danh sách, nhanh hơn, mặc dù tôi không thể nghĩ ra cách nào hoạt động đặc biệt tốt trên một danh sách được liên kết.
Đây là một bài báo nhỏ tốt đẹp về chủ đề này. Kết luận thực nghiệm của ông là Treesort là tốt nhất, tiếp theo là Quicksort và Mergesort. Phân loại trầm tích, phân loại bong bóng, phân loại lựa chọn hoạt động rất tệ.
NGHIÊN CỨU SO SÁNH CÁC THUẬT TOÁN SẮP XẾP DANH SÁCH LIÊN KẾT của Ching-Kuang Shene
http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.31.9981
Như đã nêu nhiều lần, giới hạn dưới của việc sắp xếp dựa trên so sánh cho dữ liệu chung sẽ là O (n log n). Tóm tắt lại một cách ngắn gọn những lập luận này, có n! các cách khác nhau một danh sách có thể được sắp xếp. Bất kỳ loại cây so sánh nào có n! (nằm trong O (n ^ n)) sắp xếp cuối cùng có thể sẽ cần ít nhất log (n!) làm chiều cao của nó: điều này cho bạn giới hạn dưới O (log (n ^ n)), là O (n log n).
Vì vậy, đối với dữ liệu chung trên danh sách liên kết, cách sắp xếp tốt nhất có thể sẽ hoạt động trên bất kỳ dữ liệu nào có thể so sánh hai đối tượng sẽ là O (n log n). Tuy nhiên, nếu bạn có phạm vi công việc hạn chế hơn, bạn có thể cải thiện thời gian thực hiện (ít nhất là tỷ lệ thuận với n). Ví dụ: nếu bạn đang làm việc với các số nguyên không lớn hơn một giá trị nào đó, bạn có thể sử dụng Sắp xếp đếm hoặc Sắp xếp theo cơ số , vì chúng sử dụng các đối tượng cụ thể mà bạn đang sắp xếp để giảm độ phức tạp theo tỷ lệ n. Tuy nhiên, hãy cẩn thận, những thứ này thêm một số thứ khác vào độ phức tạp mà bạn có thể không xem xét (ví dụ: Sắp xếp đếm và Sắp xếp theo cơ số đều thêm vào các yếu tố dựa trên kích thước của các số bạn đang sắp xếp, O (n + k ) trong đó k là kích thước của số lớn nhất cho Sắp xếp Đếm chẳng hạn).
Ngoài ra, nếu bạn tình cờ có các đối tượng có một hàm băm hoàn hảo (hoặc ít nhất là một hàm băm ánh xạ tất cả các giá trị khác nhau), bạn có thể thử sử dụng sắp xếp đếm hoặc cơ số trên các hàm băm của chúng.
Một Radix sort đặc biệt phù hợp với một danh sách liên kết, vì nó dễ dàng để tạo ra một bảng gợi ý đầu tương ứng với mỗi giá trị có thể có của một chữ số.
Sắp xếp hợp nhất không yêu cầu quyền truy cập O (1) và là O (n ln n). Không có thuật toán nào được biết đến để sắp xếp dữ liệu chung tốt hơn O (n ln n).
Các thuật toán dữ liệu đặc biệt như sắp xếp cơ số (kích thước giới hạn của dữ liệu) hoặc sắp xếp biểu đồ (đếm dữ liệu rời rạc) có thể sắp xếp danh sách được liên kết có hàm tăng trưởng thấp hơn, miễn là bạn sử dụng cấu trúc khác với quyền truy cập O (1) làm bộ nhớ tạm thời .
Một lớp dữ liệu đặc biệt khác là một loại so sánh của một danh sách gần như được sắp xếp với k phần tử không theo thứ tự. Điều này có thể được sắp xếp trong các phép toán O (kn).
Sao chép danh sách vào một mảng và ngược lại sẽ là O (N), vì vậy, bất kỳ thuật toán sắp xếp nào cũng có thể được sử dụng nếu không gian không phải là vấn đề.
Ví dụ: cho một danh sách được liên kết có chứa uint_8
, mã này sẽ sắp xếp nó theo thời gian O (N) bằng cách sử dụng sắp xếp biểu đồ:
#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)
sẽ không dựa trên so sánh (ví dụ: sắp xếp cơ số). Theo định nghĩa, sắp xếp so sánh áp dụng cho bất kỳ miền nào có tổng thứ tự (tức là có thể được so sánh).
Không phải là câu trả lời trực tiếp cho câu hỏi của bạn, nhưng nếu bạn sử dụng Danh sách bỏ qua , danh sách đó đã được sắp xếp và có thời gian tìm kiếm O (log N).
O(lg N)
thời gian tìm kiếm dự kiến - nhưng không được đảm bảo, vì danh sách bỏ qua phụ thuộc vào tính ngẫu nhiên. Nếu bạn nhận được đầu vào không tin cậy, hãy chắc chắn các nhà cung cấp các đầu vào không thể dự đoán RNG của bạn, hoặc họ có thể gửi cho bạn dữ liệu mà gây nên hiệu suất trường hợp tồi tệ nhất
Như tôi biết, thuật toán sắp xếp tốt nhất là O (n * log n), bất kể vùng chứa nào - nó đã được chứng minh rằng sắp xếp theo nghĩa rộng của từ này (kiểu mergesort / quicksort, v.v.) không thể thấp hơn. Sử dụng danh sách liên kết sẽ không mang lại cho bạn thời gian chạy tốt hơn.
Thuật toán duy nhất chạy trong O (n) là thuật toán "hack" dựa vào việc đếm các giá trị thay vì thực sự sắp xếp.
O(n lg c)
. Nếu tất cả các phần tử của bạn là duy nhất, thì c >= n
, và do đó sẽ mất nhiều thời gian hơn O(n lg n)
.
Đây là cách triển khai duyệt qua danh sách chỉ một lần, thu thập các lần chạy, sau đó lên lịch cho các lần hợp nhất theo cách tương tự như cách hợp nhất thực hiện.
Độ phức tạp là O (n log m) với n là số mục và m là số lần chạy. Trường hợp tốt nhất là O (n) (nếu dữ liệu đã được sắp xếp) và trường hợp xấu nhất là O (n log n) như mong đợi.
Nó yêu cầu bộ nhớ tạm thời O (log m); việc sắp xếp được thực hiện tại chỗ trên danh sách.
(được cập nhật bên dưới. một người bình luận đưa ra một điểm tốt mà tôi nên mô tả nó ở đây)
Ý chính của thuật toán là:
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
Việc tích lũy số lần chạy không cần giải thích nhiều, nhưng thật tốt nếu bạn có cơ hội để tích lũy số lần chạy tăng dần và số lần chạy giảm dần (đảo ngược). Ở đây, nó thêm vào các mục nhỏ hơn phần đầu của cuộc chạy và thêm các mục lớn hơn hoặc bằng phần cuối của lần chạy. (Lưu ý rằng chi tiêu trước nên sử dụng ít hơn nghiêm ngặt để duy trì sự ổn định của sắp xếp.)
Dễ nhất là chỉ cần dán mã hợp nhất vào đây:
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);
}
Xem xét sắp xếp danh sách (dagibecfjh) (bỏ qua các lần chạy). Các trạng thái ngăn xếp tiến hành như sau:
[ ]
[ (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) ]
Sau đó, cuối cùng, hợp nhất tất cả các danh sách này.
Lưu ý rằng số lượng mục (chạy) tại ngăn xếp [i] bằng 0 hoặc 2 ^ i và kích thước ngăn xếp được giới hạn bởi 1 + log2 (nruns). Mỗi phần tử được hợp nhất một lần trên mỗi mức ngăn xếp, do đó so sánh O (n log m). Có một điểm tương đồng với Timsort ở đây, mặc dù Timsort duy trì ngăn xếp của nó bằng cách sử dụng một thứ gì đó giống như dãy Fibonacci trong đó điều này sử dụng lũy thừa của hai.
Việc tích lũy các lần chạy tận dụng mọi dữ liệu đã được sắp xếp sao cho độ phức tạp của trường hợp tốt nhất là O (n) cho danh sách đã được sắp xếp (một lần chạy). Vì chúng tôi đang tích lũy cả số lần chạy tăng dần và giảm dần, các lần chạy sẽ luôn có độ dài ít nhất là 2. (Điều này làm giảm độ sâu ngăn xếp tối đa ít nhất một, trả cho chi phí tìm kiếm các lần chạy ngay từ đầu.) O (n log n), như mong đợi, đối với dữ liệu được ngẫu nhiên hóa cao.
(Ừm ... Bản cập nhật thứ hai.)
Hoặc chỉ cần xem wikipedia trên hợp nhất từ dưới lên .
O(log m)
Không cần thêm bộ nhớ - chỉ cần thêm lần chạy luân phiên vào hai danh sách cho đến khi một danh sách trống.
Bạn có thể sao chép nó vào một mảng và sau đó sắp xếp nó.
Sao chép vào mảng O (n),
sắp xếp O (nlgn) (nếu bạn sử dụng một thuật toán nhanh như sắp xếp hợp nhất),
sao chép trở lại danh sách liên kết O (n) nếu cần,
vì vậy nó sẽ là O (nlgn).
lưu ý rằng nếu bạn không biết số phần tử trong danh sách liên kết, bạn sẽ không biết kích thước của mảng. Nếu bạn đang viết mã trong java, bạn có thể sử dụng Arraylist chẳng hạn.
Mergesort là tốt nhất bạn có thể làm ở đây.
Câu hỏi là LeetCode # 148 , và có rất nhiều giải pháp được cung cấp bằng tất cả các ngôn ngữ chính. Của tôi như sau, nhưng tôi tự hỏi về độ phức tạp thời gian. Để tìm phần tử ở giữa, chúng tôi duyệt qua danh sách đầy đủ mỗi lần. Các n
phần tử thời gian đầu được lặp lại, 2 * n/2
các phần tử thời gian thứ hai được lặp đi lặp lại, v.v. Có vẻ như đã đến O(n^2)
lúc.
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)