Está en la página 1de 12

Capítulo 3

CAPÍTULO TRES

El código más hermoso que nunca escribí


Jon Bentley

yo UNA VEZ ESCUCHÓ UN PROGRAMADOR MAESTRO ALABADO CON LA FRASE, "Agrega función al eliminar
código." Antoine de Saint-Exupéry, el escritor y aviador francés, expresó este sentimiento de manera más
general cuando dijo: "Un diseñador sabe que ha logrado la perfección no cuando no hay nada más que agregar,
sino cuando no hay nada más que quitar". En el software, el código más bello, las funciones más bellas y los
programas más bellos a veces no existen.

Por supuesto, es difícil hablar de cosas que no están allí. Este capítulo intenta esta tarea desalentadora presentando un

análisis novedoso del tiempo de ejecución del clásico programa Quicksort. La primera sección prepara el escenario al

revisar Quicksort desde una perspectiva personal. La siguiente sección es la carne de este capítulo. Comenzaremos

agregando un contador al programa, luego manipularemos el código para hacerlo cada vez más pequeño y aún más

poderoso hasta que solo unas pocas líneas de código capturen completamente su tiempo de ejecución promedio. La

tercera sección resume las técnicas y presenta un análisis particularmente sucinto del costo de los árboles de búsqueda

binarios. Las dos secciones finales extraen ideas del capítulo para ayudarlo a escribir programas más elegantes.

29
El código más hermoso que jamás escribí

Cuando Greg Wilson describió por primera vez la idea de este libro, me pregunté cuál era el código más hermoso que jamás

había escrito. Después de que esta deliciosa pregunta rodó por mi cerebro durante la mayor parte del día, me di cuenta de que la

respuesta era fácil: Quicksort. Desafortunadamente, la única pregunta tiene tres respuestas diferentes, dependiendo de cómo se

formule exactamente.

Escribí mi tesis sobre algoritmos de divide y vencerás, y descubrí que Quicksort de CAR Hoare ("Quicksort" Diario de
computadora 5) es sin duda el abuelo de todos ellos. Es un algoritmo hermoso para un problema fundamental que se puede
implementar en código elegante. Me encantó el algoritmo, pero siempre andaba de puntillas en su bucle más interno. Una
vez pasé dos días depurando un programa complejo que se basaba en ese ciclo, y durante años copié cuidadosamente ese
código cada vez que necesitaba realizar una tarea similar. Resolvió mis problemas, pero no lo hice. De Verdad entiendelo.

Eventualmente aprendí un elegante esquema de partición de Nico Lomuto, y finalmente pude escribir un Quicksort que
podía entender e incluso probar que era correcto. La observación de William Strunk Jr. de que "la escritura vigorosa es
concisa" se aplica tanto al código como al inglés, así que seguí su advertencia de "omitir palabras innecesarias" ( Los
elementos del estilo). Finalmente reduje aproximadamente 40 líneas de código a una docena par. Entonces, si la pregunta
es: "¿Cuál es el código pequeño más hermoso que hayas escrito?" mi respuesta es el Quicksort de mi libro Perlas de
programación, Segunda edición (Addison-Wesley). Esta función Quicksort, implementada en C, se muestra en el Ejemplo
3-1. Seguiremos estudiando y refinando este ejemplo en la siguiente sección.

EJEMPLO 3 - 1. Función Quicksort

voy quicksort (int l, int u) {int i, m;

si (l> = u) devuelve; swap (l,


randint (l, u)); m = l;

para (i = l + 1; i <= u; i ++)


si (x [i] <x [l])
intercambio (++ m, i);

intercambio (l, m); clasificación rápida (l,

m-1); clasificación rápida (m + 1, u); }

Este código ordena una matriz global x [n] cuando se llama con los argumentos clasificación rápida (0, n-1). Los dos parámetros de

la función son los índices de la submatriz a ordenar: l para baja y tu para superior. La llamada intercambio (i, j) intercambia el

contenido de x [i] y x [j]. El primer intercambio elige aleatoriamente un elemento de partición uniformemente seleccionado entre l y

u.

Programando Perlas contiene una derivación detallada y prueba de corrección para el ordenación rápida

