Как правильно писать циклы?


65

В большинстве случаев при написании циклов я обычно пишу неправильные граничные условия (например, неверный результат) или мои предположения о завершении цикла неверны (например, бесконечно работающий цикл). Хотя после некоторых проб и ошибок мои предположения были правильными, я слишком расстроился из-за отсутствия правильной вычислительной модели в моей голове.

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

Первоначально я допустил следующие ошибки:

  1. Вместо j> = 0 я сохранил это j> 0.
  2. Запутался ли массив [j + 1] = значение или массив [j] = значение.

Каковы инструменты / ментальные модели, чтобы избежать таких ошибок?


6
При каких обстоятельствах вы считаете j >= 0это ошибкой? Я был бы более настороженно относятся к тому , что вы обращаетесь array[j]и array[j + 1]без предварительной проверки , что array.length > (j + 1).
Бен Коттрелл

5
Сродни тому, что сказал @LightnessRacesinOrbit, вы, вероятно, решаете проблемы, которые уже были решены. Вообще говоря, любой цикл, который вам нужно выполнить над структурой данных, уже существует в каком-то основном модуле или классе ( Array.prototypeв примере с JS). Это предотвращает возникновение граничных условий, поскольку что-то подобное mapработает на всех массивах. Вы можете решить вышеизложенное, используя slice и concat, чтобы избежать зацикливания всего: codepen.io/anon/pen/ZWovdg?editors=0012 Самый правильный способ написать цикл - вообще не писать его.
Джед Шнайдер

13
На самом деле, идти вперед и решать решаемые проблемы. Это называется практикой. Только не надо их публиковать. То есть, если вы не найдете способ улучшить решения. Тем не менее, изобретать колесо включает в себя больше, чем колесо. Он включает в себя целую систему контроля качества колес и поддержку клиентов. Тем не менее, пользовательские диски хороши.
candied_orange

53
Боюсь, что мы движемся не в том направлении. Давать CodeYogi дерьмо, потому что его пример является частью хорошо известного алгоритма, довольно безосновательно. Он никогда не утверждал, что изобрел что-то новое. Он спрашивает, как избежать некоторых очень распространенных ошибок при написании цикла. Библиотеки прошли долгий путь, но я все еще вижу будущее для людей, которые знают, как писать циклы.
candied_orange

5
В общем, когда вы имеете дело с циклами и индексами, вы должны понимать, что индексы указывают между элементами и знакомиться с полуоткрытыми интервалами (на самом деле это две стороны одного и того же понятия). Как только вы получите эти факты, большая часть циклов / индексов исчезает полностью.
Matteo Italia

Ответы:


208

Контрольная работа

Нет, серьезно, тест.

Я занимаюсь программированием более 20 лет и до сих пор не верю себе, что правильно написал цикл с первого раза. Я пишу и запускаю тесты, которые доказывают, что это работает, прежде чем я подозреваю, что это работает. Проверьте каждую сторону каждого граничного условия. Например, что rightIndexиз 0 должно делать что? Как насчет -1?

Будь проще

Если другие сразу не видят, что он делает, ты делаешь это слишком сложно. Пожалуйста, не стесняйтесь игнорировать производительность, если это означает, что вы можете написать что-то простое для понимания. Делайте это быстрее только в том маловероятном случае, который вам действительно нужен. И даже тогда, только когда вы абсолютно уверены, что точно знаете, что вас тормозит. Если вы можете добиться реального улучшения Big O, это действие может быть бессмысленным, но даже в этом случае сделайте свой код максимально читабельным.

От одного

Знайте разницу между подсчетом пальцев и подсчетом промежутков между пальцами. Иногда пробелы - это то, что действительно важно. Не позволяйте своим пальцам отвлекать вас. Знайте, является ли ваш большой палец пальцем. Знайте, считается ли разрыв между вашим мизинцем и большим пальцем пробелом.

Комментарии

Прежде чем потеряться в коде, постарайтесь сказать, что вы имеете в виду на английском языке. Четко изложите свои ожидания. Не объясняйте, как работает код. Объясните, почему у вас есть то, что он делает. Храните детали реализации из этого. Должна быть возможность рефакторинга кода без необходимости изменения комментария.

Лучший комментарий - это доброе имя.

Если вы можете сказать все, что вам нужно, с добрым именем, НЕ говорите это снова с комментарием.

Абстракции

Объекты, функции, массивы и переменные - это абстракции, которые настолько хороши, насколько им даны имена. Дайте им имена, которые гарантируют, что, когда люди заглянут внутрь, они не будут удивлены тем, что они найдут.

Короткие имена

Используйте короткие имена для недолговечных вещей. iэто хорошее имя для индекса в хорошем узком цикле в небольшом объеме, что делает его смысл очевидным. Если iжизнь длится достаточно долго, чтобы распространяться по строке за строкой с другими идеями и именами, с которыми можно спутать, iто пришло время дать iхорошее длинное объяснительное имя.

Длинные имена

Никогда не сокращайте имя просто из соображений длины строки. Найдите другой способ выложить свой код.

Пробелы

Дефекты любят прятать в нечитаемый код. Если ваш язык позволяет вам выбрать стиль отступа, по крайней мере, будьте последовательны. Не делайте ваш код похожим на поток шума. Код должен выглядеть так, как будто он находится в процессе формирования.

Петлевые конструкции

Изучите и просмотрите структуры петель на вашем языке. Наблюдение за выделением for(;;)цикла отладчиком может быть очень поучительным. Изучите все формы. while, do while, while(true), for each. Используйте самый простой, который вы можете избежать. Посмотрите на прокачку насоса . Узнайте , что breakи continueделать , если у вас есть. Знайте разницу между c++и ++c. Не бойтесь возвращаться рано, если вы всегда закрываете все, что нужно закрыть. Наконец, блокирует или, предпочтительно, что-то, что помечает его для автоматического закрытия при открытии: Использование оператора / Попробуйте с ресурсами .

Альтернативные петли

Пусть что-то еще делает цикл, если вы можете. На глазах легче и уже отлажено. Они бывают разных форм: коллекции или потоки , которые позволяют map(), reduce(), foreach(), и другие подобные методы , которые применяют лямбда. Ищите специальные функции, как Arrays.fill(). Существует также рекурсия, но только ожидать, чтобы облегчить ситуацию в особых случаях. Обычно не используйте рекурсию, пока не увидите, как будет выглядеть альтернатива.

Ох, и проверить.

Тест, тест, тест.

Я упоминал тестирование?

Была еще одна вещь. Не могу вспомнить Началось с T ...


36
Хороший ответ, но, возможно, стоит упомянуть тестирование. Как справиться с бесконечным циклом в модульном тесте? Разве такой цикл не «ломает» тесты ???
GameAlchemist

