Як записати правильні петлі?


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працює на всіх масивах. Ви можете вирішити вищезазначене за допомогою фрагмента та контету, щоб уникнути циклічного циклу: codepen.io/anon/pen/ZWovdg?editors=0012 Найбільш правильний спосіб написання циклу - це взагалі не писати.
Джед Шнайдер

13
Власне, йти вперед і вирішувати вирішені проблеми. Це називається практикою. Просто не переймайтеся їх публікацією. Тобто, якщо ви не знайдете спосіб вдосконалити рішення. Однак, винахід колеса - це більше, ніж колесо. Вона передбачає цілу систему контролю якості колеса та підтримку клієнтів. Тим не менше, спеціальні диски приємні.
candied_orange

53
Я боюся, що ми направилися в неправильному напрямку. Даючи CodeNogi лайно, тому що його приклад є частиною добре відомого алгоритму, є досить необґрунтованим. Він ніколи не стверджував, що винайшов щось нове. Він запитує, як уникнути деяких дуже поширених граничних помилок під час написання циклу. Бібліотеки пройшли довгий шлях, але я все ще бачу майбутнє для людей, які вміють писати петлі.
candied_orange

5
Загалом, працюючи з петлями та індексами, ви повинні дізнатися, що індекси вказують між елементами та ознайомтесь з напіввідкритими інтервалами (насправді це дві сторони одних і тих же понять). Як тільки ви отримаєте ці факти, значна частина циклів / покажчиків скрегіт головою повністю зникає.
Маттео Італія

Відповіді:


208

Тест

Ні, серйозно, тест.

Я кодую вже більше 20 років, і досі не довіряю собі правильно написати цикл з першого разу. Я пишу і запускаю тести, які доводять, що він працює, перш ніж підозрюю, що він працює. Перевірте кожну сторону кожної граничної умови. Наприклад, a rightIndex0 повинен робити що? Як щодо -1?

Не ускладнювати

Якщо інші з першого погляду не бачать, що це робить, ви робите це занадто важко. Будь ласка, ігноруйте продуктивність, якщо це означає, що ви можете написати щось зрозуміле. Тільки зробіть це швидше в тому малоймовірному випадку, який вам справді потрібно. І навіть тоді лише коли ти абсолютно впевнений, що точно знаєш, що сповільнює тебе. Якщо ви можете досягти фактичного вдосконалення Big O, ця діяльність може бути не безглуздою, але навіть тоді зробіть свій код максимально читабельним.

Вимкнено одним

Знайте різницю між підрахунком пальців і підрахунком проміжків між пальцями. Іноді простори - це те, що насправді важливо. Не дозволяйте пальцям відволікати вас. Знайте, чи великий палець - палець. Знайте, чи розрив між вашим рожевим і великим пальцем вважається пробілом.

Коментарі

Перш ніж загубитися в коді, спробуйте сказати, що ви маєте на увазі англійською мовою. Чітко викладіть свої очікування. Не поясніть, як працює код. Поясніть, чому ви змушуєте його робити те, що робить. Слідкуйте за цим деталями впровадження. Код повинен бути можливим переробляти, не змінюючи коментаря.

Найкращий коментар - це гарне ім’я.

Якщо ви можете сказати все, що вам потрібно сказати, з добрим іменем, НЕ говоріть це знову з коментарем.

Абстракції

Об'єкти, функції, масиви та змінні - це всі абстракції, які є настільки ж хорошими, як імена, які їм надано. Дайте їм імена, які забезпечують, коли люди зазирнуть всередину, вони не будуть здивовані тим, що вони знаходять.

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

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

Довгі імена

Ніколи не вкорочуйте ім’я просто з огляду на довжину рядка. Знайдіть інший спосіб викласти свій код.

Пробіл

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

Контурні контури

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

Петлі альтернативи

Нехай ще щось роблять циклічно, якщо можете. На очах легше і вже налагоджено. Вони бувають різних форм: колекції або потоки , які дозволяють map(), reduce(), foreach(), та інші подібні методи , які застосовують лямбда. Шукайте спеціальні функції на кшталт Arrays.fill(). Існує також рекурсія, але очікуйте, що це полегшить ситуацію в особливих випадках. Зазвичай не використовуйте рекурсії, поки не побачите, як виглядатиме альтернатива.

Ой, і тест.

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

Я згадав про тестування?

