Т-Банк

Алгоритмы.

Мини-курс по C++

Лекция 2. Циклы while и for. Массивы и строки

В этой лекции мы научим программу повторять команды и работать с последовательностями значений. Для этого познакомимся с циклами while и for, массивами (векторами) и строками.

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

Зачем нужны циклы

Представим, что нужно пять раз вывести слово Hello.

Можно написать так:

cout << "Hello" << endl;
cout << "Hello" << endl;
cout << "Hello" << endl;
cout << "Hello" << endl;
cout << "Hello" << endl;

Для пяти повторений это возможно. Но если действие нужно выполнить сто, тысячу или миллион раз, копировать одну команду неудобно.

Цикл позволяет один раз написать фрагмент программы и указать правило, по которому он должен повторяться.

Цикл while

Общий вид цикла while:

while (условие) {
    // повторяющиеся команды
}

Слово while можно перевести как «пока». Перед каждым повторением программа проверяет условие:

Например, выведем числа от 1 до 5:

#include <iostream>
using namespace std;

int main() {
    int i = 1;

    while (i <= 5) {
        cout << i << " ";
        i = i + 1;
    }

    return 0;
}

Результат:

1 2 3 4 5

Переменная i называется счётчиком цикла. Посмотрим, как меняется её значение:

  1. Сначала i равно 1.
  2. Условие i <= 5 истинно, поэтому программа выводит 1.
  3. Команда i = i + 1 увеличивает i на единицу.
  4. Программа возвращается к условию уже с i, равным 2.
  5. Когда i станет равно 6, условие окажется ложным и цикл завершится.

У цикла со счётчиком есть три важные части:

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

Короткие формы изменения переменной

Увеличение переменной на единицу часто записывают так:

i++;

Эта команда делает то же самое, что и:

i = i + 1;

Уменьшение на единицу записывается как i--:

i--;

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

sum += x;
x -= 3;

Они эквивалентны следующим командам:

sum = sum + x;
x = x - 3;

Число повторений заранее неизвестно

while особенно удобен, когда количество повторений зависит от входных данных.

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

Ввод:  5 3 -2 4 0
Вывод: 10

Программа:

#include <iostream>
using namespace std;

int main() {
    int x;
    cin >> x;

    int sum = 0;

    while (x != 0) {
        sum += x;
        cin >> x;
    }

    cout << sum;
    return 0;
}

Первое число считывается до цикла, потому что условию x != 0 уже нужно какое-то значение x.

На каждом шаге программа:

  1. прибавляет текущее число к сумме;
  2. считывает следующее число;
  3. снова проверяет, не равно ли оно нулю.

Количество повторений заранее неизвестно: оно определяется положением первого нуля во входных данных.

Бесконечный цикл

Рассмотрим ошибочный фрагмент:

while (x != 0) {
    sum += x;
}

Если x изначально не равно нулю, внутри цикла оно не меняется. Поэтому условие x != 0 навсегда останется истинным, и цикл не завершится.

Такой цикл называется бесконечным.

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

Цикл for

Когда границы повторений известны заранее, обычно удобно использовать цикл for.

Вывод чисел от 1 до 5 можно записать так:

for (int i = 1; i <= 5; i++) {
    cout << i << " ";
}

Общий вид:

for (начало; условие; изменение) {
    // повторяющиеся команды
}

Части заголовка выполняются в следующем порядке:

  1. начало выполняется один раз перед первым шагом;
  2. условие проверяется перед каждым шагом;
  3. если условие истинно, выполняется тело цикла;
  4. после тела выполняется изменение;
  5. программа снова возвращается к условию.

Например, следующие два фрагмента работают одинаково.

Через while:

int i = 1;

while (i <= 5) {
    cout << i << " ";
    i++;
}

Через for:

for (int i = 1; i <= 5; i++) {
    cout << i << " ";
}

Два стандартных диапазона

Если нужно выполнить действие n раз, часто используется цикл:

for (int i = 0; i < n; i++) {
    // действие
}

Переменная i принимает значения:

0, 1, 2, ..., n - 1

Всего получается ровно n значений.

Если нужны сами числа от 1 до n, удобно написать:

for (int i = 1; i <= n; i++) {
    // действие с числом i
}

Здесь i принимает значения:

1, 2, 3, ..., n

Оба цикла выполняются n раз, но значения счётчика в них различаются.

Сумма чисел от 1 до n

Решим задачу: дано число n, нужно найти сумму всех целых чисел от 1 до n.