139
@GameAlchemist Это тест на пиццу. Если мой код не перестает работать в то время, когда мне нужно сделать пиццу, я начинаю подозревать, что что-то не так. Конечно, это не решит проблему остановки Алана Тьюринга, но, по крайней мере, я получу пиццу с сделки.
candied_orange

12
@CodeYogi - на самом деле, это может быть очень близко. Начните с теста, который оперирует одним значением. Реализуйте код без цикла. Затем напишите тест, который оперирует двумя значениями. Реализуйте цикл. Это очень маловероятно , что вы получите граничное условие неправильно на петле , если вы делаете это так, потому что почти во всех обстоятельствах ни один или другой из этих двух тестов потерпит неудачу , если вы сделаете такую ошибку.
Жюль

15
@CodeYogi Чувак, вся заслуга TDD, но Тестирование >> TDD. Вывод значения может быть тестированием, получение второго взгляда на ваш код - тестирование (вы можете формализовать это как обзор кода, но я часто просто беру кого-то за 5-минутную беседу). Тест - это любой шанс, который вы дадите выражению своего намерения потерпеть неудачу. Черт возьми, вы можете проверить свой код, поговорив с мамой об идее. Я обнаружил ошибки в своем коде, когда смотрел на плитку в душе. TDD - эффективная формализованная дисциплина, которую вы не найдете в каждом магазине. Я никогда не кодировал нигде, где люди не тестировали.
candied_orange

12
Я программировал и тестировал годы и годы, прежде чем услышал о TDD. Только теперь я понимаю, как соотносятся эти годы с годами, потраченными на программирование без штанов.
candied_orange

72

При программировании полезно подумать о:

и при изучении неисследованной территории (например, манипулирования с индексами) может быть очень и очень полезно не просто думать о них, а фактически делать их явными в коде с утверждениями .

Давайте возьмем ваш оригинальный код:

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

И проверьте, что у нас есть:

  • предварительное условие: array[0..rightIndex]отсортировано
  • постусловие: array[0..rightIndex+1]отсортировано
  • инвариант: 0 <= j <= rightIndexно кажется немного избыточным; или как указано @Jules в комментариях, в конце «раунда» for n in [j, rightIndex+1] => array[j] > value.
  • инвариант: в конце «раунда» array[0..rightIndex+1]сортируется

Таким образом, вы можете сначала написать is_sortedфункцию, а также minфункцию, работающую со срезом массива, а затем подтвердить:

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

Существует также тот факт, что ваше условие цикла немного сложнее; Вы можете захотеть сделать это проще для себя, разделив вещи на части:

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

Теперь цикл прост ( jидет от rightIndexк 0).

Наконец, теперь это нужно проверить:

  • думать о граничных условиях ( rightIndex == 0, rightIndex == array.size - 2)
  • думать о том, valueчтобы быть меньше array[0]или больше, чемarray[rightIndex]
  • думать value, равным array[0], array[rightIndex]или какой - то средний индекс

Также не стоит недооценивать пух . У вас есть утверждения для выявления ошибок, поэтому создайте случайный массив и отсортируйте его, используя свой метод. Если утверждение сработало, вы обнаружили ошибку и можете расширить набор тестов.


8
@CodeYogi: С ... тестами. Дело в том, что нецелесообразно выражать все в утверждениях: если утверждение просто повторяет код, то оно не приносит ничего нового (повторение не помогает с качеством). Вот почему здесь я не утверждал в цикле, что, 0 <= j <= rightIndexили array[j] <= value, он просто повторил бы код. С другой стороны, is_sortedприносит новую гарантию, поэтому она ценна. После этого для этого и нужны тесты. Если вы вызываете insert([0, 1, 2], 2, 3)свою функцию, а результат - нет, [0, 1, 2, 3]то вы нашли ошибку.
Матье М.

3
@MatthieuM. не сбрасывайте со счетов значение утверждения просто потому, что оно дублирует код. Фактически, это могут быть очень ценные утверждения, если вы решите переписать код. Тестирование имеет полное право быть дублирующим. Скорее рассмотрите, является ли утверждение настолько связанным с единственной реализацией кода, что любое переписывание аннулирует утверждение. Вот когда ты тратишь свое время. Хороший ответ, кстати.
candied_orange

1
@CandiedOrange: дублируя код, я имею в виду буквально array[j+1] = array[j]; assert(array[j+1] == array[j]);. В этом случае значение кажется очень низким (это копирование / вставка). Если вы дублируете значение, но выражаете его по-другому, оно становится более ценным.
Матье М.

10
Правила Хоара: помогать писать правильные циклы с 1969 года. «Тем не менее, хотя эти методы известны уже более десяти лет, большинство программистов никогда не слышали о них».
Joker_vD

1
@MatthieuM. Я согласен, что это имеет очень низкую ценность. Но я не думаю, что это вызвано копированием / вставкой. Скажем, я хотел провести рефакторинг, insert()чтобы он работал путем копирования из старого массива в новый. Это можно сделать, не нарушая других assert. Но не этот последний. Просто показывает, насколько хорошо assertбыли разработаны ваши другие .
candied_orange

29

Используйте юнит-тестирование / TDD

Если вам действительно нужен доступ к последовательностям через forцикл, вы можете избежать ошибок с помощью модульного тестирования , и особенно разработки, управляемой тестированием .

Представьте, что вам нужно реализовать метод, который принимает значения, превосходящие ноль, в обратном порядке. Какие тестовые случаи вы могли бы подумать?

  1. Последовательность содержит одно значение, которое больше нуля.

    Фактический: [5]. Ожидаемый: [5].

    Самая простая реализация, которая удовлетворяет требованиям, состоит в простом возврате исходной последовательности вызывающей стороне.

  2. Последовательность содержит два значения, оба превосходят ноль.

    Фактический: [5, 7]. Ожидаемый: [7, 5].

    Теперь вы не можете просто вернуть последовательность, но вы должны полностью изменить ее. Будете ли вы использовать for (;;)цикл, другую языковую конструкцию или метод библиотеки, не имеет значения.

  3. Последовательность содержит три значения, одно из которых равно нулю.

    Фактический: [5, 0, 7]. Ожидаемый: [7, 5].

    Теперь вы должны изменить код для фильтрации значений. Опять же, это можно выразить с помощью ifоператора или вызова вашего любимого метода структуры.

  4. В зависимости от вашего алгоритма (так как это тестирование белого ящика, реализация имеет значение), вам может понадобиться обрабатывать [] → []случай с пустой последовательностью , а может и нет. Или вы можете гарантировать , что крайний случай , когда все значения отрицательны [-4, 0, -5, 0] → []обрабатываются правильно, или даже что граничные отрицательные значения: [6, 4, -1] → [4, 6]; [-1, 6, 4] → [4, 6]. Однако во многих случаях у вас будет только три теста, описанных выше: любой дополнительный тест не заставит вас изменить код, и поэтому не будет иметь значения.