Була ще одна річ. Не можу згадати. Почав з т ...


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. не знижуйте значення твердження просто тому, що воно дублює код. Infact, це можуть бути дуже цінними твердженнями, якщо ви вирішили переписати код. Тестування має повне право бути дублюючим. Швидше врахуйте, якщо твердження воно настільки поєднане з реалізацією єдиного коду, що будь-яке перезапис скасує це твердження. Ось тоді ви витрачаєте свій час. Гарна відповідь до речі.
candied_orange

1
@CandiedOrange: Під дублюванням коду я маю на увазі буквально array[j+1] = array[j]; assert(array[j+1] == array[j]);. У цьому випадку значення здається дуже низьким (це копія / вставка). Якщо ти дублюєш значення, але виражене іншим способом, то воно стає більш цінним.
Матьє М.

10
Правила Хоара: допомагати писати правильні петлі з 1969 року. "Але, хоча ці методи відомі вже не одне десятиліття, більшість програмістів ніколи про них не чули".
Джокер_вД

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 повинен бути (довжина - 1) == -1, тому j починається з -1, тому, щоб вставити в індекс 0, потрібно додати 1.

Отже, ми довели остаточну умову правильної, але не внутрішню частину циклу.

Що станеться, якщо у вас є масив з 1 елементом, 10, і ми спробуємо вставити 5? З одним елементом rightIndex повинен починатися з 0. Отже, перший раз через цикл, j = 0, тому "0> = 0 && 10> 5". Оскільки ми хочемо вставити 5 в індексі 0, 10 слід перемістити до індексу 1, тому масив [1] = масив [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.
TOOGAM

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

Звичайно, якщо ви використовуєте мову, яка потребує " > 0", щоб отримати бажану функціональність, то такі символи повинні використовуватися, оскільки їх вимагає ця мова. Але навіть у цих випадках використовувати просто " > 0" простіше, ніж виконати двоскладовий процес спочатку віднімання одного, а потім також використання " >= 0". Після того, як я дізнався, що через невеликий досвід я потрапив у звичку використовувати знак рівності (наприклад, " >= 0") в моїх тестових умовах набагато рідше, і з цього часу код, як правило, відчувається простішим.
TOOGAM

1
@BryceWagner , якщо вам потрібно зробити 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, щоб ви повторювали час j >= 0.

Враховуючи це, ваш вибір, щоб rightIndexаргумент представляв останній індекс в підматриці, порушує умову. Це означає, що ви повинні включити обидві кінцеві точки (0 і rightIndex) в ітерацію. Це також ускладнює подання порожнього сегмента (який вам потрібен, коли ви починаєте сортування). Ви фактично повинні використовувати -1 як rightIndex, вставляючи перше значення. Це здається досить неприродним. Здається більш природним rightIndexвказати індекс після відрізка, тому 0 представляє порожній сегмент.

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


4
@CodeYogi: Я намалював би невеликий масив у вигляді сітки на аркуші паперу, а потім переробив ітерацію циклу вручну олівцем. Це допомагає мені уточнити, що насправді відбувається, наприклад, що діапазон комірок зміщений вправо і куди вставлено нове значення.
ЖакB

3
"У інформатиці є дві важкі речі: недійсність кешу, іменування речей та помилки окремо."
Цифрова травма

1
@CodeYogi: Додано невелику схему, щоб показати, про що я говорю.
ЖакБ

1
Прекрасне розуміння, особливо для вас останні два пар, варто прочитати, плутанина також пов’язана з характером циклу for, навіть я знаходжу правильний показник зменшення циклу j за один раз до припинення, і, отже, отримує мене один крок назад.
CodeYogi

1
Дуже. Відмінно. Відповідь. І я додам, що ця інклюзивна / ексклюзивна угода щодо індексу також мотивована значенням myArray.Lengthабо myList.Count- що завжди на один більше, ніж на нульовому "правому" індексі. ... Тривалість пояснення відповідає практичному та простому застосуванню цих явних кодуючих евристик. Натовп TL; DR відсутній.
radarbob

5

Вступ до вашого запитання змушує мене думати, що ви не навчилися правильно кодувати. Кожен, хто програмує на необхідній мові більше декількох тижнів, справді повинен вперше отримати правильні межі в більш ніж 90% випадків. Можливо, ви поспішаєте розпочати кодування, перш ніж достатньо продумали проблему.

Я пропоную вам виправити цей недолік, (повторно) навчившись писати петлі - і я б рекомендував кілька годин працювати через певний цикл з папером та олівцем. Для цього зробіть вихідний день. Потім витратьте 45 хвилин або близько того дня, працюючи над темою, поки ви її справді не зрозумієте.

Це все дуже добре тестується, але ви повинні тестувати, сподіваючись, що ви зазвичай отримаєте межі циклу (і решту вашого коду) правильно.


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

3
@MainMa - Я думаю, що в той час як Марк міг бути більш чутливим, я думаю, він має рацію - є стрес на інтерв'ю, і там просто хакерський код разом, без належної уваги до визначення проблеми. Те, як сформульовано питання, дуже сильно вказує на останнє, і це те, що найкраще можна вирішити в довгостроковій перспективі, впевнившись, що у вас є міцна основа, а не взломання в IDE
James Snell

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

4
@CodeYogi Якщо вам доведеться робити "проб і помилки" і ви "засмучуєтесь" і "робите ті самі помилки" зі своїм кодуванням, то це ознаки того, що ви недостатньо добре зрозуміли свою проблему до того, як почали писати . Ніхто не каже, що ти цього не розумієш, але що твій код міг бути краще продуманий, і вони знаки, що ти борешся, з якого ти можеш взяти і навчитися чи ні.
Джеймс Снелл

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

3

Можливо, я повинен поставити трохи коментаря до свого коментаря:

Є лише один спосіб: краще зрозуміти свою проблему. Але це так само загально, як і ваше питання

Ваша думка

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

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

Незалежно від конкретної проблеми ви задавали питання про те, як покращити:

1) Тест

