JavaRush /Курсы /Go SELF /Алгоритмические шаблоны в циклах

Алгоритмические шаблоны в циклах

Go SELF
4 уровень , 5 лекция
Открыта

1. Введение

Когда вы впервые видите задачу “посчитать сумму”, “найти максимум” или “проверить, встречается ли число”, кажется, что каждый раз надо изобретать велосипед. На деле почти все такие задачи решаются одинаковыми схемами: мы заводим переменную результата, правильно её инициализируем и обновляем в цикле. Это и есть алгоритмические шаблоны.

Представьте, что цикл — это конвейер, по которому едут коробки (вводимые числа). Вы стоите рядом и делаете одно и то же действие для каждой коробки: прибавляете к сумме, увеличиваете счётчик, обновляете максимум, ставите флаг “нашёл” и т.д. Мозгу проще, когда он узнаёт знакомую схему: “ага, это аккумулятор”, “ага, это поиск с флагом и break”.

Чтобы примеры были связными, заведём мини-приложение, которое будем развивать: numstat — консольный анализатор чисел. Он читает числа и печатает простую статистику. Сегодня добавим в него “умения”: сумму, подсчёт, максимум, поиск.

2. Шаблон «сумма»: аккумулятор, который не забывает

Сумма — самый добрый шаблон, потому что обычно не требует хитрой инициализации: достаточно начать с нуля, и дальше аккуратно прибавлять. Важно понимать роль переменной суммы: она хранит промежуточный результат, и на каждой итерации цикла мы её обновляем. Такие переменные часто называют аккумуляторами (накопителями).

Начнём с базового сценария: вводится n, затем n целых чисел. Нужно вывести их сумму.

package main

import "fmt"

func main() {
	var n int
	fmt.Scan(&n)

	sum := 0
	for i := 0; i < n; i++ {
		var x int
		fmt.Scan(&x)
		sum = sum + x
	}

	fmt.Println(sum) // например: 15
}

Здесь важно заметить “скелет” шаблона:

  • sum := 0 — стартовое значение,
  • внутри цикла sum = sum + x — правило обновления.

Сумма по условию: суммируем только нужное

Теперь чуть интереснее: допустим, мы хотим суммировать только чётные числа. В цикле появляется if, но шаблон остаётся тем же — просто обновление суммы происходит не всегда.

package main

import "fmt"

func main() {
	var n int
	fmt.Scan(&n)

	sumEven := 0
	for i := 0; i < n; i++ {
		var x int
		fmt.Scan(&x)
		if x%2 == 0 {
			sumEven = sumEven + x
		}
	}

	fmt.Println(sumEven) // например: 12
}

Заметьте хорошую привычку: имя sumEven сразу объясняет, что мы считаем. Если вы назовёте переменную s, она будет как кот Шрёдингера: вроде есть, но непонятно что внутри.

Мини-схема шаблона суммы

flowchart TD
    A[sum = 0] --> B{есть ещё число?}
    B -->|да| C[прочитать x]
    C --> D[sum = sum + x]
    D --> B
    B -->|нет| E[вывести sum]

3. Шаблон «подсчёт»: считаем по условию

После суммы обычно идёт подсчёт: “сколько чисел положительные?”, “сколько раз встретилась буква?”, “сколько значений делится на 3?”. Это почти то же самое, что сумма, только мы прибавляем не x, а 1, и чаще всего — по условию.

Здесь тоже есть аккумулятор, просто он считается не “суммой”, а “количеством”. И стартовое значение обычно 0, потому что до обработки данных мы ничего ещё не нашли.

Посчитаем, сколько среди введённых n чисел строго больше нуля.

package main

import "fmt"

func main() {
	var n int
	fmt.Scan(&n)

	countPos := 0
	for i := 0; i < n; i++ {
		var x int
		fmt.Scan(&x)
		if x > 0 {
			countPos++
		}
	}

	fmt.Println(countPos) // например: 3
}

Обратите внимание на countPos++. Это просто короткая форма “прибавь 1”. Внутри Go-программы это как “плюс один к уверенности”.

Подсчёт по строке: форма цикла

Раз вы уже видели range по строкам, можно сделать мини-пример: посчитаем количество символов 'a' в строке. Для этого нам важна сама идея “в цикле проверяем элемент и увеличиваем счётчик”.

package main

import "fmt"

func main() {
	s := "bananas"

	countA := 0
	for _, ch := range s {
		if ch == 'a' {
			countA++
		}
	}

	fmt.Println(countA) // 3
}

4. Шаблон «максимум»: правильная инициализация