#include <iostream>
using namespace std;

int main() {
    int n;
    cin >> n;

    int sum = 0;

    for (int i = 1; i <= n; i++) {
        sum += i;
    }

    cout << sum;
    return 0;
}

Если ввести:

4

программа выведет:

10

Переменная sum создаётся до цикла, потому что она хранит общий результат всех повторений. Внутри цикла сумма увеличивается, а после цикла выводится готовый ответ.

Как выбрать между while и for

Обычно используют следующее правило:

Примеры для for:

Примеры для while:

Большинство циклов можно переписать из одного вида в другой. Выбирай вариант, который понятнее описывает решение.

Вектор

Представим, что дано n чисел и их нужно вывести в обратном порядке.

Ввод:
5
10 20 30 40 50

Вывод:
50 40 30 20 10

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

Для хранения последовательности элементов одного типа в C++ есть vector.

Чтобы использовать вектор, нужно подключить библиотеку:

#include <vector>

Создадим вектор из n целых чисел:

vector<int> a(n);

Здесь:

Индексы вектора

У каждого элемента есть индекс, то есть номер его позиции. Индексы в C++ начинаются с нуля.

Для вектора из пяти элементов позиции выглядят так:

индекс:      0    1    2    3    4
элемент:   a[0] a[1] a[2] a[3] a[4]

Если размер вектора равен n, допустимые индексы идут от 0 до n - 1:

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

Ввод вектора

Чтобы считать n элементов, переберём все допустимые индексы:

int n;
cin >> n;

vector<int> a(n);

for (int i = 0; i < n; i++) {
    cin >> a[i];
}

На первом шаге читается a[0], на втором - a[1], а на последнем - a[n - 1].

Именно поэтому в условии используется i < n, а не i <= n.

Вывод вектора в обратном порядке

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

Полная программа для вывода вектора справа налево:

#include <iostream>
#include <vector>
using namespace std;

int main() {
    int n;
    cin >> n;

    vector<int> a(n);

    for (int i = 0; i < n; i++) {
        cin >> a[i];
    }

    for (int i = n - 1; i >= 0; i--) {
        cout << a[i] << " ";
    }

    return 0;
}

Во втором цикле i принимает значения:

n - 1, n - 2, ..., 1, 0

Например, при n = 5 это индексы 4, 3, 2, 1, 0.

Изменение элементов и размер вектора

Элемент вектора можно использовать почти как обычную переменную:

cout << a[2];
a[0] = 100;
a[1] += 5;

Количество элементов можно получить с помощью метода size():

cout << a.size();

Если вектор содержит пять элементов, a.size() вернёт 5.

Метод - это функция, которая применяется к конкретному объекту. Сначала пишется имя вектора, затем точка и название метода.

Добавление элементов через push_back

Иногда размер последовательности заранее неизвестен. Тогда можно создать пустой вектор:

vector<int> a;

Метод push_back добавляет новый элемент в конец:

a.push_back(7);
a.push_back(3);
a.push_back(9);

После этих команд вектор содержит:

7 3 9

а его размер равен 3.

Сохраним в векторе все числа до первого нуля:

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> a;

    int x;
    cin >> x;

    while (x != 0) {
        a.push_back(x);
        cin >> x;
    }

    int n = a.size();

    for (int i = 0; i < n; i++) {
        cout << a[i] << " ";
    }

    return 0;
}

Есть два основных способа создать вектор:

vector<int> a(n);  // сразу создаём n элементов
vector<int> b;     // создаём пустой вектор

Во втором случае элементы можно добавлять через b.push_back(x).

Задача: поменять минимум и максимум местами

Дан вектор из n различных чисел. Нужно поменять местами его минимальный и максимальный элементы.

Ввод:
5
4 9 1 7 3

Вывод:
4 1 9 7 3

Минимальный элемент 1 находится на позиции 2, а максимальный элемент 9 - на позиции 1.

План решения:

  1. считать весь вектор;
  2. найти позиции минимального и максимального элементов;
  3. поменять элементы на этих позициях;
  4. вывести изменённый вектор.

Полная программа:

#include <iostream>
#include <vector>
using namespace std;