Це сказали інші, і я не мав би нічого цінного додати

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

Важко дати поради з цього приводу. Я можу дати вам лише два підказки, які, ймовірно, допоможуть вам покращити свої навички на цю тему:

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

Код Катас - це спосіб, який може трохи допомогти.

Як вам стати чудовим музикантом? Це допомагає знати теорію та зрозуміти механіку вашого інструменту. Це допомагає мати талант. Але врешті-решт велич походить від практики; застосовуючи теорію знову і знову, використовуючи зворотний зв'язок, щоб кожен раз покращуватися.

Код Ката

Один сайт, який мені дуже подобається: Code Wars

Досягнення оволодіння за допомогою виклику Вдосконаліть свої навички, навчаючись з іншими щодо справжніх проблем із кодом

Вони відносно невеликі проблеми, які допомагають відточити свої навички програмування. І що мені найбільше подобається в Code Wars - це те, що ти можеш порівняти своє рішення з одним із інших .

Або, можливо, вам слід подивитися на Exercism.io, де ви отримуєте відгуки від громади.

  • Інша порада є майже такою ж тривіальною: навчіться розбирати проблеми. Ви повинні тренуватися, розбиваючи проблеми на справді невеликі проблеми. Якщо ви скажете, що у вас є проблеми з написанням циклів , ви робите помилку, що бачите цикл як цілу конструкцію і не розшифровуєте її на частини. Якщо ви навчитесь розбирати речі крок за кроком , ви навчитесь уникати таких помилок.

Я знаю - як я говорив вище, іноді ви перебуваєте в такому стані - що важко розбити "прості" речі на більш "мертві прості" завдання; але це дуже допомагає

Пам’ятаю, коли я вперше навчився професійно програмувати, у мене виникли величезні проблеми з налагодженням коду. У чому була проблема? Гібрид - помилка не може бути в такій і такій області коду, тому що я знаю, що не може бути. А як наслідок? Я пробирав код, замість того, щоб аналізувати його, я повинен був навчитися - навіть якщо було нудно порушувати свій код за інструкцією .

3) Розробити інструмент

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

Ось дві книги для початку:

Це як вивчити деякі рецепти, щоб почати готувати. Спочатку ви не знаєте, що робити, тому ви повинні дивитись, що раніше готували для вас кухарі . Те саме стосується алгоритмів. Алгоритми - це як приготування рецептів для звичайних страв (структури даних, сортування, хеш і т.д.). Якщо ви знаєте їх (принаймні, намагайтеся) напам’ять, у вас є хороша відправна точка.

3а) Знати конструкти програм

Цей пункт є похідним - так би мовити. Знай свою мову - і ще краще: знайте, які конструкції можливі у вашій мові.

