Como escrever loops corretos?


65

Na maioria das vezes, durante a gravação de loops, geralmente escrevo condições de contorno erradas (por exemplo: resultado errado) ou minhas suposições sobre terminações de loop estão erradas (por exemplo: loop em execução infinita). Embora eu tenha acertado minhas suposições após algumas tentativas e erros, fiquei muito frustrado por causa da falta de um modelo de computação correto na minha cabeça.

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

Os erros que cometi inicialmente foram:

  1. Em vez de j> = 0, eu o mantive j> 0.
  2. Fiquei confuso se array [j + 1] = valor ou array [j] = valor.

O que são ferramentas / modelos mentais para evitar esses erros?


6
Em que circunstâncias você acredita que isso j >= 0é um erro? Eu ficaria mais cauteloso com o fato de você estar acessando array[j]e array[j + 1]sem primeiro verificar isso array.length > (j + 1).
precisa

5
semelhante ao que o @LightnessRacesinOrbit disse, você provavelmente está resolvendo problemas que já foram resolvidos. De maneira geral, qualquer loop que você precise executar em uma estrutura de dados já existe em algum módulo ou classe principal (no Array.prototypeexemplo de JS). Isso evita que você encontre condições de borda, pois algo como mapfunciona em todas as matrizes. Você pode resolver o problema acima usando slice e concat para evitar o loop completo : codepen.io/anon/pen/ZWovdg?editors=0012 A maneira mais correta de escrever um loop é não escrever um.
perfil completo de Jed Schneider

13
Na verdade, vá em frente e resolva problemas resolvidos. Isso se chama prática. Só não se preocupe em publicá-las. Ou seja, a menos que você encontre uma maneira de melhorar as soluções. Dito isto, reinventar a roda envolve mais do que a roda. Envolve um sistema completo de controle de qualidade das rodas e suporte ao cliente. Ainda assim, as jantes personalizadas são legais.
Candied_orange 17/04

53
Receio que estejamos indo na direção errada aqui. Dar uma porcaria no CodeYogi porque seu exemplo faz parte de um algoritmo conhecido é bastante infundado. Ele nunca afirmou que tinha inventado algo novo. Ele está perguntando como evitar alguns erros de limite muito comuns ao escrever um loop. As bibliotecas já percorreram um longo caminho, mas ainda vejo um futuro para as pessoas que sabem escrever loops.
Candied_orange 17/04

5
Em geral, ao lidar com loops e índices, você deve aprender que os índices apontam entre os elementos e se familiariza com os intervalos semiabertos (na verdade, são dois lados dos mesmos conceitos). Depois de obter esses fatos, grande parte dos loops / índices de arranhões na cabeça desaparecem completamente.
Matteo Italia

Respostas:


208

Teste

Não, sério, teste.

Estou codificando há mais de 20 anos e ainda não confio em mim para escrever um loop corretamente na primeira vez. Escrevo e executo testes que provam que funciona antes que eu suspeite. Teste cada lado de cada condição de contorno. Por exemplo, um rightIndexde 0 deve fazer o que? Que tal -1?

Mantenha simples

Se os outros não conseguem ver o que ele faz de relance, você está dificultando demais. Fique à vontade para ignorar o desempenho, se isso significa que você pode escrever algo fácil de entender. Apenas a torne mais rápida no caso improvável de que você realmente precisa. E mesmo assim, apenas quando você tiver certeza absoluta de que sabe exatamente o que está atrasando você. Se você pode obter uma melhoria real do Big O , essa atividade pode não ser inútil, mas mesmo assim, torne seu código o mais legível possível.

Desligado por um

Saiba a diferença entre contar os dedos e contar os espaços entre os dedos. Às vezes, os espaços são o que é realmente importante. Não deixe seus dedos te distraírem. Saiba se o seu polegar é um dedo. Saiba se a diferença entre o dedo mindinho e o polegar conta como um espaço.

Comentários

Antes de se perder no código, tente dizer o que você quer dizer em inglês. Afirme suas expectativas claramente. Não explique como o código funciona. Explique por que você está fazendo o que faz. Mantenha os detalhes da implementação fora disso. Deve ser possível refatorar o código sem precisar alterar o comentário.

O melhor comentário é um bom nome.

Se você pode dizer tudo o que precisa dizer com um bom nome, NÃO o diga novamente com um comentário.

Abstrações

Objetos, funções, matrizes e variáveis ​​são todas abstrações que são tão boas quanto os nomes que recebem. Dê a eles nomes que garantam que, quando as pessoas olham dentro delas, não serão surpreendidas pelo que encontrarem.

Nomes curtos

Use nomes curtos para coisas de vida curta. ié um nome fino para um índice em um loop apertado agradável em um escopo pequeno que torna seu significado óbvio. Se a ivida é longa o suficiente para se espalhar, linha após linha, com outras idéias e nomes que podem ser confundidos i, é hora de dar ium bom e longo nome explicativo.

Nomes longos

Nunca encurte um nome simplesmente devido a considerações sobre o comprimento da linha. Encontre outra maneira de definir seu código.

Espaço em branco

Os defeitos adoram se esconder em códigos ilegíveis. Se o seu idioma permitir que você escolha seu estilo de indentação, pelo menos seja consistente. Não faça seu código parecer um fluxo de ruído. O código deve parecer que está em formação.

Construções de loop

Aprenda e revise as estruturas de loop no seu idioma. Observar um depurador destacar um for(;;)loop pode ser muito instrutivo. Aprenda todas as formas. while, do while, while(true), for each. Use o mais simples que você pode se safar. Procure preparar a bomba . Aprenda o que breake continuefaça se você os tiver. Saiba a diferença entre c++e ++c. Não tenha medo de voltar mais cedo, desde que sempre feche tudo o que precisa ser fechado. Por fim, bloqueia ou preferencialmente algo que o marca para fechamento automático quando você o abre: Using statement / Try with Resources .

Alternativas de loop

Deixe outra coisa fazer o loop, se puder. É mais fácil para os olhos e já está depurado. Estes vêm em muitas formas: coleções ou riachos que permitem map(), reduce(), foreach(), e outros tais métodos que se aplicam a lambda. Procure funções especiais como Arrays.fill(). Também há recursão, mas apenas esperamos que isso facilite as coisas em casos especiais. Geralmente, não use recursão até ver como seria a alternativa.

Ah, e teste.

Teste, teste, teste.

Eu mencionei o teste?

Havia mais uma coisa. Não me lembro. Começou com um T ...


36
Boa resposta, mas talvez você deva mencionar o teste. Como alguém lida com loop infinito no teste de unidade? Esse loop não 'quebra' os testes ???
GameAlchemist #