función. Durante el resto de este capítulo, supondré que el lector está familiarizado con Quicksort en el nivel presentado
en esa descripción y en la mayoría de los libros de texto de algoritmos elementales.
Si cambia la pregunta a "¿Cuál es el código más hermoso que ha escrito que se utilizó ampliamente?" mi respuesta es

nuevamente un Quicksort. Un artículo que escribí con MD McIlroy ("Ingeniería de una función de clasificación" Software:

práctica y experiencia, Vol. 23, N ° 11) describe un grave error de rendimiento en el venerable Unix qsort función. Nos
propusimos construir una nueva biblioteca C ordenar función, y consideró muchos algoritmos diferentes para la tarea, incluida

la Clasificación de combinación y la Clasificación de montón. Después de comparar varias implementaciones posibles, nos

decidimos por una versión del algoritmo Quicksort. Ese documento describe cómo diseñamos una nueva función que era más

clara, más rápida y más robusta que sus competidores, en parte porque era más pequeña. El sabio consejo de Gordon Bell

demostró ser cierto: "Los componentes más baratos, rápidos y confiables de un sistema informático son aquellos que no

están allí". Esa función se ha utilizado ampliamente durante más de una década sin informes de fallas.

Considerando las ganancias que se podrían lograr al reducir el tamaño del código, finalmente me hice una tercera variante

de la pregunta que comenzó este capítulo. "¿Cuál es el código más hermoso que usted Nunca escribió? ¿Cómo pude lograr

mucho con muy poco? La respuesta estuvo nuevamente relacionada con Quicksort, específicamente, el análisis de su

desempeño. La siguiente sección cuenta esa historia.

Más y más con menos y menos


Quicksort es un algoritmo elegante que se presta a análisis sutiles. Alrededor de 1980, tuve una maravillosa discusión con
Tony Hoare sobre la historia de su algoritmo. Me dijo que cuando desarrolló Quicksort por primera vez, pensó que era
demasiado simple de publicar, y solo escribió su clásico artículo "Quicksort" después de que pudo analizar su tiempo de
ejecución esperado.

Es fácil ver que, en el peor de los casos, Quicksort podría demorar norte 2 hora de ordenar una serie de norte elementos. En el mejor de

los casos, elige el valor medio como un elemento de partición y, por lo tanto, ordena una matriz de aproximadamente norte lg norte comparaciones

Entonces, ¿cuántas comparaciones usa en promedio para una matriz aleatoria de norte valores distintos?

El análisis de Hoare de esta pregunta es hermoso, pero desafortunadamente sobre las cabezas matemáticas de muchos programadores.

Cuando enseñé Quicksort a estudiantes universitarios, me sentí frustrado porque muchos simplemente no "obtuvieron" la prueba, incluso

después de un esfuerzo sincero. Ahora atacaremos ese problema experimentalmente. Comenzaremos con el programa de Hoare y

finalmente terminaremos con un análisis cercano al suyo.

Nuestra tarea es modificar el Ejemplo 3-1 del código Quicksort al azar para analizar el número promedio de comparaciones utilizadas

para ordenar una matriz de entradas distintas. También intentaremos obtener una visión máxima con un código, tiempo de ejecución

y espacio mínimos.

Para determinar el número promedio de comparaciones, primero aumentamos el programa para contarlas. Para hacer esto,

incrementamos la variable comps antes de la comparación en el bucle interno (Ejemplo 3-2).


EJEMPLO 3 - 2. Quicksort loop interno instrumentado para contar comparaciones

para (i = l + 1; i <= u; i ++) {


comps ++;
si (x [i] <x [l])
intercambio (++ m, i); }

Si ejecutamos el programa por un valor de norte, veremos cuántas comparaciones toma esa carrera en particular. Si repetimos eso para

muchas ejecuciones sobre muchos valores de norte, y analizar los resultados estadísticamente, observaremos que, en promedio, Quicksort toma

alrededor de 1.4 norte lg norte comparaciones para ordenar norte elementos.

Esa no es una mala manera de obtener información sobre el comportamiento de un programa. Trece líneas de código y

