joe di castrohttp://joedicastro.com2011-06-09T23:35:00+02:00Algoritmos Shuffle2011-06-09T23:35:00+02:00joe di castrohttp://joedicastro.com/algoritmos-shuffle.html<p>Los algoritmos <strong>shuffle</strong> se emplean para barajar o desordenar los elementos de
una lista (como si fueran las cartas de una baraja) al azar. Se intenta
reproducir lo que ocurre en la realidad cuando barajamos un mazo de cartas o
extraemos bolas de un bombo para formar una combinación. El comportamiento
ideal de estos algoritmos debe de ser imparcial, es decir que todas las
posibles <a href="http://es.wikipedia.org/wiki/Permutaci%C3%B3n">permutaciones</a> de esta lista tengan las mismas oportunidades
de aparecer como resultado. Este reparto uniforme es clave para el buen
funcionamiento del mismo, pues de no ser así se crearían tendencias y el
resultado seria predecible, lo que acabaría con la aleatoriedad que se
pretende conseguir.</p>
<p>Imaginemos tres distintas cartas de una baraja inglesa: As, Rey y Reina (Ace,
King & Queen) como una lista en <strong>Python</strong>, y ahora veamos todas las posibles
permutaciones de la misma (<em>n!</em>, es decir, <em>3! = 6</em>):</p>
<p style="text-align:center;"><img src="pictures/permutations.png" width="500"
height="249" alt="permutations.png" /></p>
<p><em>Representación gráfica de las distintas permutaciones.<sup id="fnref:diseño"><a href="#fn:diseño" rel="footnote">1</a></sup></em></p>
<div class="codehilite"><pre><span class="gp">>>> </span><span class="n">lista</span> <span class="o">=</span> <span class="p">[</span><span class="s">'A'</span><span class="p">,</span> <span class="s">'K'</span><span class="p">,</span> <span class="s">'Q'</span><span class="p">]</span>
<span class="gp">>>> </span><span class="kn">import</span> <span class="nn">itertools</span>
<span class="gp">>>> </span><span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="n">itertools</span><span class="o">.</span><span class="n">permutations</span><span class="p">(</span><span class="n">lista</span><span class="p">):</span>
<span class="gp">... </span> <span class="k">print</span><span class="p">(</span><span class="n">i</span><span class="p">)</span>
<span class="gp">...</span>
<span class="go">('A', 'K', 'Q')</span>
<span class="go">('A', 'Q', 'K')</span>
<span class="go">('K', 'A', 'Q')</span>
<span class="go">('K', 'Q', 'A')</span>
<span class="go">('Q', 'A', 'K')</span>
<span class="go">('Q', 'K', 'A')</span>
<span class="go">>>></span>
</pre></div>
<p>Vemos entonces que existen 6 permutaciones posibles que pueden darse como
resultado de barajar las cartas. Ahora imaginemos que escogemos una estas
permutaciones, y jugamos a barajar las cartas una y otra vez partiendo siempre
desde esta permutación. Como ya adelantábamos, si el algoritmo shuffle fuese
perfecto, el resultado de estas operaciones debería repartirse de forma
imparcial (equiprobable) entre las distintas permutaciones posibles. Es decir,
que al ejecutar un fragmento de código como el que sigue, donde empleamos el
algoritmo shuffle que por defecto trae Python (en el modulo <strong>random</strong> de la
librería estándar), <code>ramdom.suffle()</code></p>
<div class="codehilite"><pre><span class="gp">>>> </span><span class="kn">from</span> <span class="nn">random</span> <span class="kn">import</span> <span class="n">shuffle</span>
<span class="gp">>>> </span><span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">12</span><span class="p">):</span>
<span class="gp">... </span> <span class="n">lista</span> <span class="o">=</span> <span class="p">[</span><span class="s">'A'</span><span class="p">,</span> <span class="s">'K'</span><span class="p">,</span> <span class="s">'Q'</span><span class="p">]</span>
<span class="gp">... </span> <span class="n">shuffle</span><span class="p">(</span><span class="n">lista</span><span class="p">)</span>
<span class="gp">... </span> <span class="k">print</span><span class="p">(</span><span class="n">lista</span><span class="p">)</span>
<span class="gp">...</span>
<span class="go">['A', 'K', 'Q']</span>
<span class="go">['K', 'Q', 'A']</span>
<span class="go">['A', 'K', 'Q']</span>
<span class="go">['Q', 'A', 'K']</span>
<span class="go">['K', 'Q', 'A']</span>
<span class="go">['Q', 'K', 'A']</span>
<span class="go">['A', 'K', 'Q']</span>
<span class="go">['Q', 'K', 'A']</span>
<span class="go">['K', 'Q', 'A']</span>
<span class="go">['K', 'A', 'Q']</span>
<span class="go">['Q', 'K', 'A']</span>
<span class="go">['Q', 'A', 'K']</span>
<span class="go">>>></span>
</pre></div>
<p>los resultados en pantalla deberían irse repartiendo a partes iguales entre las
distintas permutaciones. Del mismo modo, si ejecutáramos este algoritmo
<em>600.000</em> veces sobre la misma permutación, los resultados deberían repartirse
de esta forma:</p>
<p style="text-align:center;"><img src="pictures/ideal_shuffle.png" width="373"
height="248" alt="ideal_shuffle.png" /></p>
<p>Donde las apariciones de cada permutación posible son idénticas, es decir,
<em>600.000/6 = 100.000</em>. Pero veremos que en la realidad ocurre algo distinto,
como ya podíamos adivinar al ver los resultados del código anterior. Veamos con
detalle que es lo que ocurre con el algoritmo <code>shuffle</code> para ese mismo número
de ejecuciones.</p>
<p style="text-align:center;"><img src="pictures/realvsideal_radar.png"
width="680" height="248" alt="realvsideal_radar.png" /></p>
<p>Como podemos ver, el comportamiento real de un algoritmo shuffle no se ajusta
al comportamiento ideal. Unas combinaciones están sobre-representadas como QAK
y KQA y el resto sub-representadas, siendo la combinación AQK la más cercana a
los valores ideales. Pero estos valores no debemos verlos como una tendencia,
puesto que si volvemos a ejecutar otras <em>100.000</em> operaciones de barajado, aunque
seguirá sin ser un reparto equitativo, si es imparcial en cuanto que no se
repiten las mismas desviaciones por permutación. Es decir, unas veces unas serán
unas permutaciones las más sobre-representadas (y viceversa) y otras lo serán
otras distintas, no hay una tendencia clara. Las variaciones en este caso oscilan
entre <em>99.678 (-322)</em> y <em>100.446 (+446)</em> apariciones sobre un valor ideal de
<em>100.000</em> con una <a href="http://es.wikipedia.org/wiki/Desviaci%C3%B3n_media">desviación media</a> de <em>±0.039%</em>, unos valores no ideales
pero si muy razonables y más que suficientes para la mayoría de necesidades.</p>
<p>Es importante volver a incidir en que un algoritmo de barajado bien diseñado
siempre ha de ser imparcial, de hecho <code>random.shuffle</code> lo es, si no será erróneo
y nos creara tendencias que pueden pasar inadvertidas a no ser que se comprueben
correctamente (en este caso de forma estadística). Tendencias que pueden suponer
un verdadero problema y echar al traste todos los cálculos y procesos que empleen
este algoritmo como base. El emplear un algoritmo de barajado erróneo le ha
causado <a href="http://www.cigital.com/news/index.php?pg=art&artid=20">más de un</a> <a href="http://www.cigital.com/papers/download/developer_gambling.php">disgusto a algún casino online</a>, ya que las
tendencias permiten predecir el resultado de el barajado y usarlo para hacer
trampas en el juego.</p>
<p>Pero si <code>random.suffle</code> es imparcial, ¿por qué no se reparten los resultados de
manera uniforme? El problema es aquí entra en juego un segundo actor, que
independientemente de lo bien diseñado que este nuestro algoritmo shuffle, nos
condicionará que el reparto sea uniforme o no. Este segundo actor es la
<a href="http://es.wikipedia.org/wiki/Aleatoriedad">aleatoriedad</a>, lo que precisamente intentamos conseguir. Para que nuestro
algoritmo funcione necesitamos que se elijan las cartas al azar para ir
desordenándolas también al azar. Y es aquí donde está el problema, el que
impide que <code>random.shuffle</code> tenga un comportamiento ideal, que los ordenadores y
la aleatoriedad, no son buenos "amigos". No existe algo así como un generador de
números aleatorios perfecto basado en software (si los hay casi perfectos
basados en hardware), debido a que son deterministas al estar basados en formulas
matemáticas. Por lo que empleamos lo que se denomina <a href="http://es.wikipedia.org/wiki/Generador_de_n%C3%BAmeros_pseudoaleatorios">generadores de números
pseudoaleatorios</a>. Estos generadores pueden ser más o menos "perfectos", pero
al no generar verdadera aleatoriedad, nos impiden que nuestro algoritmo consiga
efectuar un reparto uniforme. Matemáticamente se ha demostrado que algunos
algoritmos, como el que emplea <code>ramdon.shuffle</code> (luego aclararemos cual es)
tienen un funcionamiento ideal, y de hecho podemos acercarnos muchísimo al
modelo ideal cuando empleamos un <a href="http://en.wikipedia.org/wiki/Hardware_random_number_generator">generador de aleatoriedad por hardware</a>
(que se acerca mucho a la verdadera aleatoriedad). Al final, aunque sean
matemáticamente correctos, tu algoritmo de barajado será tan bueno como lo sea
tu generador de números aleatorios.</p>
<p>Python, en su modulo <code>random</code>, emplea por defecto uno de los mejores generadores
de números pseudoaleatorios existentes actualmente, el <a href="http://en.wikipedia.org/wiki/Mersenne_twister">Mersenne twister</a>
(hasta la versión 2.4 se empleaba el <a href="http://www2.imperial.ac.uk/~hohs/S9/2007-2008/wichmannhill.pdf">Wichmann-Hill</a>, que sigue disponible
como clase) y está implementado en C. Los resultados entregados por él son muy
fiables (excepto para su empleo en <a href="http://es.wikipedia.org/wiki/Criptograf%C3%ADa">criptografía</a>), generando números float
con una precisión de 53 bits y con un elevado periodo de 2¹⁹⁹³⁷-1. Otra buena
opción que nos ofrece el modulo <code>random</code> para entornos Unix/Linux es la clase
<code>random.SystemRandom()</code> que nos ofrece la entropía generada por <code>os.random()</code>
que a su vez la toma de la generada en <code>/dev/random</code>. Esta es una fuente
bastante decente de aleatoriedad que en las pruebas me ha ofrecido resultados
similares a el generador por defecto en Python.</p>
<h2 id="los_algoritmos">Los algoritmos</h2>
<p>Utilizar la función <code>random.shuffle()</code> es la forma más cómoda de barajar una
lista en Python, y desde luego la más recomendable. Pero si quisiéramos
desarrollar nuestro propio algoritmo, esta es una de esas ocasiones en las que o
tienes unos solidos conocimientos matemáticos (o estadísticos) o mejor no
intentes reinventar la rueda, recurre a lo conocido y solido, y como mínimo
prueba siempre que los resultados que obtienes son lo que tu esperas. Esto no es
una afirmación gratuita, esta es una de las ocasiones en la que la diferencia es
tan sutil, que es facilísimo equivocarse sin siquiera darse cuenta. Veamos porqué.</p>
<h3 id="la_intuici+n_a_veces_nos_enga+a_no_seas_ingenuo">La intuición a veces nos engaña, no seas ingenuo!</h3>
<p>Aparentemente barajar una lista es de lo más sencillo, después de razonar un
poco, podríamos acabar con un código parecido a este:</p>
<div class="codehilite"><pre><span class="k">def</span> <span class="nf">faulty</span><span class="p">(</span><span class="n">lst</span><span class="p">):</span>
<span class="s">"An example of a intuitive but very bad algorithm."</span>
<span class="n">lst_length</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">lst</span><span class="p">)</span>
<span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">lst_length</span><span class="p">):</span>
<span class="n">j</span> <span class="o">=</span> <span class="n">random</span><span class="o">.</span><span class="n">randrange</span><span class="p">(</span><span class="n">lst_length</span><span class="p">)</span>
<span class="n">lst</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="n">lst</span><span class="p">[</span><span class="n">j</span><span class="p">]</span> <span class="o">=</span> <span class="n">lst</span><span class="p">[</span><span class="n">j</span><span class="p">],</span> <span class="n">lst</span><span class="p">[</span><span class="n">i</span><span class="p">]</span>
</pre></div>
<p>Que aparentemente debería ser correcto, algo que han pensado muchos
programadores ingenuamente hasta caer en la cuenta "a la fuerza" de que no es
correcto. Es más, si por ejemplo nos limitáramos a ejecutarlo unas <em>360</em> veces
para probarlo (las cuales aparentemente no son pocas), veríamos que tiene un
comportamiento muy parecido al que nos da la función <code>random.shuffle</code>:</p>
<p style="text-align:center;"><img src="pictures/shuffle_faulty.png" width="467"
height="350" alt="shuffle_faulty.png" /></p>
<p>Como podemos ver las desviaciones del valor ideal son muy similares y con una
desviación media muy parecida, <em>±2.130%</em> para <code>shuffle</code> y <em>±2.222%</em> para nuestro
algoritmo, <code>faulty</code>. Y creeríamos erróneamente que nuestro algoritmo funcionaría
correctamente. Pero es que para comprobar que no haya tendencia alguna es
necesario ejecutarlo, muchas, pero muchas veces (por encima de los cientos de
miles), ya que a mayor número de repeticiones, más se manifiestan esas
tendencias. Al mismo tiempo, cuantos más elementos tenga la lista, mayores son
las desviaciones y la tendencia. Además, con este número de ejecuciones aún no
observamos las tendencias reales que se manifestarán luego en lo sucesivo.
Veamos unos ejemplos para <em>2.400.000</em> ejecuciones para nuestra lista de tres
cartas y para otra a la que hemos añadido la Sota (Jack).</p>
<p style="text-align:center;"><a href="pictures/shuffle_faulty_2M4.png"><img
src="pictures/shuffle_faulty_2m4_small.png" width="700" height="245"
alt="shuffle_faulty_2m4_small.png" title="Pulsar para ver a tamaño completo"/>
</a></p>
<p>En el gráfico de la izquierda ya se muestran claramente las diferencias, mientras
la función <code>shuffle</code> varía entre <em>-741</em> y <em>+668</em> (sobre <em>400.000</em>) con una
desviación media de <em>±0.017%</em>, nuestra función tiene una desviación media de
<em>±1.869%</em> (<em>~110</em> veces superior) y varia entre <em>-46.062</em> y <em>+46.090</em>. Como
podemos ver es una autentica barbaridad. El gráfico de la derecha, nos confirma
que a mayor número de elementos en la lista, mayores aún son las tendencias.
Mientras que <code>shuffle</code> se sigue comportando correctamente con una desviación
media de <em>±0.009%</em> y varía entre <em>-486</em> y <em>+427</em> (sobre <em>100.000</em>), nuestro
pobre algoritmo se desmanda completamente, variando entre <em>+31.326</em> y <em>-25.254</em>,
una desviación media de <em>±0.541%</em> (<em>~61</em> veces más). Hay que tener en cuenta,
que al ser mayor número de cartas, son <em>24</em> permutaciones, lo que nos da unas
<em>4</em> veces menos repeticiones por permutación, es decir, que para tener el mismo
número tendríamos que hacer <em>9.600.00</em> ejecuciones. Y como podemos ver, los
resultados no son cuatro veces mejores, ni mucho menos, además sabemos que la
tendencia se acentúa con el número de ejecuciones. Viendo estos resultados vemos
claramente que determinadas permutaciones tienen muy pocas probabilidades de
salir, cuando otras son altamente predecibles.</p>
<p>¿Entiendes ahora porqué no debes fiarte únicamente de tu instinto?</p>
<p>Vamos a conocer ahora los algoritmos que funcionan, están matemáticamente
comprobados y son sobradamente conocidos (aunque a la vista está que no lo
suficiente).</p>
<h3 id="fisher-yates">Fisher-Yates</h3>
<p>Este algoritmo fue desarrollado por <a href="http://en.wikipedia.org/wiki/Ronald_Fisher">Ronald Fisher</a> y <a href="http://en.wikipedia.org/wiki/Frank_Yates">Frank Yates</a> en
1938<sup id="fnref:FYa"><a href="#fn:FYa" rel="footnote">2</a></sup> como un método a realizar con lápiz y papel. Para ello empleaban una
tabla de números aleatorios calculada anteriormente para generar la aleatoriedad
que necesitaban para el algoritmo. Este algoritmo esta comprobado
matemáticamente y funciona correctamente siempre que la fuente de aleatoriedad
sea completamente aleatoria. No deja de ser curioso, que un algoritmo que fue
creado hace más de 70 años y que sigue actualmente vigente y presente en
multitud de aplicaciones, sea al mismo tiempo desconocido por muchos
desarrolladores, lo que les lleva a cometer los errores que comentaba
anteriormente al implementar los suyos propios. Por eso como programadores
deberíamos ser humildes y procurar no inventar siempre lo ya inventado y
buscar siempre una solución previa para nuestros propósitos. Y probar, probar y
volver a probar nuestro código...</p>
<p>La típica implementación del algoritmo en Python sería así:</p>
<div class="codehilite"><pre><span class="k">def</span> <span class="nf">fisher_yates</span><span class="p">(</span><span class="n">lst</span><span class="p">):</span>
<span class="s">"Python implementation of the original Fisher-Yates algorithm."</span>
<span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">lst</span><span class="p">)</span> <span class="o">></span> <span class="mi">1</span><span class="p">:</span>
<span class="n">idx</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">lst</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span>
<span class="k">while</span> <span class="n">idx</span> <span class="o">></span> <span class="mi">0</span><span class="p">:</span>
<span class="n">sel</span> <span class="o">=</span> <span class="n">random</span><span class="o">.</span><span class="n">randint</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">idx</span><span class="p">)</span>
<span class="n">lst</span><span class="p">[</span><span class="n">idx</span><span class="p">],</span> <span class="n">lst</span><span class="p">[</span><span class="n">sel</span><span class="p">]</span> <span class="o">=</span> <span class="n">lst</span><span class="p">[</span><span class="n">sel</span><span class="p">],</span> <span class="n">lst</span><span class="p">[</span><span class="n">idx</span><span class="p">]</span>
<span class="n">idx</span> <span class="o">-=</span> <span class="mi">1</span>
</pre></div>
<h3 id="knuth-durstenfeld">Knuth-Durstenfeld</h3>
<p>Este algoritmo es en realidad una variante del Fisher-Yates y fue publicado
originalmente por Richard Durstenfeld en 1964<sup id="fnref:RDa"><a href="#fn:RDa" rel="footnote">3</a></sup>, pero fue popularizado por
el genial <a href="http://en.wikipedia.org/wiki/Donald_E._Knuth">Donald Knuth</a> en el volumen 2 de su gran obra,
<a href="http://en.wikipedia.org/wiki/The_Art_of_Computer_Programming">The Art of Computer Programming</a>, como el <strong>algoritmo P</strong><sup id="fnref:DKa"><a href="#fn:DKa" rel="footnote">4</a></sup>. De ahí
que sea generalmente conocido como Knuth-Durstenfeld o simplemente como Knuth
Shuffle. Aunque curiosamente ninguno de los dos conocía anteriormente el trabajo
previo de Fisher y Yates. Este algoritmo se ejecuta en tiempo polinómico lineal
<em>O(n)</em>, por lo que es muy eficiente.</p>
<p>Implementado en Python, sería algo así:</p>
<div class="codehilite"><pre><span class="k">def</span> <span class="nf">knuth_durstenfeld</span><span class="p">(</span><span class="n">lst</span><span class="p">):</span>
<span class="s">"Python implementation of the Durstenfeld algorithm popularized by Knuth."</span>
<span class="k">for</span> <span class="n">idx</span> <span class="ow">in</span> <span class="nb">reversed</span><span class="p">(</span><span class="nb">range</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">lst</span><span class="p">))):</span>
<span class="c"># pick an element in lst[:idx+1] with which to exchange lst[idx]</span>
<span class="n">sel</span> <span class="o">=</span> <span class="n">random</span><span class="o">.</span><span class="n">randrange</span><span class="p">(</span><span class="n">idx</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span>
<span class="n">lst</span><span class="p">[</span><span class="n">idx</span><span class="p">],</span> <span class="n">lst</span><span class="p">[</span><span class="n">sel</span><span class="p">]</span> <span class="o">=</span> <span class="n">lst</span><span class="p">[</span><span class="n">sel</span><span class="p">],</span> <span class="n">lst</span><span class="p">[</span><span class="n">idx</span><span class="p">]</span>
</pre></div>
<p>Este algoritmo es el que está detrás de la función <code>random.shuffle</code> que hemos
venido usando desde el principio del articulo. Lógicamente si está empleando el
mejor algoritmo hasta la fecha, es absurdo implementar el nuestro propio cuando
podemos usar esta función estándar de Python. De hecho el código de esta función
es el siguiente:</p>
<div class="codehilite"><pre><span class="k">def</span> <span class="nf">shuffle</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">random</span><span class="o">=</span><span class="bp">None</span><span class="p">,</span> <span class="nb">int</span><span class="o">=</span><span class="nb">int</span><span class="p">):</span>
<span class="sd">"""x, random=random.random -> shuffle list x in place; return None.</span>
<span class="sd"> Optional arg random is a 0-argument function returning a random</span>
<span class="sd"> float in [0.0, 1.0); by default, the standard random.random.</span>
<span class="sd"> """</span>
<span class="k">if</span> <span class="n">random</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
<span class="n">random</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">random</span>
<span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">reversed</span><span class="p">(</span><span class="nb">xrange</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">x</span><span class="p">))):</span>
<span class="c"># pick an element in x[:i+1] with which to exchange x[i]</span>
<span class="n">j</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">random</span><span class="p">()</span> <span class="o">*</span> <span class="p">(</span><span class="n">i</span><span class="o">+</span><span class="mi">1</span><span class="p">))</span>
<span class="n">x</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="n">x</span><span class="p">[</span><span class="n">j</span><span class="p">]</span> <span class="o">=</span> <span class="n">x</span><span class="p">[</span><span class="n">j</span><span class="p">],</span> <span class="n">x</span><span class="p">[</span><span class="n">i</span><span class="p">]</span>
</pre></div>
<p>Que como podemos ver, el algoritmo es el Knuth Shuffle.</p>
<h3 id="a_tener_en_cuenta">A tener en cuenta</h3>
<p>Conviene tener en cuenta una cosa, todos estos algoritmos modifican la propia
lista <em>in-place</em>, es decir que no crean una nueva, y por lo tanto no devuelven
ningún valor. Por lo tanto, no puedes asignar el resultado de una de estas
operaciones a una variable, porque por defecto devuelven <code>None</code>. Es un error
muy común, de hecho <a href="http://learning-python.com/formalbio.html">Mark Lutz</a> lo define en su libro "Learning Python"
como uno de los <em>Common Coding Gotchas</em>, es decir, uno de los errores más
comunes en Python. Y dice claramente (pág. 388, cap. 15)</p>
<blockquote>
<p>Don’t expect results from functions that change objects in-place</p>
</blockquote>
<p>No esperes resultados desde funciones que cambien objetos en el sitio (sin crear
nuevos objetos modificados). Y pone como ejemplos los métodos <code>append</code>, <code>sort</code>
y <code>reverse</code> en listas.</p>
<p>Para verlo aún más claro, nada mejor que verlo con un ejemplo:</p>
<div class="codehilite"><pre><span class="gp">>>> </span><span class="kn">from</span> <span class="nn">random</span> <span class="kn">import</span> <span class="n">shuffle</span>
<span class="gp">>>> </span><span class="n">lista</span> <span class="o">=</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">3</span><span class="p">]</span>
<span class="gp">>>> </span><span class="nb">id</span><span class="p">(</span><span class="n">lista</span><span class="p">)</span> <span class="c"># Obtenemos el id del objeto (todo en Python es un objeto)</span>
<span class="go">3077959916L</span>
<span class="gp">>>> </span><span class="n">shuffle</span><span class="p">(</span><span class="n">lista</span><span class="p">)</span>
<span class="gp">>>> </span><span class="n">lista</span> <span class="c"># La lista ya aparece barajada</span>
<span class="go">[2, 3, 1]</span>
<span class="gp">>>> </span><span class="nb">id</span><span class="p">(</span><span class="n">lista</span><span class="p">)</span> <span class="c"># Sin embargo vemos que el identificador es el mismo</span>
<span class="go">3077959916L</span>
<span class="gp">>>> </span><span class="n">variable</span> <span class="o">=</span> <span class="n">shuffle</span><span class="p">(</span><span class="n">lista</span><span class="p">)</span> <span class="c"># Esto no tiene sentido, porque no se devuelve nada</span>
<span class="gp">>>> </span><span class="n">variable</span>
<span class="gp">>>> </span><span class="k">print</span><span class="p">(</span><span class="n">shuffle</span><span class="p">(</span><span class="n">lista</span><span class="p">))</span> <span class="c"># Aquí vemos que la función devuelve None</span>
<span class="go">None</span>
<span class="gp">>>> </span><span class="nb">id</span><span class="p">(</span><span class="n">lista</span><span class="p">)</span>
<span class="go">3077959916L</span>
<span class="gp">>>> </span><span class="n">lista</span>
<span class="go">[1, 3, 2]</span>
<span class="go">>>></span>
</pre></div>
<p>En esta gráfica vemos los tres algoritmos comparados, los he ejecutado
<em>solamente 48.000</em> veces, para apreciar mejor las diferencias entre ellos.</p>
<p style="text-align:center;"><img src="pictures/shuffle_algorithms.png"
width="600" height="500" alt="suffle_algorithms.png" /></p>
<p>Las desviaciones medias son de <em>±0.068%</em> para Fisher-Yates, <em>±0.069%</em> para
Knuth-Durstenfeld y de ±0.554% para Faulty.</p>
<h3 id="sattolo-cycle">Sattolo-Cycle</h3>
<p>Este algoritmo es muy parecido a los anteriores, pero con una significativa
diferencia, este algoritmo solo genera ciclos, de ahí su nombre. Es decir,
reparte uniformemente los resultados solo entre algunas permutaciones que se van
rotando (aunque comparte el mismo problema con los anteriores al depender de una
aleatoriedad no perfecta). Para resumir su funcionamiento, lo que hace es que
después de ejecutarlo, ningún elemento de la lista repite la posición anterior
que tenía en la misma. Ese algoritmo fue publicado por Sandra Sattolo en 1986</p>
<p>El algoritmo en Python:</p>
<div class="codehilite"><pre><span class="k">def</span> <span class="nf">sattolo_cycle</span><span class="p">(</span><span class="n">lst</span><span class="p">):</span>
<span class="s">"Python implementation of the original Sattolo Cycle algorithm."</span>
<span class="n">idx</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">lst</span><span class="p">)</span>
<span class="k">while</span> <span class="n">idx</span> <span class="o">></span> <span class="mi">1</span><span class="p">:</span>
<span class="n">idx</span> <span class="o">=</span> <span class="n">idx</span> <span class="o">-</span> <span class="mi">1</span>
<span class="n">sel</span> <span class="o">=</span> <span class="n">random</span><span class="o">.</span><span class="n">randrange</span><span class="p">(</span><span class="n">idx</span><span class="p">)</span> <span class="c"># 0 <= sel <= idx-1</span>
<span class="n">lst</span><span class="p">[</span><span class="n">sel</span><span class="p">],</span> <span class="n">lst</span><span class="p">[</span><span class="n">idx</span><span class="p">]</span> <span class="o">=</span> <span class="n">lst</span><span class="p">[</span><span class="n">idx</span><span class="p">],</span> <span class="n">lst</span><span class="p">[</span><span class="n">sel</span><span class="p">]</span>
<span class="k">return</span>
</pre></div>
<p>Podemos ver aquí un ejemplo de su funcionamiento al ejecutarlo sucesivamente
sobre la misma lista, sin partir siempre de la misma permutación, para comprobar
como ningún elemento conserva su posición anterior.</p>
<div class="codehilite"><pre><span class="go">lista = ['A', 'K', 'Q']</span>
<span class="gp">>>> </span><span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">12</span><span class="p">):</span>
<span class="gp">... </span> <span class="n">sattolo_cycle</span><span class="p">(</span><span class="n">lista</span><span class="p">)</span>
<span class="gp">... </span> <span class="k">print</span><span class="p">(</span><span class="n">lista</span><span class="p">)</span>
<span class="gp">...</span>
<span class="go">['K', 'Q', 'A']</span>
<span class="go">['A', 'K', 'Q']</span>
<span class="go">['K', 'Q', 'A']</span>
<span class="go">['A', 'K', 'Q']</span>
<span class="go">['Q', 'A', 'K']</span>
<span class="go">['A', 'K', 'Q']</span>
<span class="go">['K', 'Q', 'A']</span>
<span class="go">['Q', 'A', 'K']</span>
<span class="go">['K', 'Q', 'A']</span>
<span class="go">['Q', 'A', 'K']</span>
<span class="go">['K', 'Q', 'A']</span>
<span class="go">['Q', 'A', 'K']</span>
<span class="go">>>></span>
</pre></div>
<p>El código de estos algoritmos y los test de rendimiento se encuentran en el
fichero <code>shuffle.py</code> de mi repositorio <em>Python Recipes</em> que se encuentra en
<a href="http://github.com/joedicastro/python-recipes">github</a><br />
</p>
<p>Aunque estos algoritmos no tienen un funcionamiento perfecto, debido al
generador de números pseudoaleatorios, son validos para la mayoría de las
aplicaciones para las que son necesarios, digamos que son lo suficientemente
buenos para las aplicaciones prácticas de los mismos.</p>
<h4 id="otras_fuentes_para_saber_m+s">Otras fuentes para saber más</h4>
<p><a href="http://www.codinghorror.com/blog/2007/12/the-danger-of-naivete.html">The Danger of Naïveté</a>, Jeff Atwood<br />
<a href="http://www.codinghorror.com/blog/2007/12/shuffling.html">Shuffling</a>, Jeff Atwood<br />
<a href="http://www.codinghorror.com/blog/2006/11/computers-are-lousy-random-number-generators.html">Computers are lousy random numbers generators</a>, Jeff Atwood<br />
<a href="http://eli.thegreenplace.net/2010/05/28/the-intuition-behind-fisher-yates-shuffling/">The intuition behind Fisher-Yates shuffling</a>, Eli Bendersky<br />
<a href="http://www.random.org/">Ramdom.org</a></p>
<div class="footnote">
<hr />
<ol>
<li id="fn:diseño">
<p>Diseño de las cartas por <a href="http://svg-cards.sourceforge.net/">David Bellot</a>.
 <a href="#fnref:diseño" rev="footnote" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn:FYa">
<p>Fisher R., Yates F. [1938] (1943) <em>Statistical Tables for Biological,
Agricultural and Medical Research</em>, 2nd edition, London, Oliver and
Boyd, p. 23-24 <a href="#fnref:FYa" rev="footnote" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
<li id="fn:RDa">
<p>Durstenfeld R.[1964] <em>Communications of the ACM</em>, vol. 7, issue 7(July)
 <a href="#fnref:RDa" rev="footnote" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
<li id="fn:DKa">
<p>Knuth D.[1969] (1981) <em>The Art of Computer Programming</em>, 2nd edition,
Addison-Wesley, pp. 139-140 <a href="#fnref:DKa" rev="footnote" title="Jump back to footnote 4 in the text">↩</a></p>
</li>
</ol>
</div>