Работа на более высоком уровне абстракции

Однако во многих случаях вы можете избежать большинства этих ошибок, работая на более высоком уровне абстракции, используя существующие библиотеки / фреймворки. Эти библиотеки / фреймворки позволяют возвращать, сортировать, разбивать и объединять последовательности, вставлять или удалять значения в массивах или списках с двойной связью и т. Д.

Обычно foreachего можно использовать вместо forпроверки граничных условий, что не имеет значения: язык делает это за вас. Некоторые языки, такие как Python, даже не имеют for (;;)конструкции, но только for ... in ....

В C # LINQ особенно удобен при работе с последовательностями.

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

гораздо более читабелен и менее подвержен ошибкам по сравнению с его forвариантом:

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

    yield return value;
}

3
Итак, из вашего первоначального вопроса у меня сложилось впечатление, что выбор, с одной стороны, заключается в использовании TDD и получении правильного решения, а с другой стороны, пропуская тестовую часть и получая неправильные граничные условия.
Арсений Мурзенко

18
Спасибо за то , что один упомянуть слон в комнате: не используя петлю на всех . Почему люди до сих пор пишут код, как в 1985 году (а я щедрый), мне не под силу. BOCTAOE.
Джаред Смит

4
@JaredSmith Как только компьютер действительно выполнит этот код, сколько вы хотите поспорить, что там нет инструкции перехода? Используя LINQ, вы абстрагируете цикл, но он все еще там. Я объяснил это коллегам, которые не смогли узнать о тяжелой работе художника Шлемеля . Неспособность понять, где происходят циклы, даже если они абстрагированы в коде и в результате код значительно более читабелен, в результате почти неизменно возникают проблемы с производительностью, которые трудно объяснить, не говоря уже о том, чтобы их исправить.
CVn

6
@ MichaelKjörling: при использовании LINQ, то цикл есть, но конструкция будет не очень описательный этого цикла . Важным аспектом является то, что LINQ (а также списки в Python и подобные элементы в других языках) делает граничные условия в основном неактуальными, по крайней мере, в рамках исходного вопроса. Однако я не могу согласиться с тем, что нужно понимать, что происходит под капотом при использовании LINQ, особенно когда речь идет о ленивой оценке. for(;;)
Арсений Мурзенко

4
@ MichaelKjörling Я не обязательно говорил о LINQ, и я не понимаю твою точку зрения. forEach, map, LazyIteratorИ т.д., при условии , компилятор или среда выполнения этого языка и , возможно , менее вероятно, будет идти назад к ведру краски на каждую итерации. Это - читабельность и ошибки «по одному» - две причины, по которым эти функции были добавлены в современные языки.
Джаред Смит

15

Я согласен с другими людьми, которые говорят, проверить свой код. Тем не менее, это также хорошо, чтобы сделать это правильно в первую очередь. Во многих случаях я склонен неправильно понимать граничные условия, поэтому я разработал умственные приемы для предотвращения подобных проблем.

С массивом с 0 индексами ваши нормальные условия будут:

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

или же

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

Эти шаблоны должны стать второй натурой, вам вообще не нужно думать о них.

Но не все следует именно этому образцу. Так что, если вы не уверены, что написали правильно, вот ваш следующий шаг:

Вставьте значения и оцените код в своем собственном мозгу. Сделайте так, чтобы о них думали как можно проще. Что произойдет, если соответствующие значения равны 0? Что произойдет, если они 1 с?

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

В вашем примере вы не уверены, должно ли это быть [j] = значение или [j + 1] = значение. Время начать оценивать это вручную:

Что произойдет, если у вас длина массива 0? Ответ становится очевидным: rightIndex должен быть (length - 1) == -1, поэтому j начинается с -1, поэтому для вставки с индексом 0 необходимо добавить 1.

Таким образом, мы доказали правильность конечного условия, но не внутри цикла.

Что произойдет, если у вас есть массив с 1 элементом, 10, и мы пытаемся вставить 5? С одним элементом rightIndex должно начинаться с 0. Итак, в первый раз в цикле j = 0, поэтому «0> = 0 && 10> 5». Поскольку мы хотим вставить 5 в индекс 0, 10 нужно переместить в индекс 1, поэтому array [1] = array [0]. Так как это происходит, когда j равно 0, массив [j + 1] = массив [j + 0].

Если вы попытаетесь представить какой-то большой массив и что произойдет, вставив его в произвольное место, ваш мозг, вероятно, будет перегружен. Но если вы будете придерживаться простых примеров размером 0/1/2, то будет легко сделать быстрый мысленный анализ и увидеть, где нарушаются ваши граничные условия.

Представьте, что вы никогда раньше не слышали о проблеме столба забора, и я говорю вам, что у меня 100 столбов забора по прямой линии, сколько отрезков между ними. Если вы попытаетесь представить 100 постов забора в вашей голове, вы просто ошеломлены. Так что же самое меньшее количество постов, чтобы сделать правильный забор? Вам нужно 2, чтобы сделать забор, так что представьте 2 поста, и мысленный образ одного сегмента между постами прояснит это. Вам не нужно сидеть и считать посты и сегменты, потому что вы превратили проблему в нечто интуитивно понятное для вашего мозга.

Как только вы думаете, что это правильно, тогда хорошо пройти тест и убедиться, что компьютер делает то, что вы думаете, что должно, но на этом этапе это должно быть просто формальностью.


4
Мне очень нравится for (int i = 0; i < length; i++). Как только я вошел в эту привычку, я перестал использовать <=почти так же часто, и почувствовал, что петли стали проще. Но for (int i = length - 1; i >= 0; i--)кажется слишком сложным, по сравнению с: for (int i=length; i--; )(что, вероятно, было бы еще более разумно записать в виде whileцикла, если бы я не пытался сделать так i, чтобы область действия была меньше). Результат по-прежнему проходит через цикл с i == length-1 (изначально) через i == 0, с той лишь функциональной разницей, что while()версия заканчивается на i == -1 после цикла (если он продолжается) вместо i = = 0.
ТООГАМ

2
@TOOGAM (int i = length; i--;) работает в C / C ++, потому что 0 оценивается как ложное, но не все языки имеют такую ​​эквивалентность. Я думаю, вы могли бы сказать, что я--> 0.
Брайс Вагнер

