연결된 목록에서 빠른 정렬을 사용하지 않는 이유는 무엇입니까?


16

빠른 정렬 알고리즘은 다음 단계로 나눌 수 있습니다

  1. 피벗을 식별하십시오.

  2. 피벗을 기준으로 연결된 목록을 분할하십시오.

  3. 연결된 목록을 재귀 적으로 두 부분으로 나눕니다.

이제 항상 마지막 요소를 피벗으로 선택하면 피벗 요소 (1 단계)를 식별하는 데 시간이 걸립니다.O(n)

피벗 요소를 식별 한 후 데이터를 저장하고 다른 모든 요소와 비교하여 올바른 파티션 포인트를 식별 할 수 있습니다 (2 단계). 피벗 데이터를 저장할 때 각 비교는 시간이 걸리고 각 스왑은 시간이 걸립니다. 따라서 총 요소에 대해 시간 이 걸립니다 .O(1)O(1)O(n)n

따라서 재발 관계는 다음과 같습니다.

T(n)=2T(n/2)+n 이는 이며 이는 연결된 목록과의 병합 정렬과 동일합니다.O(nlog⁡n)

그렇다면 링크 된 목록의 빠른 정렬보다 병합 정렬이 선호되는 이유는 무엇입니까?


첫 번째 요소 대신 마지막 요소를 피벗으로 선택할 필요가 없습니다.
— TheCppZoo

답변:


19

Quicksort의 메모리 액세스 패턴은 임의적이며 기본 구현은 제자리에 있으므로 순서가 지정된 결과를 얻기 위해 셀이 많은 경우 많은 스왑을 사용합니다.
병합 정렬이 외부와 동시에 정렬 된 결과를 반환하려면 추가 배열이 필요합니다. 배열에서는 추가 공간 오버 헤드를 의미하며, 연결된 목록 인 경우 값을 가져 와서 노드 병합을 시작할 수 있습니다. 액세스는 본질적으로 더 순차적입니다.

이 때문에 퀵 정렬은 링크 된 목록에 대한 자연스러운 선택이 아니지만 병합 정렬은 큰 이점을 갖습니다.

Landau 표기법은 (Quicksort가 여전히 이므로) 다소 동의 할 수 있지만 상수는 훨씬 높습니다.O(n2)

평균적인 경우 두 알고리즘 모두 있으므로 점근 적 경우는 동일하지만 선호 사항은 숨겨진 상수로 인해 엄격하며 때로는 안정성이 문제입니다. .O(nlog⁡n)


그러나 평균 시간 복잡도는 똑같습니까? 연결된 목록에 대한 빠른 정렬 및 병합 정렬 사용.
— Zephyr

10
@ Zephyr, 복잡성 표기법이 일정한 요소를 떨어 뜨린다는 것을 기억해야합니다. 예, 연결된 목록의 빠른 정렬과 연결된 목록의 병합 정렬은 동일한 복잡성 클래스이지만 표시되지 않는 상수는 병합 정렬을 균일하게 더 빠르게 만듭니다.
— Mark

@Zephyr 기본적으로 이론적 결과와 경험적 결과의 차이입니다. 경험적으로 퀵 정렬은 더 빠릅니다.
— ferit

1
또한 연결된 목록에는 올바른 피벗을 선택하기가 어렵습니다. OP가 제안하는 것처럼 마지막 요소를 취하면 최악의 경우 ( )는 이미 정렬 된 목록 또는 하위 목록 임을 의미합니다 . 그리고 최악의 경우는 실제로 나타날 가능성이 높습니다. O(n2)
— Stig Hemmer

3
Quicksort는 제자리에 있지 않습니다 . 이것은 일반적인 오해입니다. 그것은 필요 추가 공간. 또한 "무작위"메모리 액세스 패턴도 매우 정확하지 않습니다. 다른 답변에서 설명한대로 피벗 선택에 결정적으로 의존합니다. O(log⁡n)
— Konrad Rudolph

5

링크 된 목록을 빠르게 정렬 할 수 있지만 피벗 선택의 측면에서 매우 제한적이므로 각 세그먼트를 두 번 반복하지 않는 한 거의 정렬 된 입력에 좋지 않은 목록 앞쪽 피벗으로 제한합니다 (피벗 및 파티션에 한 번). 그리고 정렬해야 할 목록에 대해 파티션 경계의 스택을 유지해야합니다. 피벗 선택이 좋지 않고 시간 복잡성이 증가하면 해당 스택이 증가 할 수 있습니다 .O ( N 2 )O(n)O(n2)

