Цикл со счётчиком, while и do-while

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

Цикл — конструкция повторения. Различают три вида: со счётчиком (известно число повторений), с предусловием (проверка до тела) и с постусловием (проверка после тела).

В предыдущих уроках цикл был «чёрным ящиком, который повторяет». Пора разобрать, какими они бывают. Выбор вида цикла — это не вкусовщина: от него зависит, сколько минимум раз отработает тело и удобно ли вообще записывается задача. Перепутали вид — получили лишний проход или, наоборот, программу, которая «съела» одно повторение.

Зачем это нужно

Если заранее известно, сколько раз повторять (напечатать 10 строк, сложить 5 чисел), берут цикл со счётчиком. Если число повторений зависит от данных и может оказаться нулевым (читать числа, пока не встретится 0; делить, пока частное не станет нулём), берут цикл с предусловием. А если тело обязано выполниться хотя бы раз (спросить пароль и переспрашивать при ошибке) — цикл с постусловием. Понимание различий экономит и строки кода, и нервы на отладке.

Цикл со счётчиком (for)

Самый предсказуемый: задаём начальное значение, конечное и шаг, исполнитель сам считает повторения. Блок-схема текстом:

[i := 1]
  |
  V
<i <= n?> --нет--> [выход]
  | да
  V
[тело цикла]
  |
  V
[i := i + 1]
  |
  +--> назад к проверке <i <= n?>

Число повторений известно сразу: если i идёт от 1 до n с шагом 1, тело выполнится n раз. В общем случае для шага d, начала a и конца b число шагов равно

$$ k = \left\lfloor \frac{b - a}{d} \right\rfloor + 1 $$

(при условии, что цикл вообще стартует). Например, for i от 2 до 10 шаг 2 даёт $\lfloor (10-2)/2 \rfloor + 1 = 5$ повторений: 2, 4, 6, 8, 10.

Цикл с предусловием (while)

Условие проверяется до тела. Если оно ложно с самого начала, тело не выполнится ни разу — это его важнейшее свойство.

[начало]
  |
  V
<условие?> --ложь--> [выход]
  | истина
  V
[тело цикла]
  |
  +--> назад к проверке <условие?>

Псевдокод: пока условие повторять … всё. Такой цикл выполняется от 0 раз и сколько угодно. Им читают данные неизвестной длины и сводят величину к пределу — например, делят число на 2, пока оно больше 1.

Цикл с постусловием (do-while)

Тело идёт первым, а условие проверяется после. Поэтому тело гарантированно выполнится хотя бы один раз, даже если условие сразу ложно.

[начало]
  |
  V
[тело цикла] <----------+
  |                     |
  V                     |
<повторять?> --да-------+
  | нет
  V
[выход]

Псевдокод: выполнять … пока условие (или повторять … до условие). Идеален, когда действие нужно сделать минимум однажды: запросить ввод и переспрашивать, пока пользователь не введёт корректное значение.

Сводная таблица

ВидГде проверкаМинимум повторенийКогда брать
Со счётчиком (for)счётчик0 (или известно заранее)число повторений известно
С предусловием (while)до тела0условие зависит от данных, может не сработать
С постусловием (do-while)после тела1тело нужно выполнить хотя бы раз

Как это работает

Покажем главное различие на одном и том же «плохом» условии — заведомо ложном. Сравним предусловие и постусловие, сэмулировав do-while в Python (в самом языке его нет, поэтому цикл с постусловием обычно собирают через while True с проверкой в конце):

x = 10

# предусловие: while x < 5 — условие ложно сразу
cnt_pre = 0
while x < 5:
    cnt_pre += 1
    x += 1
print("Предусловие выполнило тело раз:", cnt_pre)

# постусловие: тело сначала, проверка потом
cnt_post = 0
while True:
    cnt_post += 1
    x += 1
    if not (x < 5):   # условие продолжения ложно
        break
print("Постусловие выполнило тело раз:", cnt_post)

Вывод:

Предусловие выполнило тело раз: 0
Постусловие выполнило тело раз: 1

Условие x < 5 при x = 10 ложно. Предусловие проверило его сразу и не вошло в тело ни разу. Постусловие выполнило тело и только потом проверило — отсюда ровно одно повторение. Вот почему выбор вида цикла меняет результат.

А теперь практичный пример цикла с предусловием — сколько раз число делится на 2, пока не станет меньше или равно 1:

n = 100
shagov = 0
while n > 1:
    n = n // 2
    shagov += 1
    print("n =", n)
print("Всего делений:", shagov)

Вывод:

n = 50
n = 25
n = 12
n = 6
n = 3
n = 1
Всего делений: 6

Число повторений тут заранее неизвестно — оно равно $\lfloor \log_2 n \rfloor + 1$ при делениях нацело; для $n = 100$ это 6. Счётчиком такое записать неудобно, а предусловие подходит идеально.

Частые ошибки

  • Предусловие вместо постусловия (и наоборот). Если нужно «спросить хотя бы раз», а взяли while с ложным стартовым условием — тело не выполнится. Если взяли постусловие там, где допустим ноль повторений, — получите лишний проход.
  • Забыли менять переменную условия — вечный цикл. В while и do-while тело обязано приближать условие к завершению (уменьшать n, продвигать чтение). Иначе цикл не остановится.
  • Ошибка на единицу в счётчике. for i от 1 до n — это n повторений, а for i от 0 до n — уже n+1. Внимательно к тому, включена ли граница.
  • Счётчик для неизвестного числа повторений. Если число шагов зависит от данных (деление до предела, чтение до маркера), цикл со счётчиком не подходит — нужен while.

Итоги

  • Три вида циклов: со счётчиком (число повторений известно), с предусловием (проверка до тела) и с постусловием (проверка после тела).
  • Цикл с предусловием (while) может выполниться 0 раз; цикл с постусловием (do-while) — минимум 1 раз. Это их главное различие.
  • Счётчик берут, когда число повторений известно заранее; число шагов считают по формуле $k = \lfloor (b-a)/d \rfloor + 1$.
  • While удобен, когда количество повторений зависит от данных; в любом while/do-while тело обязано продвигать условие к завершению, иначе вечный цикл.
Проверьте себя
1. Какой цикл гарантированно выполнит тело хотя бы один раз, даже если условие изначально ложно?
AЦикл с предусловием (while)
BЦикл с постусловием (do-while)
CЦикл со счётчиком при шаге 0
DЛюбой из трёх
2. Сколько раз выполнится тело цикла: for i от 2 до 10 с шагом 2?
A4
B5
C8
D10