139
@ GameAlchemist Esse é o teste da pizza. Se meu código não parar de ser executado no tempo necessário para fazer uma pizza, começo a suspeitar que algo está errado. Claro que não há cura para o problema de suspensão de Alan Turing, mas pelo menos recebo uma pizza do acordo.
Candied_orange 17/04

12
@ CodeYogi - na verdade, pode chegar muito perto. Comece com um teste que opere em um único valor. Implemente o código sem um loop. Em seguida, escreva um teste que opere em dois valores. Implemente o loop. É muito improvável que você tenha uma condição de contorno incorreta no loop se fizer isso dessa maneira, porque em quase todas as circunstâncias um ou outro desses dois testes falhará se você cometer esse erro.
Jules

15
@CodeYogi Dude todo o crédito devido ao TDD, mas Testing >> TDD. A saída de um valor pode ser um teste, e um segundo par de olhos em seu código está sendo testado (você pode formalizar isso como uma revisão de código, mas muitas vezes eu apenas agarro alguém para uma conversa de 5 minutos). Um teste é uma chance de você expressar sua intenção de falhar. Inferno, você pode testar seu código conversando com uma idéia com sua mãe. Encontrei bugs no meu código ao olhar para o azulejo no chuveiro. TDD é uma disciplina formalizada eficaz que você não encontra em todas as lojas. Eu nunca codifiquei uma vez qualquer lugar onde as pessoas não testassem.
Candied_orange 17/04/19

12
Eu estava codificando e testando anos e anos antes de ouvir falar em TDD. É só agora que percebo a correlação desses anos com os anos gastos em codificação sem usar calças.
Candied_orange

72

Ao programar, é útil pensar em:

e ao explorar um território desconhecido (como fazer malabarismos com índices), pode ser muito, muito útil não apenas pensar neles, mas torná-los explícitos no código com asserções .

Vamos pegar seu 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; 
};

E verifique o que temos:

  • pré-condição: array[0..rightIndex]é classificada
  • pós-condição: array[0..rightIndex+1]é classificado
  • invariável: 0 <= j <= rightIndexmas parece um pouco redundante; ou como @Jules observou nos comentários, no final de uma "rodada" for n in [j, rightIndex+1] => array[j] > value,.
  • invariável: no final de uma "rodada", array[0..rightIndex+1]é classificado

Assim, você pode primeiro escrever uma is_sortedfunção, bem como uma minfunção que trabalha em uma fatia da matriz, e depois 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]));
};

Há também o fato de que sua condição de loop é um pouco complicada; convém facilitar as coisas dividindo as coisas:

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]));
};

Agora, o loop é direto ( jpassa de rightIndexpara 0).

Finalmente, agora isso precisa ser testado:

  • pense em condições de contorno ( rightIndex == 0, rightIndex == array.size - 2)
  • pense em valueser menor array[0]ou maior quearray[rightIndex]
  • pense em valueser igual a array[0], array[rightIndex]ou algum índice do meio

Além disso, não subestime a difusão . Você possui asserções para detectar erros; portanto, gere uma matriz aleatória e classifique-a usando seu método. Se uma afirmação for acionada, você encontrou um erro e pode estender seu conjunto de testes.


8
@ CodeYogi: Com ... testes. O fato é que pode ser impraticável expressar tudo em afirmações: se a afirmação meramente repetir o código, não trará nada de novo (a repetição não ajuda na qualidade). É por isso que aqui não afirmei no loop que 0 <= j <= rightIndexou array[j] <= value, apenas repetiria o código. Por outro lado, is_sortedtraz uma nova garantia, portanto é valiosa. Depois, é para isso que servem os testes. Se você chamar insert([0, 1, 2], 2, 3)sua função e a saída não for, [0, 1, 2, 3]então você encontrou um erro.
Matthieu M.

3
@MatthieuM. não descarte o valor de uma asserção simplesmente porque ela duplica o código. De fato, essas podem ser afirmações muito valiosas se você decidir reescrever o código. O teste tem todo o direito de ser duplicado. Em vez disso, considere se a asserção está tão acoplada a uma implementação de código único que qualquer reescrita invalidaria a asserção. É quando você está perdendo seu tempo. Boa resposta, a propósito.
candied_orange

11
@CandiedOrange: duplicando o código, quero dizer literalmente array[j+1] = array[j]; assert(array[j+1] == array[j]);. Nesse caso, o valor parece muito baixo (é uma cópia / pasta). Se você duplicar o significado, mas expresso de outra maneira, ele se tornará mais valioso.
Matthieu M.

10
Regras de Hoare: ajudando a escrever loops corretos desde 1969. "Ainda que essas técnicas sejam conhecidas há mais de uma década, a maioria dos porgrammers nunca ouviu falar delas".
Joker_vD

11
@MatthieuM. Concordo que tem um valor muito baixo. Mas eu não acho que isso seja causado por ser uma cópia / pasta. Digamos que eu quisesse refatorar insert()para que funcionasse copiando de uma matriz antiga para uma nova. Isso pode ser feito sem quebrar os outros assert. Mas não este último. Apenas mostra o quão bem os seus outros assertforam projetados.
Candied_orange

29

Usar teste de unidade / TDD

Se você realmente precisar acessar seqüências através de um forloop, poderá evitar os erros através de testes de unidade e, principalmente , de desenvolvimento orientado a testes .

Imagine que você precise implementar um método que aceite valores superiores a zero, na ordem inversa. Em quais casos de teste você pôde pensar?

  1. Uma sequência contém um valor que é superior a zero.

    Actual: [5]. Espera: [5].

    A implementação mais direta que satisfaz os requisitos consiste em simplesmente retornar a sequência de origem ao chamador.

  2. Uma sequência contém dois valores, ambos superiores a zero.

    Actual: [5, 7]. Espera: [7, 5].

    Agora, você não pode simplesmente retornar a sequência, mas deve revertê-la. Você usaria um for (;;)loop, outra construção de linguagem ou um método de biblioteca não importa.

  3. Uma sequência contém três valores, um sendo zero.

    Actual: [5, 0, 7]. Espera: [7, 5].

    Agora você deve alterar o código para filtrar os valores. Novamente, isso pode ser expresso por meio de uma ifdeclaração ou de uma chamada para seu método de estrutura favorito.

  4. Dependendo do seu algoritmo (como esse é um teste de caixa branca, a implementação é importante), pode ser necessário lidar especificamente com o [] → []caso de sequência vazio , ou talvez não. Ou você pode garantir que o caso extremo em que todos os valores são negativos [-4, 0, -5, 0] → []é tratado corretamente, ou mesmo que os valores negativos de fronteira são: [6, 4, -1] → [4, 6]; [-1, 6, 4] → [4, 6]. Em muitos casos, no entanto, você terá apenas os três testes descritos acima: qualquer teste adicional não fará com que você altere seu código e, portanto, seria irrelevante.

