¿Cómo escribir bucles correctos?


65

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:

  1. En lugar de j> = 0 lo guardé j> 0.
  2. Me confundí si array [j + 1] = valor o array [j] = valor.

¿Cuáles son las herramientas / modelos mentales para evitar tales errores?


66
¿En qué circunstancias crees que j >= 0es un error? Sería más cauteloso con el hecho de que está accediendo array[j]y array[j + 1]sin verificarlo primero array.length > (j + 1).
Ben Cottrell

55
similar a lo que dijo @LightnessRacesinOrbit, es probable que esté resolviendo problemas que ya se han resuelto. En general, cualquier bucle que necesite hacer sobre una estructura de datos ya existe en algún módulo o clase central ( Array.prototypeen el ejemplo de JS). Esto evita que encuentre condiciones de borde ya que algo como mapfunciona 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.
Jed Schneider

13
En realidad, adelante y resuelve problemas resueltos. Se llama práctica. Simplemente no te molestes en publicarlos. Es decir, a menos que encuentre una manera de mejorar las soluciones. Dicho esto, reinventar la rueda implica más que la rueda. Implica un sistema completo de control de calidad de la rueda y atención al cliente. Aún así, las llantas personalizadas son agradables.
candied_orange

53
Me temo que estamos yendo en la dirección equivocada aquí. Darle basura a CodeYogi porque su ejemplo es parte de un algoritmo bien conocido es bastante infundado. Nunca afirmó que había inventado algo nuevo. Pregunta cómo evitar algunos errores de límites muy comunes al escribir un bucle. Las bibliotecas han recorrido un largo camino, pero todavía veo un futuro para las personas que saben cómo escribir bucles.
candied_orange

55
En general, cuando se trata de bucles e índices, debe aprender que los índices apuntan entre elementos y familiarizarse con intervalos medio abiertos (en realidad, son dos lados de los mismos conceptos). Una vez que obtenga estos datos, gran parte de los bucles / índices que rasca la cabeza desaparece por completo.
Matteo Italia

Respuestas:


208

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 rightIndex0 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. ies un buen nombre para un índice en un agradable ciclo cerrado en un pequeño alcance que hace que su significado sea obvio. Si ivive lo suficiente como para extenderse línea por línea con otras ideas y nombres con los que se puede confundir, ientonces es hora de dar iun 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é breaky continuehaz si los tienes. Conoce la diferencia entre c++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 como Arrays.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 ...


36
Buena respuesta, pero tal vez deberías mencionar las pruebas. ¿Cómo se trata el bucle infinito en la prueba unitaria? ¿Ese bucle no "rompe" las pruebas?
GameAlchemist

139
@GameAlchemist Esa es la prueba de pizza. Si mi código no deja de funcionar en el tiempo que me lleva hacer una pizza, empiezo a sospechar que algo está mal. Claro que no hay cura para el problema de Alan Turing, pero al menos obtengo una pizza del trato.
candied_orange

12
@CodeYogi: en realidad, puede acercarse mucho . Comience con una prueba que opera en un solo valor. Implemente el código sin bucle. Luego escriba una prueba que opere en dos valores. Implementa el bucle. Es muy poco probable que obtenga una condición de límite incorrecta en el ciclo si lo hace así, porque en casi todas las circunstancias, una u otra de estas dos pruebas fallará si comete tal error.
Jules

15
@CodeYogi Dude todo el crédito debido a TDD pero Pruebas >> TDD. La salida de un valor puede ser una prueba, obtener una segunda mirada sobre su código es una prueba (puede formalizar esto como una revisión del código, pero a menudo solo agarro a alguien para una conversación de 5 minutos). Una prueba es cualquier posibilidad de que expreses tu intención de fallar. Demonios, puedes probar tu código hablando de una idea con tu madre. He encontrado errores en mi código al mirar el azulejo en la ducha. TDD es una disciplina formal efectiva que no se encuentra en todas las tiendas. Nunca he codificado en ningún lugar donde la gente no haya realizado la prueba.
candied_orange

