¿Por qué no usamos clasificación rápida en una lista vinculada?

16

El algoritmo de ordenación rápida se puede dividir en los siguientes pasos

  1. Identificar pivote.

  2. Particionar la lista vinculada basada en pivote.

  3. Divida la lista vinculada recursivamente en 2 partes.

Ahora, si siempre elijo el último elemento como pivote, identificar el elemento pivote (1er paso) lleva tiempo .O(n)

Después de identificar el elemento pivote, podemos almacenar sus datos y compararlos con todos los demás elementos para identificar el punto de partición correcto (segundo paso). Cada comparación tomará tiempo ya que almacenamos los datos dinámicos y cada intercambio lleva tiempo . Entonces, en total, lleva tiempo para n elementos.O(1)O(1)O(n)n

Entonces la relación de recurrencia es:

T(n)=2T(n/2)+n que es O(nlogn) que es lo mismo que en el orden de fusión con una lista vinculada.

Entonces, ¿por qué se prefiere la ordenación por fusión sobre la ordenación rápida para las listas vinculadas?

Céfiro
fuente
No hay necesidad de elegir el último elemento como pivote en lugar del primero
TheCppZoo

Respuestas:

19

El patrón de acceso a la memoria en Quicksort es aleatorio, también la implementación inmediata está en su lugar, por lo que utiliza muchos intercambios si las celdas logran el resultado ordenado.
Al mismo tiempo, la ordenación de fusión es externa, requiere una matriz adicional para devolver el resultado ordenado. En la matriz significa una sobrecarga de espacio adicional, en el caso de que sea una lista vinculada, es posible extraer el valor y comenzar a fusionar nodos. El acceso es más secuencial en la naturaleza.

Debido a esto, el ordenamiento rápido no es una opción natural para la lista vinculada, mientras que la ordenación por fusión tiene una gran ventaja.

La notación de Landau podría (más o menos, porque Quicksort todavía está ) de acuerdo, pero la constante es mucho más alta.O(norte2)

En el caso promedio, ambos algoritmos están en por lo que el caso asintótico es el mismo, pero la preferencia se debe estrictamente a la constante oculta y, a veces, el problema es la estabilidad (quicksort es inherentemente inestable, mergsort es estable) .O(norteIniciar sesiónnorte)

Mal
fuente
Pero la complejidad del tiempo promedio es la misma, ¿verdad? Usando la ordenación rápida y la ordenación por fusión para la lista vinculada.
Zephyr
10
@ Zephyr, debe recordar que la notación de complejidad elimina factores constantes. Sí, el ordenamiento rápido en una lista vinculada y el ordenamiento combinado en una lista vinculada son la misma clase de complejidad, pero esas constantes que no está viendo hacen que el ordenamiento uniforme sea más rápido de manera uniforme.
Mark
@Zephyr Básicamente es la diferencia de resultados teóricos y empíricos. Empíricamente, quicksort es más rápido.
ferit
1
O(norte2)
3
O(Iniciar sesiónnorte)
5

Puede ordenar rápidamente las listas vinculadas, sin embargo, estará muy limitado en términos de selección de pivote, restringiéndolo a pivotes cerca del frente de la lista, lo cual es malo para entradas casi ordenadas, a menos que desee recorrer cada segmento dos veces (una vez para pivote y una vez para la partición). Y deberá mantener una pila de los límites de partición para las listas que aún necesita ordenar. Esa pila puede crecer a cuando la selección de pivote es mala junto con la complejidad del tiempo creciendo a .O ( n 2 )O(norte)O(norte2)

La ordenación por fusión en listas vinculadas se puede ejecutar usando solo espacio adicional si adopta un enfoque ascendente contando dónde están los límites de las particiones y fusionándose en consecuencia.O(1)

Sin embargo, al agregar una única matriz de punteros de 64 elementos, puede evitar esa iteración adicional y ordenar listas de hasta elementos en O ( 1 ) espacio adicional adicional.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;

Este es el algoritmo que utiliza el kernel de Linux para ordenar sus listas vinculadas. Aunque con algunas optimizaciones adicionales, como ignorar el previouspuntero durante todas las operaciones excepto la última fusión.

monstruo de trinquete
fuente
-2

Puede escribir ordenamiento por mezcla, partición tipo, especie de árbol y comparar los resultados
Es bastante fácil de árbol de escritura tipo si permitir algo más de espacio
para el árbol de una especie cada nodo de la lista enlazada debe tener dos punteros aunque especie por separado lista enlazada
En lista enlazada prefiero insertar y eliminar en lugar de intercambiar la
partición Hoare solo se puede hacer para una lista doblemente vinculada

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.

Este código necesita algunas mejoras.
En primer lugar, debemos limitar el almacenamiento adicional a las necesidades de recursión,
luego debemos tratar de reemplazar la recursión con iteración.
Si queremos mejorar aún más el algoritmo, deberíamos usar el árbol de equilibrio automático.

Mariusz
fuente
Gracias por su contribución detallada, pero este no es un sitio de codificación. 200 líneas de código no hacen nada para explicar por qué se prefiere la ordenación por fusión sobre la ordenación rápida para las listas vinculadas.
David Richerby
En la partición, la elección del pivote se limita al primer o último elemento (el último si mantenemos el puntero al nodo de cola) de lo contrario, la elección del pivote es lenta La partición Hoare es posible solo para listas doblemente vinculadas El intercambio debe reemplazarse mediante la inserción y eliminación de la ordenación de árbol con desequilibrado el árbol tiene la misma competencia que la clasificación rápida si ignoramos el factor constante, pero es más fácil evitar el peor de los casos en la clasificación de árbol. Para la clasificación por fusión hay pocos caracteres en el comentario
Mariusz
-2

Quicksort
Quizás muestre los pasos para quicksort

Si la lista contiene más de un nodo

  1. Selección de pivote
  2. La lista de particiones en tres sublistas. La
    primera sublista contiene nodos con teclas menores que la tecla pivote. La
    segunda sublista contiene nodos con teclas iguales a la tecla pivote. La
    tercera sublista contiene nodos con teclas mayores que la tecla pivote.
  3. Llamadas recursivas para sublistas que contienen nodos no iguales al nodo pivote
  4. Concatenar sublistas ordenadas en una lista ordenada

Anuncio 1.
Si queremos elegir un pivote rápido, la elección es limitada.
Podemos elegir el nodo principal o el nodo de cola.
Nuestra lista debe tener un puntero al nodo si queremos que nuestro pivote
sea ​​accesible rápidamente; de ​​lo contrario, tenemos que buscar el nodo.

Anuncio 2.
Podemos usar operaciones de cola para este paso
. Primero, podemos quitar el nodo de la lista original vinculada,
comparar su clave con la tecla pivote y poner el nodo en cola en la sublista correcta. Las sublistas
se crean a partir de nodos existentes y no hay necesidad de
asignar memoria para nuevos nodos.

El puntero al nodo de cola será útil porque las operaciones de cola
y la concatenación se ejecutan más rápido con la presencia de este puntero

Mariusz
fuente
Me temo que no pude ver cómo esto responde la pregunta.
John L.