Trabalhe em um nível mais alto de abstração

No entanto, em muitos casos, você pode evitar a maioria desses erros trabalhando em um nível de abstração mais alto, usando bibliotecas / estruturas existentes. Essas bibliotecas / estruturas permitem reverter, classificar, dividir e unir as seqüências, inserir ou remover valores em matrizes ou listas duplamente vinculadas, etc.

Geralmente, foreachpode ser usado em vez de for, tornando irrelevantes as condições de contorno: o idioma faz isso por você. Algumas linguagens, como Python, nem sequer têm a for (;;)construção, mas apenas for ... in ....

Em C #, o LINQ é particularmente conveniente ao trabalhar com sequências.

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

é muito mais legível e menos propenso a erros em comparação com sua forvariante:

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

    yield return value;
}

3
Bem, a partir da sua pergunta original, tenho a impressão de que, por um lado, a escolha é usar TDD e obter a solução correta, e por outro lado, pular a parte do teste e errar as condições de contorno.
Arseni Mourzenko

18
Obrigado por ser o único a mencionar o elefante na sala: não usando loops em tudo . Por que as pessoas ainda codificam como o seu 1985 (e eu estou sendo generoso) está além de mim. BOCTAOE.
Jared Smith

4
@JaredSmith Quando o computador executa esse código, quanto você quer apostar que não há instruções de salto lá? Ao usar o LINQ, você está abstraindo o loop, mas ele ainda está lá. Eu expliquei isso aos colegas de trabalho que não aprenderam sobre o trabalho duro de Shlemiel, o pintor . Não entender onde ocorrem os loops, mesmo se eles forem abstraídos no código e, como resultado, o código for significativamente mais legível, quase sempre leva a problemas de desempenho que não podemos explicar, e muito menos corrigir.
um CVn

6
@ MichaelKjörling: quando você usa LINQ, o loop está lá, mas uma for(;;)construção não seria muito descritiva desse loop . Um aspecto importante é que o LINQ (assim como a compreensão de listas em Python e elementos semelhantes em outras linguagens) torna as condições de contorno praticamente irrelevantes, pelo menos no escopo da pergunta original. No entanto, não posso concordar mais sobre a necessidade de entender o que acontece ocultamente ao usar o LINQ, especialmente quando se trata de uma avaliação lenta.
Arseni Mourzenko

4
@ MichaelKjörling Eu não estava necessariamente falando sobre LINQ e meio que falhei em entender seu ponto de vista. forEach, map, LazyIterator, Etc. são fornecidos pelo compilador ou tempo de execução ambiente da linguagem e são, indiscutivelmente, menos provável que seja andando de volta para o balde de tinta em cada iteração. Que, legibilidade e erros pontuais são os dois motivos pelos quais esses recursos foram adicionados às linguagens modernas.
21416 Jared Smith

15

Concordo com outras pessoas que dizem testar seu código. No entanto, também é bom acertar em primeiro lugar. Eu tenho uma tendência a errar nas condições de contorno em muitos casos, então desenvolvi truques mentais para evitar esses problemas.

Com uma matriz indexada 0, suas condições normais serão:

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

ou

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

Esses padrões devem se tornar uma segunda natureza, você não precisa pensar neles.

Mas nem tudo segue esse padrão exato. Portanto, se você não tiver certeza se escreveu corretamente, aqui está o próximo passo:

Conecte valores e avalie o código em seu próprio cérebro. Torne o pensamento mais simples possível. O que acontece se os valores relevantes forem 0s? O que acontece se eles são 1s?

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

No seu exemplo, você não tem certeza se deve ser [j] = valor ou [j + 1] = valor. Hora de começar a avaliar manualmente:

O que acontece se você tiver um comprimento de matriz 0? A resposta se torna óbvia: rightIndex deve ser (length - 1) == -1, para que j comece em -1, para inserir no índice 0, é necessário adicionar 1.

Portanto, provamos que a condição final está correta, mas não o interior do loop.

O que acontece se você tiver uma matriz com 1 elemento, 10 e tentarmos inserir um 5? Com um único elemento, rightIndex deve começar em 0. Portanto, na primeira vez no loop, j = 0, então "0> = 0 && 10> 5". Como queremos inserir o 5 no índice 0, os 10 devem ser movidos para o índice 1, então array [1] = array [0]. Como isso acontece quando j é 0, matriz [j + 1] = matriz [j + 0].

Se você tentar imaginar uma grande variedade e o que acontece inserindo-se em algum local arbitrário, seu cérebro provavelmente ficará sobrecarregado. Mas se você se ater a exemplos simples de tamanho 0/1/2, deve ser fácil fazer uma rápida corrida mental e ver onde suas condições de contorno se deterioram.

Imagine que você nunca ouviu falar do problema do poste antes e eu lhe digo que tenho 100 postes em uma linha reta, quantos segmentos entre eles. Se você tentar imaginar 100 postes na sua cabeça, ficará impressionado. Então, qual é o menor número de postes para fazer uma cerca válida? Você precisa de 2 para fazer uma cerca, então imagine 2 postagens, e a imagem mental de um único segmento entre as postagens deixa muito claro. Você não precisa ficar sentado contando mensagens e segmentos porque transformou o problema em algo intuitivamente óbvio para o seu cérebro.

Depois que você achar que está correto, é bom executá-lo através de um teste e garantir que o computador faça o que você acha que deveria, mas nesse momento deve ser apenas uma formalidade.


4
Eu realmente gosto for (int i = 0; i < length; i++). Quando entrei nesse hábito, parei de usar <=quase sempre e senti que os loops ficaram mais simples. Mas for (int i = length - 1; i >= 0; i--)parece excessivamente complicado, comparado com: for (int i=length; i--; )(o que provavelmente seria ainda mais sensato escrever como um whileloop, a menos que eu estivesse tentando fazer icom um escopo / vida menor). O resultado ainda é executado através do loop com i == length-1 (inicialmente) através de i == 0, com apenas diferença funcional sendo que a while()versão termina com i == -1 após o loop (se estiver ativo), em vez de i = = 0.
TOOGAM

2
@TOOGAM (int i = length; i--;) funciona em C / C ++ porque 0 é avaliado como falso, mas nem todos os idiomas têm essa equivalência. Eu acho que você poderia dizer eu-> 0.
Bryce Wagner

