La mayoría de las veces, mientras escribo bucles, generalmente escribo condiciones de límite incorrectas (p. Ej., Resultado incorrecto) o mis suposiciones acerca de las terminaciones de bucle son incorrectas (p. Ej .: bucle infinitamente en ejecución). Aunque obtuve mis suposiciones correctas después de algunas pruebas y errores, me sentí demasiado frustrado por la falta de un modelo informático correcto en mi cabeza.
/**
* Inserts the given value in proper position in the sorted subarray i.e.
* array[0...rightIndex] is the sorted subarray, on inserting a new value
* our new sorted subarray becomes array[0...rightIndex+1].
* @param array The whole array whose initial elements [0...rightIndex] are
* sorted.
* @param rightIndex The index till which sub array is sorted.
* @param value The value to be inserted into sorted sub array.
*/
function insert(array, rightIndex, value) {
for(var j = rightIndex; j >= 0 && array[j] > value; j--) {
array[j + 1] = array[j];
}
array[j + 1] = value;
};
Los errores que hice inicialmente fueron:
- En lugar de j> = 0 lo guardé j> 0.
- Me confundí si array [j + 1] = valor o array [j] = valor.
¿Cuáles son las herramientas / modelos mentales para evitar tales errores?
programming-practices
loops
CodeYogi
fuente
fuente
j >= 0
es un error? Sería más cauteloso con el hecho de que está accediendoarray[j]
yarray[j + 1]
sin verificarlo primeroarray.length > (j + 1)
.Array.prototype
en el ejemplo de JS). Esto evita que encuentre condiciones de borde ya que algo comomap
funciona en todas las matrices. Puede resolver lo anterior usando slice y concat para evitar bucles: codepen.io/anon/pen/ZWovdg?editors=0012 La forma más correcta de escribir un bucle es no escribir ninguno.Respuestas:
Prueba
No, en serio, prueba.
Llevo más de 20 años codificando y todavía no confío en mí mismo para escribir un bucle correctamente la primera vez. Escribo y ejecuto pruebas que prueban que funciona antes de sospechar que funciona. Pruebe cada lado de cada condición límite. Por ejemplo, un
rightIndex
0 debería hacer qué? ¿Qué tal -1?Mantenlo simple
Si otros no pueden ver lo que hace de un vistazo, lo estás haciendo demasiado difícil. No dude en ignorar el rendimiento si eso significa que puede escribir algo fácil de entender. Solo hazlo más rápido en el improbable caso de que realmente necesites hacerlo. E incluso entonces solo una vez que esté absolutamente seguro de saber exactamente qué es lo que lo está frenando. Si puede lograr una mejora real de Big O , esta actividad puede no ser inútil, pero aun así, haga que su código sea lo más legible posible.
Fuera por uno
Conoce la diferencia entre contar tus dedos y contar los espacios entre tus dedos. A veces los espacios son lo que realmente es importante. No dejes que tus dedos te distraigan. Sepa si su pulgar es un dedo. Sepa si la brecha entre el meñique y el pulgar cuenta como un espacio.
Comentarios
Antes de perderse en el código, trate de decir lo que quiere decir en inglés. Expresa tus expectativas con claridad. No explique cómo funciona el código. Explica por qué haces que haga lo que hace. Mantenga los detalles de implementación fuera de él. Debería ser posible refactorizar el código sin necesidad de cambiar el comentario.
El mejor comentario es un buen nombre.
Si puede decir todo lo que necesita decir con un buen nombre, NO lo repita con un comentario.
Abstracciones
Los objetos, funciones, matrices y variables son abstracciones que son tan buenas como los nombres que se les dan. Dales nombres que aseguren que cuando las personas miran dentro de ellos no se sorprendan por lo que encuentren.
Nombres cortos
Use nombres cortos para cosas de corta duración.
i
es un buen nombre para un índice en un agradable ciclo cerrado en un pequeño alcance que hace que su significado sea obvio. Sii
vive lo suficiente como para extenderse línea por línea con otras ideas y nombres con los que se puede confundir,i
entonces es hora de dari
un buen nombre explicativo largo.Nombres largos
Nunca acorte un nombre simplemente debido a consideraciones de longitud de línea. Encuentre otra forma de diseñar su código.
Espacio en blanco
A los defectos les encanta esconderse en un código ilegible. Si su idioma le permite elegir su estilo de sangría, al menos sea coherente. No haga que su código se vea como una corriente de ruido. El código debería verse como si estuviera marchando en formación.
Construcciones de bucle
Aprenda y revise las estructuras de bucle en su idioma. Ver a un depurador resaltar un
for(;;)
bucle puede ser muy instructivo. Aprende todas las formas.while
,do while
,while(true)
,for each
. Use el más simple con el que pueda salirse con la suya. Mire hacia arriba cebando la bomba . Aprende québreak
ycontinue
haz si los tienes. Conoce la diferencia entrec++
y++c
. No tenga miedo de regresar temprano siempre y cuando cierre todo lo que necesita cerrar. Finalmente bloquea o preferiblemente algo que lo marca para el cierre automático cuando lo abre: Uso de la declaración / Probar con recursos .Alternativas de bucle
Deje que otra cosa haga el bucle si puede. Es más fácil a la vista y ya está depurado. Estos vienen en muchas formas: colecciones o arroyos que permitirá
map()
,reduce()
,foreach()
, y otros tales métodos que aplican una lambda. Busque funciones especiales comoArrays.fill()
. También hay recurrencia, pero solo se espera que eso facilite las cosas en casos especiales. En general, no use la recursividad hasta que vea cómo se vería la alternativa.Ah, y prueba.
Prueba, prueba, prueba.
¿Mencioné las pruebas?
Había una cosa más. No puedo recordar Comenzó con una T ...
fuente
Al programar es útil pensar en:
y cuando se explora un territorio inexplorado (como hacer malabares con los índices) puede ser muy, muy útil no solo pensar en ellos sino hacerlos explícitos en el código con afirmaciones .
Tomemos su código original:
Y mira lo que tenemos:
array[0..rightIndex]
se ordenaarray[0..rightIndex+1]
se ordena0 <= j <= rightIndex
pero parece un poco redundante; o como @Jules observó en los comentarios, al final de un "redondo",for n in [j, rightIndex+1] => array[j] > value
.array[0..rightIndex+1]
se ordenaEntonces, primero puede escribir una
is_sorted
función y unamin
función que funcione en un segmento de matriz, y luego afirmar:También existe el hecho de que su condición de bucle es un poco complicada; es posible que desee facilitarlo dividiendo las cosas:
Ahora, el bucle es sencillo (
j
va derightIndex
a0
).Finalmente, ahora esto necesita ser probado:
rightIndex == 0
,rightIndex == array.size - 2
)value
ser más pequeñoarray[0]
o más grande quearray[rightIndex]
value
que es igual aarray[0]
,array[rightIndex]
o algún índice medioAdemás, no subestimes el fuzzing . Tiene afirmaciones para detectar errores, así que genere una matriz aleatoria y ordénela utilizando su método. Si se activa una aserción, encontró un error y puede extender su conjunto de pruebas.
fuente
0 <= j <= rightIndex
oarray[j] <= value
simplemente repetiría el código. Por otro lado,is_sorted
trae una nueva garantía, por lo tanto, es valiosa. Después, para eso están las pruebas. Si llamainsert([0, 1, 2], 2, 3)
a su función y la salida no es,[0, 1, 2, 3]
entonces ha encontrado un error.array[j+1] = array[j]; assert(array[j+1] == array[j]);
. En este caso, el valor parece muy bajo (es copiar / pegar). Si duplica el significado pero lo expresa de otra manera, se vuelve más valioso.insert()
para que funcionara copiando de una matriz anterior a una nueva matriz. Eso se puede hacer sin romper el de los demásassert
. Pero no este último. Solo muestra qué tan bienassert
fueron diseñados los demás .Usar pruebas unitarias / TDD
Si realmente necesita acceder a las secuencias a través de un
for
bucle, puede evitar los errores a través de pruebas unitarias , y especialmente el desarrollo impulsado por pruebas .Imagine que necesita implementar un método que tome los valores que son superiores a cero, en orden inverso. ¿Qué casos de prueba se te ocurren?
Una secuencia contiene un valor que es superior a cero.
Real:
[5]
. Esperado:[5]
.La implementación más sencilla que satisface los requisitos consiste simplemente en devolver la secuencia fuente a la persona que llama.
Una secuencia contiene dos valores, ambos superiores a cero.
Real:
[5, 7]
. Esperado:[7, 5]
.Ahora, no puede simplemente devolver la secuencia, sino que debe revertirla. ¿Usaría un
for (;;)
bucle, otra construcción de lenguaje o un método de biblioteca no importa?Una secuencia contiene tres valores, uno es cero.
Real:
[5, 0, 7]
. Esperado:[7, 5]
.Ahora debe cambiar el código para filtrar los valores. Nuevamente, esto podría expresarse a través de una
if
declaración o una llamada a su método de marco favorito.Dependiendo de su algoritmo (dado que esta es una prueba de caja blanca, la implementación es importante), es posible que deba manejar específicamente el
[] → []
caso de secuencia vacía , o tal vez no. O puede asegurar que el caso extremo en que todos los valores son negativos[-4, 0, -5, 0] → []
se maneja correctamente, o incluso que los valores negativos de contorno son:[6, 4, -1] → [4, 6]; [-1, 6, 4] → [4, 6]
. Sin embargo, en muchos casos, solo tendrá las tres pruebas descritas anteriormente: cualquier prueba adicional no le hará cambiar su código, por lo que sería irrelevante.Trabaja a un nivel de abstracción más alto
Sin embargo, en muchos casos, puede evitar la mayoría de esos errores trabajando en un nivel de abstracción más alto, utilizando bibliotecas / marcos existentes. Esas bibliotecas / marcos permiten revertir, ordenar, dividir y unir las secuencias, insertar o eliminar valores en matrices o listas doblemente vinculadas, etc.
Por lo general,
foreach
se puede utilizar en lugar defor
hacer irrelevante la comprobación de las condiciones de contorno: el lenguaje lo hace por usted. Algunos lenguajes, como Python, ni siquiera tienen lafor (;;)
construcción, sino solofor ... in ...
.En C #, LINQ es particularmente conveniente cuando se trabaja con secuencias.
es mucho más legible y menos propenso a errores en comparación con su
for
variante:fuente
for(;;)
construcción no sería muy descriptiva de este bucle . Un aspecto importante es que LINQ (así como las comprensiones de listas en Python y elementos similares en otros lenguajes) hace que las condiciones de contorno sean irrelevantes, al menos dentro del alcance de la pregunta original. Sin embargo, no puedo estar más de acuerdo sobre la necesidad de comprender qué sucede debajo del capó cuando uso LINQ, especialmente cuando se trata de una evaluación perezosa.forEach
,map
,LazyIterator
, Etc son proporcionados por medio compilador o el tiempo de ejecución del lenguaje y podría decirse que son menos propensos a ser caminar de vuelta al cubo de pintura en cada iteración. Eso, la legibilidad y los errores fuera de uno son las dos razones por las que esas características se agregaron a los idiomas modernos.Estoy de acuerdo con otras personas que dicen probar su código. Sin embargo, también es bueno hacerlo bien en primer lugar. Tengo la tendencia a equivocarme en las condiciones de contorno en muchos casos, así que he desarrollado trucos mentales para prevenir tales problemas.
Con una matriz indexada en 0, sus condiciones normales serán:
o
Esos patrones deberían convertirse en una segunda naturaleza, no debería tener que pensar en ellos en absoluto.
Pero no todo sigue ese patrón exacto. Entonces, si no está seguro de si lo escribió correctamente, este es su próximo paso:
Conecte los valores y evalúe el código en su propio cerebro. Haz que sea tan simple de pensar como puedas. ¿Qué sucede si los valores relevantes son 0s? ¿Qué pasa si son 1s?
En su ejemplo, no está seguro de si debería ser [j] = valor o [j + 1] = valor. Hora de comenzar a evaluarlo manualmente:
¿Qué sucede si tienes una longitud de matriz 0? La respuesta se vuelve obvia: rightIndex debe ser (longitud - 1) == -1, por lo que j comienza en -1, por lo que para insertar en el índice 0, debe agregar 1.
Así que hemos demostrado que la condición final es correcta, pero no el interior del bucle.
¿Qué sucede si tiene una matriz con 1 elemento, 10, e intentamos insertar un 5? Con un solo elemento, rightIndex debería comenzar en 0. Entonces, la primera vez a través del ciclo, j = 0, entonces "0> = 0 && 10> 5". Como deseamos insertar el 5 en el índice 0, el 10 debería moverse al índice 1, entonces array [1] = array [0]. Como esto sucede cuando j es 0, matriz [j + 1] = matriz [j + 0].
Si intenta imaginar una gran variedad y lo que sucede al insertarse en una ubicación arbitraria, su cerebro probablemente se sentirá abrumado. Pero si se atiene a ejemplos simples de tamaño 0/1/2, debería ser fácil hacer un recorrido mental rápido y ver dónde se rompen sus condiciones límite.
Imagine que nunca antes había escuchado sobre el problema de las cercas y le digo que tengo 100 postes en una línea recta, cuántos segmentos hay entre ellos. Si intentas imaginar 100 postes de cerca en tu cabeza, te vas a abrumar. Entonces, ¿cuál es la menor cantidad de publicaciones de cerca para hacer una cerca válida? Necesitas 2 para hacer una cerca, así que imagina 2 publicaciones, y la imagen mental de un solo segmento entre las publicaciones lo deja muy claro. No tiene que sentarse allí contando publicaciones y segmentos porque ha convertido el problema en algo intuitivamente obvio para su cerebro.
Una vez que crees que es correcto, entonces es bueno ejecutarlo a través de una prueba y asegurarte de que la computadora haga lo que crees que debería, pero en ese momento debería ser solo una formalidad.
fuente
for (int i = 0; i < length; i++)
. Una vez que adquirí ese hábito, dejé de usarlo<=
casi con la misma frecuencia y sentí que los bucles se simplificaron. Perofor (int i = length - 1; i >= 0; i--)
parece demasiado complicado, en comparación con:for (int i=length; i--; )
(lo que probablemente sería aún más sensato escribir como unwhile
bucle a menos que estuviera tratando de haceri
que el alcance / vida fuera más pequeño). El resultado todavía se ejecuta a través del bucle con i == longitud-1 (inicialmente) hasta i == 0, con la única diferencia funcional de que lawhile()
versión termina con i == -1 después del bucle (si continúa), en lugar de i = = 0.> 0
" para obtener la funcionalidad deseada, entonces dichos caracteres deben usarse porque son requeridos por ese idioma. Aún así, incluso en esos casos, usar solo el "> 0
" es más simple que hacer el proceso de dos partes de restar primero uno y luego también usar ">= 0
". Una vez que aprendí eso a través de un poco de experiencia, adquirí el hábito de necesitar usar el signo igual (por ejemplo, ">= 0
") en mis condiciones de prueba de bucle con mucha menos frecuencia, y el código resultante se ha sentido más simple desde entonces.i-- > 0
, ¿por qué no pruebas el clásico chistei --> 0
?Es un punto muy interesante para esta pregunta y generó este comentario:
... y Thomas tiene razón. No tener una intención clara para una función debe ser una señal de alerta: una clara indicación de que debe PARAR inmediatamente, tomar un lápiz y papel, alejarse del IDE y resolver el problema correctamente; o, al menos, la cordura, verifique lo que ha hecho.
He visto tantas funciones y clases que se han convertido en un completo desastre porque los autores han intentado definir la implementación antes de haber definido completamente el problema. Y es tan fácil de tratar.
Si no comprende completamente el problema, tampoco es probable que esté codificando la solución óptima (ya sea en términos de eficiencia o claridad), ni podrá crear pruebas unitarias realmente útiles en una metodología TDD.
Tome su código aquí como ejemplo, contiene una serie de fallas potenciales que aún no ha considerado, por ejemplo:
Hay algunos otros problemas relacionados con el rendimiento y el diseño del código ...
SortedList<t>
en C #)?Array.Insert(...)
? sería este código más claro?Hay muchas formas en que este código podría mejorarse, pero hasta que haya definido correctamente lo que necesita hacer, no está desarrollando código, solo lo está pirateando con la esperanza de que funcione. Invierta el tiempo en ello y su vida será más fácil.
fuente
Los errores fuera de uno son uno de los errores de programación más comunes. Incluso los desarrolladores experimentados se equivocan a veces. Los lenguajes de nivel superior generalmente tienen construcciones de iteración como
foreach
omap
que evitan la indexación explícita por completo. Pero a veces necesita una indexación explícita, como en su ejemplo.El desafío es cómo pensar en rangos de celdas de matriz. Sin un modelo mental claro, se vuelve confuso cuándo incluir o excluir los puntos finales.
Al describir rangos de matriz, la convención es incluir el límite inferior, excluir el límite superior . Por ejemplo, el rango 0..3 es las celdas 0,1,2. Este convenciones se utilizan a lo largo idiomas 0 indexados, por ejemplo, el
slice(start, end)
método de JavaScript devuelve el subconjunto comenzando con el índicestart
hasta , pero sin incluir índiceend
.Es más claro cuando piensa en los índices de rango que describen los bordes entre las celdas de la matriz. La siguiente ilustración es una matriz de longitud 9, y los números debajo de las celdas están alineados con los bordes, y es lo que se usa para describir los segmentos de la matriz. Por ejemplo, está claro en la ilustración que el rango 2..5 son las celdas 2,3,4.
Este modelo es consistente con que la longitud de la matriz sea el límite superior de una matriz. Una matriz con longitud 5 tiene celdas 0..5, lo que significa que hay cinco celdas 0,1,2,3,4. Esto también significa que la longitud de un segmento es el límite superior menos el límite inferior, es decir, el segmento 2..5 tiene 5-2 = 3 celdas.
Tener en cuenta este modelo cuando se itera hacia arriba o hacia abajo hace que sea mucho más claro cuándo incluir o excluir los puntos finales. Al iterar hacia arriba, debe incluir el punto de inicio pero excluir el punto final. Al iterar hacia abajo, debe excluir el punto de inicio (el límite superior) pero incluir el punto final (el límite inferior).
Dado que está iterando hacia abajo en su código, debe incluir el límite inferior, 0, para iterar mientras
j >= 0
.Dado esto, su elección para que el
rightIndex
argumento represente el último índice en el subconjunto rompe la convención. Significa que debe incluir ambos puntos finales (0 y rightIndex) en la iteración. También hace que sea difícil representar el segmento vacío (que necesita cuando comienza la ordenación). En realidad, debe usar -1 como rightIndex al insertar el primer valor. Esto parece bastante antinatural. Parece más natural haberrightIndex
indicado el índice después del segmento, por lo que 0 representa el segmento vacío.Por supuesto, su código es extra confuso porque expande el subconjunto ordenado con uno, sobrescribiendo el elemento inmediatamente después del subconjunto ordenado inicialmente. Entonces lees del índice j pero escribes el valor en j + 1. Aquí debe quedar claro que j es la posición en la submatriz inicial, antes de la inserción. Cuando las operaciones de índice se vuelven demasiado complicadas, me ayuda a diagramarlo en un papel cuadriculado.
fuente
myArray.Length
omyList.Count
, que siempre es uno más que el índice "más a la derecha" basado en cero. ... La extensión de la explicación desmiente la aplicación práctica y simple de estas heurísticas de codificación explícitas. La multitud TL; DR se está perdiendo.La introducción a su pregunta me hace pensar que no ha aprendido a codificar correctamente. Cualquier persona que esté programando en un lenguaje imperativo durante más de unas pocas semanas realmente debería tener sus límites de bucle correctos la primera vez en más del 90% de los casos. Quizás esté apresurándose a comenzar a codificar antes de haber pensado suficientemente en el problema.
Le sugiero que corrija esta deficiencia al (re) aprender a escribir bucles, y le recomendaría unas horas trabajando en una variedad de bucles con papel y lápiz. Tómate una tarde libre para hacer esto. Luego, pase 45 minutos más o menos un día trabajando en el tema hasta que realmente lo entienda.
Todo está muy bien probado, pero deberías estar probando con la expectativa de que generalmente obtienes tus límites de bucle (y el resto de tu código) correctamente.
fuente
Quizás debería poner algo de carne en mi comentario:
Tu punto es
Cuando leo
trial and error
, suenan las campanas de alarma. Por supuesto, muchos de nosotros conocemos el estado de ánimo, cuando uno quiere solucionar un pequeño problema y ha analizado otras cosas y comienza a adivinar de una forma u otra, para hacer que el códigoseem
haga lo que se supone que debe hacer. De esto surgen algunas soluciones piratas, y algunas de ellas son puro genio ; pero para ser sincero: la mayoría de ellos no lo son . Yo incluido, conociendo este estado.Independientemente de su problema concreto, hizo preguntas sobre cómo mejorar:
1) prueba
Eso fue dicho por otros y no tendría nada valioso que agregar
2) Análisis de problemas
Es difícil dar algunos consejos al respecto. Solo puedo darte dos sugerencias, que probablemente te ayuden a mejorar tus habilidades sobre ese tema:
Code Katas son una forma, que podría ayudar un poco.
Code Kata
Un sitio que me gusta mucho: Code Wars
Son problemas relativamente pequeños, que te ayudan a agudizar tus habilidades de programación. Y lo que más me gusta en el Código Wars es, que se podría comparar su solución a uno de los demás .
O tal vez, deberías echar un vistazo a Exercism.io donde obtienes comentarios de la comunidad.
Sé, como dije anteriormente, a veces estás en tal estado, que es difícil dividir las cosas "simples" en tareas más "simples"; pero ayuda mucho
Recuerdo que cuando aprendí a programar profesionalmente , tuve grandes problemas para depurar mi código. ¿Cual fue el problema? Hybris : el error no puede estar en tal o cual región del código, porque sé que no puede estarlo. ¿Y en consecuencia? Leí el código en lugar de analizarlo, tuve que aprender, incluso si era tedioso romper mi código de instrucciones para instrucciones .
3) Desarrollar un cinturón de herramientas
Además de conocer su idioma y sus herramientas, sé que estas son las cosas brillantes que los desarrolladores piensan primero: aprenda Algoritmos (también conocido como lectura).
Aquí hay dos libros para comenzar:
Introducción a los algoritmos
Algoritmos
Esto es como aprender algunas recetas para comenzar a cocinar. Al principio no sabes qué hacer, así que debes mirar qué cocineros anteriores te cocinaron. Lo mismo ocurre con algortihms. Los algoritmos son como recetas de cocina para comidas comunes (estructuras de datos, clasificación, hash, etc.) Si los conoce (al menos lo intenta) de memoria, tiene un buen punto de partida.
3a) Conocer construcciones de programación
Este punto es un derivado, por así decirlo. Conozca su idioma, y mejor: sepa qué construcciones son posibles en su idioma.
Un punto común para código incorrecto o ineficaz es a veces, que el programador no conoce la diferencia entre los diferentes tipos de bucles (
for-
,while-
ydo
-loops). De alguna manera son intercambiables; pero en algunas circunstancias, elegir otra construcción en bucle conduce a un código más elegante.Y ahí está el dispositivo de Duff ...
PD:
Sí, ¡deberíamos hacer que la codificación sea excelente nuevamente!
Un nuevo lema para Stackoverflow.
fuente
pre-post
condiciones hasta ahora y lo aprecio.Si entendí el problema correctamente, su pregunta es cómo pensar en obtener bucles correctamente desde el primer intento, no cómo asegurarse de que su bucle sea correcto (para lo cual la respuesta sería la prueba como se explica en otras respuestas).
Lo que considero un buen enfoque es escribir la primera iteración sin ningún bucle. Una vez que hayas hecho esto, deberías notar qué se debe cambiar entre iteraciones.
¿Es un número, como un 0 o un 1? Entonces lo más probable es que necesites un for, y bingo, también tienes tu i inicial. Luego piense cuántas veces desea ejecutar la misma cosa, y también tendrá su condición final.
Si no sabe EXACTAMENTE cuántas veces se ejecutará, entonces no necesita un tiempo, sino un tiempo o un tiempo.
Técnicamente, cualquier bucle se puede traducir a cualquier otro bucle, pero el código es más fácil de leer si usa el bucle correcto, así que aquí hay algunos consejos:
Si te encuentras escribiendo un if () {...; break;} dentro de un for, necesitas un tiempo y ya tienes la condición
"While" es quizás el bucle más utilizado en cualquier idioma, pero no debería imo. Si te encuentras escribiendo bool ok = True; while (marcar) {hacer algo y con suerte cambiar ok en algún momento}; entonces no necesita un tiempo, sino un tiempo, porque significa que tiene todo lo que necesita para ejecutar la primera iteración.
Ahora un poco de contexto ... Cuando aprendí a programar (Pascal) por primera vez, no hablaba inglés. Para mí, "para" y "mientras", no tenía mucho sentido, pero la palabra clave "repetir" (hacer mientras que en C) es casi la misma en mi lengua materna, por lo que la usaría para todo. En mi opinión, repetir (hacer mientras) es el ciclo más natural, porque casi siempre quieres que se haga algo y luego quieres que se haga una y otra vez, hasta que se alcanza una meta. "For" es solo un acceso directo que le proporciona un iterador y coloca la condición de manera extraña al comienzo del código, aunque, casi siempre, desea que se haga algo hasta que algo suceda. Además, while es solo un acceso directo para if () {do while ()}. Los atajos son buenos para más tarde,
fuente
Voy a dar un ejemplo más detallado de cómo usar condiciones pre / post e invariantes para desarrollar un bucle correcto. En conjunto, tales afirmaciones se denominan especificación o contrato.
No estoy sugiriendo que intentes hacer esto para cada ciclo. Pero espero que le sea útil ver el proceso de pensamiento involucrado.
Para hacerlo, traduciré su método en una herramienta llamada Microsoft Dafny , que está diseñada para probar la exactitud de tales especificaciones. También verifica la terminación de cada bucle. Tenga en cuenta que Dafny no tiene un
for
bucle, por lo que he tenido que usar unwhile
bucle.Finalmente, mostraré cómo puede usar tales especificaciones para diseñar una versión, posiblemente, un poco más simple de su bucle. De hecho, esta versión de bucle más simple tiene la condición de bucle
j > 0
y la asignaciónarray[j] = value
, como era su intuición inicial.Dafny nos demostrará que ambos bucles son correctos y hacen lo mismo.
Luego, haré un reclamo general, basado en mi experiencia, sobre cómo escribir el bucle correcto hacia atrás, que tal vez te ayude si te enfrentas a esta situación en el futuro.
Primera parte: escribir una especificación para el método
El primer desafío al que nos enfrentamos es determinar qué se supone que debe hacer el método. Con este fin, diseñé condiciones previas y posteriores que especifican el comportamiento del método. Para que la especificación sea más exacta, he mejorado el método para que devuelva el índice donde
value
se insertó.Esta especificación captura completamente el comportamiento del método. Mi observación principal acerca de esta especificación es que se simplificaría si el procedimiento pasara el valor en
rightIndex+1
lugar de hacerlorightIndex
. Pero como no puedo ver desde dónde se llama a este método, no sé qué efecto tendría ese cambio en el resto del programa.Segunda parte: determinar un bucle invariante
Ahora que tenemos una especificación para el comportamiento del método, tenemos que agregar una especificación del comportamiento del bucle que convencerá a Dafny de que la ejecución del bucle terminará y dará como resultado el estado final deseado de
array
.El siguiente es su ciclo original, traducido a la sintaxis de Dafny con invariantes de ciclo agregados. También lo he cambiado para devolver el índice donde se insertó el valor.
Esto se verifica en Dafny. Puede verlo usted mismo siguiendo este enlace . Entonces su ciclo implementa correctamente la especificación del método que escribí en la primera parte. Deberá decidir si esta especificación del método es realmente el comportamiento que desea.
Tenga en cuenta que Dafny está produciendo una prueba de corrección aquí. Esta es una garantía de corrección mucho más sólida que la que puede obtenerse mediante pruebas.
Parte tres: un bucle más simple
Ahora que tenemos una especificación de método que captura el comportamiento del bucle. Podemos modificar de forma segura la implementación del bucle sin perder la confianza de que no hemos cambiado el comportamiento del bucle.
He modificado el bucle para que coincida con sus intuiciones originales sobre la condición del bucle y el valor final de
j
. Yo diría que este ciclo es más simple que el ciclo que describiste en tu pregunta. Es más capaz de usar enj
lugar dej+1
.Comience j en
rightIndex+1
Cambie la condición del bucle a
j > 0 && arr[j-1] > value
Cambiar la tarea a
arr[j] := value
Disminuya el contador del bucle al final del bucle en lugar de comenzar
Aquí está el código. Tenga en cuenta que los invariantes de bucle también son algo más fáciles de escribir ahora:
Parte cuatro: consejos sobre bucles hacia atrás
Después de haber escrito y demostrado ser correcto en muchos bucles durante varios años, tengo el siguiente consejo general sobre bucles hacia atrás.
Casi siempre es más fácil pensar y escribir un bucle hacia atrás (decreciente) si el decremento se realiza al principio del bucle en lugar de al final.
Desafortunadamente, la
for
construcción del bucle en muchos idiomas hace que esto sea difícil.Sospecho (pero no puedo probar) que esta complejidad es lo que causó la diferencia en su intuición sobre lo que debería ser el bucle y lo que realmente debía ser. Está acostumbrado a pensar en bucles hacia adelante (incrementales). Cuando desee escribir un bucle hacia atrás (decreciente), intente crear el bucle intentando revertir el orden en que las cosas suceden en un bucle hacia adelante (incremental). Pero debido a la forma en que funciona la
for
construcción, descuidó invertir el orden de la asignación y la actualización de la variable de bucle, que es necesaria para una verdadera inversión del orden de operaciones entre un bucle hacia atrás y hacia adelante.Quinta parte - bonificación
Solo para completar, aquí está el código que obtienes si pasas
rightIndex+1
al método en lugar de hacerlorightIndex
. Estos cambios eliminan todas las+2
compensaciones que de otro modo se requieren para pensar sobre la corrección del bucle.fuente
Hacer esto con facilidad y fluidez es una cuestión de experiencia. A pesar de que el lenguaje no le permite expresarlo directamente, o está utilizando un caso más complejo de lo que puede manejar el simple elemento incorporado, lo que cree que es un nivel superior como "visite cada elemento una vez en orden de reverencia" y el codificador más experimentado traduce eso en los detalles correctos al instante porque lo ha hecho muchas veces.
Incluso entonces, en casos más complejos, es fácil equivocarse, porque lo que estás escribiendo generalmente no es lo común enlatado. Con lenguajes y bibliotecas más modernas, no se escribe lo fácil porque hay una construcción enlatada o se requiere. En C ++, el mantra moderno es "usar algoritmos en lugar de escribir código".
Entonces, la manera de asegurarse de que sea correcto, para este tipo de cosas en particular, es observar las condiciones de contorno . Traza el código en tu cabeza por los pocos casos en los bordes de donde cambian las cosas. Si el
index == array-max
, ¿qué pasa? ¿Qué hay demax-1
? Si el código hace un giro incorrecto, estará en uno de estos límites. Algunos bucles deben preocuparse por el primer o el último elemento, así como por la construcción en bucle que consigue los límites correctos; por ejemplo, si se refierea[I]
ya[I-1]
qué sucede cuandoI
es el valor mínimo?Además, observe los casos en los que el número de iteraciones (correctas) es extremo: si los límites se cumplen y tendrá 0 iteraciones, ¿funcionará sin un caso especial? ¿Y qué hay de solo 1 iteración, donde el límite más bajo es también el límite más alto al mismo tiempo?
Examinar los casos de borde (ambos lados de cada borde) es lo que debe hacer al escribir el bucle, y lo que debe hacer en las revisiones de código.
fuente
Intentaré mantenerme alejado de los temas mencionados en abundancia.
Herramientas
Para mí, la herramienta más grande para escribir mejor
for
ywhile
bucles es no escribir ningunofor
owhile
bucles en absoluto.La mayoría de los idiomas modernos intentan abordar este problema de una forma u otra. Por ejemplo, Java, si
Iterator
bien desde el principio, que solía ser un poco complicado de usar, introdujo una sintaxis de acceso directo para usarlos más fácilmente en una versión más reciente. C # también los tiene, etc.Mi lenguaje favorito actualmente, Ruby, ha adoptado el enfoque funcional (
.each
,.map
etc.) de frente completo. Esto es muy poderoso. Acabo de hacer un recuento rápido en una base de código Ruby en la que estoy trabajando: en aproximadamente 10.000 líneas de código, hay cerofor
y aproximadamente 5while
.Si me viera obligado a elegir un nuevo idioma, buscar bucles funcionales / basados en datos como ese sería muy alto en la lista de prioridades.
Modelos mentales
Tenga en cuenta que
while
es el mínimo mínimo de abstracción que puede obtener, solo un paso más arribagoto
. En mi opinión, lofor
hace aún peor en lugar de mejorar, ya que une las tres partes del bucle.Entonces, si estoy en un entorno donde
for
se usa, entonces me aseguro de que las 3 partes sean simples y siempre iguales. Esto significa que escribiréPero nada más complejo. Tal vez, tal vez tenga una cuenta regresiva a veces, pero haré todo lo posible para evitarla.
Si lo uso
while
, me mantengo alejado de intrincados travesuras internas que involucran la condición del bucle. La prueba dentro delwhile(...)
será lo más simple posible, y evitarébreak
lo mejor que pueda. Además, el bucle será corto (contando líneas de código) y se eliminará cualquier cantidad mayor de código.Especialmente si la condición while real es compleja, usaré una "variable de condición" que es muy fácil de detectar y no colocaré la condición en la
while
declaración misma:(O algo así, en la medida correcta, por supuesto).
Esto le da un modelo mental muy fácil que es "esta variable se ejecuta de 0 a 10 monótonamente" o "este ciclo se ejecuta hasta que esa variable sea falsa / verdadera". La mayoría de los cerebros parecen ser capaces de manejar este nivel de abstracción perfectamente.
Espero que ayude.
fuente
Los bucles inversos, en particular, pueden ser difíciles de razonar porque muchos de nuestros lenguajes de programación están sesgados hacia la iteración hacia adelante, tanto en la sintaxis común del bucle for como mediante el uso de intervalos medio abiertos basados en cero. No digo que esté mal que los idiomas hayan tomado esas decisiones; Solo digo que esas elecciones complican pensar en bucles inversos.
En general, recuerde que un ciclo for es solo azúcar sintáctico construido alrededor de un ciclo while:
es equivalente a:
(posiblemente con una capa adicional de alcance para mantener las variables declaradas en el paso de inicio local al bucle).
Esto está bien para muchos tipos de bucles, pero tener el último paso es incómodo cuando caminas hacia atrás. Cuando trabajo hacia atrás, encuentro que es más fácil comenzar el índice del bucle con el valor después del que quiero y mover la
step
parte a la parte superior del bucle, de esta manera:o, como un bucle for:
Esto puede parecer desconcertante, ya que la expresión de paso está en el cuerpo en lugar del encabezado. Ese es un efecto secundario desafortunado de la polarización directa inherente para la sintaxis de bucle. Debido a esto, algunos argumentarán que en su lugar haces esto:
Pero eso es un desastre si su variable de índice es un tipo sin signo (como podría ser en C o C ++).
Con esto en mente, escriba su función de inserción.
Dado que estaremos trabajando hacia atrás, dejaremos que el índice del bucle sea la entrada después de la ranura de matriz "actual". Diseñaría la función para tomar el tamaño del número entero en lugar de un índice al último elemento porque los rangos medio abiertos son la forma natural de representar rangos en la mayoría de los lenguajes de programación y porque nos da una forma de representar una matriz vacía sin recurrir a un valor mágico como -1.
Si bien el nuevo valor es más pequeño que el elemento anterior , siga cambiando. Por supuesto, el elemento anterior se puede comprobar sólo si no es un elemento anterior, por lo que primero tenemos que comprobar que no estamos en el principio:
Esto deja
j
justo donde queremos el nuevo valor.Programming Pearls de Jon Bentley brinda una explicación muy clara del tipo de inserción (y otros algoritmos), que pueden ayudarlo a desarrollar sus modelos mentales para este tipo de problemas.
fuente
¿Estás simplemente confundido acerca de lo que hace un
for
bucle y la mecánica de cómo funciona?Puede ser útil reescribir este código usando una construcción diferente. Aquí es lo mismo usando un ciclo while:
Aquí hay algunas otras sugerencias:
for
bucles. Los usarás todo el tiempo. Le sugiero que pase por un bucle for en un depurador para ver cómo se ejecuta.* Tenga en cuenta que si bien uso el término "incremento", es solo un código que está detrás del cuerpo y antes de la verificación de la condición. Puede ser una disminución o nada en absoluto.
fuente
Intento de una visión adicional
Para algoritmos no triviales con bucles, puede probar el siguiente método:
i
oj
, e incremente / disminuya estas variables según sea necesario (pero aún sin ningún bucle);Tu problema
Escribe el cuerpo del bucle manualmente varias veces
Usemos una matriz fija con 4 posiciones e intentemos escribir el algoritmo manualmente, sin bucles:
Reescribir, eliminando valores codificados
Traducir a un bucle
Con
while
:Refactoriza / reescribe / optimiza el ciclo de la forma que quieras:
Con
while
:con
for
:PD: el código supone que la entrada es válida y que la matriz no contiene repeticiones;
fuente
Es por eso que evitaría escribir bucles que operen en índices sin procesar, a favor de operaciones más abstractas, siempre que sea posible.
En este caso, haría algo como esto (en pseudocódigo):
fuente
En su ejemplo, el cuerpo del bucle es bastante obvio. Y es bastante obvio que algún elemento tiene que ser cambiado al final. Entonces escribe el código, pero sin el inicio del ciclo, la condición final y la asignación final.
Luego, se aleja del código y descubre cuál es el primer movimiento que debe realizarse. Cambia el inicio del bucle para que el primer movimiento sea correcto. Te alejas del código nuevamente y descubres cuál es el último movimiento que debe realizarse. Cambia la condición final para que el último movimiento sea correcto. Y finalmente, te alejas de tu código y descubres cuál debe ser la asignación final, y arreglas el código en consecuencia.
fuente