Материалы по курсу «Основы программирования»

Цикл while

Синтаксис цикла while выглядит так:

while ‹условие›:
    ‹тело цикла›

Если тело цикла состоит из одного оператора, то цикл, также как и if, можно записать в одну строчку:

while ‹условие›: ‹тело цикла›

Но так лучше не писать из стилистических соображений.

Как он выполняется:

Т.е. пока значение ‹условия› истинное, поочерёдно вычисляется ‹условие› и выполняется тело цикла.

Сравним с if:

if ‹условие›:
    ‹блок›

Блок либо выполнится, либо не выполнится.

while ‹условие›:
    ‹блок›

Блок может выполняться много раз.

Итерация — однократное выполнение тела цикла.

Пример. Просуммировать все числа от 1 до 100.

sum = 0
i = 1
while i <= 100:
    sum = sum + i
    i = i + 1

В переменной i находится очередное число. В начале оно равно 1, затем на каждой итерации (проходе, витке) цикла оно увеличивается на 1. В результате на первом витке i равно 1, на втором — i равно 2 и т.д. Таким образом, в переменной i на входе в цикл находится номер текущей итерации.

Переменная sum изначально равна нулю, на каждом витке цикла её содержимое увеличивается на номер этой итерации цикла (значение переменной i). В итоге, значение переменной sum после завершения цикла увеличится на сумму чисел от 1 до 100. Начальным её значением было число 0, поэтому после завершения цикла в ней будет сумма чисел от 0 до 100.

Этот цикл обязательно завершится, т.к. до начала цикла переменная i хранила значение меньше 100, на каждом проходе цикла переменная i увеличивается и поэтому рано или поздно превысит 100 — условие i <= 100 станет ложным и цикл прервётся.

Этот пример можно записать компактнее:

sum = 0
i = 1
while i <= 100:
    sum += i
    i += 1

Здесь мы использовали сокращённую запись операторов присваивания. Вспомним, что запись вида

‹перем›  ‹знак›=   ‹выраж›

является сокращением для

‹перем› = ‹перем›  ‹знак›  ‹выраж›

Вернёмся к циклу while. Для цикла вида

while ‹условие›:
    ‹тело цикла›

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

А внутри самого цикла ‹условие› истинное.

Пример. Написать функцию is_prime(n), проверяющую число на простоту.

def is_prime(n):
    ‹проверить число на простоту›

Число простое, если оно больше чем 2 и делится только на себя и на единицу. Число делится на другое, если остаток от деления равен нулю.

def is_prime(n):
    ‹перебирать все числа от 2 до бесконечности,
     пока не найдётся делитель›
    ‹сравнить делитель с n›

Считаем, что n у нас не меньше 2. Среди чисел от 2 до бесконечности рано или поздно найдётся делитель числа n — для составного числа это будет один из делителей n, для простого — само n.

def is_prime(n):
    divisor = 2
    while ‹n не делится на divisor›:
        ‹увеличить divisor на 1›

    return divisor == n

Уточняем дальше:

def is_prime(n):
    divisor = 2
    while n % divisor != 0:
        divisor += 1

    return divisor == n

Данная функция зациклится, если n будет отрицательным, нулём или единицей. Она определена только для n >= 2.

Функцию можно оптимизировать, перебирая делители не до бесконечности (фактически, до n), а до √n:

def is_prime(n):
    divisor = 2
    while (divisor <= n**0.5) and (n % divisor != 0):
        divisor += 1

    return ‹а что тут написать?›

Цикл продолжается, пока divisor <= √n и при этом n не делится на divisor без остатка. Соответственно, чтобы цикл прекратился, должно нарушится хотя бы одно из условий:

Признаком того, что число простое, является первое условие — очередной кандидат в делители превысил √n.

def is_prime(n):
    divisor = 2
    while (divisor <= n**0.5) and (n % divisor != 0):
        divisor += 1

    return divisor > n**0.5

Пример. Найти сумму цифр целого числа.

Нам нужно разбить число на отдельные цифры и их сложить. Разбивать число будем справа налево: последняя цифра числа — это его остаток от деления на 10, все цифры, кроме последней — результат целочисленного деления на 10.

Целочисленное деление в Python — это операция //, возвращает целую часть частного:

>>> 30 / 4
7.5
>>> 36 / 4
9.0
>>> 30 // 4
7
>>> 36 // 4
9

Назовём функцию sum_digits(n):

def sum_digits(n):
    ‹найти сумму цифр›

Для хранения суммы заведём переменную-аккумулятор sum, которую на каждой итерации будем увеличивать на величину очередной цифры. До цикла положим в неё 0, после завершения цикла в ней будет находиться сумма цифр.

def sum_digits(n):
    sum = 0
    while ‹цифры не кончились›:
        ‹найти очередную (последнюю) цифру›
        ‹увеличить sum на очередную цифру›
        ‹отбросить последнюю цифру числа›
    return sum

Уточняем:

def sum_digits(n):
    sum = 0
    while n != 0:
        last_digit = n % 10
        sum += last_digit
        n //= 10
    return sum