Naturalmente, se estiver usando um idioma que " > 0" requer a funcionalidade desejada, esses caracteres devem ser usados ​​porque são requeridos por esse idioma. Ainda assim, mesmo nesses casos, usar apenas o " > 0" é mais simples do que executar o processo de duas partes, primeiro subtraindo um e depois também usando " >= 0". Depois que aprendi que, com um pouco de experiência, adquiri o hábito de precisar usar o sinal de igual (por exemplo, " >= 0") em minhas condições de teste de loop com muito menos frequência, e o código resultante geralmente parecia mais simples desde então.
TOOGAM 18/04/2016

11
@BryceWagner, se você precisar i-- > 0, por que não experimentar a piada clássica i --> 0!
porglezomp

3
@porglezomp Ah, sim, o vai para operador . A maioria das linguagens do tipo C, incluindo C, C ++, Java e C #, possui essa.
um CVn

11

Fiquei muito frustrado por causa da falta de um modelo de computação correto na minha cabeça.

É um ponto muito interessante para esta pergunta e gerou este comentário: -

Há apenas uma maneira: entender melhor o seu problema. Mas isso é tão geral quanto a sua pergunta. - Thomas Junk

... e Thomas está certo. Não ter uma intenção clara de uma função deve ser uma bandeira vermelha - uma indicação clara de que você deve PARAR imediatamente, pegar um lápis e papel, se afastar do IDE e resolver o problema adequadamente; ou pelo menos sanidade - verifique o que você fez.

Eu já vi tantas funções e classes que se tornaram uma bagunça completa porque os autores tentaram definir a implementação antes de definirem o problema completamente. E é tão fácil de lidar.

Se você não entender completamente o problema, também é improvável que você esteja codificando a solução ideal (em termos de eficiência ou clareza), nem poderá criar testes de unidade genuinamente úteis em uma metodologia TDD.

Tome seu código aqui como exemplo, ele contém várias falhas em potencial que você ainda não considerou, por exemplo: -

  • e se rightIndex estiver muito baixo? (dica: ele vai envolver a perda de dados)
  • e se rightIndex estiver fora dos limites da matriz? (você receberá uma exceção ou acabou de criar um estouro de buffer?)

