为什么不对链接列表使用快速排序?


16

快速排序算法可以分为以下步骤

  1. 确定枢纽。

  2. 根据数据透视表对链接列表进行分区。

  3. 将链表递归分为两部分。

现在,如果我始终选择最后一个元素作为枢轴,那么识别枢轴元素(第一步)将花费时间。O(n)

识别枢轴元素之后,我们可以存储其数据并将其与所有其他元素进行比较以识别正确的分区点(第二步)。当我们存储数据透视表数据时,每个比较将花费时间,而每个交换将花费O1 时间。因此,总共需要n 元素的On 时间。O(1)O(1)O(n)n

因此,递归关系为:

On log n ,它与带有链接列表的合并排序中的相同。T(n)=2T(n/2)+nO(nlogn)

那么,为什么对链表而言合并排序比快速排序更受青睐?


无需选择最后一个元素作为主要元素,而不是第一个元素
TheCppZoo

Answers:


19

Quicksort中的内存访问模式是随机的,开箱即用的实现也是就地实现的,因此,如果单元格获得有序的结果,它将使用许多交换。
同时,合并排序是外部的,它需要附加数组才能返回有序结果。在数组中,这意味着额外的空间开销,在使用链表的情况下,可以拉出值并开始合并节点。该访问本质上是顺序的。

因此,快速排序不是链接列表的自然选择,而合并排序则具有很大的优势。

Landau表示法可能会(或多或少,因为Quicksort仍为)是一致的,但常数要高得多。O(n2)

在一般情况下,两种算法都为因此渐近情况相同,但是优先级严格是由于隐藏常数,有时会引起稳定性(快速排序固有地不稳定,mergsort稳定)。O(nlogn)


但是平均时间复杂度是正确的吗?对链表使用快速排序和合并排序。
Zephyr

10
@Zephyr,您需要记住,复杂度表示法会降低常数因子。是的,链表上的quicksort和链表上的mergesort是相同的复杂度类,但是您没有看到的那些常数使mergesort统一地更快。
标记

@Zephyr基本上,这是理论和经验结果之间的差异。根据经验,quicksort更快。
费里特(Feit)'17

1
Øñ2

3
Ø日志ñ

5

您可以对链表进行快速排序,但是在枢轴选择方面会受到很大限制,除非您想循环遍历每个段两次(对于枢轴和一次进行分区)。并且您将需要为仍需要排序的列表保留分区边界的堆栈。该堆栈可以增长到Øñ 当枢轴选择不好并且时间复杂度增加到 Øñ2

链表上的合并排序只能使用 Ø1个 如果您通过自下而上的方法来计算分区边界的位置并相应地合并,则可以节省额外的空间。

但是,添加单个64个元素的指针数组可以避免额外的迭代,并且最多可以排序列表 264 元素 Ø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;

这是Linux内核用于对其链表进行排序的算法。尽管进行了一些额外的优化,例如previous在最后一次合并操作期间忽略了所有指针。


-2

你可以写归并排序,分区排序,树排序和比较的结果
是很容易写树排序如果你允许一些额外的空间,
对于树排序链表的每个节点都必须具有即使我们那种单向链表两个指针
链表我更喜欢插入和删除而不是交换
Hoare分区只能用于双链表

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分区仅适用于双向链表,交换应替换为插入和删除不平衡的树排序如果忽略常量因子,则树具有与quicksort相同的兼容性,但是更容易避免在树排序中出现最坏情况。对于合并排序,注释中的字符很少
Mariusz

-2

Quicksort
也许我会显示Quicksort的步骤

如果列表包含多个节点

  1. 枢轴选择
  2. 分为三个子列表的分区列表
    第一个子列表包含键小于枢轴键的
    节点第二个子列表包含键等于枢轴键的
    节点第三个子列表包含键大于枢轴键的节点
  3. 子列表的递归调用包含不等于枢轴节点的节点
  4. 将已排序的子列表连接到一个已排序的列表中

广告1。
如果我们想快速选择枢轴,则选择是有限的。
我们可以选择头节点或尾节点
。如果我们想
快速访问枢轴,我们的清单必须要点节点,否则我们必须搜索节点

广告2。
在此步骤中,我们可以使用队列操作。
首先,我们从原始链表中将节点出队,
将其键与数据透视键进行比较,然后将节点入队到正确的子列表中。子列表
是从现有节点创建的,不需要
为新节点分配内存

指向尾节点的指针将很有用,因为
在存在此指针的情况下,队列操作和串联运行得更快


恐怕我看不到如何回答这个问题。
John L.
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.