Загальна точка для поганий або inefficent коди іноді, що програміст не знає різницю між різними типами петель ( for-, while-і do-loops). Вони якось усі взаємозамінні; але за певних обставин вибір іншої петлевої конструкції призводить до більш елегантного коду.

І є пристрій Даффа ...

PS:

інакше ваш коментар не є кращим, ніж Дональд Трамп.

Так, ми повинні знову зробити кодування чудовим!

Новий девіз Stackoverflow.


Ах, дозвольте сказати вам одне дуже серйозно. Я робив усе, що ви згадали, і я навіть можу надати вам своє посилання на цих сайтах. Але те, що мене засмучує, це замість того, щоб отримати відповідь на моє запитання, я отримую всі можливі поради щодо програмування. Лише один хлопець згадав pre-postумови до цього часу, і я ціную це.
CodeYogi

З того, що ви говорите, важко уявити, де ваша проблема. Можливо, метафора допомагає: для мене це як би сказати "як я можу бачити" - очевидна відповідь для мене - "користуйся очима", тому що бачити для мене так природно, я не уявляю, як не можна бачити. Те саме стосується вашого питання.
Томас Джунк

Погодьтесь повністю з дзвінками тривоги щодо "проб і помилок". Я думаю, що найкращий спосіб повного вивчення розуму вирішення проблем - це запуск алгоритмів та коду папером та олівцем.
Wildcard

Гм ... чому ти маєш граматично незадовільний граматичний бід у політичного кандидата, цитованого без контексту в середині вашої відповіді про програмування?
Wildcard

2

Якщо я правильно зрозумів проблему, ваше питання полягає в тому, як подумати, щоб отримати петлі прямо з першої спроби, а не як переконатися, що ваш цикл є правильним (для якого відповідь буде тестуванням, як пояснено в інших відповідях).

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

Це число, як 0 або 1? Тоді вам, швидше за все, знадобиться для і бінго, у вас також є свій стартовий я. Тоді подумайте, скільки разів ви хочете запустити одну і ту ж річ, і ви також матимете свою кінцеву умову.

Якщо ви точно не знаєте, скільки разів він буде працювати, то вам потрібен не час, а певний час чи час.

Технічно будь-який цикл може бути переведений у будь-який інший цикл, але код легше читати, якщо ви користуєтеся правильним циклом, тому ось кілька порад:

  1. Якщо ви виявите, що пишете if () {...; break;} усередині, вам потрібен час, і ви вже маєте умову

  2. "Хоча" є, можливо, найбільш вживаним циклом у будь-якій мові, але це не повинно. Якщо ви виявили, що ви пишете bool ok = True; в той час, як (перевірити) {зробити щось і сподіваємось змінити нормально в якийсь момент}; тоді вам не потрібен час, а час, оскільки це означає, що у вас є все необхідне для запуску першої ітерації.

Тепер трохи контексту ... Коли я вперше навчився програмувати (Паскаль), я не розмовляв англійською. Для мене "за" і "поки" не мало особливого сенсу, але ключове слово "повторити" (робити поки в C) майже однакове в моїй рідній мові, тому я б використовував його для всього. На мою думку, повторення (робити поки це) є найбільш природним циклом, тому що майже завжди ви хочете, щоб щось було зроблено, і тоді ви хочете, щоб це було зроблено знову, і знову, поки не буде досягнута мета. "За" - це лише ярлик, який дає вам ітератор і дивно розміщує умову на початку коду, хоча, майже завжди, ви хочете, щоб щось було зроблено, поки щось не відбудеться. Також, поки це лише ярлик для if () {do while ()}. Ярлики приємні для подальшого,


2

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

Я не пропоную вам намагатися робити це для кожного циклу. Але я сподіваюся, що вам буде корисно побачити процес думки.

Для цього я переведу ваш метод на інструмент під назвою Microsoft 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
}

Це підтверджується в Dafny. Ви можете переконатися у цьому самі, перейшовши за цим посиланням . Отже, ваш цикл правильно реалізує специфікацію методу, яку я написав у першій частині. Вам потрібно буде визначитися, чи справді специфікація цього методу є поведінкою, якої ви хотіли.

Зауважте, що Дафні надає тут доказ коректності. Це набагато сильніша гарантія правильності, ніж це можливо отримати шляхом тестування.

Частина третя - простіший цикл

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

Я змінив цикл, щоб він відповідав вашим оригінальним інтуїціям про стан циклу та кінцеве значення 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 # має їх також і т.д.