Existem alguns outros problemas relacionados ao desempenho e design do código ...

  • esse código precisará ser escalado? Manter a matriz classificada é a melhor opção ou você deve procurar outras opções (como uma lista vinculada?)
  • você pode ter certeza de suas suposições? (você pode garantir que a matriz seja classificada e se não for?)
  • você está reinventando a roda? Matrizes ordenadas são um problema bem conhecido. Você estudou as soluções existentes? Já existe uma solução disponível no seu idioma (como SortedList<t>em C #)?
  • você deve copiar manualmente uma entrada de matriz por vez? ou sua linguagem fornece funções comuns como a de JScript Array.Insert(...)? esse código seria mais claro?

Existem várias maneiras de melhorar esse código, mas até que você tenha definido corretamente o que você precisa fazer, você não estará desenvolvendo o código, apenas o cortando na esperança de que funcione. Invista o tempo e sua vida ficará mais fácil.


2
Mesmo se você estiver passando seus índices para uma função existente (como Array.Copy), ainda será necessário pensar para que as condições associadas sejam corretas. Imaginar o que acontece em situações de 0 e 1 comprimento e 2 de comprimento pode ser a melhor maneira de garantir que você não esteja copiando muito ou pouco.
perfil completo de Bryce Wagner

@BryceWagner - Absolutamente verdade, mas sem uma idéia clara de qual é o problema que você está resolvendo, você passará muito tempo se debatendo no escuro em uma estratégia de 'acerto e esperança' que é de longe o OP. maior problema neste momento.
James Snell

2
@ CodeYogi - você e, como indicado por outras pessoas, dividiu o problema em subproblemas de maneira bastante ruim, e é por isso que várias respostas mencionam sua abordagem para resolver o problema como a maneira de evitá-lo. Não é algo que você deva considerar pessoalmente, apenas a experiência daqueles que já estiveram lá.
James Snell

2
@ CodeYogi, acho que você pode ter confundido este site com Stack Overflow. Este site é equivalente a uma sessão de perguntas e respostas em um quadro branco , não em um terminal de computador. "Mostre-me o código" é uma indicação bastante explícita de que você está no site errado.
Curinga

2
@Wildcard +1: "Mostre-me o código" é, para mim, um excelente indicador de por que essa resposta está correta e que talvez eu precise trabalhar em maneiras de demonstrar melhor que é um problema de fator humano / design que só pode ser tratado por mudanças no processo humano - nenhuma quantidade de código poderia ensiná-lo.
James Snell

10

Erros de um por um são um dos erros de programação mais comuns. Até desenvolvedores experientes entendem isso errado algumas vezes. Linguagens de nível superior geralmente têm construções de iteração como foreachou mapque evitam a indexação explícita por completo. Mas, às vezes, você precisa de uma indexação explícita, como no seu exemplo.

O desafio é como pensar em intervalos de células da matriz. Sem um modelo mental claro, torna-se confuso quando incluir ou excluir os pontos finais.

Ao descrever intervalos de matriz, a convenção é incluir o limite inferior, excluir o limite superior . Por exemplo, o intervalo 0..3 são as células 0,1,2. Essas convenções são usadas em idiomas indexados a 0, por exemplo, o slice(start, end)método em JavaScript retorna a sub-matriz que começa com índice startaté, mas não inclui, o índice end.

É mais claro quando você pensa nos índices de intervalo como descrevendo as bordas entre as células da matriz. A ilustração abaixo é uma matriz de comprimento 9, e os números abaixo das células são alinhados às bordas e é o que é usado para descrever os segmentos da matriz. Por exemplo, é claro na ilustração que o intervalo 2..5 é as células 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

Esse modelo é consistente em ter o comprimento da matriz como o limite superior de uma matriz. Uma matriz com comprimento 5 possui células 0..5, o que significa que existem as cinco células 0,1,2,3,4. Isso também significa que o comprimento de um segmento é o limite superior menos o limite inferior, ou seja, o segmento 2..5 possui 5-2 = 3 células.

Ter esse modelo em mente ao iterar para cima ou para baixo torna muito mais claro quando incluir ou excluir os pontos finais. Ao iterar para cima, você precisa incluir o ponto inicial, mas excluir o ponto final. Ao iterar para baixo, você precisa excluir o ponto inicial (o limite superior), mas incluir o ponto final (o limite inferior).

Como você está iterando para baixo no seu código, você precisa incluir o limite inferior, 0, para iterar enquanto j >= 0.

Dado isso, sua escolha de ter o rightIndexargumento representa o último índice no subarray quebra a convenção. Isso significa que você precisa incluir os dois pontos de extremidade (0 e rightIndex) na iteração. Também dificulta a representação do segmento vazio (que você precisa quando inicia a classificação). Você realmente precisa usar -1 como rightIndex ao inserir o primeiro valor. Isso parece bastante antinatural. Parece mais natural ter rightIndexindicado o índice após o segmento, então 0 representa o segmento vazio.

É claro que seu código é extremamente confuso porque expande a sub-matriz classificada com uma, substituindo o item imediatamente após a sub-matriz inicialmente classificada. Então você lê o índice j, mas escreve o valor em j + 1. Aqui você deve deixar claro que j é a posição no sub-arranjo inicial, antes da inserção. Quando as operações de índice ficam complicadas, isso me ajuda a fazer um diagrama em um pedaço de papel quadriculado.


4
@ CodeYogi: eu desenharia uma pequena matriz como uma grade em um pedaço de papel e depois passaria por uma iteração do loop manualmente com um lápis. Isso me ajuda a esclarecer o que realmente acontece, por exemplo, que um intervalo de células é deslocado para a direita e onde o novo valor é inserido.
precisa saber é o seguinte

3
"há duas coisas difíceis na ciência da computação: invalidação de cache, nomeação de coisas e erros pontuais".
Digital Trauma

11
@ CodeYogi: Adicionado um pequeno diagrama para mostrar o que estou falando.
precisa saber é o seguinte

11
Excelente insight, especialmente se você vale a pena ler os dois últimos pares, a confusão também se deve à natureza do loop for, mesmo que eu encontre o índice certo que o loop diminui j uma vez antes do término e, portanto, me leva um passo atrás.
código é o seguinte

11
Muito. Excelente. Responda. E eu acrescentaria que essa convenção de índice inclusivo / exclusivo também é motivada pelo valor de myArray.Lengthou myList.Count- que é sempre um a mais que o índice "mais à direita" baseado em zero. ... O comprimento da explicação esconde a aplicação prática e simples dessas heurísticas de codificação explícita. A multidão TL; DR está perdendo.
Radarbob

5

A introdução à sua pergunta me faz pensar que você não aprendeu a codificar corretamente. Qualquer pessoa que esteja programando em uma linguagem imperativa por mais de algumas semanas deve realmente estar acertando seus limites de loop pela primeira vez em mais de 90% dos casos. Talvez você esteja se apressando para começar a codificar antes de pensar o suficiente sobre o problema.

Sugiro que você corrija essa deficiência (re) aprendendo a escrever loops - e recomendo algumas horas trabalhando em vários loops com papel e lápis. Tire uma tarde de folga para fazer isso. Depois, gaste 45 minutos ou mais por dia trabalhando no tópico até você realmente entender.

Está tudo muito bem testando, mas você deve estar testando na expectativa de que você geralmente acerte seus limites de loop (e o resto do seu código).


4
Eu não seria tão assertivo sobre as habilidades do OP. Cometer erros de fronteira é fácil, especialmente em um contexto estressante, como uma entrevista de contratação. Um desenvolvedor experiente poderia cometer esses erros também, mas, obviamente, um desenvolvedor experiente evitaria esses erros em primeiro lugar através de testes.
Arseni Mourzenko

3
@MainMa - Eu acho que enquanto Mark poderia ter sido mais sensível, acho que ele estava certo - Há estresse na entrevista e apenas hackear códigos juntos sem a devida consideração para definir o problema. A forma como a questão está redigida pontos muito fortemente para o último e isso é algo que pode ser melhor resolvido a longo prazo por certificando-se de que você tem uma base sólida, não por mexer com o IDE
James Snell

@JamesSnell Acho que você está super confiante em si mesmo. Olhe o código e me diga o que faz você pensar que está sub-documentado? Se você vê claramente, não há nenhum lugar mencionado que eu não possa resolver o problema? Eu só queria saber como evitar repetir o mesmo erro. Eu acho que você obtém todo o seu programa correto de uma só vez.
código é o seguinte

4
@CodeYogi Se você está tendo que fazer 'tentativa e erro' e está 'ficando frustrado' e 'cometendo os mesmos erros' com a sua codificação, esses são sinais de que você não entendeu bem o seu problema antes de começar a escrever . Ninguém está dizendo que você não entendeu, mas que seu código poderia ter sido melhor pensado e são sinais de que você está tendo problemas, dos quais tem a opção de aprender e aprender, ou não.
James Snell

2
@CodeYogi ... e desde que você pergunta, eu raramente entendo mal meus laços e ramificações porque faço questão de entender claramente o que preciso alcançar antes de escrever o código, não é difícil fazer algo simples como um classe de matriz. Como programador, uma das coisas mais difíceis de fazer é admitir que você é o problema, mas até fazer isso, você não começará a escrever um código realmente bom.
James Snell

3

Talvez eu deva colocar um pouco de carne no meu comentário:

Há apenas uma maneira: entender melhor o seu problema. Mas isso é tão geral quanto sua pergunta

Seu ponto é

Embora eu tenha acertado minhas suposições após algumas tentativas e erros, fiquei muito frustrado por causa da falta de um modelo de computação correto na minha cabeça.

Quando eu leio trial and error, meus sinos de alarme começam a tocar. É claro que muitos de nós conhecemos o estado de espírito, quando se deseja resolver um pequeno problema e envolveu outras coisas e começou a adivinhar, de uma ou de outra maneira, que o código seemdeveria fazer o que deveria fazer. Algumas soluções hackish surgem disso - e algumas são pura genialidade ; mas para ser honesto: a maioria deles não é . Eu inclusive, conhecendo esse estado.

Independentemente do seu problema concreto, você fez perguntas sobre como melhorar:

1) Teste

Isso foi dito por outros e eu não teria nada valioso a acrescentar

2) Análise de Problemas

É difícil dar alguns conselhos para isso. Há apenas duas dicas que eu poderia lhe dar, o que provavelmente o ajudará a melhorar suas habilidades nesse tópico:

  • o óbvio e o mais trivial é, a longo prazo, o mais eficaz: resolva muitos problemas. Enquanto pratica e repete, você desenvolve uma mentalidade que o ajuda para tarefas futuras. Programar é como qualquer outra atividade a ser aprimorada pelo trabalho árduo

O Code Katas é uma maneira, o que pode ajudar um pouco.

Como você se torna um ótimo músico? Ajuda a conhecer a teoria e a entender a mecânica do seu instrumento. Ajuda a ter talento. Mas, finalmente, a grandeza vem da prática; aplicando a teoria repetidamente, usando feedback para melhorar a cada vez.

Code Kata

Um site que eu gosto muito: Code Wars

Alcance o domínio através do desafio Melhore suas habilidades treinando com outras pessoas em desafios reais de código

São problemas relativamente pequenos, que ajudam a aprimorar suas habilidades de programação. E o que eu mais gosto no Code Wars é que você pode comparar sua solução com uma das outras .