Естественно, если вы используете язык, который требует " > 0", чтобы получить желаемую функциональность, то такие символы следует использовать, потому что они требуются этим языком. Тем не менее, даже в этих случаях просто использовать « > 0» проще, чем выполнить процесс, состоящий из двух частей: сначала вычесть один, а затем также использовать « >= 0». После того, как я узнал об этом, благодаря небольшому опыту, я привык к необходимости использовать знак равенства (например, " >= 0") в моих тестовых циклах гораздо реже, и с тех пор полученный код, как правило, чувствовал себя проще.
TOOGAM

1
@ БрайсВагнер, если вам нужно сделать i-- > 0, почему бы не попробовать классическую шутку i --> 0,!
porglezomp

3
@porglezomp Ах, да, переходит к оператору . Большинство C-подобных языков, включая C, C ++, Java и C #, имеют такой.
CVn

11

Я слишком расстроился из-за отсутствия правильной вычислительной модели в моей голове.

Это очень интересный момент к этому вопросу, и он породил этот комментарий:

Есть только один способ: лучше понять свою проблему. Но это так же, как и ваш вопрос. - Томас Джанк

... и Томас прав. Отсутствие четкого намерения для функции должно быть красным флажком - явным признаком того, что вы должны немедленно ОСТАНОВИТЬСЯ, схватить карандаш и бумагу, отойти от IDE и правильно решить проблему; или, по крайней мере, рассудок - проверьте, что вы сделали.

Я видел так много функций и классов, которые превратились в полный беспорядок, потому что авторы пытались определить реализацию до того, как полностью определили проблему. И с этим так легко иметь дело.

Если вы не до конца понимаете проблему, вы также вряд ли будете кодировать оптимальное решение (с точки зрения эффективности или ясности), и при этом вы не сможете создавать действительно полезные модульные тесты в методологии TDD.

Возьмите ваш код здесь в качестве примера, он содержит ряд потенциальных недостатков, которые вы еще не рассмотрели, например: -

  • что если rightIndex слишком низкий? (подсказка: это повлечет за собой потерю данных)
  • что если rightIndex находится за пределами массива? (Вы получите исключение или вы просто создали переполнение буфера?)

