O (n log n)이 링크드리스트가 할 수있는 최선인지 궁금합니다.
O (n log n)이 링크드리스트가 할 수있는 최선인지 궁금합니다.
답변:
실행 시간 에 O (N log N)보다 더 잘할 수 없다고 예상하는 것이 합리적 입니다.
그러나 흥미로운 부분은 제자리 에서 안정적으로 정렬 할 수 있는지 , 최악의 경우 동작 등 을 조사하는 것 입니다.
Putty로 유명한 Simon Tatham 이 병합 정렬을 사용하여 연결 목록 을 정렬하는 방법을 설명합니다 . 그는 다음과 같이 결론을 내립니다.
모든 자존심 정렬 알고리즘과 마찬가지로 실행 시간은 O (N log N)입니다. 이것은 Mergesort이기 때문에 최악의 실행 시간은 여전히 O (N log N); 병리학 적 사례가 없습니다.
보조 스토리지 요구 사항은 작고 일정합니다 (예 : 정렬 루틴 내 몇 가지 변수). 배열에서 연결된 목록의 본질적으로 다른 동작 덕분에이 Mergesort 구현은 일반적으로 알고리즘과 관련된 O (N) 보조 저장 비용을 방지합니다.
또한 단일 및 이중 연결 목록 모두에서 작동하는 C 구현의 예가 있습니다.
@ Jørgen Fogh가 아래에서 언급했듯이 big-O 표기법은 메모리 지역성, 적은 수의 항목 등으로 인해 하나의 알고리즘이 더 나은 성능을 발휘할 수있는 몇 가지 상수 요소를 숨길 수 있습니다.
listsort
이 표시되면 매개 변수를 사용하여 전환 할 수 있음을 알 수 있습니다 int is_double
.
listsort
C 코드를 지원하는 유일한 싱글 링크드리스트
여러 요인에 따라 목록을 배열에 복사 한 다음 Quicksort 를 사용하는 것이 실제로 더 빠를 수 있습니다 .
이것이 더 빠를 수있는 이유는 배열이 연결된 목록보다 캐시 성능이 훨씬 더 우수하기 때문입니다. 목록의 노드가 메모리에 분산되어있는 경우 모든 곳에서 캐시 미스가 생성 될 수 있습니다. 그런 다음 어레이가 크면 어쨌든 캐시 누락이 발생합니다.
병합 정렬이 더 잘 병렬화되므로 원하는 경우 더 나은 선택이 될 수 있습니다. 연결 목록에서 직접 수행하면 훨씬 빠릅니다.
두 알고리즘 모두 O (n * log n)에서 실행되기 때문에 정보에 입각 한 결정을 내리려면 실행하려는 시스템에서 두 알고리즘을 모두 프로파일 링해야합니다.
--- 편집하다
나는 내 가설을 테스트하기로 결정 clock()
하고 연결된 int 목록을 정렬하는 데 걸린 시간을 측정하는 C 프로그램을 작성했습니다 . 나는 각 노드에 할당 된 연결리스트로 시도 malloc()
캐시 성능이 더 좋을 것이다 있도록하고, 노드가 배열에 선형으로 배치 된 연결리스트. 나는 이것을 내장 qsort와 비교했는데, 여기에는 조각난 목록에서 배열로 모든 것을 복사하고 결과를 다시 복사하는 것이 포함되었습니다. 각 알고리즘은 동일한 10 개의 데이터 세트에서 실행되었으며 결과는 평균화되었습니다.
결과는 다음과 같습니다.
N = 1000 :
병합 정렬이있는 조각난 목록 : 0.000000 초
qsort가있는 어레이 : 0.000000 초
병합 정렬이 포함 된 패킹 된 목록 : 0.000000 초
N = 100000 :
병합 정렬이있는 조각난 목록 : 0.039000 초
qsort가있는 어레이 : 0.025000 초
병합 정렬이 포함 된 패킹 된 목록 : 0.009000 초
N = 1000000 :
병합 정렬이있는 조각난 목록 : 1.162000 초
qsort가있는 어레이 : 0.420000 초
병합 정렬이 포함 된 패킹 된 목록 : 0.112000 초
N = 100000000 :
병합 정렬이있는 조각난 목록 : 364.797000 초
qsort가있는 어레이 : 61.166000 초
병합 정렬이있는 압축 목록 : 16.525000 초
결론:
적어도 내 컴퓨터에서는 배열로 복사하는 것이 캐시 성능을 향상시키는 데 그만한 가치가 있습니다. 실생활에서 완전히 묶인 연결 목록이 거의 없기 때문입니다. 내 컴퓨터에는 2.8GHz Phenom II가 있지만 0.6GHz RAM 만 있으므로 캐시가 매우 중요합니다.
비교 정렬 (즉, 비교 요소를 기반으로하는 정렬)은보다 빠를 수 없습니다 n log n
. 기본 데이터 구조가 무엇인지는 중요하지 않습니다. Wikipedia를 참조하십시오 .
목록에 동일한 요소가 많이 있음 (카운팅 정렬 등)을 활용하는 다른 종류의 정렬 또는 목록에서 예상되는 요소 분포가 더 빠릅니다. 특히 잘 작동하는 항목은 생각할 수 없습니다. 연결 목록에.
이것은이 주제에 대한 멋진 작은 논문입니다. 그의 경험적 결론은 Treesort가 최고이며 Quicksort와 Mergesort가 그 뒤를 잇습니다. 퇴적물 분류, 기포 분류, 선택 분류가 매우 나쁘게 수행됩니다.
Ching-Kuang Shene의 링크드리스트 정렬 알고리즘 비교 연구
http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.31.9981
여러 번 언급했듯이 일반 데이터에 대한 비교 기반 정렬의 하한은 O (n log n)입니다. 이러한 인수를 간단히 다시 요약하기 위해 n! 목록을 정렬 할 수있는 다양한 방법. n이있는 모든 종류의 비교 트리! (O (n ^ n)에 있음) 가능한 최종 정렬은 높이로 최소한 log (n!)이 필요합니다. 이것은 O (log (n ^ n)) 하한을 제공합니다. 즉, O (n lg n).
따라서 연결된 목록의 일반 데이터의 경우 두 개체를 비교할 수있는 모든 데이터에 대해 작동 할 수있는 최상의 정렬은 O (n log n)입니다. 그러나 작업 할 영역이 더 제한적이라면 소요 시간을 개선 할 수 있습니다 (적어도 n에 비례). 예를 들어, 어떤 값보다 크지 않은 정수로 작업하는 경우 Counting Sort 또는 Radix Sort를 사용할 수 있습니다. 이러한 개체는 정렬중인 특정 개체를 사용하여 n에 비례하여 복잡성을 줄이기 때문입니다. 하지만주의해야 할 점은 고려하지 않을 수있는 복잡성에 몇 가지 다른 사항을 추가합니다 (예를 들어 Counting Sort 및 Radix 정렬은 모두 정렬하는 숫자의 크기를 기반으로하는 요소를 추가합니다. O (n + k ) 여기서 k는 Counting Sort에서 가장 큰 숫자의 크기입니다).
또한 완벽한 해시 (또는 적어도 모든 값을 다르게 매핑하는 해시)를 가진 객체가있는 경우 해당 해시 함수에 대해 계수 또는 기수 정렬을 사용해 볼 수 있습니다.
기수 정렬 특히는 한 디지트의 각각의 가능한 값에 대응하는 헤드 포인터 테이블을 쉽게 만들 수 있으므로, 링크 된리스트에 적합하다.
병합 정렬에는 O (1) 액세스가 필요하지 않으며 O (n ln n)입니다. 일반 데이터를 정렬하는 알려진 알고리즘이 O (n ln n)보다 낫습니다.
기수 정렬 (데이터 크기 제한) 또는 히스토그램 정렬 (개별 데이터 계산)과 같은 특수 데이터 알고리즘은 O (1) 액세스가있는 다른 구조를 임시 저장소로 사용하는 한 낮은 성장 함수로 연결된 목록을 정렬 할 수 있습니다. .
특수 데이터의 또 다른 클래스는 k 요소가 순서가 맞지 않는 거의 정렬 된 목록의 비교입니다. 이것은 O (kn) 연산으로 정렬 할 수 있습니다.
목록을 배열에 복사하고 다시 복사하는 것은 O (N)이므로 공간이 문제가되지 않는 경우 모든 정렬 알고리즘을 사용할 수 있습니다.
예를 들어를 포함하는 연결 목록이 주어지면 uint_8
이 코드는 히스토그램 정렬을 사용하여 O (N) 시간으로 정렬합니다.
#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)
비교 기반이 아닌 것보다 빠른 정렬 알고리즘 (예 : 기수 정렬). 정의에 따라 비교 정렬은 총 순서가있는 모든 도메인에 적용됩니다 (즉, 비교할 수 있음).
내가 아는 바와 같이, 가장 좋은 정렬 알고리즘은 O (n * log n)입니다. 컨테이너가 무엇이든간에-단어의 넓은 의미 (병합 / 빠른 정렬 등)의 정렬이 더 낮아질 수 없다는 것이 입증되었습니다. 연결된 목록을 사용하면 더 나은 실행 시간을 얻을 수 없습니다.
O (n)에서 실행되는 유일한 알고리즘은 실제로 정렬하는 대신 값을 계산하는 "해킹"알고리즘입니다.
O(n lg c)
. 모든 요소가 고유 한 경우 c >= n
이므로보다 오래 걸립니다 O(n lg n)
.
다음 은 목록을 한 번만 탐색하고 실행을 수집 한 다음 mergesort와 동일한 방식으로 병합을 예약 하는 구현 입니다.
복잡성은 O (n log m)입니다. 여기서 n은 항목 수이고 m은 실행 수입니다. 최상의 경우는 O (n) (데이터가 이미 정렬 된 경우)이고 최악의 경우는 O (n log n)입니다.
O (log m) 임시 메모리가 필요합니다. 정렬은 목록에서 제자리에서 수행됩니다.
(아래 업데이트 됨. 댓글 작성자는 여기에 설명해야한다는 점을 지적합니다)
알고리즘의 요점은 다음과 같습니다.
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
달리기 누적은 많은 설명이 필요하지 않지만 상승하는 달리기와 내림 차기 (반전)를 모두 축적 할 수있는 기회를 잡는 것이 좋습니다. 여기서는 실행 헤드보다 작은 항목을 앞에 추가하고 실행 종료보다 크거나 같은 항목을 추가합니다. (prepending은 정렬 안정성을 유지하기 위해 strict less-than을 사용해야합니다.)
여기에 병합 코드를 붙여 넣는 것이 가장 쉽습니다.
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);
}
목록 (dagibecfjh) 정렬을 고려하십시오 (실행 무시). 스택 상태는 다음과 같이 진행됩니다.
[ ]
[ (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) ]
그런 다음 마지막으로이 모든 목록을 병합합니다.
stack [i]의 항목 (실행) 수는 0 또는 2 ^ i이고 스택 크기는 1 + log2 (nruns)로 제한됩니다. 각 요소는 스택 수준 당 한 번씩 병합되므로 O (n log m) 비교가 수행됩니다. Timsort는 2의 거듭 제곱을 사용하는 피보나치 수열과 같은 것을 사용하여 스택을 유지하지만 여기에 Timsort와 유사한 점이 있습니다.
누적 실행은 이미 정렬 된 데이터를 활용하므로 이미 정렬 된 목록 (한 번 실행)에 대한 최상의 경우 복잡성은 O (n)입니다. 오름차순 실행과 내림차순 실행을 모두 누적하고 있으므로 실행은 항상 최소 길이 2입니다. (이렇게하면 실행을 찾는 비용을 지불하면서 최대 스택 깊이가 최소 1 개 감소합니다.) 최악의 경우 복잡성은 다음과 같습니다. O (n log n), 예상대로 고도로 무작위 화 된 데이터의 경우.
(음 ... 두 번째 업데이트입니다.)
또는 상향식 병합 정렬 에 대한 위키피디아를 참조하십시오 .
O(log m)
추가 메모리가 필요하지 않습니다. 하나가 비워 질 때까지 두 목록에 번갈아 실행을 추가하면됩니다.
배열에 복사 한 다음 정렬 할 수 있습니다.
배열 O (n)에 복사,
정렬 O (nlgn) (병합 정렬과 같은 빠른 알고리즘을 사용하는 경우),
필요한 경우 연결 목록 O (n)에 다시 복사
그래서 그것은 O (nlgn)가 될 것입니다.
연결된 목록의 요소 수를 모르는 경우 배열의 크기를 알 수 없습니다. Java로 코딩하는 경우 예를 들어 Arraylist를 사용할 수 있습니다.
문제는 LeetCode # 148 이며 모든 주요 언어로 제공되는 많은 솔루션이 있습니다. 내 것은 다음과 같지만 시간의 복잡성이 궁금합니다. 중간 요소를 찾기 위해 매번 전체 목록을 탐색합니다. 첫 번째 n
요소는 반복되고 두 번째 2 * n/2
요소는 반복됩니다. 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)