Ou talvez, você deve dar uma olhada no Exercism.io, onde obtém feedback da comunidade.

  • O outro conselho é quase tão trivial: Aprenda a resolver problemas Você precisa se treinar, dividindo-os em problemas realmente pequenos. Se você diz que tem problemas ao escrever loops , comete o erro, que vê o loop como uma construção inteira e não o desconstrói em pedaços. Se você aprender a desmontar as coisas passo a passo , aprenderá a evitar esses erros.

Eu sei - como eu disse acima, às vezes você está em tal estado - que é difícil dividir coisas "simples" em tarefas mais "simples"; mas ajuda muito.

Lembro que, quando aprendi a programar profissionalmente , tive grandes problemas com a depuração do meu código. Qual era o problema? Hybris - O erro não pode estar nessa e em tal região do código, porque eu sei que não pode estar. E em conseqüência? Eu vasculhei o código em vez de analisá-lo e tive que aprender - mesmo que fosse tedioso quebrar meu código para obter instruções .

3) Desenvolver um cinto de ferramentas

Além de conhecer sua linguagem e suas ferramentas - eu sei que essas são as coisas brilhantes sobre as quais os desenvolvedores pensam primeiro - aprendem algoritmos (também conhecidos como leitura).

Aqui estão dois livros para começar:

É como aprender algumas receitas para começar a cozinhar. No começo, você não sabe o que fazer, então precisa procurar o que os chefs anteriores prepararam para você. O mesmo vale para algortihms. Os algoritmos são como cozinhar receitas para refeições comuns (estruturas de dados, classificação, hash etc.) Se você os conhece (pelo menos tente) de cor, você tem um bom ponto de partida.

3a) Conheça construções de programação

Este ponto é um derivado - por assim dizer. Conheça o seu idioma - e melhor: saiba quais construções são possíveis no seu idioma.

Às vezes, um ponto comum para código ruim ou ineficiente é que o programador não sabe a diferença entre os diferentes tipos de loops ( for-, while-e do-loops). Eles são de alguma maneira todos intercambiáveis ​​e utilizáveis; mas, em algumas circunstâncias, escolher outra construção em loop leva a um código mais elegante.

E existe o dispositivo de Duff ...

PS:

caso contrário, seu comentário não é bom que Donal Trump.

Sim, devemos tornar a codificação excelente novamente!

Um novo lema para Stackoverflow.


Ah, deixe-me dizer uma coisa muito a sério. Fiz tudo o que você mencionou e posso até fornecer meu link nesses sites. Mas o que está ficando frustrante para mim é que, em vez de obter a resposta da minha pergunta, estou recebendo todos os conselhos possíveis sobre programação. Apenas um cara mencionou pre-postcondições até agora e eu aprecio isso.
código é o seguinte

Pelo que você diz, é difícil imaginar onde está o seu problema. Talvez uma metáfora ajude: para mim, é como dizer "como posso ver" - a resposta óbvia para mim é "use os olhos", porque ver é tão natural para mim que não consigo imaginar como alguém não pode ver. O mesmo vale para sua pergunta.
Thomas Junk

Concordo completamente com os alarmes em "tentativa e erro". Penso que a melhor maneira de aprender completamente a mentalidade de resolução de problemas é executar algoritmos e códigos com papel e lápis.
Curinga

Hum ... por que você tem um espetáculo gramaticalmente ruim em um candidato político citado sem contexto no meio de sua resposta sobre programação?
Curinga

2

Se eu entendi o problema corretamente, sua pergunta é como pensar em obter loops desde a primeira tentativa, não como garantir que seu loop esteja correto (para o qual a resposta seria testada conforme explicado em outras respostas).

O que considero uma boa abordagem é escrever a primeira iteração sem nenhum loop. Depois de fazer isso, observe o que deve ser alterado entre as iterações.

É um número, como um 0 ou um 1? Então você provavelmente precisará de um para, e bingo, você também terá o seu i. Em seguida, pense quantas vezes você deseja executar a mesma coisa e também terá sua condição final.

Se você não sabe EXATAMENTE quantas vezes ele será executado, não precisará de um tempo, mas de um tempo ou de um tempo.

Tecnicamente, qualquer loop pode ser traduzido para qualquer outro loop, mas o código é mais fácil de ler se você usar o loop correto, então aqui estão algumas dicas:

  1. Se você está escrevendo um if () {...; break;} dentro de um for, precisa de um tempo e já tem a condição

  2. "While" é talvez o loop mais usado em qualquer idioma, mas não deve ser imo. Se você estiver escrevendo bool ok = True; enquanto (marque) {faça ​​alguma coisa e espero que mude ok em algum momento}; então você não precisa de um tempo, mas de um tempo, porque significa que você tem tudo o que precisa para executar a primeira iteração.

Agora um pouco de contexto ... Quando aprendi a programar (Pascal), não falava inglês. Para mim, "for" e "while" não fazia muito sentido, mas a palavra-chave "repeat" (faça enquanto em C) é quase a mesma na minha língua materna, então eu a usaria para tudo. Na minha opinião, a repetição (do while) é o loop mais natural, porque quase sempre você quer que algo seja feito e, em seguida, deseja que seja feito novamente e novamente, até que um objetivo seja alcançado. "For" é apenas um atalho que fornece um iterador e coloca estranhamente a condição no início do código, mesmo que, quase sempre, você queira que algo seja feito até que algo aconteça. Além disso, while é apenas um atalho para if () {do while ()}. Atalhos são bons para mais tarde,


2

Vou dar um exemplo mais detalhado de como usar condições pré / pós e invariantes para desenvolver um loop correto. Juntas, essas afirmações são chamadas de especificação ou contrato.

Não estou sugerindo que você tente fazer isso para cada loop. Mas espero que você ache útil ver o processo de pensamento envolvido.

Para isso, traduzirei seu método em uma ferramenta chamada Microsoft Dafny , projetada para provar a exatidão de tais especificações. Ele também verifica o término de cada loop. Observe que o Dafny não possui um forloop, então tive que usar um whileloop.

Por fim, mostrarei como você pode usar essas especificações para projetar uma versão, indiscutivelmente, um pouco mais simples do seu loop. Esta versão mais simples do loop, de fato, tem a condição do loop j > 0e a atribuição array[j] = value- como foi sua intuição inicial.

Dafny nos provará que esses dois loops estão corretos e fazem a mesma coisa.

Farei então uma afirmação geral, com base na minha experiência, sobre como escrever um loop reverso correto, que talvez o ajude a enfrentar essa situação no futuro.

Parte Um - Escrevendo uma Especificação para o Método

