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


16

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

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

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

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

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

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

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

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

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


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

답변:


19

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

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

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

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


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

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

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

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

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