algunos experimentos pueden revelar mucho. Una famosa cita atribuida a escritores como Blaise Pascal y TS Eliot afirma que

"si hubiera tenido más tiempo, te habría escrito una carta más corta". Tenemos tiempo, así que experimentemos con el código

para intentar crear un programa más corto (y mejor).

Jugaremos el juego de acelerar ese experimento, tratando de aumentar la precisión estadística y el conocimiento de la

programación. Porque el circuito interno siempre hace precisamente tu –L comparaciones, podemos hacer que el programa sea un

poco más rápido contando esas comparaciones en una sola operación fuera del ciclo. Este cambio produce el Quicksort que se

muestra en el Ejemplo 3-3.

EJEMPLO 3 - 3. Quicksort loop interno con incremento movido fuera del loop

comps + = ul;
para (i = l + 1; i <= u; i ++)
si (x [i] <x [l])
intercambio (++ m, i);

Este programa ordena una matriz y cuenta el número de comparaciones utilizadas al hacerlo. Sin embargo, si nuestro

objetivo es contar las comparaciones, realmente no necesitamos ordenar la matriz. El ejemplo 3-4 elimina el "trabajo real"

de ordenar los elementos y mantiene solo el "esqueleto" de las diversas llamadas realizadas por el programa.

EJEMPLO 3 - 4. Esqueleto de clasificación rápida reducido a contar

cuenta rápida nula (int l, int u) {int m;

si (l> = u) devuelve; m =
randint (l, u); comps + = ul;
cuenta rápida (l, m-1);
cuenta rápida (m + 1, u); }

Este programa funciona debido a la forma de "aleatorización" en la que Quicksort elige su elemento de partición, y porque se

supone que todos los elementos son distintos. Este nuevo programa ahora se ejecuta en tiempo proporcional a norte, y

mientras que el Ejemplo 3-3 requería un espacio proporcional a norte, el espacio ahora se reduce a la pila de recursión, que

en promedio es proporcional a lg norte.


Mientras que los índices ( l y u) de la matriz son críticos en un programa real, no importan en esta versión esqueleto.
Podemos reemplazar estos dos índices con un solo entero ( norte) que especifica el tamaño de la submatriz a ordenar
(ver Ejemplo 3-5).

EJEMPLO 3 - 5. Esqueleto de clasificación rápida con argumento de tamaño único

nulo qc (int n) {int


m;
si (n <= 1) devuelve; m =
randint (1, n); comps + =
n-1; qc (m-1); qc (nm); }

Ahora es más natural reformular este procedimiento como recuento de comparación función que devuelve el número de

comparaciones utilizadas por una ejecución aleatoria de Quicksort. Esta función se muestra en el Ejemplo 3-6.

EJEMPLO 3 - 6. Esqueleto de Quicksort implementado como una función

int cc (int n) {int m;

if (n <= 1) devuelve 0; m =
randint (1, n);
retorno n-1 + cc (m-1) + cc (nm); }

Los ejemplos 3-4, 3-5 y 3-6 resuelven el mismo problema básico, y lo hacen con el mismo tiempo de ejecución y uso
de memoria. Cada sucesor mejora la forma de la función y, por lo tanto, es más claro y un poco más sucinto que su
predecesor.

Al definir el paradoja del inventor (Cómo resolverlo, Princeton University Press), George Pólya dice que "el plan más
ambicioso puede tener más posibilidades de éxito". Ahora intentaremos explotar esa paradoja en el análisis de Quicksort.
Hasta ahora hemos preguntado: "¿Cuántas comparaciones hace Quicksort en una serie de tamaños? ¿norte? "Ahora
haremos la pregunta más ambiciosa:" ¿Cuántas comparaciones hace Quicksort, en promedio, para un conjunto aleatorio de
tamaños? ¿norte? "Podemos extender el Ejemplo 3-6 para obtener el pseudocódigo en el Ejemplo 3-7.

EJEMPLO 3 - 7. Quicksort comparaciones promedio como pseudocódigo

flotador c (int n)
si (n <= 1) devuelve 0 suma =
0
para (m = 1; m <= n; m ++)
suma + = n-1 + c (m-1) + c (nm) suma de
retorno / n