O primeiro desafio que enfrentamos é determinar o que o método realmente deve fazer. Para esse fim, projetei condições pré e pós que especificam o comportamento do método. Para tornar a especificação mais exata, aprimorei o método para fazê-lo retornar o índice onde valuefoi inserido.

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 especificação captura totalmente o comportamento do método. Minha principal observação sobre essa especificação é que seria simplificado se o procedimento passasse o valor em rightIndex+1vez de rightIndex. Mas como não consigo ver de onde esse método é chamado, não sei que efeito essa mudança teria no restante do programa.

Parte Dois - determinando um loop invariável

Agora temos uma especificação para o comportamento do método, temos que adicionar uma especificação do comportamento do loop que convencerá Dafny de que a execução do loop será encerrada e resultará no estado final desejado de array.

O seguinte é o seu loop original, traduzido para a sintaxe do Dafny, com os invariantes do loop adicionados. Também mudei para retornar o índice onde o valor foi inserido.

{
    // 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
}

Isso verifica em Dafny. Você pode vê-lo seguindo este link . Portanto, seu loop implementa corretamente a especificação de método que escrevi na parte um. Você precisará decidir se essa especificação de método é realmente o comportamento que você deseja.

Observe que Dafny está produzindo uma prova de correção aqui. Essa é uma garantia de correção muito mais forte do que a que pode ser obtida pelo teste.

Parte três - um loop mais simples

Agora que temos uma especificação de método que captura o comportamento do loop. Podemos modificar com segurança a implementação do loop, mantendo a confiança de que não alteramos o comportamento do loop.

Modifiquei o loop para que ele corresponda às suas intuições originais sobre a condição do loop e o valor final de j. Eu argumentaria que esse loop é mais simples que o loop que você descreveu na sua pergunta. É mais frequentemente capaz de usar jdo que j+1.

  1. Comece j em rightIndex+1

  2. Altere a condição do loop para j > 0 && arr[j-1] > value

  3. Altere a atribuição para arr[j] := value

  4. Reduza o contador do loop no final do loop em vez do início

Aqui está o código. Observe que os invariantes de loop também são um pouco mais fáceis de escrever agora:

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 Quatro - conselhos sobre loop reverso

Depois de ter escrito e provado que estava correto em muitos ciclos ao longo de alguns anos, tenho o seguinte conselho geral sobre fazer um loop para trás.

É quase sempre mais fácil pensar e escrever um loop para trás (decrescente) se o decremento for realizado no início do loop e não no final.

Infelizmente, a forconstrução do loop em muitos idiomas dificulta isso.

Suspeito (mas não posso provar) que essa complexidade foi o que causou a diferença na sua intuição sobre o que o loop deveria ser e o que realmente precisava ser. Você está acostumado a pensar em loops avançados (incrementais). Quando você deseja escrever um loop para trás (decrescente), tenta criar o loop, tentando reverter a ordem em que as coisas acontecem em um loop para frente (incrementando). Mas, devido à maneira como a forconstrução funciona, você negligenciou para reverter a ordem da atribuição e atualização da variável de loop - o que é necessário para uma verdadeira reversão da ordem das operações entre um loop para trás e para frente.

Parte Cinco - bônus

Apenas para completar, eis o código que você obtém se passar rightIndex+1para o método em vez de rightIndex. Essa alteração elimina todos os +2deslocamentos necessários para pensar sobre a correção do loop.

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 aprecio um comentário se você downvote
flamingpenguin

2

Fazer isso com facilidade e fluência é uma questão de experiência. Mesmo que o idioma não permita que você o expresse diretamente, ou você esteja usando um caso mais complexo do que o simples item interno pode suportar, o que você acha que é um nível mais alto como "visite cada elemento uma vez em ordem de reverência" e o codificador mais experiente traduz isso nos detalhes certos instantaneamente, porque ele fez isso tantas vezes.

Mesmo assim, em casos mais complexos, é fácil errar, porque o que você está escrevendo normalmente não é o comum. Com linguagens e bibliotecas mais modernas, você não escreve a coisa mais fácil porque existe uma construção enlatada ou exige isso. Em C ++, o mantra moderno é "use algoritmos em vez de escrever código".

Portanto, a maneira de garantir que esteja certo, para esse tipo de coisa em particular, é observar as condições de contorno . Rastrear o código em sua cabeça para os poucos casos nas bordas de onde as coisas mudam. Se o index == array-maxque acontece? Que tal max-1? Se o código fizer uma curva errada, ele estará em um desses limites. Alguns loops precisam se preocupar com o primeiro ou o último elemento, bem como com a construção de loop, acertando os limites; por exemplo, se você se refere a[I]e o a[I-1]que acontece quando Ié o valor mínimo?

Além disso, observe os casos em que o número de iterações (corretas) é extremo: se os limites forem atingidos e você tiver 0 iterações, será que isso funcionará sem um caso especial? E quanto a apenas uma iteração, em que o limite mais baixo também é o mais alto ao mesmo tempo?

Examinar os casos de borda (ambos os lados de cada borda) é o que você deve fazer ao escrever o loop e o que deve fazer nas revisões de código.


1

Vou tentar ficar longe dos tópicos mencionados em abundância.

O que são ferramentas / modelos mentais para evitar esses erros?

Ferramentas

Para mim, a maior ferramenta para escrever melhor fore fazer whileloops não é escrever nenhum forou fazer whileloops.

A maioria das linguagens modernas tenta direcionar esse problema de uma maneira ou de outra. Por exemplo, Java, apesar de ter Iteratordesde o início, que costumava ser um pouco desajeitado de usar, introduziu uma sintaxe de atalho para usá-los mais facilmente em uma versão mais recente. O C # também os possui, etc.

Minha linguagem atualmente favorita, Ruby, adotou a abordagem funcional ( .each, .mapetc.) de frente para frente. Isso é muito poderoso. Acabei de fazer uma contagem rápida em algumas bases de código do Ruby em que estou trabalhando: em cerca de 10.000 linhas de código, existem zero fore cerca de 5 while.

Se eu fosse forçado a escolher um novo idioma, procurar loops funcionais / baseados em dados como esse seria muito alto na lista de prioridades.

Modelos mentais

Lembre-se de que whileé o mínimo mínimo de abstração possível, apenas um passo acima goto. Na minha opinião, forpiora ainda mais, em vez de melhorar, pois divide as três partes do loop firmemente.

Portanto, se estou em um ambiente em que foré usado, garanto que todas as três partes sejam simples e sempre iguais. Isso significa que eu vou escrever

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

Mas nada muito mais complexo. Talvez, talvez eu tenha uma contagem regressiva às vezes, mas farei o possível para evitá-la.

