Структурированные типы данных. Массивы
Массивы — это фундаментальная структура данных, которая используется повсюду: от алгоритмов рекомендаций в YouTube до обработки пикселей в играх. Сегодня мы разберёмся, как устроен этот механизм и как использовать его для решения реальных задач на языке C.
Что такое массив и зачем он нужен?
Массив — это структура данных, представляющая собой упорядоченную коллекцию элементов одного типа, каждый из которых идентифицируется своим индексом (номером позиции).
💡 Зачем нужны массивы?
Представь, что тебе нужно обработать результаты 1000 студентов. Создавать 1000 отдельных переменных (score1, score2, ..., score1000) — абсурд. Массив позволяет хранить все данные под одним именем и обращаться к каждому элементу по индексу.
Объявление массива в языке C
В C массив объявляется следующим образом:
тип_данных имя_массива[размер];
Примеры объявления массивов:
int temperatures[365]; // массив из 365 целых чисел (температуры по дням года)
double prices[100]; // массив из 100 вещественных чисел (цены товаров)
char password[20]; // массив из 20 символов (пароль)
⚠️ Критически важно!
В C индексация массивов начинается с 0. Это значит, что первый элемент имеет индекс 0, второй — индекс 1, и так далее. Массив из 10 элементов имеет индексы от 0 до 9.
Инициализация массива
Массив можно инициализировать при объявлении:
int scores[5] = {95, 87, 76, 92, 88};
double weights[] = {65.5, 72.3, 58.1}; // размер определяется автоматически
Или заполнить в цикле:
int numbers[100];
for (int i = 0; i < 100; i++) {
numbers[i] = i * 2; // заполняем чётными числами
}
Основные операции с массивами
Рассмотрим базовые операции, которые выполняются с массивами в подавляющем большинстве задач.
Обработка массива: последовательный доступ к элементам через индексацию
📥 Ввод и вывод элементов
Пример: Ввод оценок студента и вывод в обратном порядке.
#include <stdio.h>
int main() {
int grades[5];
printf("Введите 5 оценок:\n");
for (int i = 0; i < 5; i++) {
printf("Оценка %d: ", i + 1);
scanf("%d", &grades[i]);
}
printf("\nОценки в обратном порядке:\n");
for (int i = 4; i >= 0; i--) {
printf("%d ", grades[i]);
}
printf("\n");
return 0;
}
Что происходит? Первый цикл for последовательно заполняет массив с индекса 0 до 4. Второй цикл выводит элементы в обратном порядке — с индекса 4 до 0.
🔍 Поиск элемента
Задача: Найти, есть ли в массиве заданное число.
Линейный поиск — самый простой алгоритм: последовательно проверяем каждый элемент.
#include <stdio.h>
int main() {
int data[10] = {23, 45, 12, 67, 34, 89, 21, 56, 78, 90};
int target, found = 0;
printf("Введите число для поиска: ");
scanf("%d", &target);
for (int i = 0; i < 10; i++) {
if (data[i] == target) {
printf("Число %d найдено на позиции %d\n", target, i);
found = 1;
break; // прерываем поиск
}
}
if (!found) {
printf("Число %d не найдено\n", target);
}
return 0;
}
Анализ сложности: В худшем случае (элемент в конце или отсутствует) нужно проверить все n элементов. Сложность: O(n) — линейная.
📊 Поиск максимума и минимума
Подход: Принимаем первый элемент за текущий максимум/минимум, затем сравниваем с ним все остальные элементы.
#include <stdio.h>
int main() {
int numbers[8] = {42, 15, 73, 8, 91, 27, 64, 35};
int max = numbers[0];
int min = numbers[0];
int max_index = 0;
int min_index = 0;
for (int i = 1; i < 8; i++) {
if (numbers[i] > max) {
max = numbers[i];
max_index = i;
}
if (numbers[i] < min) {
min = numbers[i];
min_index = i;
}
}
printf("Максимум: %d (индекс %d)\n", max, max_index);
printf("Минимум: %d (индекс %d)\n", min, min_index);
return 0;
}
Почему начинаем с i=1? Потому что numbers[0] уже стал начальным значением для max и min. Это классическая оптимизация.
Модификация массивов: вставка и удаление
Рассмотрим операции, которые изменяют структуру массива.
Операции вставки и удаления: реорганизация элементов массива
Удаление элемента
Задача: Удалить элемент с индексом k и сдвинуть все последующие элементы влево.
Логика: Элементы с индексами k+1, k+2, ..., n-1 перемещаются на позиции k, k+1, ..., n-2.
#include <stdio.h>
int main() {
int arr[7] = {10, 20, 30, 40, 50, 60, 70};
int n = 7; // текущий размер массива
int k = 3; // индекс удаляемого элемента
printf("Массив до удаления:\n");
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
// Сдвигаем элементы влево
for (int i = k; i < n - 1; i++) {
arr[i] = arr[i + 1];
}
n--; // уменьшаем размер
printf("Массив после удаления элемента с индексом %d:\n", k);
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
Результат: 10 20 30 50 60 70 — элемент 40 (индекс 3) удалён.
Вставка элемента
Задача: Вставить новое значение на позицию с индексом k.
Логика: Элементы с индексами k, k+1, ..., n-1 сдвигаются вправо на позиции k+1, k+2, ..., n.
⚠️ Критично!
Сдвиг должен идти справа налево, иначе данные перезапишутся!
#include <stdio.h>
int main() {
int arr[10] = {10, 20, 30, 40, 50, 60, 70}; // объявляем с запасом
int n = 7; // текущий размер
int k = 3; // позиция вставки
int value = 35; // вставляемое значение
printf("Массив до вставки:\n");
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
// Сдвигаем элементы вправо (ВАЖНО: справа налево!)
for (int i = n; i > k; i--) {
arr[i] = arr[i - 1];
}
arr[k] = value;
n++;
printf("Массив после вставки %d на позицию %d:\n", value, k);
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
Результат: 10 20 30 35 40 50 60 70
Реверс массива
Задача: Перевернуть массив (первый элемент становится последним, и так далее).
Алгоритм: Меняем местами элементы с индексами i и n-1-i, где i идёт от 0 до n/2.
#include <stdio.h>
int main() {
int arr[6] = {1, 2, 3, 4, 5, 6};
int n = 6;
printf("Исходный массив: ");
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
// Реверс
for (int i = 0; i < n / 2; i++) {
int temp = arr[i];
arr[i] = arr[n - 1 - i];
arr[n - 1 - i] = temp;
}
printf("Перевёрнутый массив: ");
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
Результат: 6 5 4 3 2 1
💡 Почему n/2?
Если массив содержит 6 элементов, достаточно 3 обменов: (0↔5), (1↔4), (2↔3). Центральный элемент (если n нечётное) остаётся на месте автоматически.
Сортировка массивов
Зачем сортировать? Упорядоченные данные обрабатываются эффективнее. Например, двоичный поиск в отсортированном массиве имеет сложность O(log n) вместо O(n).
Сортировка: упорядочивание хаотичных данных в структурированную последовательность
💧 Сортировка пузырьком (Bubble Sort)
Принцип: Сравниваем соседние элементы и меняем их местами, если они в неправильном порядке. "Тяжёлые" элементы "тонут" вниз, "лёгкие" "всплывают" вверх.
#include <stdio.h>
void bubbleSort(int arr[], int n) {
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
// Обмен элементов
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
int main() {
int data[6] = {64, 34, 25, 12, 22, 11};
int n = 6;
printf("До сортировки: ");
for (int i = 0; i < n; i++) printf("%d ", data[i]);
printf("\n");
bubbleSort(data, n);
printf("После сортировки: ");
for (int i = 0; i < n; i++) printf("%d ", data[i]);
printf("\n");
return 0;
}
Анализ:
- Внешний цикл (
i): n-1 проход - Внутренний цикл (
j): на каждом проходе сравниваем всё меньше элементов - Сложность: O(n²) — квадратичная
Почему n-i-1? После каждого прохода самый большой элемент оказывается в конце, его больше не нужно проверять.
🎯 Сортировка выбором (Selection Sort)
Принцип: Находим минимальный элемент в неотсортированной части массива и меняем его местами с первым неотсортированным элементом.
#include <stdio.h>
void selectionSort(int arr[], int n) {
for (int i = 0; i < n - 1; i++) {
int min_index = i;
// Находим индекс минимального элемента
for (int j = i + 1; j < n; j++) {
if (arr[j] < arr[min_index]) {
min_index = j;
}
}
// Обмен
if (min_index != i) {
int temp = arr[i];
arr[i] = arr[min_index];
arr[min_index] = temp;
}
}
}
int main() {
int data[6] = {64, 25, 12, 22, 11, 34};
int n = 6;
printf("До сортировки: ");
for (int i = 0; i < n; i++) printf("%d ", data[i]);
printf("\n");
selectionSort(data, n);
printf("После сортировки: ");
for (int i = 0; i < n; i++) printf("%d ", data[i]);
printf("\n");
return 0;
}
Преимущество: Минимальное количество обменов — всего n-1. Полезно, когда операция обмена дорогая.
Сложность: O(n²) по сравнениям, O(n) по обменам.
Практическое применение: реальные задачи
Рассмотрим задачи, которые демонстрируют практическую ценность работы с массивами.
Эффективность алгоритмов: от линейного к логарифмическому поиску
🌡️ Задача 1: Анализ метеоданных
Условие: Даны температуры за 30 дней июня. Найти среднюю температуру и дни с отклонением более чем на 5°C.
#include <stdio.h>
int main() {
double temp[30];
double sum = 0, avg;
printf("Введите температуры за 30 дней июня:\n");
for (int i = 0; i < 30; i++) {
printf("День %d: ", i + 1);
scanf("%lf", &temp[i]);
sum += temp[i];
}
avg = sum / 30;
printf("\nСредняя температура: %.2f°C\n", avg);
printf("\nДни с отклонением >5°C от средней:\n");
for (int i = 0; i < 30; i++) {
double deviation = temp[i] - avg;
if (deviation > 5 || deviation < -5) {
printf("День %d: %.2f°C (отклонение: %+.2f°C)\n",
i + 1, temp[i], deviation);
}
}
return 0;
}
🔐 Задача 2: Криптографическая головоломка Буратино
Условие: Шифр замка — двузначное число. Сумма цифр + произведение цифр = само число. Найти все варианты.
Математический подход: Пусть число 10a + b, где a и b — цифры.
Условие: (a + b) + (a * b) = 10a + b
Упрощаем: a + a*b = 10a → a*b = 9a → b = 9 (при a ≠ 0)
#include <stdio.h>
int main() {
printf("Возможные коды замка:\n");
for (int num = 10; num <= 99; num++) {
int a = num / 10; // первая цифра
int b = num % 10; // вторая цифра
if ((a + b) + (a * b) == num) {
printf("%d ", num);
}
}
printf("\n");
return 0;
}
Результат: 19, 29, 39, 49, 59, 69, 79, 89, 99
Проверка для 19: (1+9) + (1×9) = 10 + 9 = 19 ✓
✨ Ключевые выводы
Подведём итоги изученного материала.
🤔 Проверь себя
Мини-кейсы для проверки понимания материала.
Мини-кейс 1: У тебя есть массив ID пользователей, вошедших в систему за последний час. Как быстро проверить, входил ли конкретный пользователь, если массив неотсортирован? Отсортирован?
Подумай о:
- Какой алгоритм поиска использовать в неотсортированном массиве?
- Можно ли ускорить поиск в отсортированном массиве?
- Какова будет сложность каждого подхода?
Мини-кейс 2: В игре персонажи хранятся в массиве. При смерти персонажа его нужно удалить. Какая операция более эффективна: сдвиг всех элементов или просто пометка ячейки как "пустая"?
Рассмотри:
- Сколько операций требует каждый подход?
- Как это повлияет на обход массива?
- Какие есть плюсы и минусы каждого метода?
Мини-кейс 3: Тебе нужно найти медиану (среднее значение при упорядочивании) массива из 1001 элемента. Какой первый шаг?
Подсказка:
- Что нужно сделать с массивом перед поиском медианы?
- На какой позиции будет находиться медиана после сортировки?
- Нужно ли полностью сортировать массив?
Задача: Напиши функцию, которая проверяет, является ли массив палиндромом (читается одинаково слева направо и справа налево).
Алгоритм:
- Сравнивай элементы с индексами i и n-1-i
- Достаточно проверить до середины массива
- Если хотя бы одна пара не совпадает — не палиндром
💻 Практические задания
Примени полученные знания на практике!
📝 Задание 1: Работа с температурами
Создай программу, которая:
- Вводит температуры за неделю
- Находит самую высокую и низкую температуру
- Вычисляет среднюю температуру
- Определяет, сколько дней было теплее/холоднее средней
🔢 Задание 2: Обработка чисел
Напиши программу, которая:
- Заполняет массив из 20 случайных чисел от 1 до 100
- Находит сумму чётных элементов
- Подсчитывает количество элементов, кратных 3
- Выводит элементы в обратном порядке
🎲 Задание 3: Анализ оценок
Разработай программу для анализа оценок класса:
- Ввод оценок 10 учеников (от 2 до 5)
- Подсчёт количества каждой оценки
- Нахождение средней оценки класса
- Определение количества отличников (5) и двоечников (2)
🔄 Задание 4: Циклический сдвиг
Реализуй программу циклического сдвига массива:
- Создай массив из 8 элементов
- Реализуй сдвиг всех элементов на 1 позицию вправо
- Последний элемент должен стать первым
- Например: {1,2,3,4,5} → {5,1,2,3,4}