¿Cuál es un ejemplo de un montón que requiere exactamente n * log (n) pasos? Sé que el límite superior de un montón es O (n log n), pero ¿cómo hago para mostrar un ejemplo donde requiera exactamente n * log (n) pasos?

Lamentablemente, los “pasos” son muy imprecisos, pero la situación no es tan desesperada como sugieren algunas de las otras respuestas. Podemos instrumentar una implementación de ordenamiento dinámico para contar explícitamente el número de comparaciones o intercambios (o cualquier otro “paso” que desee contar) y buscar en todas las permutaciones uno con el recuento deseado. Es posible que no exista, porque el límite puede estar desactivado por un factor constante. Pero al menos podemos mirar.

Tomé la implementación de Python aquí: Heap Sort – GeeksforGeeks y la instrumenté para contar:

  1. Siempre que se compararon dos valores de entrada (es decir, no conté las comparaciones de límites).
  2. Cada vez que se intercambian dos valores en la matriz.

Ejecutar esto en todas las permutaciones de longitud 4 da:

permutación de permutación de comps
7 8 (1, 2, 3, 4)
6 7 (1, 2, 4, 3)
7 7 (1, 3, 2, 4)
6 6 (1, 3, 4, 2)
7 6 (1, 4, 2, 3)
7 7 (1, 4, 3, 2)
7 7 (2, 1, 3, 4)
6 6 (2, 1, 4, 3)
7 8 (2, 3, 1, 4)
6 5 (2, 3, 4, 1)
7 7 (2, 4, 1, 3)
7 6 (2, 4, 3, 1)
7 6 (3, 1, 2, 4)
6 7 (3, 1, 4, 2)
7 7 (3, 2, 1, 4)
6 6 (3, 2, 4, 1)
7 6 (3, 4, 1, 2)
7 5 (3, 4, 2, 1)
6 5 (4, 1, 2, 3)
6 6 (4, 1, 3, 2)
6 6 (4, 2, 1, 3)
6 5 (4, 2, 3, 1)
6 5 (4, 3, 1, 2)
6 4 (4, 3, 2, 1)

[matemáticas] 4 \ log_2 4 = 8 [/ matemáticas]. Podemos ver que no hay ejemplos que requieran 8 comparaciones, pero sí hay ejemplos que requieren exactamente 8 intercambios.

La próxima vez que [math] \ log_2 n [/ math] sea un número entero es 8, que es mucho más permutaciones para buscar, pero podemos encontrar ejemplos que requieren exactamente [math] 8 \ log_2 8 = 24 [/ math] comparaciones o intercambios:

permutación de permutación de comps
28 24 (1, 2, 3, 5, 4, 7, 6, 8)
24 20 (1, 2, 4, 3, 8, 5, 6, 7)
29 24 (2, 5, 3, 6, 4, 1, 7, 8)
24 16 (8, 7, 6, 5, 4, 3, 2, 1)

Lamentablemente, [math] 16! [/ Math] es demasiadas posibilidades para examinar por fuerza bruta, y la lista ordenada requiere 85 comparaciones y 58 intercambios, por lo que no tenemos una respuesta fácil en este caso.

Debe tener en cuenta que el número de comparaciones es proporcional a [matemáticas] N + N \ log N [/ matemáticas], por lo que no deberíamos esperar ver un factor constante de ‘1’. Podemos ver cuánto tiempo lleva esta implementación de ordenamiento dinámico ordenar la lista ya ordenada:

n comps swaps n log n
2 1 2 2
4 7 8 8
8 27 22 24
16 85 58 64
32 231 146 160
64 593 362 384
128 1459 850 896
256 3452 1972 2048
512 7958 4464 4608
1024 18060 9968 10240
2048 40204 21864 22528
4096 88835 48360 49152
8192 194316 105306 106496
16384 421566 226058 229376
32768 908636 483628 491520
65536 1948406 1034492 1048576
131072 4162037 2206510 2228224
262144 8849195 4664612 4718592
524288 18755632 9834698 9961472

Esta tabla muestra que el número de intercambios es ligeramente menor que [math] N \ log_2 N [/ math], y el número de comparaciones es de [math] 2N \ log_2 N [/ math]. Esto sugiere (pero no prueba) que no podremos encontrar ejemplos más grandes donde el número de operaciones sea exacto.

En realidad, no sabía mucho sobre el montón en particular antes de leer esta pregunta, pero creo que puedo dar una respuesta.

Si está solicitando una entrada específica que le dé O (nlogn), normalmente se le puede dar una lista ordenada inversa, ya que esto garantiza que cada elemento se tocará durante la ordenación.

En cuanto a por qué el tiempo de ejecución es O (nlogn): heapsort es una ordenación de inserción mejorada y, como tal, mantiene una lista ordenada y no ordenada a medida que avanza. Su mejora proviene del hecho de que mantiene todos los elementos no ordenados en un montón (qué tipo de montón depende de cómo desea ordenar). Suponiendo que desea ordenar en orden ascendente, puede comenzar en el último elemento de la lista y llamar a delete-max desde un montón máximo, dándole el último elemento de la lista. Luego puede repetir, moviendo el puntero hacia adelante y disminuyendo el tamaño del montón, hasta que se ordene la lista.