В предпоследней строке мы воспользовались сокращённой записью для n = n // 10.

Приёмы использования цикла while

Перебор всех значений

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

Шаблон решения:

‹установка на начало›
while ‹значения не кончились›:
    ‹сделать что-то с текущим значением›
    ‹перейти к следующему›

Под этот шаблон хорошо подходит предыдущий пример — суммирование цифр числа:

def sum_digits(n):
    sum = 0
    while n != 0:
        last_digit = n % 10
        sum += last_digit
        n //= 10
    return sum

Другой пример с сегодняшней лекции:

sum = 0
i = 1
while i <= 100:
    sum += i
    i += 1

Поиск значения при помощи цикла while

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

Шаблон решения.

‹установка на первое значение›
while ‹значения не кончились› and ‹текущее — не искомое›:
    ‹переходим к следующему›

if ‹значения кончились›:
    ‹сделать что-то, когда искомого значения нет›
else:
    ‹текущее значение — искомое, сделать что-то с ним›

Пример. Ранее мы рассматривали пример на определение простоты числа. Рассмотрим его ещё раз, но уже с точки зрения приёма поиска при помощи цикла while.

Если число составное, то у него найдутся два делителя: m = n × k, при этом либо n == k, т.е. m = n² = k², либо один из делителей будет меньше √n, а второй — больше, чем √n.

Поэтому, чтобы определить, составное ли это число, нужно найти делитель, который будет не больше, чем √n. Т.е. задача проверки на простоту сводится к проверке на поиск делителя.

def is_prime(n):
    ‹установка на начало›
    while ‹не достигли конца› and ‹не нашли›:
        ‹перейти к следующему›

    if ‹достигли конца›:
        ‹сделать что-то, когда не нашли›
    else:
        ‹сделать что-то, когда нашли›

Немного уточним фразы в псевдокоде:

def is_prime(n):
    ‹установка на первый кандидат в делители›
    while ‹кандидат в делители не больше √n› and ‹это не делитель›:
        ‹перейти к следующему›

    if ‹кандидат в делители больше √n›:
        ‹сделать что-то, когда не нашли›
    else:
        ‹сделать что-то, когда нашли›

Можем уточнить две последние фразы: если делитель найден, число составное, функция возвращает ложь. Если не найден — простое, функция возвращает истину:

def is_prime(n):
    ‹установка на первый кандидат в делители›
    while ‹кандидат в делители не больше √n› and ‹это не делитель›:
        ‹перейти к следующему›

    if ‹кандидат в делители больше √n›:
        return True
    else:
        return False

Кандидата в делители положим в переменную divisor и перепишем на Python ещё несколько фраз в псевдокоде:

def is_prime(n):
    divisor = 2
    while divisor <= n**0.5 and n % divisor != 0:
        divisor += 1

    if divisor > n**0.5:
        return True
    else:
        return False

Получили готовую работающую функцию. Но её можно упростить.

Как мы знаем, конструкцию вида

if ‹условие›:
    return True
else:
    return False

можно упростить до

return ‹условие›

В рассматриваемой функции мы видим похожую конструкцию:

    if divisor > n**0.5:
        return True
    else:
        return False

Теперь мы можем упростить его до одной строчки:

    return divisor > n**0.5

Целиком функция примет вид:

def is_prime(n):
    divisor = 2
    while divisor <= n**0.5 and n % divisor != 0:
        divisor += 1

    return divisor > n**0.5

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

def gcd(m, n):
    ‹найти НОД›

Делитель числа не превышает модуль (абсолютное значение) этого числа.

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

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

def gcd(m, n):
    ‹установка на начало›
    while ‹не достигли конца› and ‹текущее — не искомое›:
        ‹перейти к следующему›

    if ‹достигли конца›:
        ‹случай, когда НЕ нашлось›
    else:
        ‹случай, когда нашлось›

Кандидат в делители будем хранить в переменной res. Перебирать числа будем в порядке убывания, начнём с абсолютного значения m. На самом деле, можно и с n, можно выбрать из них наименьшее, но это нам не важно, нам важно рассмотреть поиск.

Вернёмся к коду:

def gcd(m, n):
    res = abs(m)
    while res > 0 and not (m % res == 0 and n % res == 0):
        res -= 1

    if ‹достигли конца›:
        ‹случай, когда НЕ нашлось›
    else:
        ‹случай, когда нашлось›

Вопрос. А может ли res быть равным нулю? Отрицательным быть не может, т.к. это абсолютное значение числа (res = abs(m)). Нулём может, только когда m сам равен нулю. Но если одно из чисел равно нулю, то наибольший общий делитель — второе число.

В остальных случаях res не будет равен нулю, т.к. искомый элемент (общий делитель) обязательно найдётся — в худшем случае это будет 1.

Поэтому, в случае, когда делителя не нашлось (m == 0), возвращаем n, а в остальных случаях — найденный общий делитель:

def gcd(m, n):
    res = abs(m)
    while res > 0 and not (m % res == 0 and n % res == 0):
        res -= 1

    if res == 0:
        return n
    else:
        return res

Примечание. Часто для результата функции заводят переменную с именем res.