Моя в даний час прихильна мова, Рубі, взяла на себе функціональний підхід ( .eachі .mapт.д.) повністю. Це дуже потужно. Я просто зробив швидкий підрахунок у коді бази Ruby, над якою працюю: приблизно в 10 000 рядків коду є нуль forі близько 5 while.

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

Ментальні моделі

Майте на увазі, що whileце найменший мінімум абстракції, який ви можете отримати, лише на один крок вище goto. На мою думку, forце робить ще гірше, а не краще, оскільки він стикує всі три частини петлі щільно разом.

Отже, якщо я перебуваю в навколишньому середовищі, де forйого використовують, то я переконуюсь, що всі 3 частини мертві прості і завжди однакові. Це означає, що я напишу

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

Але нічого дуже складнішого. Можливо, можливо, у мене іноді буде відлік часу, але я зроблю все можливе, щоб цього уникнути.

Якщо ви користуєтесь while, я залишаюся осторонь перекручених внутрішніх шенаніганів, пов'язаних із станом петлі. Тест всередині while(...)заповіту буде максимально простим, і я уникатиму, breakяк тільки можу. Також цикл буде коротким (підрахунок рядків коду), і будь-які великі кількості коду будуть враховані.

Особливо, якщо фактична умова в той час як складна, я буду використовувати "змінну умови", яку дуже легко помітити, а не розміщувати умову в самому whileзаяві:

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

(Або щось подібне, в правильній мірі, звичайно.)

Це дає вам дуже просту ментальну модель, яка "ця змінна працює від 0 до 10 монотонно" або "ця петля працює до тих пір, поки ця змінна не буде хибною / істинною". Більшість мізків, здається, здатні впоратися з цим рівнем абстракції просто чудово.

Сподіваюся, що це допомагає.


1

Зворотний цикл, зокрема, може бути важким для того, щоб пояснити, тому що багато наших мов програмування є упередженими до ітерації вперед, як у загальному синтаксисі для циклу, так і за допомогою використання нульових напіввідкритих інтервалів. Я не кажу, що неправильно, що мови зробили цей вибір; Я просто кажу, що цей вибір ускладнює роздуми про зворотні петлі.

Загалом, пам’ятайте, що for-loop - це лише синтаксичний цукор, який будується навколо циклу:

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

еквівалентно:

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

(можливо, з додатковим шаром області для збереження змінних, оголошених на кроці init, локальним до циклу).

Це добре для багатьох видів петель, але робити останній крок - незручно, коли ви йдете назад. Працюючи назад, мені здається, що простіше запустити індекс циклу зі значенням після потрібного і перемістити 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 (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. Гото №2
  6. Кінець петлі

Вам може бути корисно переписати цей код за допомогою іншої конструкції. Ось те саме, використовуючи цикл час:

initialization
while(condition)
{
    body
    increment
}

Ось кілька інших пропозицій:

  • Чи можете ви використовувати іншу мовну конструкцію, як цикл foreach? Це турбується про стан та приріст кроку для вас.
  • Чи можете ви використовувати функцію Map або Filter? Деякі мови мають функції з цими іменами, які внутрішньо перебирають колекцію для вас. Ви просто постачаєте колекцію та корпус.
  • Ви дійсно повинні витрачати більше часу на ознайомлення з forпетлями. Ви будете ними користуватися постійно. Я пропоную вам перейти через цикл for у відладчику, щоб побачити, як він виконується.

* Зауважте, що хоча я використовую термін "приріст", це лише деякий код, який знаходиться після тіла та перед перевіркою стану. Це може бути декрет або взагалі нічого.


1
чому потік?
користувач2023861

0

Спроба додаткового розуміння

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

  1. Створіть фіксований масив з 4-х позицій та введіть деякі значення, щоб імітувати проблему;
  2. Напишіть свій алгоритм для вирішення заданої проблеми , без будь-якого циклу та з жорстко кодованою індексацією ;
  3. Після цього замініть жорстко кодовані індексації у вашому коді якоюсь змінною iабо j, і збільшуйте / зменшуйте ці змінні за необхідності (але все ще без будь-якого циклу);
  4. Перепишіть свій код і помістіть повторювані блоки всередину циклу , виконуючи умови до та після;
  5. [ необов’язково ] перепишіть цикл у потрібну форму (на / час / робити поки);
  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;
    }
}

Refactor / переписати / оптимізувати цикл так, як вам потрібно:

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