분할 영역의 경계를 계산하고 그에 따라 병합하여 상향식 접근 방식을 사용하는 경우 추가 공간 만 사용하여 연결된 목록의 병합 정렬을 실행할 수 있습니다 .O(1)

그러나 단일 64 요소 포인터 배열을 추가하면 추가 반복을 피하고 추가 공간 에 최대 요소 의 정렬 목록을 추가 할 수 있습니다 . O ( 1 )264O(1)

head = list.head;
head_array = array of 64 nulls

while head is not null
    current = head;
    head = head.next;
    current.next = null;
    for(i from 0 to 64)
        if head_array[i] is null
            head_array[i] = current;
            break from for loop;
        end if
        current = merge_lists(current, array[i]);
        head_array[i] = null;
     end for
end while

current = null;
for(i from 0 to 64)
    if head_array[i] is not null
        if current is not null
            current = merge_lists(current, head_array[i]);
        else
            current = head_array[i];
        end if
     end if
 end for

 list.head = current;

이것은 리눅스 커널이 링크 된 목록을 정렬하는 데 사용하는 알고리즘입니다. previous마지막 병합 작업을 제외한 모든 포인터 동안 포인터를 무시하는 것과 같은 추가 최적화가 있지만.


-2

병합 정렬, 파티션 정렬, 트리 정렬 및 결과 비교
를 작성할 수 있습니다. 추가 공간을 허용하면 트리 정렬을 작성하는 것이 매우 쉽습니다. 트리 정렬을 위해
링크 된 목록의 각 노드에는 단일 링크 된 목록을 정렬하더라도 두 개의 포인터가 있어야
합니다. 내가 삽입 및 교환 대신 삭제 선호
호어 파티션을 이중 연결리스트 만 수행 할 수 있습니다

program untitled;


type TData = longint;
     PNode = ^TNode;
     TNode = record
                data:TData;
                prev:PNode;
                next:PNode;
             end;

procedure ListInit(var head:PNode);
begin
  head := NIL;
end;

function ListIsEmpty(head:PNode):boolean;
begin
  ListIsEmpty := head = NIL;
end;

function ListSearch(var head:PNode;k:TData):PNode;
var x:PNode;
begin
  x := head;
  while (x <> NIL)and(x^.data <> k)do
     x := x^.next;
  ListSearch := x;
end;

procedure ListInsert(var head:PNode;k:TData);
var x:PNode;
begin
  new(x);
  x^.data := k;
  x^.next := head;
  if head <> NIL then
     head^.prev := x;
   head := x;
   x^.prev := NIL;
end;

procedure ListDelete(var head:PNode;k:TData);
var x:PNode;
begin
   x := ListSearch(head,k);
   if x <> NIL then
   begin
     if x^.prev <> NIL then
        x^.prev^.next := x^.next
      else 
        head := x^.next;
     if x^.next <> NIL then
        x^.next^.prev := x^.prev;
     dispose(x);
   end;
end;

procedure ListPrint(head:PNode);
var x:PNode;
    counter:longint;
begin
  x := head;
  counter := 0;
  while x <> NIL do
  begin
    write(x^.data,' -> ');
    x := x^.next;
    counter := counter + 1;
  end;
  writeln('NIL');
  writeln('Liczba elementow listy : ',counter);
end;

procedure BSTinsert(x:PNode;var t:PNode);
begin
  if t = NIL then
    t := x
  else
    if t^.data = x^.data then
            BSTinsert(x,t^.prev)
        else if t^.data < x^.data then
            BSTinsert(x,t^.next)
        else
            BSTinsert(x,t^.prev);
end;

procedure BSTtoDLL(t:PNode;var L:PNode);
begin
   if t <> NIL then
   begin
     BSTtoDLL(t^.next,L);
     ListInsert(L,t^.data);
     BSTtoDLL(t^.prev,L);
   end;
end;

procedure BSTdispose(t:PNode);
begin
   if t <> NIL then
   begin
    BSTdispose(t^.prev);
    BSTdispose(t^.next);
    dispose(t);
   end; 