Si la entrada tiene un máximo de un elemento, Quicksort no utiliza comparaciones, como en el Ejemplo 3-6. Para mayor norte, este

código considera cada valor de partición m ( desde el primer elemento hasta el último, cada uno igualmente probable) y determina

el costo de la partición allí. Luego, el código calcula la suma de estos valores (resolviendo así recursivamente un problema de

tamaño m-1 y un problema de tamaño Nuevo Méjico), y luego divide esa suma por norte para devolver el promedio.
Si pudiéramos calcular este número, nuestros experimentos serían mucho más poderosos. En lugar de tener que realizar

muchos experimentos con un solo valor de norte Para estimar la media, un solo experimento nos daría la verdadera media.

Desafortunadamente, ese poder tiene un precio: el programa se ejecuta en un tiempo proporcional a 3 n ( Es un ejercicio

interesante, aunque autorreferencial, analizar ese tiempo de ejecución utilizando las técnicas descritas en este capítulo).

El ejemplo 3-7 toma el tiempo que hace porque calcula las sub-respuestas una y otra vez. Cuando un programa hace eso, a

menudo podemos usar programación dinámica para almacenar las subrespuestas para evitar volver a calcularlas. En este caso,

presentaremos la tabla t [N + 1] en el cual Tennesse] historias c (n), y calcule sus valores en orden creciente. Vamos a dejar norte denotar

el tamaño máximo de norte, cuál es el tamaño de la matriz que se ordenará. El resultado se muestra en el Ejemplo 3-8.

EJEMPLO 3 - 8. Cálculo de clasificación rápida con programación dinámica

t [0] = 0
para (n = 1; n <= N; n ++)
suma = 0
para (i = 1; i <= n; i ++)
suma + = n-1 + t [i-1] + t [ni] t [n] =
suma / n

Este programa es una transcripción aproximada del Ejemplo 3-7 y reemplaza c (n) con Tennesse]. Su tiempo de ejecución es proporcional

a norte 2 y su espacio es proporcional a NORTE. Uno de sus beneficios es que al final de la ejecución, la matriz t contiene los valores

promedio verdaderos (no solo la estimación de medias de muestra) para elementos de matriz 0 0 mediante NORTE. Esos valores se

pueden analizar para obtener información sobre la forma funcional del número esperado de comparaciones utilizadas por Quicksort.

Ahora simplificaremos más ese programa. El primer paso es mover el término norte- 1 fuera del ciclo, como se muestra en

el Ejemplo 3-9.

EJEMPLO 3 - 9. Cálculo de clasificación rápida con código movido fuera del ciclo

t [0] = 0
para (n = 1; n <= N; n ++)
suma = 0
para (i = 1; i <= n; i ++)
suma + = t [i-1] + t [ni] t [n] =
n-1 + suma / n

Ahora ajustaremos aún más el bucle explotando la simetría. Cuando norte es 4, por ejemplo, el bucle interno calcula la
suma:

t [0] + t [3] + t [1] + t [2] + t [2] + t [1] + t [3] + t [0]

En la secuencia de pares, los primeros elementos aumentan mientras que los segundos elementos disminuyen. Por lo tanto, podemos

reescribir la suma como:

2 * (t [0] + t [1] + t [2] + t [3])

Podemos usar esa simetría para obtener el Quicksort que se muestra en el Ejemplo 3-10.
EJEMPLO 3 - 10. Cálculo de clasificación rápida con simetría

t [0] = 0
para (n = 1; n <= N; n ++)
suma = 0
para (i = 0; i <n; i ++)
suma + = 2 * t [i] t [n]
= n-1 + suma / n

Sin embargo, este código es una vez más derrochador porque vuelve a calcular la misma suma una y otra vez. En lugar de

agregar todos los términos anteriores, podemos inicializar suma fuera del ciclo y agregue el siguiente término para obtener el

Ejemplo 3-11.

EJEMPLO 3-11. Cálculo de clasificación rápida con el bucle interno eliminado