Есть несколько других проблем, связанных с производительностью и дизайном кода ...

  • этот код нужно масштабировать? Сохранение отсортированного массива - лучший вариант, или вы должны посмотреть на другие варианты (например, связанный список?)
  • Можете ли вы быть уверены в своих предположениях? (Можете ли вы гарантировать сортировку массива, а что, если это не так?)
  • ты изобретаешь колесо? Сортированные массивы - это хорошо известная проблема. Вы изучали существующие решения? Есть ли решение, уже доступное на вашем языке (например, SortedList<t>в C #)?
  • Вы должны вручную копировать одну запись массива за раз? или ваш язык предоставляет общие функции, такие как JScript Array.Insert(...)? этот код будет более понятным?

Существует множество способов улучшить этот код, но до тех пор, пока вы правильно не определите, что вам нужно для этого кода, вы не разрабатываете код, вы просто взламываете его вместе в надежде, что он будет работать. Вложите в это время, и ваша жизнь станет легче.


2
Даже если вы передаете свои индексы в существующую функцию (например, Array.Copy), все равно может потребоваться продуманность для получения правильных связанных условий. Представление того, что происходит в ситуациях с 0 длиной и 1 длиной и 2 длинами, может быть лучшим способом убедиться, что вы не копируете слишком мало или слишком много.
Брайс Вагнер

@BryceWagner - Абсолютно верно, но без четкого представления о том, что проблема в том, что вы на самом деле решаете, вы будете тратить много времени на то, чтобы копаться в темноте в стратегии «хит и надежда», которая, безусловно, является ОП самая большая проблема на данный момент.
Джеймс Снелл

2
@CodeYogi - у вас есть, и, как указали другие, вы довольно плохо разбили проблему на подзадачи, поэтому в ряде ответов упоминается ваш подход к решению проблемы как способ ее избежать. Это не то, что вы должны принять лично, просто опыт тех из нас, кто был там.
Джеймс Снелл

2
@ CodeYogi, я думаю, вы перепутали этот сайт с переполнением стека. Этот сайт является эквивалентом сеанса вопросов и ответов на доске , а не на компьютерном терминале. «Покажите мне код» - это довольно явное указание на то, что вы находитесь не на том сайте.
Wildcard

2
@Wildcard +1: «Покажи мне код», для меня это отличный показатель того, почему этот ответ правильный, и что, возможно, мне нужно поработать над тем, чтобы лучше продемонстрировать, что это проблема человеческого фактора / дизайна, которая может только реагировать на изменения в человеческом процессе - никакое количество кода не может этому научить.
Джеймс Снелл

10

Одиночные ошибки - одна из самых распространенных ошибок программирования. Даже опытные разработчики иногда ошибаются. Языки более высокого уровня обычно имеют итерационные конструкции, такие как foreachили mapкоторые вообще избегают явной индексации. Но иногда вам нужна явная индексация, как в вашем примере.

Задача состоит в том, как определить диапазон ячеек массива. Без четкой ментальной модели становится непонятным, когда включать или исключать конечные точки.

При описании диапазонов массива принято включать нижнюю границу, исключать верхнюю границу . Например, диапазон 0..3 - это ячейки 0,1,2. Это соглашение используется во всех языках с 0 индексами, например, slice(start, end)метод в JavaScript возвращает подмассив, начиная с индекса start, но не включая индекс end.

Это становится понятнее, когда вы думаете об индексах диапазона как об описании границ между ячейками массива. На рисунке ниже показан массив длиной 9, а числа под ячейками выровнены по краям и используются для описания сегментов массива. Например, из иллюстрации ясно, что диапазон 2.,5 - это ячейки 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

Эта модель согласуется с тем, чтобы длина массива была верхней границей массива. Массив длиной 5 имеет ячейки 0..5, что означает наличие пяти ячеек 0,1,2,3,4. Это также означает, что длина сегмента является верхней границей минус нижняя граница, то есть сегмент 2..5 имеет 5-2 = 3 ячейки.

Имея в виду эту модель при переборе вверх или вниз, становится намного понятнее, когда включать или исключать конечные точки. При выполнении итерации вверх вам нужно включить начальную точку, но исключить конечную точку. При выполнении итерации вниз необходимо исключить начальную точку (верхняя граница), но включить конечную точку (нижняя граница).

Поскольку в вашем коде вы выполняете итерацию вниз, вам нужно включить нижнюю границу 0, поэтому вы выполняете итерацию while j >= 0.

Учитывая это, ваш выбор, чтобы rightIndexаргумент представлял последний индекс в подмассиве, нарушает соглашение. Это означает, что вы должны включить обе конечные точки (0 и rightIndex) в итерации. Это также затрудняет представление пустого сегмента (который необходим при запуске сортировки). Вы фактически должны использовать -1 как rightIndex при вставке первого значения. Это кажется довольно неестественным. Кажется более естественным rightIndexуказывать индекс после сегмента, поэтому 0 представляет пустой сегмент.

Конечно, ваш код очень запутанный, потому что он расширяет отсортированный подмассив на один, перезаписывая элемент сразу после первоначально отсортированного подмассива. Таким образом, вы читаете из индекса j, но записывает значение в j + 1. Здесь вам должно быть ясно, что j - это позиция в начальном подмассиве перед вставкой. Когда операции с индексами становятся слишком сложными, это помогает мне изобразить это на листе сетки.


4
@CodeYogi: Я бы нарисовал небольшой массив в виде сетки на листе бумаги, а затем вручную прошел карандашную итерацию цикла. Это помогает мне прояснить, что на самом деле происходит, например, что диапазон ячеек смещен вправо и куда вставляется новое значение.
JacquesB

3
«В компьютерной науке есть две сложные вещи: аннулирование кэша, присвоение имен и ошибки типа« один на один ».»
Цифровая травма

1
@CodeYogi: Добавлена ​​небольшая диаграмма, чтобы показать, о чем я говорю.
JacquesB

1
Отличное понимание, особенно то, что вы читаете последние два раздела, путаница также связана с природой цикла for, даже если я нахожу правильный индекс, цикл уменьшается на j один раз до завершения и, следовательно, возвращает меня на шаг назад.
CodeYogi

1
Очень. Отлично. Ответ. И я бы добавил, что это соглашение об инклюзивном / эксклюзивном индексе также мотивируется значением myArray.Lengthили myList.Count- которое всегда на единицу больше, чем «самый правый» индекс, начинающийся с нуля. ... Длина объяснения противоречит практическому и простому применению этих явных эвристик кодирования. TL; DR толпа отсутствует.
Радар Боб

5

Введение в ваш вопрос заставляет меня думать, что вы не научились правильно кодировать. Любой, кто программирует на императивном языке более нескольких недель, должен действительно правильно установить границы цикла в более чем 90% случаев. Возможно, вы спешите начать кодирование до того, как вы достаточно продумали проблему.

Я предлагаю вам исправить этот недостаток путем (пере) обучения написанию циклов - и я бы рекомендовал несколько часов проработать ряд циклов с бумагой и карандашом. Возьмите выходной, чтобы сделать это. Затем потратьте 45 минут или около того дня, работая над темой, пока вы действительно не получите ее.

Это все очень хорошо тестирует, но вы должны тестировать, ожидая, что вы, как правило, получите правильные границы цикла (и остальную часть кода).


4
Я бы не стал так настойчиво относиться к навыкам ОП. Делать пограничные ошибки легко, особенно в стрессовом контексте, например при приеме на работу. Опытный разработчик также может делать эти ошибки, но, очевидно, опытный разработчик избежит таких ошибок в первую очередь путем тестирования.
Арсений Мурзенко

3
@MainMa - я думаю, что, хотя Марк мог быть более чувствительным, я думаю, что он прав - стресс на собеседовании и просто взламывание кода вместе без должного рассмотрения проблемы. То, как сформулирован вопрос, очень сильно указывает на последнее, и это то, что лучше всего решить в долгосрочной перспективе, если у вас есть прочная основа, а не хакерство в IDE
Джеймс Снелл

@JamesSnell Я думаю, вы становитесь более уверенными в себе. Посмотрите на код и скажите мне, что заставляет вас думать, что оно недостаточно документировано? Если вы ясно видите, нигде не упоминается, что я не смог решить проблему? Я просто хотел знать, как избежать повторения одной и той же ошибки. Я думаю, что вы получите всю свою программу правильно за один раз.
CodeYogi

4
@CodeYogi Если вам приходится делать «методом проб и ошибок», и вы «расстраиваетесь» и «делаете те же ошибки» с вашим кодированием, то это признаки того, что вы не достаточно хорошо поняли свою проблему до того, как начали писать , Никто не говорит, что вы этого не поняли, но ваш код мог быть лучше продуман, и это признаки того, что вы боретесь, и у вас есть выбор, на что вы можете пойти и поучиться, или нет.
Джеймс Снелл

2
@CodeYogi ... и так как вы спрашиваете, я редко ошибаюсь в своих циклах и ветвлениях, потому что я имею четкое понимание того, что мне нужно достичь, прежде чем писать код, нетрудно сделать что-то простое, вроде сортировки класс массива. Как программисту, одна из самых трудных задач - признать, что это проблема, но пока вы не сделаете это, вы не начнете писать действительно хороший код.
Джеймс Снелл

3

Возможно, я должен добавить немного мяса в свой комментарий:

Есть только один способ: лучше понять свою проблему. Но это так же, как ваш вопрос

Ваша точка зрения

Хотя после некоторых проб и ошибок мои предположения были правильными, я слишком расстроился из-за отсутствия правильной вычислительной модели в моей голове.

Когда я читаю trial and error, у меня начинают звонить мои будильники. Конечно, многие из нас знают состояние ума, когда кто-то хочет решить небольшую проблему, обдумывает другие вещи и начинает так или иначе угадывать, чтобы заставить код seemделать то, что должен делать. Некоторые хакерские решения выходят из этого - и некоторые из них - чистый гений ; но если честно: большинство из них нет . Меня включили, зная это состояние.

Независимо от вашей конкретной проблемы, вы задавали вопросы о том, как улучшить:

1) Тест

Это было сказано другими, и мне нечего добавить

2) Анализ проблем

Трудно дать какой-то совет по этому поводу. Я могу дать вам только два совета, которые, вероятно, помогут вам улучшить свои навыки в этой области:

  • очевидный и самый тривиальный в долгосрочной перспективе является наиболее эффективным: решить многие проблемы. Практикуя и повторяя, вы развиваете мышление, которое помогает вам в будущих задачах. Программирование, как и любая другая деятельность, должно быть улучшено с помощью усердной практики.

Код Katas - это способ, который может немного помочь.

Как ты становишься великим музыкантом? Это помогает узнать теорию и понять механику вашего инструмента. Это помогает иметь талант. Но в конечном итоге величие приходит от практики; применяя теорию снова и снова, используя обратную связь, чтобы становиться лучше каждый раз.

Код Ката

Один сайт, который мне очень нравится: Code Wars

Достигните мастерства с помощью задач. Совершенствуйте свои навыки, обучая других задачам, связанным с реальным кодом

Это относительно небольшие проблемы, которые помогут вам отточить свои навыки программирования. И что мне больше всего нравится в Code Wars, так это то, что вы можете сравнить свое решение с одним из других .