int main() {
    int n;
    cin >> n;

    vector<int> a(n);

    for (int i = 0; i < n; i++) {
        cin >> a[i];
    }

    int min_pos = 0;
    int max_pos = 0;

    for (int i = 1; i < n; i++) {
        if (a[i] < a[min_pos]) {
            min_pos = i;
        }

        if (a[i] > a[max_pos]) {
            max_pos = i;
        }
    }

    int temp = a[min_pos];
    a[min_pos] = a[max_pos];
    a[max_pos] = temp;

    for (int i = 0; i < n; i++) {
        cout << a[i] << " ";
    }

    return 0;
}

Сначала считаем, что минимум и максимум находятся на позиции 0. Затем проверяем все остальные позиции.

Если a[i] меньше текущего минимального элемента, запоминаем i как новую позицию минимума. Аналогично ищется позиция максимума.

Для обмена используется временная переменная temp. Без неё первое присваивание затёрло бы одно из двух значений.

Строки

string хранит строку, то есть последовательность символов.

Чтобы использовать строки, можно подключить библиотеку:

#include <string>

Создание и ввод строки:

string s;
cin >> s;

Команда cin >> s читает одно слово до первого пробела.

Строка похожа на вектор:

Например, строка code устроена так:

индекс:  0   1   2   3
символ: 'c' 'o' 'd' 'e'

Символы строки

Для хранения одного символа в C++ используется тип char:

char c = 'a';

Можно сохранить в переменную типа char один из символов строки:

char c = s[0];

Один символ в C++ записывается в одинарных кавычках:

'a'
'7'
'?'

Строка записывается в двойных кавычках:

"a"
"hello"

Это разные виды значений:

Элемент s[i] является одним символом, поэтому сравнивать его нужно со значением в одинарных кавычках:

if (s[i] == 'a') {
    // символ на позиции i равен букве a
}

Символ строки можно изменять:

s[0] = 'A';

Перебор символов строки

Выведем индекс и символ для каждой позиции строки:

#include <iostream>
#include <string>
using namespace std;

int main() {
    string s;
    cin >> s;

    int n = s.size();

    for (int i = 0; i < n; i++) {
        cout << i << " " << s[i] << endl;
    }

    return 0;
}

Для строки code программа выведет:

0 c
1 o
2 d
3 e

Количество букв a

Посчитаем, сколько раз буква a встречается в слове.

#include <iostream>
#include <string>
using namespace std;

int main() {
    string s;
    cin >> s;

    int answer = 0;
    int n = s.size();

    for (int i = 0; i < n; i++) {
        if (s[i] == 'a') {
            answer++;
        }
    }

    cout << answer;
    return 0;
}

Примеры:

banana -> 3
apple  -> 1
cpp    -> 0

Алгоритм похож на обработку вектора: перебираем все допустимые индексы, проверяем текущий элемент и при выполнении условия увеличиваем ответ.

Логический тип bool

Тип bool хранит одно из двух логических значений:

true
false

Например:

bool ok = true;

В переменной ok можно хранить ответ на вопрос «выполняется ли нужное свойство?».

Задача: палиндром

Палиндром - строка, которая одинаково читается слева направо и справа налево.

Примеры:

level -> YES
abba  -> YES
hello -> NO

Чтобы проверить строку, будем сравнивать симметричные символы:

Если длина строки равна n, симметричным для индекса i является индекс:

n - 1 - i

Полная программа:

#include <iostream>
#include <string>
using namespace std;

int main() {
    string s;
    cin >> s;

    int n = s.size();
    bool ok = true;

    for (int i = 0; i < n; i++) {
        if (s[i] != s[n - 1 - i]) {
            ok = false;
        }
    }

    if (ok) {
        cout << "YES";
    } else {
        cout << "NO";
    }

    return 0;
}

Сначала предполагаем, что строка является палиндромом. Если находится хотя бы одна несовпадающая пара, значение ok становится равным false.

В этой программе каждая пара проверяется дважды. Можно проверить только первую половину строки:

for (int i = 0; i < n / 2; i++) {
    if (s[i] != s[n - 1 - i]) {
        ok = false;
    }
}

Оба варианта дают правильный ответ.

Главное из лекции

Цикл while:

while (condition) {
    // повторять, пока condition истинно
}

Цикл for:

for (int i = 0; i < n; i++) {
    // i принимает значения от 0 до n - 1
}

Вектор известного размера:

vector<int> a(n);

Пустой вектор и добавление элемента:

vector<int> a;
a.push_back(x);

Обращение к элементам последовательности:

a[i]  // элемент вектора
s[i]  // символ строки

Размер последовательности:

a.size()
s.size()

Главное правило для индексов:

размер n -> допустимые индексы от 0 до n - 1