Цикл со счётчиком, 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 тело обязано продвигать условие к завершению, иначе вечный цикл.