Или, может быть, вы должны взглянуть на Exercism.io, где вы получите обратную связь от сообщества.

  • Другой совет почти так же тривиален: учитесь разбивать проблемы Вы должны тренироваться, разбивая проблемы на действительно мелкие проблемы. Если вы говорите, что у вас проблемы с написанием циклов , вы делаете ошибку, что вы видите цикл как целостную конструкцию и не разбиваете его на части. Если вы научитесь разбирать вещи шаг за шагом , вы научитесь избегать таких ошибок.

Я знаю - как я уже говорил выше, иногда вы находитесь в таком состоянии - что трудно разбить «простые» вещи на более «мертвые простые» задачи; но это очень помогает.

Я помню, когда я впервые научился профессиональному программированию, у меня были огромные проблемы с отладкой кода. В чем была проблема? Hybris - Ошибка не может быть в той и другой области кода, потому что я знаю, что это не может быть. И как следствие? Я пробежался по коду вместо того, чтобы анализировать его, который мне пришлось выучить - даже если было утомительно разбивать мой код на инструкции для инструкций .

3) Разработка Toolbelt

Помимо знания вашего языка и ваших инструментов - я знаю, что это блестящие вещи, о которых разработчики думают в первую очередь, - изучение алгоритмов (чтение).

Вот две книги для начала:

Это похоже на изучение некоторых рецептов, чтобы начать готовить. Сначала вы не знаете, что делать, поэтому вам нужно посмотреть, что приготовили для вас предыдущие повара . То же самое касается алгоритмов. Алгоритмы подобны приготовлению рецептов для обычных блюд (структуры данных, сортировка, хеширование и т. Д.). Если вы знаете их (по крайней мере, пытаетесь) наизусть, у вас есть хорошая отправная точка.

3a) Знать программные конструкции

Эта точка является производной - так сказать. Знайте свой язык - и лучше: знайте, какие конструкции возможны на вашем языке.

Общая точка для плохой или inefficent коды иногда, что программист не знает разницу между различными типами петель ( for-, while-и do-loops). Как-то все они взаимозаменяемы; но в некоторых случаях выбор другой циклической конструкции приводит к более элегантному коду.

И есть устройство Даффа ...

PS:

в противном случае ваш комментарий не лучше, чем Донал Трамп.

Да, мы должны сделать кодирование снова великолепным!

Новый девиз для Stackoverflow.


Ах, позвольте мне сказать вам одну вещь очень серьезно. Я делаю все, что вы упомянули, и я даже могу дать вам свою ссылку на этих сайтах. Но меня расстраивает то, что вместо ответа на мой вопрос я получаю всевозможные советы по программированию. Только один парень упомянул pre-postусловия до сих пор, и я ценю это.
CodeYogi

Из того, что вы говорите, трудно представить, где ваша проблема. Возможно, метафора помогает: для меня это все равно, что сказать «как я могу видеть» - очевидный ответ для меня - «использовать свои глаза», потому что зрение для меня настолько естественно, что я не могу представить, как никто не может видеть. То же самое касается вашего вопроса.
Томас Джанк

Полностью согласен с тревожными звонками о «проб и ошибках». Я думаю, что лучший способ полностью изучить мышление решения проблем - это запуск алгоритмов и кода с помощью бумаги и карандаша.
Wildcard

Хм ... почему у вас есть несеквитурный грамматически плохой удар по политическому кандидату, цитируемому без контекста в середине вашего ответа о программировании?
Wildcard

2

Если я правильно понял проблему, ваш вопрос заключается в том, как правильно продумать циклы с первой попытки, а не в том, чтобы убедиться, что ваш цикл верен (для чего ответ будет проверяться, как объяснено в других ответах).

Что я считаю хорошим подходом, так это написать первую итерацию без какого-либо цикла. После того, как вы это сделали, вы должны заметить, что следует менять между итерациями.

Это число, например, 0 или 1? Тогда вам, скорее всего, нужно для, и бинго, у вас также есть стартовый я. Затем подумайте, сколько раз вы хотите запустить одно и то же, и у вас также будет конечное состояние.

Если вы не знаете ТОЧНО, сколько раз он будет работать, тогда вам не понадобится время, но какое-то время.

Технически, любой цикл может быть преобразован в любой другой цикл, но код легче прочитать, если вы используете правильный цикл, поэтому вот несколько советов:

  1. Если вы пишете if () {...; break;} внутри for, вам нужно некоторое время, и у вас уже есть условие

  2. «Хотя», возможно, является наиболее часто используемым циклом в любом языке, но это не должно быть imo. Если вы обнаружите, что пишете bool ok = True; while (check) {сделайте что-нибудь и, надеюсь, в какой-то момент измените ok}; тогда вам не нужно время, а время, потому что это означает, что у вас есть все необходимое для запуска первой итерации.

Теперь немного контекста ... Когда я впервые научился программировать (Паскаль), я не говорил по-английски. Для меня слова «for» и «while» не имели особого смысла, но ключевое слово «repeat» (do while in C) на моем родном языке почти одинаково, поэтому я бы использовал его для всего. По моему мнению, повторить (делать пока) - это наиболее естественный цикл, потому что почти всегда вы хотите, чтобы что-то было сделано, а затем вы хотите, чтобы это было сделано снова и снова, пока не будет достигнута цель. «For» - это просто ярлык, который дает вам итератор и странно помещает условие в начало кода, даже если почти всегда вы хотите что-то сделать, пока что-то не произойдет. Кроме того, while это просто ярлык для if () {do while ()}. Ярлыки хороши на потом,


2

Я собираюсь привести более подробный пример того, как использовать предварительные / последующие условия и инварианты для разработки правильного цикла. Вместе такие утверждения называются спецификацией или контрактом.

Я не предлагаю вам пытаться делать это для каждого цикла. Но я надеюсь, что вам будет полезно увидеть процесс мышления.

Для этого я переведу ваш метод в инструмент под названием Microsoft Dafny , который призван доказать правильность таких спецификаций. Он также проверяет завершение каждого цикла. Обратите внимание, что у Dafny нет forцикла, поэтому мне пришлось использовать whileцикл вместо этого.

Наконец, я покажу, как вы можете использовать такие спецификации для разработки, возможно, немного более простой версии вашего цикла. Эта более простая версия цикла в действительности имеет условие цикла j > 0и присваивание array[j] = value- как было в вашей первоначальной интуиции.

Дафни докажет нам, что обе эти петли верны и делают одно и то же.

Затем я сделаю общее утверждение, основываясь на моем опыте, о том, как написать правильный обратный цикл, который, возможно, поможет вам, если вы столкнетесь с этой ситуацией в будущем.

Часть первая - Написание спецификации для метода

Первая проблема, с которой мы сталкиваемся, - это определение того, что метод должен делать. Для этого я разработал предварительные и последующие условия, которые определяют поведение метода. Чтобы сделать спецификацию более точной, я усовершенствовал метод, чтобы он возвращал индекс, в который valueбыл вставлен.

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..])