suma = 0; t [0] = 0 para (n = 1; n


<= N; n ++)
suma + = 2 * t [n-1] t [n] =
n-1 + suma / n

Este pequeño programa es realmente útil. En tiempo proporcional a NORTE, produce una tabla de los tiempos de ejecución reales esperados

de Quicksort para cada número entero de 1 a NORTE.

El ejemplo 3-11 es fácil de implementar en una hoja de cálculo, donde los valores están disponibles de inmediato
para su posterior análisis. La Tabla 3-1 muestra las primeras filas.

CUADRO 3 - 1. Salida de la implementación de la hoja de cálculo del Ejemplo 3-11

norte Suma t [n]

00 00 00

1 00 00

2 00 1

3 2 2.667

44 7.333 4.833

55 17 7.4

66 31,8 10,3

77 52,4 13.486

8 79,371 16.921

La primera fila de números en esta tabla se inicializa con las tres constantes del código. En la notación de hoja de
cálculo, la siguiente fila de números (la tercera fila de la hoja de cálculo) se calcula utilizando las siguientes relaciones:

A3 = A2 + 1 B3 = B2 + 2 * C2 C3 = A3-1 + B3 / A3

Arrastrando esas relaciones (relativas) hacia abajo completa la hoja de cálculo. Esa hoja de cálculo es un verdadero

contendiente para "el código más hermoso que jamás haya escrito", utilizando el criterio de lograr mucho con solo unas pocas

líneas de código.

Pero, ¿y si no necesitamos todos los valores? ¿Qué pasaría si preferiríamos analizar solo algunos de los valores en el
camino (por ejemplo, todas las potencias de 2 de 2 0 0 a 2 32) Aunque el ejemplo 3-11 construye la tabla completa t, utiliza
Por lo tanto, podemos reemplazar el espacio lineal de la tabla t [] con el espacio constante de la variable t, como se muestra en

el Ejemplo 3-12.

EXAMPLE 3 - 12 . Quicksort calculation—final version

sum = 0; t = 0
for (n = 1; n <= N; n++)
sum += 2*t t = n-1 +
sum/n

Luego podríamos insertar una línea de código adicional para comprobar la idoneidad de norte, e imprima esos resultados según sea necesario.

Este pequeño programa es el paso final en nuestro largo camino. La observación de Alan Perlis es adecuada en consideración del

