Чому вкладені петлі вважаються поганою практикою?


33

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

Я не розумію, чому? Це тому, що це впливає на читабельність коду? Або це щось більш "технічне"?


4
Якщо я добре пам’ятаю свій курс CS3, це тому, що це часто призводить до експоненціального часу, а це означає, що якщо ви отримаєте великий набір даних, ваша програма стане непридатною.
Травіс Пессетто

21
Одне, що вам слід дізнатись про викладачів CS - це те, що не все, що вони говорять, стосується реального світу на 100%. Я б відмовив, що петлі вкладені більш ніж декількома глибокими, але якщо вам доведеться обробити m x n елементів, щоб вирішити свою проблему, ви збираєтесь зробити багато ітерацій.
Blrfl

8
@TravisPessetto Насправді це все ще поліномна складність - O (n ^ k), k - кількість вкладених, а не експоненціальних O (k ^ n), де k - константа.
m3th0dman

1
@ m3th0dman Дякую за те, що мене виправили. Мій учитель був не найбільшим з цього приводу. Він ставився до O (n ^ 2) та O (k ^ n) як до того ж.
Травіс Пессетто

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

Відповіді:


62

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

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

Мітки циклів на Java дозволяють передчасно вирватися з декількох вкладених циклів, коли інші способи зробити це було б громіздко. Наприклад, якась гра може мати такий код:

Player chosen_one = null;
...
outer: // this is a label
for (Player player : party.getPlayers()) {
  for (Cell cell : player.getVisibleMapCells()) {
    for (Item artefact : cell.getItemsOnTheFloor())
      if (artefact == HOLY_GRAIL) {
        chosen_one = player;
        break outer; // everyone stop looking, we found it
      }
  }
}

Хоча код, як наведений вище приклад, іноді може бути оптимальним способом вираження певного алгоритму, зазвичай краще розбити цей код на більш дрібні функції і, ймовірно, використовувати returnзамість цього break. Так що breakз етикеткою - слабкий кодовий запах ; зверніть додаткову увагу, коли це побачите.


2
Як графіка бічної ноти використовується алгоритм, який має доступ до кожного фрагмента матриці. Однак GPU спеціалізується на тому, щоб вирішити це часом.
Травіс Пессетто

Так, GPU робить це масово-паралельно; питання було про одну нитку виконання, я думаю.
9000

2
Однією з причин того, що етикетки, як правило, є підозрюваними, є те, що часто існує альтернатива. У цьому випадку ви можете повернутися замість цього.
jgmjgm

22

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

Наприклад, якщо у вас є 100 предметів у списку А та 100 елементів у списку В, і ви знаєте, що для кожного елемента в списку А є один елемент у списку В, який відповідає йому (з визначенням "відповідність" залишається навмисно незрозумілим. тут), і ви хочете скласти список пар, простий спосіб це зробити так:

for each item X in list A:
  for each item Y in list B:
    if X matches Y then
      add (X, Y) to results
      break

Що стосується 100 позицій у кожному списку, це займе в середньому 100 * 100/2 (5000) matchesоперацій. Якщо більше предметів, або якщо співвідношення 1: 1 не забезпечене, воно стає ще дорожчим.

З іншого боку, існує набагато швидший спосіб виконати таку операцію:

sort list A
sort list B (according to the same sort order)
I = 0
J = 0
repeat
  X = A[I]
  Y = B[J]
  if X matches Y then
    add (X, Y) to results
    increment I
    increment J
  else if X < Y then
    increment I
  else increment J
until either index reaches the end of its list

Якщо ви робите це таким чином, замість кількості matchesоперацій, що базуються length(A) * length(B), тепер це засновано length(A) + length(B), а значить, ваш код буде працювати набагато швидше.


10
З застереженням, що налаштування сортування займає нетривіальну кількість часу, O(n log n)вдвічі, якщо використовується Quicksort.
Роберт Харві

@RobertHarvey: Звичайно. Але це ще набагато менше, ніж O(n^2)для неглибоких значень Н.
Мейсон Уілер

10
Друга версія алгоритму, як правило, неправильна. По-перше, передбачається, що X і Y порівнянні за допомогою <оператора, що, як правило, не може бути отримане від matchesоператора. По-друге, навіть якщо і X, і Y є числовими, 2-й алгоритм може все-таки давати неправильні результати, наприклад, коли X matches Yє X + Y == 100.
Паша

9
@ user958624: Очевидно, це дуже високий огляд загального алгоритму. Як і оператор "відповідності", "<" має бути визначено правильним у контексті даних, що порівнюються. Якщо це зробити правильно, результати будуть правильними.
Мейсон Уілер

PHP зробив щось подібне до останнього зі своїм унікальним масивом, я думаю, та / або протилежним йому. Люди замість цього просто використовують масиви PHP, які базуються на хеші, і це буде набагато швидше.
jgmjgm

11

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

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

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

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


1
Ви можете дуже часто просто розрівняти вкладений цикл, і це все ще займає той же час. взяти для 0 до ширини; для 0 до висоти; Ви можете замість цього просто поставити на ширину від 0 до ширини.
jgmjgm

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

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

7

Враховуючи випадок безлічі вкладених циклів, ви закінчуєте час полінома. Наприклад, з даним псевдокодом:

set i equal to 1
while i is not equal to 100
  increment i
  set j equal to 1
  while j is not equal to i
    increment j
  end
 end

Це буде вважатися часом O (n ^ 2), яке було б графіком, подібним до: введіть тут опис зображення

Де вісь у - це кількість часу, яке ваша програма потребує на завершення, а вісь x - кількість даних.

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