Эта спецификация полностью отражает поведение метода. Мое основное замечание по поводу этой спецификации состоит в том, что она будет упрощена, если процедуре будет передано значение, rightIndex+1а не rightIndex. Но так как я не могу понять, откуда вызывается этот метод, я не знаю, какое влияние это изменение окажет на остальную часть программы.

Часть вторая - определение инварианта цикла

Теперь у нас есть спецификация для поведения метода, мы должны добавить спецификацию поведения цикла, которая убедит Дафни, что выполнение цикла завершится и приведет к желаемому конечному состоянию array.

Ниже приведен исходный цикл, переведенный в синтаксис Dafny с добавленными инвариантами цикла. Я также изменил его, чтобы он возвращал индекс, в который было вставлено значение.

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

Это подтверждается в Дафни. Вы можете увидеть это сами, перейдя по этой ссылке . Таким образом, ваш цикл правильно реализует спецификацию метода, которую я написал в первой части. Вам нужно будет решить, действительно ли эта спецификация метода соответствует желаемому.

Обратите внимание, что Дафни дает здесь подтверждение правильности. Это гораздо более надежная гарантия правильности, чем может быть получена путем тестирования.

Часть третья - более простая петля

Теперь, когда у нас есть спецификация метода, которая фиксирует поведение цикла. Мы можем безопасно изменить реализацию цикла, сохраняя при этом уверенность в том, что мы не изменили поведение цикла.

Я изменил цикл так, чтобы он соответствовал вашей первоначальной интуиции относительно условия цикла и конечного значения j. Я бы сказал, что этот цикл проще, чем тот, который вы описали в своем вопросе. Это чаще можно использовать j, чем j+1.

  1. Начать с j rightIndex+1

  2. Измените условие цикла на j > 0 && arr[j-1] > value

  3. Измените назначение на arr[j] := value

  4. Уменьшите счетчик цикла в конце цикла, а не в начале

Вот код Обратите внимание, что инварианты цикла также несколько проще написать:

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

Часть четвертая - совет по обратной петле

После того, как я написал и подтвердил правильность многих циклов в течение нескольких лет, у меня есть следующий общий совет о циклическом обратном цикле.

Почти всегда легче думать и писать обратный (убывающий) цикл, если декремент выполняется в начале цикла, а не в конце.

К сожалению, forконструкция цикла во многих языках делает это трудным.

Я подозреваю (но не могу доказать), что эта сложность - то, что вызвало разницу в вашей интуиции о том, каким должен быть цикл и каким он должен быть на самом деле. Вы привыкли думать о прямых (инкрементных) циклах. Когда вы хотите написать обратный (убывающий) цикл, вы пытаетесь создать цикл, пытаясь изменить порядок, в котором все происходит в прямом (увеличивающемся) цикле. Но из-за того, как forработает конструкция, вы забыли изменить порядок присваивания и обновления переменной цикла - что необходимо для истинного изменения порядка операций между обратным и прямым циклами.

Часть пятая - бонус

Просто для полноты, вот код, который вы получите, если перейдете rightIndex+1к методу, а не rightIndex. Это изменение устраняет все +2смещения, которые в противном случае необходимы, чтобы думать о правильности цикла.

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
Буду очень признателен за комментарий, если вы понижаете голос
flamingpenguin

2

Понимание этого легко и свободно - вопрос опыта. Несмотря на то, что язык не позволяет вам выразить это напрямую, или вы используете более сложный случай, чем простое встроенное средство , с которым вы можете справиться, вы думаете, что это более высокий уровень, такой как «посещать каждый элемент в почтенном порядке» и более опытный кодер мгновенно переводит это в нужные детали, потому что он делал это много раз.

Даже тогда, в более сложных случаях легко ошибиться, потому что то, что вы пишете, обычно не является обычным делом. С более современными языками и библиотеками вы не пишете легкую вещь, потому что есть консервированная конструкция или вызов для этого. В C ++ современная мантра гласит: «используйте алгоритмы, а не пишите код».

Таким образом, способ убедиться, что это правильно, особенно для такого рода вещей, это посмотреть на граничные условия . Проследите код в вашей голове для нескольких случаев на краю того, где все меняется. Если index == array-max, что происходит? Как насчет max-1? Если код делает неправильный поворот, он будет на одной из этих границ. Некоторые циклы должны беспокоиться о первом или последнем элементе, а также о том, что конструкция цикла получает правильные границы; например, если вы ссылаетесь a[I]и a[I-1]что происходит, когда Iминимальное значение?

Кроме того, посмотрите на случаи, когда количество (правильных) итераций является предельным: если границы встречаются, и у вас будет 0 итераций, будет ли это работать без особого случая? А как насчет 1 итерации, где самая низкая граница одновременно является самой высокой?

Изучение вариантов ребер (обе стороны каждого ребра) - это то, что вы должны делать при написании цикла, и что вы должны делать в обзорах кода.


1

Я постараюсь держаться подальше от уже упомянутых тем.

Каковы инструменты / ментальные модели, чтобы избежать таких ошибок?

инструменты

Для меня самый большой инструмент для написания лучше forи whileциклов - это вообще не писать ни циклов, forни whileциклов.

Большинство современных языков так или иначе пытаются решить эту проблему. Например, Java Iteratorс самого начала, которая была немного неуклюжей в использовании, ввела сокращенный синтаксис, чтобы упростить их использование в более поздней версии. В C # они есть и так далее.

Мой любимый в настоящее время язык Ruby полностью использует функциональный подход ( .eachи .mapт. Д.). Это очень сильно. Я только что быстро подсчитал в какой-то кодовой базе Ruby, над которой я работаю: примерно в 10.000 строк кода - ноль forи около 5 while.

Если бы я был вынужден выбрать новый язык, поиск таких функциональных / основанных на данных циклов был бы очень высок в списке приоритетов.

Ментальные модели

Имейте в виду, что whileэто минимальный уровень абстракции, который вы можете получить, всего лишь на шаг выше goto. На мой взгляд, forэто делает его еще хуже, а не лучше, поскольку он плотно объединяет все три части цикла.

Так что, если я нахожусь в среде, где forиспользуется, то я чертовски уверен, что все 3 части абсолютно просты и всегда одинаковы. Это значит я напишу

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

Но ничего сложнее. Может быть, может быть, иногда у меня будет обратный отсчет, но я сделаю все возможное, чтобы избежать его.

При использовании whileя остаюсь в стороне от извилистых внутренних феноменов, связанных с состоянием петли. Тест внутри while(...)будет максимально простым, и я буду стараться breakизо всех сил . Также цикл будет коротким (считая строки кода), и любые большие объемы кода будут учтены.