Поиск максимума (или минимума) кажется простым: “если текущее больше максимума — обнови максимум”. Так и есть… пока вы не столкнулись с отрицательными числами и не инициализировали максимум нулём “по привычке”. Тогда максимум внезапно станет 0 даже там, где в данных нет ни одного нуля. Магия? Нет, просто плохая инициализация.

Главная мысль: для максимума важно, откуда берётся начальное значение. Есть два нормальных подхода для новичка. Самый простой и надёжный — считать первый элемент отдельно и использовать его как стартовый max. Но тогда надо обработать случай n <= 0, иначе вы попытаетесь читать то, чего нет.

Максимум из n чисел: инициализация первым элементом

package main

import "fmt"

func main() {
	var n int
	fmt.Scan(&n)

	if n <= 0 {
		fmt.Println(0) // договоримся: для пустого ввода печатаем 0
		return
	}

	var max int
	fmt.Scan(&max) // первый элемент становится стартовым максимумом

	for i := 1; i < n; i++ {
		var x int
		fmt.Scan(&x)
		if x > max {
			max = x
		}
	}

	fmt.Println(max) // например: 42
}

Здесь прям видно “правило максимума”:

  • старт: max = первый элемент,
  • обновление: if x > max { max = x }.

Максимум и “где он был”: запоминаем индекс

Иногда в задачах нужно не только значение максимума, но и позиция (каким по счёту он встретился). Это тот же шаблон, просто у максимума появляется “спутник” — индекс.

package main

import "fmt"

func main() {
	var n int
	fmt.Scan(&n)

	if n <= 0 {
		fmt.Println(-1) // нет элементов — нет индекса
		return
	}

	var max int
	fmt.Scan(&max)
	maxIndex := 0

	for i := 1; i < n; i++ {
		var x int
		fmt.Scan(&x)
		if x > max {
			max = x
			maxIndex = i
		}
	}

	fmt.Println(maxIndex) // например: 4
}

Смысл maxIndex простой: “если ты обновляешь максимум — обнови и место, где он найден”. Иначе будет как в детективе: преступник найден, но адрес потеряли.

5. Шаблон «поиск»: флаг и break

Поиск — это когда вы хотите узнать, встречается ли что-то в данных, или найти первое подходящее значение. Самая частая форма для начинающих — “да/нет”: нашли или не нашли. Тут идеально работает связка found := false и break, чтобы выйти из цикла, когда ответ уже известен.

Важно: break не “ускорение ради ускорения”, а способ сделать код честнее. Если ответ найден, продолжать цикл — это как дочитывать инструкцию после того, как уже собрал шкаф и понял, что лишние детали остались (и это явно были не ваши).

Ищем target среди n чисел

package main

import "fmt"

func main() {
	var n, target int
	fmt.Scan(&n, &target)

	found := false
	for i := 0; i < n; i++ {
		var x int
		fmt.Scan(&x)
		if x == target {
			found = true
			break
		}
	}

	fmt.Println(found) // true или false
}

Паттерн здесь почти всегда такой:

  • found := false перед циклом,
  • внутри при успехе: found = true; break,
  • после цикла: печатаем/используем found.

Поиск первого подходящего: пример с отрицательным

Иногда вместо bool нужно значение. Но всё равно удобно иметь флаг, чтобы понять, было ли найдено вообще.

package main

import "fmt"

func main() {
	var n int
	fmt.Scan(&n)

	found := false
	firstNeg := 0

	for i := 0; i < n; i++ {
		var x int
		fmt.Scan(&x)
		if x < 0 {
			firstNeg = x
			found = true
			break
		}
	}

	if found {
		fmt.Println(firstNeg) // например: -7
	} else {
		fmt.Println(0) // договоримся: если нет отрицательных, печатаем 0
	}
}

6. Несколько шаблонов за один проход

Когда вы решаете задачу, часто хочется сделать так: “сначала посчитаю сумму”, потом “отдельным циклом найду максимум”, потом “ещё одним циклом посчитаю количество положительных”. Технически это возможно, но обычно это лишняя работа: данные можно обработать за один проход, если аккуратно держать несколько переменных результата.

Тут полезно воспринимать шаблоны как независимые “модули”: сумма живёт в sum, подсчёт живёт в count, максимум живёт в max. И все они обновляются на каждой итерации (или почти на каждой).

Соберём наш numstat: читаем n, затем n чисел. Хотим вывести:

  • сумму всех чисел,
  • максимум,
  • количество положительных.
package main

import "fmt"