end;

procedure BSTsort(var L:PNode);
var T,S:PNode;
    x,xs:PNode;
begin
  T := NIL;
  S := NIL;
  x := L;
  while x <> NIL do
  begin
    xs := x^.next;
    x^.prev := NIL;
    x^.next := NIL;
    BSTinsert(x,t);
    x := xs;
  end;
  BSTtoDLL(T,S);
  BSTdispose(T);
  L := S;
end;

var i : byte;
    head:PNode;
    k:TData;
BEGIN
  ListInit(head);
  repeat
     writeln('0. Wyjscie');
     writeln('1. Wstaw element na poczatek listy');
     writeln('2. Usun element listy');
     writeln('3. Posortuj elementy drzewem binarnym');
     writeln('4. Wypisz elementy  listy');
     readln(i);
     case i of
     0:
     begin
       while not ListIsEmpty(head) do
            ListDelete(head,head^.data);
     end;
     1:
     begin
       writeln('Podaj element jaki chcesz wstawic');
       readln(k);
       ListInsert(head,k);
     end;
     2:
     begin
       writeln('Podaj element jaki chcesz usunac');
       readln(k);
       ListDelete(head,k);
     end;
     3:
     begin
       BSTsort(head);
     end;
     4:
     begin
        ListPrint(head);    
     end
     else
        writeln('Brak operacji podaj inny numer');
     end;
  until i = 0;  
END.

이 코드는 약간의 개선이 필요합니다.
첫째, 추가 스토리지를 재귀 요구
로 제한 한 다음 재귀를 반복으로 대체
해야합니다. 알고리즘을 더 개선하려면 자체 밸런싱 트리를 사용해야합니다


자세한 기여에 감사드립니다. 그러나 이것은 코딩 사이트가 아닙니다. 200 줄의 코드는 연결된 목록의 빠른 정렬보다 병합 정렬이 선호되는 이유를 설명하기 위해 아무 것도 수행하지 않습니다.
— David Richerby

파티션 정렬에서 피벗 선택은 첫 번째 또는 마지막 요소로 제한됩니다 (꼬리 노드에 대한 포인터를 유지하는 경우 마지막). 그렇지 않으면 피벗이 느립니다. Hoare 파티션은 이중 연결 목록에서만 가능합니다. 상수 인자를 무시하면 트리는 퀵 정렬과 같은 값을 갖지만 트리 정렬에서 최악의 경우를 피하는 것이 더 쉽습니다. 병합 정렬의 경우 주석에 문자가 거의 없습니다
— Mariusz

-2

퀵
정렬 아마도 퀵 정렬을위한 단계를 보여줄 것입니다

목록에 둘 이상의 노드가 포함 된 경우

  1. 피벗 선택
  2. 세 개의 하위 목록으로 분할 된 목록
    첫 번째 하위 목록 에는 피벗 키보다 작은 키를 가진 노드가 포함됩니다.
    두 번째 하위 목록에는 피벗 키
    와 같은 키를 가진 노드가 포함됩니다.
  3. 피벗 노드와 ​​다른 노드를 포함하는 하위 목록에 대한 재귀 호출
  4. 정렬 된 하위 목록을 하나의 정렬 된 목록으로 연결

광고 1.
피벗을 빠르게 선택하려면 선택이 제한됩니다
헤드 노드 또는 테일 노드 를 선택할 수 있습니다 피벗에 빠르게 액세스하려면
목록
에 노드에 대한 관심이 있어야합니다. 그렇지 않으면 노드를 검색해야합니다

Ad 2.
이 단계에서 대기열 작업을 사용할 수 있습니다.
원래 링크 된 목록에서 노드를 대기열에서 빼기 피스트
키 및 대기열 노드와 해당 키를 올바른 하위 목록 과 비교합니다. 하위
목록은 기존 노드에서 만들어지며
새 노드에 메모리 를 할당 할 필요가 없습니다.

큐 조작
및 연결이이 포인터가 있으면 더 빠르게 실행 되므로 테일 노드에 대한 포인터가 유용합니다.


이것이 어떻게 질문에 대답하는지 알 수 없습니다.
— John L.
당사 사이트를 사용함과 동시에 당사의 쿠키 정책과 개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.