12
Estaba codificando y probando años y años antes de haber oído hablar de TDD. Solo ahora me doy cuenta de la correlación de esos años con los años que pasé codificando sin pantalones.
candied_orange

72

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:

/**
 * 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; 
};

Y mira lo que tenemos:

  • precondición: array[0..rightIndex]se ordena
  • post-condición: array[0..rightIndex+1]se ordena
  • invariante: 0 <= j <= rightIndexpero 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.
  • invariante: al final de una "ronda", array[0..rightIndex+1]se ordena

Entonces, primero puede escribir una is_sortedfunción y una minfunción que funcione en un segmento de matriz, y luego afirmar:

function insert(array, rightIndex, value) {
    assert(is_sorted(array[0..rightIndex]));

    for(var j = rightIndex; j >= 0 && array[j] > value; j--) {
        array[j + 1] = array[j];

        assert(min(array[j..rightIndex+1]) > value);
        assert(is_sorted(array[0..rightIndex+1]));
    }   
    array[j + 1] = value; 

    assert(is_sorted(array[0..rightIndex+1]));
};

También existe el hecho de que su condición de bucle es un poco complicada; es posible que desee facilitarlo dividiendo las cosas:

function insert(array, rightIndex, value) {
    assert(is_sorted(array[0..rightIndex]));

    for (var j = rightIndex; j >= 0; j--) {
        if (array[j] <= value) { break; }

        array[j + 1] = array[j];

        assert(min(array[j..rightIndex+1]) > value);
        assert(is_sorted(array[0..rightIndex+1]));
    }   
    array[j + 1] = value; 

    assert(is_sorted(array[0..rightIndex+1]));
};

Ahora, el bucle es sencillo ( jva de rightIndexa 0).

Finalmente, ahora esto necesita ser probado:

  • pensar en las condiciones de contorno ( rightIndex == 0, rightIndex == array.size - 2)
  • piensa en valueser más pequeño array[0]o más grande quearray[rightIndex]
  • pensar valueque es igual a array[0], array[rightIndex]o algún índice medio

Ademá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.


8
@CodeYogi: Con ... pruebas. La cuestión es que puede ser poco práctico expresar todo en aserciones: si la aserción simplemente repite el código, entonces no trae nada nuevo (la repetición no ayuda con la calidad). Es por eso que aquí no afirmé en el bucle que 0 <= j <= rightIndexo array[j] <= valuesimplemente repetiría el código. Por otro lado, is_sortedtrae una nueva garantía, por lo tanto, es valiosa. Después, para eso están las pruebas. Si llama insert([0, 1, 2], 2, 3)a su función y la salida no es, [0, 1, 2, 3]entonces ha encontrado un error.
Matthieu M.

3
@MatthieuM. no descarte el valor de una aserción simplemente porque duplica el código. De hecho, pueden ser afirmaciones muy valiosas si decides reescribir el código. Las pruebas tienen todo el derecho de ser duplicadas. Considere más bien si la aserción está tan acoplada a una implementación de código único que cualquier reescritura invalidaría la aserción. Ahí es cuando estás perdiendo el tiempo. Buena respuesta por cierto.
candied_orange

1
@CandiedOrange: al duplicar el código me refiero literalmente 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.
Matthieu M.

10
Las reglas de Hoare: ayudar a escribir bucles correctos desde 1969. "Aún así, a pesar de que estas técnicas se conocen desde hace más de una década, la mayoría de los programadores nunca han oído hablar de ellas".
Joker_vD

1
@MatthieuM. Estoy de acuerdo en que tiene un valor muy bajo. Pero no lo considero como una copia / pegado. Digamos que quería refactorizar insert()para que funcionara copiando de una matriz anterior a una nueva matriz. Eso se puede hacer sin romper el de los demás assert. Pero no este último. Solo muestra qué tan bien assertfueron diseñados los demás .
candied_orange

29

Usar pruebas unitarias / TDD

Si realmente necesita acceder a las secuencias a través de un forbucle, 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?

  1. 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.

  2. 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?

  3. 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 ifdeclaración o una llamada a su método de marco favorito.

  4. 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, foreachse puede utilizar en lugar de forhacer irrelevante la comprobación de las condiciones de contorno: el lenguaje lo hace por usted. Algunos lenguajes, como Python, ni siquiera tienen la for (;;)construcción, sino solo for ... in ....

En C #, LINQ es particularmente conveniente cuando se trabaja con secuencias.

var result = source.Skip(5).TakeWhile(c => c > 0);

es mucho más legible y menos propenso a errores en comparación con su forvariante:

for (int i = 5; i < source.Length; i++)
{
    var value = source[i];
    if (value <= 0)
    {
        break;
    }

    yield return value;
}

3
Bueno, a partir de su pregunta original, tengo la impresión de que la elección es, por un lado, utilizar TDD y obtener la solución correcta, y por otro lado omitir la parte de prueba y obtener las condiciones de contorno incorrectas.
Arseni Mourzenko

18
Gracias por mencionar el elefante en la habitación: no usar bucles en absoluto . Por qué la gente todavía codifica como si fuera 1985 (y estoy siendo generoso) me supera. BOCTAOE.
Jared Smith

44
@JaredSmith Una vez que la computadora ejecuta ese código, ¿cuánto quieres apostar que no hay instrucciones de salto? Al usar LINQ, está abstrayendo el ciclo, pero todavía está allí. Les expliqué esto a los compañeros de trabajo que no aprendieron sobre el arduo trabajo de Shlemiel, el pintor . Si no se comprende dónde ocurren los bucles, incluso si están abstraídos en el código y, como resultado, el código es significativamente más legible, casi siempre conduce a problemas de rendimiento que uno no podrá explicar, y mucho menos solucionar.
un CVn

66
@ MichaelKjörling: cuando usas LINQ, el bucle está allí, pero una 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.
Arseni Mourzenko

44
@ MichaelKjörling No estaba necesariamente hablando de LINQ y de alguna manera no entiendo tu punto. 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.
Jared Smith

15

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:

for (int i = 0; i < length; i++)

o

for (int i = length - 1; i >= 0; i--)

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?

for(var j = rightIndex; j >= 0 && array[j] > value; j--) {
    array[j + 1] = array[j];
}   
array[j + 1] = value;

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.


44
Me gusta mucho 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. Pero for (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 un whilebucle a menos que estuviera tratando de hacer ique 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 la while()versión termina con i == -1 después del bucle (si continúa), en lugar de i = = 0.
TOOGAM

2
@TOOGAM (int i = length; i--;) funciona en C / C ++ porque 0 se evalúa como falso, pero no todos los lenguajes tienen esa equivalencia. Supongo que se podría decir i--> 0.
Bryce Wagner

Naturalmente, si usa un lenguaje que requiere " > 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.
TOOGAM

1
@BryceWagner si necesitas hacerlo i-- > 0, ¿por qué no pruebas el clásico chiste i --> 0?
porglezomp

3
@porglezomp Ah, sí, la va a operador . La mayoría de los lenguajes tipo C, incluidos C, C ++, Java y C #, tienen ese.
un CVn

11

Me sentí demasiado frustrado por la falta de un modelo informático correcto en mi cabeza.

Es un punto muy interesante para esta pregunta y generó este comentario:

Solo hay una forma: comprende mejor tu problema. Pero eso es tan general como su pregunta es. - Thomas Junk

... 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:

  • ¿Qué pasa si rightIndex es demasiado bajo? (pista: se implicará la pérdida de datos)
  • ¿Qué pasa si rightIndex está fuera de los límites de la matriz? (¿obtendrá una excepción, o simplemente se creó un desbordamiento de búfer?)

Hay algunos otros problemas relacionados con el rendimiento y el diseño del código ...

  • ¿este código necesitará escalar? ¿Mantener la matriz ordenada es la mejor opción, o debería buscar otras opciones (como una lista vinculada)?
  • ¿Puedes estar seguro de tus suposiciones? (¿puede garantizar que la matriz se clasifique, y qué pasa si no es así?)
  • ¿Estás reinventando la rueda? Las matrices ordenadas son un problema bien conocido, ¿ha estudiado las soluciones existentes? ¿Hay alguna solución disponible en su idioma (como SortedList<t>en C #)?
  • ¿debería copiar manualmente una entrada de matriz a la vez? ¿o su lenguaje proporciona funciones comunes como JScript 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.


2
Incluso si está pasando sus índices a una función existente (como Array.Copy), aún puede requerirse pensar para obtener las condiciones correctas. Imaginar lo que sucede en situaciones de longitud 0 y longitud 1 y longitud 2 puede ser la mejor manera de asegurarse de que no está copiando muy pocos o demasiados.
Bryce Wagner

@BryceWagner: absolutamente cierto, pero sin una idea clara de qué problema es que realmente estás resolviendo, pasarás mucho tiempo dando vueltas en la oscuridad en una estrategia de "golpe y esperanza" que es, con mucho, el OP mayor problema en este punto.
James Snell

2
@CodeYogi: tiene y, como lo han señalado otros, ha dividido el problema en subproblemas de manera bastante pobre, por lo que varias respuestas mencionan su enfoque para resolver el problema como la forma de evitarlo. No es algo que debas tomar personalmente, solo la experiencia de aquellos de nosotros que hemos estado allí.
James Snell

2
@CodeYogi, creo que puede haber confundido este sitio con Stack Overflow. Este sitio es el equivalente a una sesión de preguntas y respuestas en una pizarra , no en una terminal de computadora. "Muéstrame el código" es una indicación bastante explícita de que estás en el sitio equivocado.
Comodín el

2
@Wildcard +1: "Muéstrame el código" es, para mí, un excelente indicador de por qué esta respuesta es correcta y que tal vez necesito trabajar en formas de demostrar mejor que es un problema de diseño / factor humano que solo puede ser abordado por cambios en el proceso humano; ninguna cantidad de código podría enseñarlo.
James Snell

10

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 foreacho mapque 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 índice starthasta , pero sin incluir índice end.

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.

┌───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │   -- cell indexes, e.g array[3]
└───┴───┴───┴───┴───┴───┴───┴───┴───┘
0   1   2   3   4   5   6   7   8   9   -- segment bounds, e.g. slice(2,5) 
        └───────────┘ 
          range 2..5

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 rightIndexargumento 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 haber rightIndexindicado 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.


44
@CodeYogi: Dibujaría una pequeña matriz como una cuadrícula en un trozo de papel, y luego pasaría una iteración del bucle manualmente con un lápiz. Esto me ayuda a aclarar lo que realmente sucede, por ejemplo, que un rango de celdas se desplaza a la derecha y dónde se inserta el nuevo valor.
JacquesB

3
"Hay dos cosas difíciles en ciencias de la computación: invalidación de caché, nombrar cosas y errores".
Trauma digital

1
@CodeYogi: se agregó un pequeño diagrama para mostrar de lo que estoy hablando.
JacquesB

1
Gran conocimiento, especialmente los últimos dos pares que vale la pena leer, la confusión también se debe a la naturaleza del bucle for, incluso encuentro el índice correcto que el bucle disminuye j una vez antes de la finalización y, por lo tanto, me devuelve un paso atrás.
CodeYogi

1
Muy. Excelente. Responder. Y agregaría que esta convención de índice inclusiva / exclusiva también está motivada por el valor de myArray.Lengtho myList.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.
radarbob

5

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.


44
No sería tan asertivo sobre las habilidades de OP. Cometer errores en los límites es fácil, especialmente en un contexto estresante como una entrevista de contratación. Un desarrollador experimentado también podría cometer esos errores, pero, obviamente, un desarrollador experimentado evitaría tales errores en primer lugar a través de las pruebas.
Arseni Mourzenko

3
@MainMa: creo que si bien Mark podría haber sido más sensible, creo que tiene razón: hay estrés en las entrevistas y solo hay que hackear el código sin la debida consideración para definir el problema. La forma en que está formulada la pregunta apunta muy fuertemente a esto último y eso es algo que se puede resolver mejor a largo plazo asegurándose de que tenga una base sólida, no cortando el IDE
James Snell

@JamesSnell Creo que te estás volviendo demasiado seguro de ti mismo. Mira el código y dime qué te hace pensar que no está documentado. Si ve claramente, ¿no se menciona en ninguna parte que no pude resolver el problema? Solo quería saber cómo evitar repetir el mismo error. Creo que tienes todo tu programa correcto de una vez.
CodeYogi

44
@CodeYogi Si tiene que hacer 'prueba y error' y 'se frustra' y 'comete los mismos errores' con su codificación, entonces esas son señales de que no entendió su problema lo suficientemente bien antes de comenzar a escribir . Nadie dice que no lo haya entendido, pero que su código podría haberse pensado mejor y son signos de que está luchando, que tiene la opción de aprender y aprender, o no.
James Snell

2
@CodeYogi ... y como preguntas, rara vez me equivoco al hacer bucles y bifurcaciones porque me aseguro de tener una comprensión clara de lo que necesito lograr antes de escribir el código, no es difícil hacer algo simple como un ordenado clase de matriz. Como programador, una de las cosas más difíciles de hacer es reconocer que tú eres el problema, pero hasta que lo hagas no comenzarás a escribir un código realmente bueno.
James Snell

3

Quizás debería poner algo de carne en mi comentario:

Solo hay una forma: comprende mejor tu problema. Pero eso es tan general como tu pregunta es

Tu punto es

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.

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ódigo seemhaga 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:

  • el obvio y más trivial es a largo plazo el más efectivo: resolver muchos problemas. Mientras practica y repite, desarrolla la mentalidad que lo ayuda para futuras tareas. La programación es como cualquier otra actividad que se mejore mediante la práctica del trabajo duro.

Code Katas son una forma, que podría ayudar un poco.

¿Cómo llegas a ser un gran músico? Es útil conocer la teoría y comprender la mecánica de su instrumento. Ayuda a tener talento. Pero en última instancia, la grandeza viene de practicar; aplicando la teoría una y otra vez, utilizando la retroalimentación para mejorar cada vez.

Code Kata

Un sitio que me gusta mucho: Code Wars

Logra el dominio a través del desafío Mejora tus habilidades entrenando con otros en desafíos de código real

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.

  • El otro consejo es casi tan trivial: aprender a resolver los problemas. Debe entrenarse, dividiendo los problemas en problemas realmente pequeños. Si dice que tiene problemas para escribir bucles , comete el error de que ve el bucle como una construcción completa y no lo desarma en pedazos. Si aprende a desarmar las cosas paso a paso , aprende a evitar tales errores.

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 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:

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-y do-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:

de lo contrario, su comentario no es bueno que Donal Trump.

Sí, ¡deberíamos hacer que la codificación sea excelente nuevamente!

Un nuevo lema para Stackoverflow.


Ah, déjame decirte una cosa muy en serio. He estado haciendo todo lo que mencionaste e incluso puedo darte mi enlace en esos sitios. Pero lo que me resulta frustrante es que, en lugar de obtener la respuesta a mi pregunta, recibo todos los consejos posibles sobre programación. Solo un chico ha mencionado las pre-postcondiciones hasta ahora y lo aprecio.
CodeYogi

Por lo que dices, es difícil imaginar dónde está tu problema. Tal vez una metáfora ayude: para mí es como decir "cómo puedo ver": la respuesta obvia para mí es "usar los ojos" porque ver es muy natural para mí, no puedo imaginar cómo no se puede ver. Lo mismo vale para tu pregunta.
Thomas Junk

De acuerdo completamente con las alarmas en "prueba y error". Creo que la mejor manera de aprender completamente la mentalidad de resolución de problemas es ejecutar algoritmos y códigos con papel y lápiz.
Comodín el

Um ... ¿por qué tienes un jab gramaticalmente pobre y no sequitur en un candidato político citado sin contexto en el medio de tu respuesta sobre programación?
Comodín el

2

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:

  1. Si te encuentras escribiendo un if () {...; break;} dentro de un for, necesitas un tiempo y ya tienes la condición

  2. "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,


2

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 forbucle, por lo que he tenido que usar un whilebucle.

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 > 0y la asignación array[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 valuese insertó.

method insert(arr:array<int>, rightIndex:int, value:int) returns (index:int)
  // the method will modify the array
  modifies arr
  // the array will not be null
  requires arr != null
  // the right index is within the bounds of the array
  // but not the last item
  requires 0 <= rightIndex < arr.Length - 1
  // value will be inserted into the array at index
  ensures arr[index] == value 
  // index is within the bounds of the array
  ensures 0 <= index <= rightIndex + 1
  // the array to the left of index is not modified
  ensures arr[..index] == old(arr[..index])
  // the array to the right of index, up to right index is
  // shifted to the right by one place
  ensures arr[index+1..rightIndex+2] == old(arr[index..rightIndex+1])
  // the array to the right of rightIndex+1 is not modified
  ensures arr[rightIndex+2..] == old(arr[rightIndex+2..])

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+1lugar de hacerlo rightIndex. 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.

{
    // take a copy of the initial array, so we can refer to it later
    // ghost variables do not affect program execution, they are just
    // for specification
    ghost var initialArr := arr[..];


    var j := rightIndex;
    while(j >= 0 && arr[j] > value)
       // the loop always decreases j, so it will terminate
       decreases j
       // j remains within the loop index off-by-one
       invariant -1 <= j < arr.Length
       // the right side of the array is not modified
       invariant arr[rightIndex+2..] == initialArr[rightIndex+2..]
       // the part of the array looked at by the loop so far is
       // shifted by one place to the right
       invariant arr[j+2..rightIndex+2] == initialArr[j+1..rightIndex+1]
       // the part of the array not looked at yet is not modified
       invariant arr[..j+1] == initialArr[..j+1] 
    {
        arr[j + 1] := arr[j];
        j := j-1;
    }   
    arr[j + 1] := value;
    return j+1; // return the position of the insert
}

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 en jlugar de j+1.

  1. Comience j en rightIndex+1

  2. Cambie la condición del bucle a j > 0 && arr[j-1] > value

  3. Cambiar la tarea a arr[j] := value

  4. 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:

method insert2(arr:array<int>, rightIndex:int, value:int) returns (index:int)
  modifies arr
  requires arr != null
  requires 0 <= rightIndex < arr.Length - 1
  ensures 0 <= index <= rightIndex + 1
  ensures arr[..index] == old(arr[..index])
  ensures arr[index] == value 
  ensures arr[index+1..rightIndex+2] == old(arr[index..rightIndex+1])
  ensures arr[rightIndex+2..] == old(arr[rightIndex+2..])
{
    ghost var initialArr := arr[..];
    var j := rightIndex+1;
    while(j > 0 && arr[j-1] > value)
       decreases j
       invariant 0 <= j <= arr.Length
       invariant arr[rightIndex+2..] == initialArr[rightIndex+2..]
       invariant arr[j+1..rightIndex+2] == initialArr[j..rightIndex+1]
       invariant arr[..j] == initialArr[..j] 
    {
        j := j-1;
        arr[j + 1] := arr[j];
    }   
    arr[j] := value;
    return j;
}

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 forconstrucció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 forconstrucció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+1al método en lugar de hacerlo rightIndex. Estos cambios eliminan todas las +2compensaciones que de otro modo se requieren para pensar sobre la corrección del bucle.

method insert3(arr:array<int>, rightIndex:int, value:int) returns (index:int)
  modifies arr
  requires arr != null
  requires 1 <= rightIndex < arr.Length 
  ensures 0 <= index <= rightIndex
  ensures arr[..index] == old(arr[..index])
  ensures arr[index] == value 
  ensures arr[index+1..rightIndex+1] == old(arr[index..rightIndex])
  ensures arr[rightIndex+1..] == old(arr[rightIndex+1..])
{
    ghost var initialArr := arr[..];
    var j := rightIndex;
    while(j > 0 && arr[j-1] > value)
       decreases j
       invariant 0 <= j <= arr.Length
       invariant arr[rightIndex+1..] == initialArr[rightIndex+1..]
       invariant arr[j+1..rightIndex+1] == initialArr[j..rightIndex]
       invariant arr[..j] == initialArr[..j] 
    {
        j := j-1;
        arr[j + 1] := arr[j];
    }   
    arr[j] := value;
    return j;
}

2
Realmente agradecería un comentario si
vota negativamente

2

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 de max-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 refiere a[I]y a[I-1]qué sucede cuando Ies 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.


1

Intentaré mantenerme alejado de los temas mencionados en abundancia.

¿Cuáles son las herramientas / modelos mentales para evitar tales errores?

Herramientas

Para mí, la herramienta más grande para escribir mejor fory whilebucles es no escribir ninguno foro whilebucles en absoluto.

La mayoría de los idiomas modernos intentan abordar este problema de una forma u otra. Por ejemplo, Java, si Iteratorbien 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, .mapetc.) 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 cero fory aproximadamente 5 while.

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 whilees el mínimo mínimo de abstracción que puede obtener, solo un paso más arriba goto. En mi opinión, lo forhace aún peor en lugar de mejorar, ya que une las tres partes del bucle.

Entonces, si estoy en un entorno donde forse usa, entonces me aseguro de que las 3 partes sean simples y siempre iguales. Esto significa que escribiré

limit = ...;
for (idx = 0; idx < limit; idx++) { 

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 del while(...)será lo más simple posible, y evitaré breaklo 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 whiledeclaración misma:

repeat = true;
while (repeat) {
   repeat = false; 
   ...
   if (complex stuff...) {
      repeat = true;
      ... other complex stuff ...
   }
}

(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.


1

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:

// pseudo-code!
for (init; cond; step) { body; }

es equivalente a:

// pseudo-code!
init;
while (cond) {
  body;
  step;
}

(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 stepparte a la parte superior del bucle, de esta manera:

auto i = v.size();  // init
while (i > 0) {  // simpler condition because i is one after
    --i;  // step before the body
    body;  // in body, i means what you'd expect
}

o, como un bucle for:

for (i = v.size(); i > 0; ) {
    --i;  // step
    body;
}

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:

for (i = v.size() - 1; i >= 0; --i) {
    body;
}

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.

  1. 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.

    function insert(array, size, value) {
      var j = size;
    
  2. 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:

      while (j != 0 && value < array[j - 1]) {
        --j;  // now j become current
        array[j + 1] = array[j];
      }
    
  3. Esto deja jjusto donde queremos el nuevo valor.

      array[j] = value; 
    };
    

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.


0

¿Estás simplemente confundido acerca de lo que hace un forbucle y la mecánica de cómo funciona?

for(initialization; condition; increment*)
{
    body
}
  1. Primero, se ejecuta la inicialización
  2. Luego se verifica la condición
  3. Si la condición es verdadera, el cuerpo se ejecuta una vez. Si no, pasa al # 6
  4. El código de incremento se ejecuta
  5. Goto # 2
  6. Fin del ciclo

Puede ser útil reescribir este código usando una construcción diferente. Aquí es lo mismo usando un ciclo while:

initialization
while(condition)
{
    body
    increment
}

Aquí hay algunas otras sugerencias:

  • ¿Puedes usar otra construcción de lenguaje como un bucle foreach? Esto se encarga de la condición y el paso de incremento para usted.
  • ¿Se puede usar una función de Mapa o Filtro? Algunos idiomas tienen funciones con estos nombres que recorren internamente una colección para usted. Simplemente suministra la colección y el cuerpo.
  • Realmente deberías pasar más tiempo familiarizándote con los forbucles. 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.


1
¿Por qué el voto negativo?
user2023861

0

Intento de una visión adicional

Para algoritmos no triviales con bucles, puede probar el siguiente método:

  1. Cree una matriz fija con 4 posiciones y ponga algunos valores para simular el problema;
  2. Escriba su algoritmo para resolver el problema dado , sin ningún bucle y con indexaciones codificadas ;
  3. Después de eso, sustituya las indexaciones codificadas en su código con alguna variable io j, e incremente / disminuya estas variables según sea necesario (pero aún sin ningún bucle);
  4. Vuelva a escribir su código y coloque los bloques repetitivos dentro de un bucle , cumpliendo las condiciones previas y posteriores;
  5. [ opcional ] reescribe tu ciclo para que esté en la forma que quieras (para / while / do while);
  6. Lo más importante es escribir su algoritmo correctamente; después de eso, refactoriza y optimiza su código / bucles si es necesario (pero esto podría hacer que el código ya no sea trivial para un lector)

Tu problema

//TODO: Insert the given value in proper position in the sorted subarray
function insert(array, rightIndex, value) { ... };

Escribe el cuerpo del bucle manualmente varias veces

Usemos una matriz fija con 4 posiciones e intentemos escribir el algoritmo manualmente, sin bucles:

           //0 1 2 3
var array = [2,5,9,1]; //array sorted from index 0 to 2
var leftIndex = 0;
var rightIndex = 2;
var value = array[3]; //placing the last value within the array in the proper position

//starting here as 2 == rightIndex

if (array[2] > value) {
    array[3] = array[2];
} else {
    array[3] = value;
    return; //found proper position, no need to proceed;
}

if (array[1] > value) {
    array[2] = array[1];
} else {
    array[2] = value;
    return; //found proper position, no need to proceed;
}

if (array[0] > value) {
    array[1] = array[0];
} else {
    array[1] = value;
    return; //found proper position, no need to proceed;
}

array[0] = value; //achieved beginning of the array

//stopping here because there 0 == leftIndex

Reescribir, eliminando valores codificados

//consider going from 2 to 0, going from "rightIndex" to "leftIndex"

var i = rightIndex //starting here as 2 == rightIndex

if (array[i] > value) {
    array[i+1] = array[i];
} else {
    array[i+1] = value;
    return; //found proper position, no need to proceed;
}

i--;
if (i < leftIndex) {
    array[i+1] = value; //achieved beginning of the array
    return;
}

if (array[i] > value) {
    array[i+1] = array[i];
} else {
    array[i+1] = value;
    return; //found proper position, no need to proceed;
}

i--;
if (i < leftIndex) {
    array[i+1] = value; //achieved beginning of the array
    return;
}

if (array[i] > value) {
    array[i+1] = array[i];
} else {
    array[i+1] = value;
    return; //found proper position, no need to proceed;
}

i--;
if (i < leftIndex) {
    array[i+1] = value; //achieved beginning of the array
    return;
}

//stopping here because there 0 == leftIndex

Traducir a un bucle

Con while:

var i = rightIndex; //starting in rightIndex

while (true) {
    if (array[i] > value) { //refactor: this can go in the while clause
        array[i+1] = array[i];
    } else {
        array[i+1] = value;
        break; //found proper position, no need to proceed;
    }

    i--;
    if (i < leftIndex) { //refactor: this can go (inverted) in the while clause
        array[i+1] = value; //achieved beginning of the array
        break;
    }
}

Refactoriza / reescribe / optimiza el ciclo de la forma que quieras:

Con while:

var i = rightIndex; //starting in rightIndex

while ((array[i] > value) && (i >= leftIndex)) {
    array[i+1] = array[i];
    i--;
}

array[i+1] = value; //found proper position, or achieved beginning of the array

con for:

for (var i = rightIndex; (array[i] > value) && (i >= leftIndex); i--) {
    array[i+1] = array[i];
}

array[i+1] = value; //found proper position, or achieved beginning of the array

PD: el código supone que la entrada es válida y que la matriz no contiene repeticiones;


-1

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):

array = array[:(rightIndex - 1)] + value + array[rightIndex:]

-3

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.


1
¿Puedes por favor poner algún ejemplo?
CodeYogi

El OP tenía un ejemplo.
gnasher729

2
¿Qué quieres decir? Soy el OP.
CodeYogi
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.