func main() {
	var n int
	fmt.Scan(&n)

	if n <= 0 {
		fmt.Println(0) // sum
		fmt.Println(0) // max
		fmt.Println(0) // countPos
		return
	}

	var x int
	fmt.Scan(&x)

	sum := x
	max := x
	countPos := 0
	if x > 0 {
		countPos = 1
	}

	for i := 1; i < n; i++ {
		fmt.Scan(&x)

		sum = sum + x
		if x > max {
			max = x
		}
		if x > 0 {
			countPos++
		}
	}

	fmt.Println(sum)      // например: 15
	fmt.Println(max)      // например: 9
	fmt.Println(countPos) // например: 3
}

Смотрите, как тут “встроены” три шаблона:

  • сумма: sum = sum + x,
  • максимум: if x > max { max = x },
  • подсчёт: if x > 0 { countPos++ }.

И всё это — внутри одного цикла.

Таблица ролей переменных

Переменная Роль Начало Обновление в цикле
sum
сумма первый элемент
sum = sum + x
max
максимум первый элемент
if x > max
countPos
подсчёт 0 или 1 (по первому x)
if x > 0

Если вы научитесь составлять такую табличку “в голове” перед тем, как писать код — вы внезапно обнаружите, что задачи на циклы стали в разы спокойнее.

Нюанс: читаем числа до конца ввода

Иногда входные данные могут закончиться неожиданно. В учебных задачах чаще всего “всё хорошо”, но привычка проверять результат fmt.Scan делает код взрослее. В Go вообще принято относиться к ошибкам как к обычным значениям и обрабатывать их явно.

Вот минимальная форма “читаем, пока читается” — полезна, когда не задан n, а числа идут до конца ввода:

package main

import "fmt"

func main() {
	sum := 0

	for {
		var x int
		_, err := fmt.Scan(&x)
		if err != nil {
			break
		}
		sum = sum + x
	}

	fmt.Println(sum) // сумма всех прочитанных чисел
}

Здесь шаблон суммы остался прежним, просто цикл управляется не счётчиком, а “пока ввод успешен”.

7. Типичные ошибки

Ошибка №1: максимум инициализируют нулём “на автомате”.
Это ломает задачи, где все числа могут быть отрицательными. Программа бодро выведет 0, хотя нуля в данных не было. Более надёжная привычка — инициализировать максимум первым элементом, а для пустого ввода заранее договориться, что печатаем (и поставить if n <= 0 { ... return }).

Ошибка №2: путают роль переменных и начинают “считать счётчиком сумму”.
Иногда пишут sum++ вместо sum = sum + x, или используют i и как индекс цикла, и как счётчик результата. Это обычно происходит, когда переменные названы слишком коротко. Если вы чувствуете, что начинаете путаться, переименуйте s в sum, c в countPos — и мозг скажет вам спасибо (обычно молча).

Ошибка №3: в поиске забывают break, и “поиск” превращается в “наблюдение”.
Флаг found вы выставили, но цикл продолжает читать вход и делать лишнюю работу. В задачах, где после нахождения элемента уже всё ясно, break — это не оптимизация, а часть корректного смысла: “ответ найден, дальше не ищем”.

Ошибка №4: неправильные границы в цикле при чтении n чисел.
Классика: вместо i < n пишут i <= n и случайно читают n + 1 число. Ошибка выглядит особенно коварно, потому что иногда “случайно” входных данных хватает, и вы не видите проблему сразу. Хорошая проверка — вслух проговорить: “я хочу ровно n итераций, значит i должен принять значения 0..n-1”.

Ошибка №5: “пустой ввод” не обрабатывается, а код всё равно пытается прочитать первый элемент.
Такое часто случается в шаблоне максимума, где мы читаем первый элемент отдельно. Если забыть if n <= 0, программа попытается читать число, которого нет, и дальше поведение станет неприятно непредсказуемым. Лучше один раз честно обработать этот случай в начале, чем потом искать, почему “иногда работает, иногда нет”.

1
Задача
Go SELF, 4 уровень, 5 лекция
Недоступна
Итог смены
Итог смены
1
Задача
Go SELF, 4 уровень, 5 лекция
Недоступна
Счётчик скидок
Счётчик скидок
1
Задача
Go SELF, 4 уровень, 5 лекция
Недоступна
Чемпион и место
Чемпион и место
1
Задача
Go SELF, 4 уровень, 5 лекция
Недоступна
Поиск маркера
Поиск маркера
1
Опрос
Циклы for, 4 уровень, 5 лекция
Недоступен
Циклы for
Циклы for
Комментарии (1)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Georgy Уровень 16
18 мая 2026
ребят это далеко не медиум задачи это какая то легкотня