Se estiver usando while, evito travessuras internas complicadas que envolvam a condição do loop. O teste dentro do teste while(...)será o mais simples possível, e evitarei breako melhor que puder. Além disso, o loop será curto (contando linhas de código) e qualquer quantidade maior de código será fatorada.

Especialmente se a condição while for complexa, usarei uma "variável de condição" que é muito fácil de detectar e não colocarei a condição na whileprópria declaração:

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

(Ou algo assim, na medida correta, é claro.)

Isso fornece um modelo mental muito fácil, que é "essa variável está executando de 0 a 10 monotonamente" ou "esse loop é executado até que a variável seja falsa / verdadeira". A maioria dos cérebros parece capaz de lidar com esse nível de abstração muito bem.

Espero que ajude.


1

Loops reversos, em particular, podem ser difíceis de raciocinar, porque muitas de nossas linguagens de programação são direcionadas para a iteração direta, tanto na sintaxe comum do loop for quanto pelo uso de intervalos semiabertos baseados em zero. Não estou dizendo que é errado que os idiomas tenham feito essas escolhas; Só estou dizendo que essas escolhas complicam o pensamento sobre loops reversos.

Em geral, lembre-se de que um loop for é apenas açúcar sintático construído em torno de um loop while:

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

é equivalente a:

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

(possivelmente com uma camada extra de escopo para manter as variáveis ​​declaradas na etapa init local ao loop).

Isso é bom para muitos tipos de loops, mas a última etapa é desajeitada quando você está andando para trás. Ao trabalhar para trás, acho mais fácil iniciar o índice do loop com o valor após o desejado e mover a stepparte para o topo do loop, assim:

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
}

ou, como um loop for:

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

Isso pode parecer irritante, pois a expressão da etapa está no corpo e não no cabeçalho. Esse é um efeito colateral lamentável da polarização direta inerente na sintaxe do loop for. Por isso, alguns argumentam que você faz o seguinte:

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

Mas isso é um desastre se sua variável de índice for um tipo não assinado (como pode ser em C ou C ++).

Com isso em mente, vamos escrever sua função de inserção.

  1. Como trabalharemos para trás, deixaremos o índice de loop ser a entrada após o slot de matriz "atual". Eu projetaria a função para levar o tamanho do número inteiro ao invés de um índice para o último elemento, porque os intervalos semi-abertos são a maneira natural de representar os intervalos na maioria das linguagens de programação e porque nos oferece uma maneira de representar um array vazio sem recorrer para um valor mágico como -1.

    function insert(array, size, value) {
      var j = size;
    
  2. Enquanto o novo valor for menor que o elemento anterior , continue mudando. Claro, o elemento anterior pode ser verificado somente se não é um elemento anterior, para que primeiro tem que verificar que não estamos bem no começo:

      while (j != 0 && value < array[j - 1]) {
        --j;  // now j become current
        array[j + 1] = array[j];
      }
    
  3. Isso deixa jexatamente onde queremos o novo valor.

      array[j] = value; 
    };
    

A programação de pérolas de Jon Bentley fornece uma explicação muito clara sobre o tipo de inserção (e outros algoritmos), o que pode ajudar a criar seus modelos mentais para esses tipos de problemas.


0

Você está simplesmente confuso sobre o que um forloop realmente faz e a mecânica de como ele funciona?

for(initialization; condition; increment*)
{
    body
}
  1. Primeiro, a inicialização é executada
  2. Então a condição é verificada
  3. Se a condição for verdadeira, o corpo será executado uma vez. Se não, vá para o 6
  4. O código de incremento é executado
  5. Goto # 2
  6. Fim do loop

Pode ser útil reescrever esse código usando uma construção diferente. Aqui está o mesmo usando um loop while:

initialization
while(condition)
{
    body
    increment
}

Aqui estão algumas outras sugestões:

  • Você pode usar outra construção de linguagem como um loop foreach? Isso cuida da condição e da etapa de incremento para você.
  • Você pode usar uma função Mapa ou Filtro? Alguns idiomas têm funções com esses nomes que passam por uma coleção interna para você. Você apenas fornece a coleção e o corpo.
  • Você realmente deve gastar mais tempo se familiarizando com forloops. Você os usará o tempo todo. Sugiro que você percorra um loop for em um depurador para ver como ele é executado.

* Observe que, enquanto eu uso o termo "incremento", são apenas alguns códigos que estão depois do corpo e antes da verificação da condição. Pode ser um decréscimo ou nada.


11
por que o voto negativo?
user2023861

0

Tentativa de insight adicional

Para algoritmos não triviais com loops, você pode tentar o seguinte método:

  1. Crie uma matriz fixa com 4 posições e insira alguns valores para simular o problema;
  2. Escreva seu algoritmo para resolver o problema em questão , sem nenhum loop e com indexações codificadas ;
  3. Depois disso, substitua as indexações codificadas no código por alguma variável iou j, e aumente / diminua essas variáveis ​​conforme necessário (mas ainda sem nenhum loop);
  4. Reescreva seu código e coloque os blocos repetitivos dentro de um loop , atendendo às condições pré e pós;
  5. [ opcional ] reescreva seu loop para estar na forma que você deseja (por / enquanto / faz enquanto);
  6. O mais importante é escrever seu algoritmo corretamente; depois disso, refatorar e otimizar seu código / loops, se necessário (mas isso pode tornar o código não trivial para o leitor)

Seu problema

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

Escreva o corpo do loop manualmente várias vezes

Vamos usar uma matriz fixa com 4 posições e tentar escrever o algoritmo manualmente, sem loops:

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

Reescreva, removendo 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

Traduzir para um loop

Com 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;
    }
}

Refatore / reescreva / otimize o loop da maneira que desejar:

Com 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

com 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

PS: o código assume que a entrada é válida e esse array não contém repetições;


-1

É por isso que eu evitaria escrever loops que operam em índices brutos, em favor de operações mais abstratas, sempre que possível.

Nesse caso, eu faria algo assim (em pseudo código):

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

-3

No seu exemplo, o corpo do loop é bastante óbvio. E é óbvio que algum elemento precisa ser alterado no final. Então você escreve o código, mas sem o início do loop, a condição final e a atribuição final.

Então você se afasta do código e descobre qual é a primeira jogada que precisa ser executada. Você altera o início do loop para que o primeiro movimento seja correto. Você se afasta do código novamente e descobre qual é a última jogada que precisa ser executada. Você altera a condição final para que o último movimento esteja correto. E, finalmente, você se afasta do seu código e descobre qual deve ser a atribuição final e corrige o código de acordo.


11
Você pode colocar algum exemplo?
CodeYogi

O OP teve um exemplo.
precisa saber é o seguinte

2
O que você quer dizer? Eu sou o OP.
precisa saber é o seguinte
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.