joe di castrohttp://joedicastro.comThu, 09 Jun 2011 23:35:00 +0200Algoritmos Shufflehttp://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 &amp; 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">&gt;&gt;&gt; </span><span class="n">lista</span> <span class="o">=</span> <span class="p">[</span><span class="s">&#39;A&#39;</span><span class="p">,</span> <span class="s">&#39;K&#39;</span><span class="p">,</span> <span class="s">&#39;Q&#39;</span><span class="p">]</span> <span class="gp">&gt;&gt;&gt; </span><span class="kn">import</span> <span class="nn">itertools</span> <span class="gp">&gt;&gt;&gt; </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">(&#39;A&#39;, &#39;K&#39;, &#39;Q&#39;)</span> <span class="go">(&#39;A&#39;, &#39;Q&#39;, &#39;K&#39;)</span> <span class="go">(&#39;K&#39;, &#39;A&#39;, &#39;Q&#39;)</span> <span class="go">(&#39;K&#39;, &#39;Q&#39;, &#39;A&#39;)</span> <span class="go">(&#39;Q&#39;, &#39;A&#39;, &#39;K&#39;)</span> <span class="go">(&#39;Q&#39;, &#39;K&#39;, &#39;A&#39;)</span> <span class="go">&gt;&gt;&gt;</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">&gt;&gt;&gt; </span><span class="kn">from</span> <span class="nn">random</span> <span class="kn">import</span> <span class="n">shuffle</span> <span class="gp">&gt;&gt;&gt; </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">&#39;A&#39;</span><span class="p">,</span> <span class="s">&#39;K&#39;</span><span class="p">,</span> <span class="s">&#39;Q&#39;</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">[&#39;A&#39;, &#39;K&#39;, &#39;Q&#39;]</span> <span class="go">[&#39;K&#39;, &#39;Q&#39;, &#39;A&#39;]</span> <span class="go">[&#39;A&#39;, &#39;K&#39;, &#39;Q&#39;]</span> <span class="go">[&#39;Q&#39;, &#39;A&#39;, &#39;K&#39;]</span> <span class="go">[&#39;K&#39;, &#39;Q&#39;, &#39;A&#39;]</span> <span class="go">[&#39;Q&#39;, &#39;K&#39;, &#39;A&#39;]</span> <span class="go">[&#39;A&#39;, &#39;K&#39;, &#39;Q&#39;]</span> <span class="go">[&#39;Q&#39;, &#39;K&#39;, &#39;A&#39;]</span> <span class="go">[&#39;K&#39;, &#39;Q&#39;, &#39;A&#39;]</span> <span class="go">[&#39;K&#39;, &#39;A&#39;, &#39;Q&#39;]</span> <span class="go">[&#39;Q&#39;, &#39;K&#39;, &#39;A&#39;]</span> <span class="go">[&#39;Q&#39;, &#39;A&#39;, &#39;K&#39;]</span> <span class="go">&gt;&gt;&gt;</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&amp;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">&quot;An example of a intuitive but very bad algorithm.&quot;</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">&quot;Python implementation of the original Fisher-Yates algorithm.&quot;</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">&gt;</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">&gt;</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">&quot;Python implementation of the Durstenfeld algorithm popularized by Knuth.&quot;</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">&quot;&quot;&quot;x, random=random.random -&gt; 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"> &quot;&quot;&quot;</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">&gt;&gt;&gt; </span><span class="kn">from</span> <span class="nn">random</span> <span class="kn">import</span> <span class="n">shuffle</span> <span class="gp">&gt;&gt;&gt; </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">&gt;&gt;&gt; </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">&gt;&gt;&gt; </span><span class="n">shuffle</span><span class="p">(</span><span class="n">lista</span><span class="p">)</span> <span class="gp">&gt;&gt;&gt; </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">&gt;&gt;&gt; </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">&gt;&gt;&gt; </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">&gt;&gt;&gt; </span><span class="n">variable</span> <span class="gp">&gt;&gt;&gt; </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">&gt;&gt;&gt; </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">&gt;&gt;&gt; </span><span class="n">lista</span> <span class="go">[1, 3, 2]</span> <span class="go">&gt;&gt;&gt;</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">&quot;Python implementation of the original Sattolo Cycle algorithm.&quot;</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">&gt;</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 &lt;= sel &lt;= 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 = [&#39;A&#39;, &#39;K&#39;, &#39;Q&#39;]</span> <span class="gp">&gt;&gt;&gt; </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">[&#39;K&#39;, &#39;Q&#39;, &#39;A&#39;]</span> <span class="go">[&#39;A&#39;, &#39;K&#39;, &#39;Q&#39;]</span> <span class="go">[&#39;K&#39;, &#39;Q&#39;, &#39;A&#39;]</span> <span class="go">[&#39;A&#39;, &#39;K&#39;, &#39;Q&#39;]</span> <span class="go">[&#39;Q&#39;, &#39;A&#39;, &#39;K&#39;]</span> <span class="go">[&#39;A&#39;, &#39;K&#39;, &#39;Q&#39;]</span> <span class="go">[&#39;K&#39;, &#39;Q&#39;, &#39;A&#39;]</span> <span class="go">[&#39;Q&#39;, &#39;A&#39;, &#39;K&#39;]</span> <span class="go">[&#39;K&#39;, &#39;Q&#39;, &#39;A&#39;]</span> <span class="go">[&#39;Q&#39;, &#39;A&#39;, &#39;K&#39;]</span> <span class="go">[&#39;K&#39;, &#39;Q&#39;, &#39;A&#39;]</span> <span class="go">[&#39;Q&#39;, &#39;A&#39;, &#39;K&#39;]</span> <span class="go">&gt;&gt;&gt;</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>. &#160;<a href="#fnref:diseño" rev="footnote" title="Jump back to footnote 1 in the text">&#8617;</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&#160;<a href="#fnref:FYa" rev="footnote" title="Jump back to footnote 2 in the text">&#8617;</a></p> </li> <li id="fn:RDa"> <p>Durstenfeld R.[1964] <em>Communications of the ACM</em>, vol. 7, issue 7(July) &#160;<a href="#fnref:RDa" rev="footnote" title="Jump back to footnote 3 in the text">&#8617;</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&#160;<a href="#fnref:DKa" rev="footnote" title="Jump back to footnote 4 in the text">&#8617;</a></p> </li> </ol> </div>joe di castroThu, 09 Jun 2011 23:35:00 +0200http://joedicastro.com/algoritmos-shuffle.htmlpythonalgoritmosknuthfisher-yatesshufflebarajaraleatoriedadrandomsattolo