[algorithm] 연결 목록을 정렬하는 가장 빠른 알고리즘은 무엇입니까?

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 표기법은 메모리 지역성, 적은 수의 항목 등으로 인해 하나의 알고리즘이 더 나은 성능을 발휘할 수있는 몇 가지 상수 요소를 숨길 수 있습니다.


답변

여러 요인에 따라 목록을 배열에 복사 한 다음 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 );
}