10
ви можете повторно вибрати свій графік: це експоненціальна крива, а не квадратична, також рекурсія не робить речі O (n log n) з O (n ^ 2)
храповик виродка

5
Для рекурсії у мене є два слова «стек» та «переповнення»
Матеуш

10
Ваше твердження про те, що рекурсія може звести операцію O (n ^ 2) до O (n log n), в основному є неточною. Той самий алгоритм, реалізований рекурсивно проти ітеративного, повинен мати таку саму складну величину часу. Крім того, рекурсія часто може бути повільнішою (залежно від реалізації мови), оскільки кожен рекурсивний виклик вимагає створення нового кадру стека, тоді як ітерація вимагає лише гілки / порівняння. Функціональні дзвінки зазвичай дешеві, але вони не безкоштовні.
dckrooney

2
@TravisPessetto за останні 6 місяців я бачив 3 переповнення стека під час розробки додатків C # через рекурсії або циклічних посилань на об'єкти. Що смішного в тому, що він обвалиться, і ви не знаєте, що вас вдарило. Коли ви бачите вкладені петлі, ви знаєте, що може статися щось погане, і виняток щодо неправильного індексу для чогось легко помітний.
Матеуш

2
@Mateusz також такі мови, як Java, дозволяють виявити помилку переповнення стека. Це зі слідом стека має дати вам бачити, що сталося. Я не маю великого досвіду, але єдиний раз, коли я бачив помилку переповнення стека, це PHP, у якого помилка викликала нескінченну рекурсію, і PHP вичерпало 512 МБ пам'яті, яка йому була призначена. Рекурсія повинна мати кінцеве кінцеве значення, яке не підходить для нескінченних циклів. Як і все в CS, є час і місце для всіх речей.
Травіс Пессетто

0

Керувати вантажівкою тридцять тонн замість невеликої пасажирської машини - це погана практика. За винятком випадків, коли вам потрібно перевезти 20 або 30 тонн речі.

Коли ви використовуєте вкладений цикл, це не погана практика. Це або зовсім дурно, або саме те, що потрібно. Тобі вирішувати.

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


Якщо ви знаєте достатньо, щоб вирішити самі, ви знаєте досить, щоб не використовувати мічені петлі. :-)
user949300

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

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

-1

У вкладених петлях немає нічого поганого або обов'язково навіть поганого. Однак вони мають певні міркування та проблеми.

Статті, до яких вам привели, ймовірно, в ім'я стислості або через психологічний процес, відомий як спалений, пропускаючи специфіку.

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

Проблема в програмуванні полягає в тому, що ви не будете досягати пікової ефективності, якщо завжди в першу чергу віддаєте перевагу безпеці. У цьому випадку дітки можуть нарізати тільки м’які овочі. Зіткнувшись з чим-небудь іншим, і вони лише збираються зіпсувати це за допомогою тупого ножа. Важливо навчитися правильному використанню циклів, включаючи вкладені петлі, і ви не можете цього робити, якщо вони вважаються поганими, і ви ніколи не намагаєтесь їх використовувати.

Оскільки тут багато відповідей зазначають, що вкладений цикл є вказівкою характеристик продуктивності вашої програми, які можуть ставати експоненціально гіршими кожного гніздування. Тобто O (n), O (n ^ 2), O (n ^ 3) і так далі, що включає O (n ^ глибина), де глибина являє собою кількість петель, які ви вклали. У міру зростання вашого гніздування необхідний час зростає експоненціально. Проблема полягає в тому, що це не впевненість у тому, що ваша часова чи просторова складність буде такою (досить часто a * b * c, але не всі петлі гнізда можуть працювати весь час), а також не впевненість, що ви мати проблеми з роботою, навіть якщо це так.

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

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

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

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

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

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

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

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

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

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

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

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

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


Приклад гострого <-> тупого ножа не є великим, оскільки тупі ножі, як правило, більш небезпечні для різання. І O (n) -> O (n ^ 2) -> O (n ^ 3) не є експоненціальним в n, це геометричний або многочлен .
Калет

Що тупі ножі гірші - це трохи міський міф. На практиці він різниться і, як правило, характерний для тих випадків, коли тьмяні ножі особливо непридатні, як правило, вимагають великих зусиль і пов'язані з великим ковзанням. Ти маєш рацію, хоча там є приховане, але притаманне обмеження, ти можеш різати лише м'які овочі. Я б вважав, що n ^ глибина експоненціальна, але ви праві, такі приклади самі по собі не є.
jgmjgm

@jgmjgm: n ^ глибина поліноміальна, глибина ^ n - експоненціальна. Це насправді не питання тлумачення.
Roel Schroeven

x ^ y відрізняється від y ^ x? Помилка, яку ви зробили, полягає в тому, що ви ніколи не читали відповіді. Ви схопилися за експоненціальні, а потім поцікавились рівняннями, які самі по собі не є експоненціальними. Якщо ви прочитаєте, ви побачите, що я сказав, що це збільшується експоненціально для кожного шару вкладення, і якщо ви поставите це для тесту самостійно, час для (a = 0; a <n; a ++); для (b = 0; b <n ; b ++); для (c = 0; c <n; c ++); додаючи або видаляючи петлі, ви побачите, що це справді експоненціально. Ви знайдете один цикл виконання на n ^ 1, два виконання на n ^ 2 і три на n ^ 3. Ви не розумієте вкладених: D. Гемометричні підмножини експоненціальні.
jgmjgm

Я думаю, що це підтверджує те, що люди справді борються з вкладеними конструкціями.
jgmjgm
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.