Si luego comprende el algoritmo, el tiempo de ejecución se vuelve obvio. Para ordenar la lista completa, debe iterar sobre cada elemento de la lista: O (n) donde n es el tamaño de la lista. Para eliminar un elemento del montón, debe llamar a delete-max (o min) en el montón, que al usar un montón de Fibonacci [1] da como resultado O (logn).

Dado que está eliminando un elemento del montón durante el proceso de iteración sobre cada elemento, debe multiplicar los tiempos de ejecución, lo que le proporciona su tiempo de ejecución final de O (nlogn).

Notas al pie

[1] Montón de Fibonacci – Wikipedia

Su pregunta es problemática en varios aspectos:

  • dices * pasos * sin especificar cuáles. Estos podrían ser comparaciones, movimientos u operaciones auxiliares (cálculos de índice).
  • O (n log n) no significa que el número de operaciones puede ser exactamente n log n. Simplemente significa que no supera algunos múltiplos de n log n.
  • en particular, si cuenta las comparaciones más los movimientos más las operaciones auxiliares, el total siempre es mayor que n log n.
  • no hay un solo Heapsort, hay variantes que difieren en detalles pequeños, pero de tal manera que el recuento de operaciones también difiere. Por lo tanto, debe proporcionar el código (pseudo) del algoritmo.
  • finalmente, Heapsort tiene dos fases, y establecer los recuentos exactos es técnicamente bastante arduo. Entonces, para el n general, supongo que encontrar configuraciones adecuadas es una tarea terrible. El enfoque de “fuerza bruta” de Mark Gritter es agradable, pero solo proporciona configuraciones para n particular, no una regla general, y no es aplicable para n grande (digamos n> 20).

O (NLogN) excluye algunas cosas que afectan el tiempo de ejecución. Es una generalización del aumento del tiempo de ejecución frente al cambio de la longitud de entrada. Existen varios algoritmos O (NLogN) que no exhiben el mismo tiempo de ejecución. MergeSort y QuickSort se consideran algoritmos de esta clase, pero tienen tiempos de ejecución marcadamente diferentes para N. grande, pero cuando el tiempo frente a N se muestran en un gráfico, sus tasas de tiempo de ejecución aumentan de manera similar, de forma logarítmica. Espero que esto ayude a aclarar las cosas. Hay muy pocos arreglos de datos y usos útiles de algoritmos que resultan en pasos EXACTAMENTE NLogN.

No puede tener el caso en el que toma exactamente [matemática] n \ log n [/ matemática] pasos, mire este código simple de montón:

void max_heapify (heap * A, int index) {
int izquierda = IZQUIERDA (índice);
int right = RIGHT (índice);
int más grande = índice;

if (left heap_size && A-> v [left]> A-> v [index])
mayor = izquierda;
if (derecha heap_size && A-> v [derecha]> A-> v [más grande])
mayor = derecha;
if (mayor! = índice) {
swap (A-> v [índice], A-> v [mayor]);
max_heapify (A, mayor);
}
}
vacío buil_max_heap (montón * A) {
A-> heap_size = A-> longitud;
para (int i = A-> heap_size / 2; i> -1; –i)
max_heapify (A, i);
}
int heap_extract_max (montón * A) {
valor int = A-> v [0];
A-> v [0] = A-> v [A-> heap_size-1];
–A-> heap_size;
max_heapify (A, 0);
valor de retorno;
}
nulo heap_sort (montón * A) {
buil_max_heap (A);
para (int i = A-> length-1; i> -1; –i) {
int x = heap_extract_max (A);
A-> v [i] = x;
}
}

Como puede ver en las líneas 29–32, en realidad hay iteración [matemática] n [/ matemática], por lo que la complejidad sería exactamente [matemática] n * (extracto \ _max () + 1) [/ matemática] pero si observa en la línea 23, verá que en cada llamada el tamaño del almacenamiento dinámico se reduce en uno.

Un número de pasos más preciso sería [math] \ sum_ {i = 0} ^ {n-1} \ lfloor {\ log (ni) + 1} \ rfloor [/ math]. Es fácil entender que el límite superior de esta suma es [matemática] n \ log n [/ matemática] pero en la práctica no hay casos en que esto suceda exactamente.

No es de extrañar que esto suceda, a menudo la notación big-O no es muy exacta en términos de pasos (por ejemplo selection_sort () y otras toneladas de algoritmos), pero eso es porque no tiene que ser tan preciso.

Heapsort es un algoritmo muy eficiente si se le proporciona una matriz en la que se mantiene la propiedad del montón para cada uno de sus elementos. De lo contrario, se volvió menos eficiente porque necesita construir el montón desde cero.