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++ }.
И всё это — внутри одного цикла.
Таблица ролей переменных
| Переменная | Роль | Начало | Обновление в цикле |
|---|---|---|---|
|
сумма | первый элемент | |
|
максимум | первый элемент | |
|
подсчёт | 0 или 1 (по первому x) | |
Если вы научитесь составлять такую табличку “в голове” перед тем, как писать код — вы внезапно обнаружите, что задачи на циклы стали в разы спокойнее.
Нюанс: читаем числа до конца ввода
Иногда входные данные могут закончиться неожиданно. В учебных задачах чаще всего “всё хорошо”, но привычка проверять результат 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, программа попытается читать число, которого нет, и дальше поведение станет неприятно непредсказуемым. Лучше один раз честно обработать этот случай в начале, чем потом искать, почему “иногда работает, иногда нет”.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