camino que ha tomado este capítulo: "La simplicidad no precede a la complejidad, sino que la sigue" ("Epigramas sobre

programación", Avisos Sigplan, Vol. 17, número 9).

Perspectiva
La Tabla 3-2 resume los programas utilizados para analizar Quicksort a lo largo de este capítulo.

TABLE 3 - 2 . Evolution of Quicksort comparison counting

Example number Lines of code Type of answer Number of answers Runtime Space

2 13 Sample 1 n lg n N

3 13 " " " "

4 8 " " n lg n

5 8 " " " "

6 6 " " " "

7 6 Exact " 3N N

8 6 " N N2 N

9 6 " " " "

10 6 " " " "

11 4 " " N "

12 4 Exact N N 1

Cada paso individual en la evolución de nuestro código fue bastante sencillo; La transición de la muestra en el
Ejemplo 3-6 a la respuesta exacta en el Ejemplo 3-7 es probablemente la más sutil. En el camino, a medida que el
código se hizo más rápido y más útil, también disminuyó de tamaño. A mediados del siglo XIX, Robert Browning
observó que "menos es más", y esta tabla ayuda a cuantificar una instancia de esa filosofía minimalista.

We have seen three fundamentally different types of programs. Examples 3-2 and 3-3 are working Quicksorts,
instrumented to count comparisons as they sort a real array. Examples 3-4 through 3-6 implement a simple
model of Quicksort: they mimic one run of the algorithm, without actually doing the work of sorting. Examples 3-7
through 3-12 implement a more sophisticated model: they compute the true average number of comparisons
without ever tracing any particular run.
The techniques used to achieve each program are summarized as follows:

• Examples 3-2, 3-4, 3-7: Fundamental change of problem definition.

• Examples 3-5, 3-6, 3-12: Slight change of function definition.

• Example 3-8: New data structure to implement dynamic programming.

These techniques are typical. We can often simplify a program by asking, “What problem do we really need to
solve?” or, “Is there a better function to solve that problem?”

When I presented this analysis to undergraduates, the program finally shrank to zero lines of code and
disappeared in a puff of mathematical smoke. We can reinterpret Example 3-7 as the following recurrence
relation:

C0 =0 cC n = ( n 1– ) + 1( n⁄ ) ∑ C i 1– + C n i–
1 i≤n≤

This is precisely the approach taken by Hoare and later presented by D. E. Knuth in his classic The Art of
Computer Programming, Volume 3: Sorting and Searching ( Addison-Wesley). The programming tricks of
re-expression and symmetry that give rise to Example 3-10 allow us to simplify the recursive part to:

Cn = n 1– 2 n + ( ⁄ ) ∑ Ci
0 i≤n≤1 –

Knuth’s technique to remove the summation sign gives (roughly) Example 3-11, which can be re-expressed as
a system of two recurrence relations in two unknowns as:

C0 =0 S0 =0 Sn = S n 1– + 2 C n 1– Cn = n 1– + S n n⁄

Knuth uses the mathematical technique of a “summing factor” to achieve the solution:

Cn = ( n 1+ ) ( 2 H n 1+ – 2) – 2n ∼ 1.386 n lg n

where H n denotes the n th harmonic number, 1 + 1/2 + 1/3 + … 1/ n. Thus we have smoothly progressed from
experimenting on a program by augmenting it with probes to a completely mathematical analysis of its behavior.

With this formula, we end our quest. We have followed Einstein’s famous advice to “make everything as simple
as possible, but no simpler.”

A Bonus Analysis

Goethe famously said that “architecture is frozen music.” In exactly that sense, I assert that “data structures are
frozen algorithms.” And if we freeze the Quicksort algorithm, we get the data structure of a binary search tree.
Knuth’s publication presents that structure and analyzes its runtime with a recurrence relation similar to that for
Quicksort.
If we wanted to analyze the average cost of inserting an element into a binary search tree, we could start with
the code, augment it to count comparisons, and then conduct experiments on the data we gather. We could then
simplify that code (and expand its power) in a manner very reminiscent of the previous section. A simpler
solution is to define a new Quicksort that uses an ideal partitioning method that leaves the elements in the same
relative order on both sides. That Quicksort is isomorphic to binary search trees, as illustrated in Figure 3-1.

31 41 59 26 53

26 31 41 59 53 31

26 41 59 53 53 26 41

59 53 59

53

F I GURE 3 - 1 . An ideal partitioning Quicksort and the corresponding binary search tree

The boxes on the left show an ideal-partitioning Quicksort in progress, and the graph on the right shows the
corresponding binary search tree that has been built from the same input. Not only do the two processes make
the same number of comparisons, they make exactly the same set of comparisons. Our previous analysis for the
average performance of randomizing Quicksort on a set of distinct elements therefore gives us the average
number of comparisons to insert randomly permuted distinct elements into a binary search tree.

What Is Writing?
In a weak sense, I “wrote” Examples 3-2 through 3-12 of the program. I wrote them first in scribbled notes, then
on a chalkboard in front of undergraduates, and eventually in this chapter. I derived the programs systematically,
I have spent considerable time analyzing them, and I believe that they are correct. Apart from the spreadsheet
implementation of Example 3-11, though, I have never run any of the examples as a computer program.

In almost two decades at Bell Labs, I learned from many teachers (and especially from Brian Kernighan, whose
chapter on the teaching of programming appears as Chapter 1 of this book) that “writing” a program to be
displayed in public involves much more than typing symbols. One implements the program in code, runs it first on
a few test cases, then builds thorough scaffolding, drivers, and a library of cases to beat on it systematically.
Ideally, one mechanically includes the compiled source code into the text without human intervention. I wrote
Example 3-1 (and all the code in Programming Pearls) in that strong sense.

As a point of honor, I wanted to keep my title honest by never implementing Examples 3-2 through 3-12. Almost
four decades of computer programming have left me with deep
respect for the difficulty of the craft (well, more precisely, abject fear of bugs). I compromised by implementing
Example 3-11 in a spreadsheet, and I tossed in an additional column that gave the closed-form solution. Imagine
my delight (and relief) when the two matched exactly! And so I offer the world these beautiful unwritten
programs, with some confidence in their correctness, yet painfully aware of the possibility of undiscovered error. I
hope that the deep beauty I find in them will be unmarred by superficial blemishes.

In my discomfort at presenting these unwritten programs, I take consolation from the insight of Alan Perlis, who
said, “Is it possible that software is not like anything else, that it is meant to be discarded: that the whole point is
to see it as a soap bubble?”

Conclusion
Beauty has many sources. This chapter has concentrated on the beauty conferred by simplicity, elegance, and
concision. The following aphorisms all express this overarching theme:

• Strive to add function by deleting code.

• A designer knows he has achieved perfection not when there is nothing left to add, but when there is nothing
left to take away. (Saint-Exupéry)

• In software, the most beautiful code, the most beautiful functions, and the most beautiful programs are
sometimes not there at all.

• Vigorous writing is concise. Omit needless words. (Strunk and White)

• The cheapest, fastest, and most reliable components of a computer system are those that aren’t there.
(Bell)

• Endeavor to do more and more with less and less.

• If I had more time, I would have written you a shorter letter. (Pascal)

• The Inventor’s Paradox: The more ambitious plan may have more chance of success. (Pólya)

• Simplicity does not precede complexity, but follows it. (Perlis)

• Less is more. (Browning)

• Make everything as simple as possible, but no simpler. (Einstein)

• Software should sometimes be seen as a soap bubble. (Perlis)

• Seek beauty through simplicity.

Here endeth the lesson. Go thou and do likewise.

For those who desire more concrete hints, here are some ideas grouped into three main categories.
Analysis of programs
One way to gain insight into the behavior of a program is to instrument it and then run it on representative
data, as in Example 3-2. Often, though, we are less concerned with the program as a whole than with
individual aspects. In this case, for instance, we considered only the number of comparisons that Quicksort
uses on the average and ignored many other aspects. Sedgewick (“The analysis of Quicksort programs,” Acta
Informatica, Vol. 7) studies issues such as the space it requires and many other components of runtime for a
variety of Quicksort variants. By concentrating on the key issues, we can ignore (for a while) other aspects of
the program. One of my articles, “A Case Study in Applied Algorithm Design” ( IEEE Computer, Vol. 17, No. 2)
describes how I once faced the problem of evaluating the performance of a strip heuristic for finding an
approximate travelling salesman tour through N points in the unit square. I estimated that a complete program
for the task might take 100 lines of code. After a series of steps similar in spirit to what we have seen in this
chapter, I used a dozen-line simulation to give much more accuracy (and after completing my little simulation,
I found that Beardwood et al. [“The Shortest Path Through Many Points,” Proc. Cambridge Philosophical Soc., Vol.
55] had re-expressed my simulation as a double integral, and thereby had solved the problem mathematically
some two decades earlier).

Small pieces of code

I believe that computer programming is a practical skill, and I agree with Pólya that we “acquire any practical
skill by imitation and practice.” Programmers who long to write beautiful code should therefore read beautiful
programs and imitate the techniques they learn as they write their own programs. I find that one of the most
useful places to practice is on small code fragments, say of just one or two dozen lines. It was hard work but
great fun preparing the second edition of Programming Pearls. I implemented every piece of code, and
labored to pare each down to its essence. I hope that others enjoy reading the code as much as I enjoyed
writing it.

Software systems

For specificity, I have described one tiny task in excruciating detail. I believe that the glory of these principles
lies not in tiny code fragments, but rather in large programs and huge computer systems. Parnas (“Designing
software for ease of extension and contraction,” IEEE T. Software Engineering, Vol. 5, No. 2) offers
techniques to whittle a system down to its essentials. For immediate applicability, don’t forget the deep insight
of Tom Duff: “Whenever possible, steal code.”

Acknowledgments
I am grateful for the insightful comments of Dan Bentley, Brian Kernighan, Andy Oram, and David Weiss.

También podría gustarte