Особенно, если фактическое условие while является сложным, я буду использовать «переменную условия», которую очень легко обнаружить, а не помещать условие в сам whileоператор:

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

(Или что-то в этом роде, в правильной мере, конечно.)

Это дает вам очень простую ментальную модель, которая гласит: «эта переменная работает от 0 до 10 монотонно» или «этот цикл работает до тех пор, пока эта переменная не станет ложной / истинной». Кажется, что большинство мозгов справляются с этим уровнем абстракции просто отлично.

Надеюсь, это поможет.


1

В частности, обратные циклы, в частности, трудно рассуждать, потому что многие из наших языков программирования смещены в сторону прямой итерации, как в обычном синтаксисе цикла for, так и с использованием полуоткрытых интервалов, начинающихся с нуля. Я не говорю, что это неправильно, что языки сделали такой выбор; Я просто говорю, что этот выбор усложняет размышления об обратных циклах.

В общем, помните, что цикл for - это просто синтаксический сахар, построенный вокруг цикла while:

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

эквивалентно:

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

(возможно, с дополнительным уровнем области видимости для хранения переменных, объявленных на этапе инициализации локально по отношению к циклу).

Это хорошо для многих видов петель, но если шаг идет последним, то неловко, когда вы идете назад. Работая в обратном направлении, я обнаружил, что легче запустить индекс цикла со значением после того, что я хочу, и переместить stepчасть в верхнюю часть цикла, например так:

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
}

или, как для цикла:

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

Это может показаться нервирующим, поскольку выражение шага находится в теле, а не в заголовке. Это неприятный побочный эффект прямого смещения в синтаксисе цикла for. Из-за этого некоторые будут утверждать, что вы вместо этого делаете это:

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

Но это катастрофа, если ваша индексная переменная имеет тип без знака (как это может быть в C или C ++).

Имея это в виду, давайте напишем вашу функцию вставки.

  1. Поскольку мы будем работать в обратном направлении, мы позволим индексу цикла быть записью после «текущего» слота массива. Я бы разработал функцию, которая будет принимать размер целого числа, а не индекса для последнего элемента, потому что полуоткрытые диапазоны - это естественный способ представления диапазонов в большинстве языков программирования, и потому что это дает нам возможность представлять пустой массив, не прибегая к до магического значения, как -1.

    function insert(array, size, value) {
      var j = size;
    
  2. Хотя новое значение меньше, чем предыдущий элемент, продолжайте смещаться. Конечно, предыдущий элемент может быть проверено только при наличии в предыдущий элемент, таким образом , мы должны сначала проверить , что мы не в самом начале:

      while (j != 0 && value < array[j - 1]) {
        --j;  // now j become current
        array[j + 1] = array[j];
      }
    
  3. Это оставляет jтам, где мы хотим новое значение.

      array[j] = value; 
    };
    

«Программирование жемчуга » Джона Бентли дает очень четкое объяснение сортировки вставок (и других алгоритмов), которые могут помочь построить ваши ментальные модели для подобных проблем.


0

Вы просто не понимаете, что на forсамом деле делает цикл и как он работает?

for(initialization; condition; increment*)
{
    body
}
  1. Сначала выполняется инициализация
  2. Затем условие проверяется
  3. Если условие истинно, тело запускается один раз. Если не перейти к № 6
  4. Код приращения выполнен
  5. Goto # 2
  6. Конец цикла

Возможно, вам будет полезно переписать этот код, используя другую конструкцию. Вот то же самое, используя цикл while:

initialization
while(condition)
{
    body
    increment
}

Вот некоторые другие предложения:

  • Можете ли вы использовать другую языковую конструкцию, например цикл foreach? Это позаботится о состоянии и шаге приращения для вас.
  • Можете ли вы использовать функцию карты или фильтра? В некоторых языках есть функции с этими именами, которые внутренне выполняют цикл для вас. Вы просто поставляете коллекцию и тело.
  • Вы действительно должны тратить больше времени на ознакомление с forпетлями. Вы будете использовать их все время. Я предлагаю вам пройти через цикл for в отладчике, чтобы посмотреть, как он выполняется.

* Обратите внимание, что хотя я использую термин «приращение», это просто некоторый код, который идет после тела и перед проверкой условия. Это может быть уменьшение или вообще ничего.


1
почему отрицание?
user2023861

0

Попытка дополнительного понимания

Для нетривиальных алгоритмов с циклами вы можете попробовать следующий метод:

  1. Создайте фиксированный массив с 4 позициями и вставьте несколько значений, чтобы смоделировать проблему;
  2. Напишите свой алгоритм для решения данной проблемы , без каких-либо циклов и с жестко закодированными индексами ;
  3. После этого замените жестко закодированные индексы в вашем коде на некоторую переменную iили j, и увеличивайте / уменьшайте эти переменные по мере необходимости (но все еще без какого-либо цикла);
  4. Перепишите ваш код и поместите повторяющиеся блоки в цикл , выполняя предварительные и последующие условия;
  5. [ опционально ] переписать ваш цикл в нужную вам форму (для / while / do while);
  6. Самое главное - правильно написать свой алгоритм; после этого вы реорганизуете и оптимизируете свой код / ​​циклы при необходимости (но это может сделать код более нетривиальным для читателя)

Твоя проблема

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

Напишите тело цикла вручную несколько раз

Давайте использовать фиксированный массив с 4 позициями и попробуем написать алгоритм вручную, без циклов:

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

Переписать, удалив жестко закодированные значения

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

Перевести на цикл

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

Рефакторинг / переписать / оптимизировать цикл так, как вы хотите:

С 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

с 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: код предполагает, что ввод действителен, и этот массив не содержит повторений;


-1

Вот почему я бы по возможности избегал написания циклов, которые работают с необработанными индексами, в пользу более абстрактных операций.

В этом случае я бы сделал что-то вроде этого (в псевдокоде):

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

-3

В вашем примере тело цикла вполне очевидно. И совершенно очевидно, что какой-то элемент должен быть изменен в самом конце. Таким образом, вы пишете код, но без начала цикла, условия конца и окончательного назначения.

Затем вы уходите от кода и выясняете, какой первый шаг необходимо выполнить. Вы изменяете начало цикла так, чтобы первый ход был правильным. Вы снова уходите от кода и выясняете, какой последний шаг нужно выполнить. Вы изменяете конечное условие так, чтобы последний ход был правильным. И, наконец, вы уходите от своего кода и выясняете, каким должно быть окончательное назначение, и соответственно исправляете код.


1
Не могли бы вы привести пример?
CodeYogi

У ОП был пример.
gnasher729

2
Что вы имеете в виду? Я ОП.
CodeYogi
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.