Задачи модуля "Линейные списки": от сравнения до реверса
Задача 1. Сравнение двух линейных списков
Условие: Напишите функцию, которая проверяет два линейных односвязных списка на полное совпадение. Списки считаются равными, если они имеют одинаковую длину и их элементы попарно равны.
Подсказка
Для сравнения элементов вам потребуется одновременно проходить по обоим спискам. Как лучше организовать цикл обхода?
Разбор алгоритма
Описание алгоритма:
Два линейных односвязных списка считаются равными, если они имеют одинаковую длину и их соответствующие элементы попарно равны.
Алгоритм заключается в параллельном обходе обоих списков с помощью цикла while:
- Пока оба указателя не достигли конца (
p1 != NULL && p2 != NULL), сравниваем их данные. - Если данные не совпадают - немедленно возвращаем 0 (ложь).
- Если данные совпадают, сдвигаем оба указателя на следующий элемент (
p1 = p1->next; p2 = p2->next;). - Когда цикл завершится, необходимо убедиться, что оба списка закончились одновременно. Если один список оказался длиннее другого, указатели на момент выхода не будут одновременно равны
NULL. Возвращаем результат проверкиp1 == NULL && p2 == NULL.
Код на C
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
// Структура узла односвязного списка
typedef struct Node {
int data;
struct Node* next;
} Node;
// Функция сравнения двух списков
bool CompareLists(Node* head1, Node* head2) {
Node* p1 = head1;
Node* p2 = head2;
// Параллельный проход по обоим спискам
while (p1 != NULL && p2 != NULL) {
// Если элементы на текущей позиции не равны - списки не равны
if (p1->data != p2->data) {
return false;
}
p1 = p1->next;
p2 = p2->next;
}
// Цикл завершился. Списки равны только в том случае,
// если они оба закончились ОДНОВРЕМЕННО (то есть имели одинаковую длину)
return (p1 == NULL && p2 == NULL);
}
// Вспомогательная функция для добавления узла в конец
Node* Append(Node* head, int data) {
Node* newNode = (Node*)malloc(sizeof(Node));
newNode->data = data;
newNode->next = NULL;
if (head == NULL) return newNode;
Node* current = head;
while (current->next != NULL) {
current = current->next;
}
current->next = newNode;
return head;
}
int main() {
Node* list1 = NULL;
Node* list2 = NULL;
// Заполнение первого списка: 1 -> 2 -> 3
list1 = Append(list1, 1);
list1 = Append(list1, 2);
list1 = Append(list1, 3);
// Заполнение второго списка: 1 -> 2 -> 3
list2 = Append(list2, 1);
list2 = Append(list2, 2);
list2 = Append(list2, 3);
if (CompareLists(list1, list2)) {
printf("Списки абсолютно идентичны.\n");
} else {
printf("Списки различаются.\n");
}
// Очистка памяти опущена
return 0;
}
Задача 2. Уничтожение (удаление) линейного списка
Условие: Напишите функцию для полного удаления динамического связного списка с освобождением памяти каждого узла.
Подсказка
Если вы освободите текущий узел (free), вы потеряете указатель на следующий элемент. Как этого избежать?
Разбор алгоритма
Описание алгоритма:
В языке Си нет автоматического сборщика мусора. Если мы создали динамический связный список (используя malloc), мы обязаны вручную освободить память каждого узла с помощью free() перед завершением программы (или когда список больше не нужен), чтобы избежать утечек памяти (memory leaks).
- Используем цикл
while (head != NULL). - Критический момент: Нельзя сделать
free(head)сразу, так как мы потеряем указатель на следующий элементhead->nextи не сможем продолжить цикл! - Поэтому сначала сохраняем адрес следующего элемента во временную переменную:
Node* temp = head->next; - Освобождаем память текущего узла:
free(head); - Перемещаем указатель
headна следующий узел:head = temp;
Код на C
#include <stdio.h>
#include <stdlib.h>
// Узел односвязного списка
typedef struct Node {
int data;
struct Node* next;
} Node;
// Функция для уничтожения всего списка
// Принимает указатель на указатель (**head), чтобы обнулить саму переменную головы
void DestroyList(Node** headRef) {
Node* current = *headRef;
Node* next_node = NULL;
// Проходим по всему списку
while (current != NULL) {
// Запоминаем следующий элемент до того, как текущий будет уничтожен
next_node = current->next;
// Уничтожаем текущий элемент (возвращаем память ОС)
free(current);
// Переходим к следующему
current = next_node;
}
// Безопасность: обнуляем указатель на начало списка,
// чтобы предотвратить обращение к освобожденной памяти (Dangling Pointer)
*headRef = NULL;
}
// Вспомогательная функция для добавления узла в начало
void Push(Node** headRef, int data) {
Node* newNode = (Node*)malloc(sizeof(Node));
newNode->data = data;
newNode->next = *headRef;
*headRef = newNode;
}
int main() {
Node* head = NULL;
Push(&head, 1);
Push(&head, 2);
Push(&head, 3);
printf("Список создан (занята динамическая память).\n");
// Уничтожение списка
DestroyList(&head);
if (head == NULL) {
printf("Список успешно уничтожен! Утечек памяти нет.\n");
}
return 0;
}
Задача 3. Умножение разреженных матриц
Условие: Реализуйте алгоритм умножения двух разреженных матриц, представленных в координатном формате (COO: строка, столбец, значение).
Подсказка
При умножении матриц элемент результирующей матрицы C[i][j] формируется из строки i матрицы A и столбца j матрицы B. Какие элементы дадут ненулевой вклад?
Разбор алгоритма
Описание алгоритма:
Разреженная матрица - это матрица, в которой подавляющее большинство элементов равны нулю. Хранить такие матрицы в виде огромных двумерных массивов A[N][M] невыгодно. Обычно их хранят в виде списка координатных троек: (строка, столбец, значение). При умножении $C = A \times B$ мы должны найти такие пары ненулевых элементов из A и B, где столбец элемента A равен строке элемента B. Если совпадение найдено, мы добавляем произведение их значений к элементу C[строка_A][столбец_B].
Код на C
#include <stdio.h>
#include <stdlib.h>
// Структура для хранения одного ненулевого элемента матрицы (COO формат)
typedef struct {
int row;
int col;
int val;
} Element;
// Структура разреженной матрицы
typedef struct {
int rows, cols; // Размер полной матрицы
int count; // Количество ненулевых элементов
Element* data; // Массив элементов
} SparseMatrix;
// Наивный алгоритм умножения C = A * B для координатного списка
SparseMatrix MultiplySparse(SparseMatrix A, SparseMatrix B) {
SparseMatrix C;
C.rows = A.rows;
C.cols = B.cols;
// В худшем случае все комбинации дадут новые элементы, выделим с запасом
// (В реальном коде нужен динамический realloc или хеш-таблица для суммирования)
C.data = (Element*)malloc(A.count * B.count * sizeof(Element));
C.count = 0;
// Временная матрица: суммируем совпадающие индексы, чтобы не было дублей
// Делаем простую инициализацию нулями
int** tempC = (int**)calloc(C.rows, sizeof(int*));
for (int i = 0; i < C.rows; i++) tempC[i] = (int*)calloc(C.cols, sizeof(int));
// Ядро умножения: перемножаем только те элементы,
// где столбец из A равен строке из B (т.к. строка умножается на столбец)
for (int i = 0; i < A.count; i++) {
for (int j = 0; j < B.count; j++) {
if (A.data[i].col == B.data[j].row) {
// Добавляем вклад в итоговую ячейку
tempC[A.data[i].row][B.data[j].col] += A.data[i].val * B.data[j].val;
}
}
}
// Собираем обратно в формат разреженной матрицы (записываем только не нули)
for (int i = 0; i < C.rows; i++) {
for (int j = 0; j < C.cols; j++) {
if (tempC[i][j] != 0) {
C.data[C.count].row = i;
C.data[C.count].col = j;
C.data[C.count].val = tempC[i][j];
C.count++;
}
}
free(tempC[i]);
}
free(tempC);
return C;
}
int main() {
// Матрица A (2x3). Ненулевые: A[0][1] = 5, A[1][2] = 2
SparseMatrix A = {2, 3, 2, (Element*)malloc(2 * sizeof(Element))};
A.data[0] = (Element){0, 1, 5};
A.data[1] = (Element){1, 2, 2};
// Матрица B (3x2). Ненулевые: B[1][0] = 4, B[2][1] = 3
SparseMatrix B = {3, 2, 2, (Element*)malloc(2 * sizeof(Element))};
B.data[0] = (Element){1, 0, 4};
B.data[1] = (Element){2, 1, 3};
SparseMatrix C = MultiplySparse(A, B);
// Ожидаемый результат:
// C[0][0] = A[0][1]*B[1][0] = 5*4 = 20
// C[1][1] = A[1][2]*B[2][1] = 2*3 = 6
printf("Результат умножения:\n");
for (int i = 0; i < C.count; i++) {
printf("C[%d][%d] = %d\n", C.data[i].row, C.data[i].col, C.data[i].val);
}
free(A.data); free(B.data); free(C.data);
return 0;
}
Задача 4. Скалярное произведение разреженных векторов
Условие: Найдите скалярное произведение двух разреженных векторов, представленных в виде связных списков ненулевых элементов, отсортированных по индексу.
Подсказка
Поскольку нули при умножении дают ноль, вас интересуют только совпадающие индексы. Это похоже на алгоритм слияния двух отсортированных списков.
Разбор алгоритма
Описание алгоритма:
Разреженные векторы обычно задаются связными списками (или массивами) структур, хранящих пары (индекс_ячейки, значение). Элементы хранятся в отсортированном по индексу порядке. Элементы с нулевым значением не хранятся вообще.
Скалярное произведение двух векторов $A$ и $B$ - это сумма произведений их соответствующих компонент: $A[0]*B[0] + A[1]*B[1] + ... + A[n]*B[n]$. Так как нули при умножении дают ноль, нас интересуют только те индексы, которые одновременно присутствуют в обоих векторах.
- Ставим два указателя
p1иp2на начало списков $A$ и $B$. - В цикле сравниваем их индексы:
- Если
p1->index == p2->index: перемножаем их значения, прибавляем к сумме и сдвигаем оба указателя. - Если
p1->index < p2->index: сдвигаемp1(догоняем). - Если
p1->index > p2->index: сдвигаемp2(догоняем).
- Если
- Цикл завершается, как только один из списков заканчивается.
Код на C
#include <stdio.h>
#include <stdlib.h>
// Узел разреженного вектора
typedef struct Node {
int index; // Индекс элемента в "полном" векторе
int value; // Значение (ненулевое)
struct Node* next;
} Node;
// Вычисление скалярного произведения
long long DotProduct(Node* v1, Node* v2) {
long long sum = 0;
Node* p1 = v1;
Node* p2 = v2;
while (p1 != NULL && p2 != NULL) {
if (p1->index == p2->index) {
// Индексы совпали - умножаем значения!
sum += (long long)p1->value * p2->value;
p1 = p1->next;
p2 = p2->next;
}
else if (p1->index < p2->index) {
// Индекс в первом векторе меньше, двигаем его вперед
p1 = p1->next;
}
else {
// Индекс во втором векторе меньше, двигаем его
p2 = p2->next;
}
}
return sum;
}
// Добавление элемента: индексы должны идти по возрастанию
void Append(Node** head, int index, int value) {
Node* n = (Node*)malloc(sizeof(Node));
n->index = index; n->value = value; n->next = NULL;
if (*head == NULL) {
*head = n;
return;
}
Node* curr = *head;
while (curr->next != NULL) curr = curr->next;
curr->next = n;
}
int main() {
Node* V1 = NULL;
Node* V2 = NULL;
// Вектор V1: [0, 5, 0, 0, 3, 0] -> Храним: (1, 5), (4, 3)
Append(&V1, 1, 5);
Append(&V1, 4, 3);
// Вектор V2: [0, 2, 0, 0, 4, 1] -> Храним: (1, 2), (4, 4), (5, 1)
Append(&V2, 1, 2);
Append(&V2, 4, 4);
Append(&V2, 5, 1);
// Скалярное произведение:
// (5 * 2) + (3 * 4) + (0 * 1) = 10 + 12 = 22
long long result = DotProduct(V1, V2);
printf("Скалярное произведение равно: %lld\n", result); // Ожидается 22
return 0;
}
Задача 5. Сложение разреженных матриц на Си
Условие: Реализуйте алгоритм сложения двух разреженных матриц, представленных в координатном формате (COO), отсортированных по строкам и столбцам.
Подсказка
Сложение разреженных матриц аналогично слиянию двух отсортированных массивов или списков. Что делать, если сумма элементов в одной ячейке равна нулю?
Разбор алгоритма
Описание алгоритма:
Сложение разреженных матриц очень похоже на операцию слияния (Merge) двух отсортированных списков. Матрицы заданы координатным списком (row, col, value), причем списки отсортированы сначала по строкам, а затем по столбцам.
Мы используем два указателя (индекса) `i` и `j` для массивов A и B:
- Сравниваем координаты
(row, col)текущих элементов A и B (лексикографически: сначала row, затем col). - Если координаты совпадают, значит элементы стоят в одной ячейке матрицы. Складываем их значения. Если сумма $\neq 0$, записываем в результат C. Двигаем оба указателя.
- Если координаты $A < B$, записываем элемент $A$ в результат C. Двигаем указатель $A$.
- Если координаты $A > B$, записываем элемент $B$ в результат C. Двигаем указатель $B$.
- Если один из списков закончился, просто дописываем остатки второго списка в результат.
Код на C
#include <stdio.h>
#include <stdlib.h>
// Элемент матрицы (Координатный формат COO)
typedef struct {
int row, col, val;
} Element;
typedef struct {
int rows, cols, count;
Element* data;
} SparseMatrix;
// Функция для сравнения координат
// Возвращает -1 если A "раньше" B, 1 если B "раньше" A, 0 если совпали
int CompareCoordinates(Element a, Element b) {
if (a.row < b.row) return -1;
if (a.row > b.row) return 1;
if (a.col < b.col) return -1;
if (a.col > b.col) return 1;
return 0;
}
SparseMatrix AddSparse(SparseMatrix A, SparseMatrix B) {
SparseMatrix C;
C.rows = A.rows; C.cols = A.cols;
// В худшем случае размер C равен сумме размеров A и B
C.data = (Element*)malloc((A.count + B.count) * sizeof(Element));
C.count = 0;
int i = 0, j = 0;
// Слияние двух массивов
while (i < A.count && j < B.count) {
int cmp = CompareCoordinates(A.data[i], B.data[j]);
if (cmp == 0) { // Координаты совпали - складываем
int sum = A.data[i].val + B.data[j].val;
if (sum != 0) { // Записываем только ненулевой результат
C.data[C.count++] = (Element){A.data[i].row, A.data[i].col, sum};
}
i++; j++;
}
else if (cmp < 0) { // Элемент A идет раньше
C.data[C.count++] = A.data[i++];
}
else { // Элемент B идет раньше
C.data[C.count++] = B.data[j++];
}
}
// Дописываем оставшиеся элементы из A
while (i < A.count) C.data[C.count++] = A.data[i++];
// Дописываем оставшиеся элементы из B
while (j < B.count) C.data[C.count++] = B.data[j++];
return C;
}
int main() {
// A:
// (0, 0, 5)
// (1, 2, -2)
SparseMatrix A = {2, 3, 2, (Element*)malloc(2 * sizeof(Element))};
A.data[0] = (Element){0, 0, 5};
A.data[1] = (Element){1, 2, -2};
// B:
// (0, 0, 3)
// (1, 2, 2) <-- В сумме даст 0! Удалится.
// (1, 1, 4) <-- Внимание, отсортируем B правильно в коде
SparseMatrix B = {2, 3, 3, (Element*)malloc(3 * sizeof(Element))};
B.data[0] = (Element){0, 0, 3};
B.data[1] = (Element){1, 1, 4};
B.data[2] = (Element){1, 2, 2};
SparseMatrix C = AddSparse(A, B);
printf("Сумма матриц:\n");
for (int k = 0; k < C.count; k++) {
printf("C[%d][%d] = %d\n", C.data[k].row, C.data[k].col, C.data[k].val);
}
// Ожидается:
// C[0][0] = 8
// C[1][1] = 4
// Элемент [1][2] исчезнет, т.к. -2 + 2 = 0
free(A.data); free(B.data); free(C.data);
return 0;
}
Задача 6. Слияние трех упорядоченных списков
Условие: Напишите функцию, которая объединяет три отсортированных по возрастанию линейных односвязных списка в один новый отсортированный список.
Подсказка
Используйте три указателя. На каждом шаге выбирайте узел с минимальным значением из трех списков и прикрепляйте его к результату.
Разбор алгоритма
Описание алгоритма:
В отличие от файлов (Задача 11), здесь мы работаем с узлами связного списка в оперативной памяти. Суть алгоритма не меняется: K-way merge.
- Создаем "фиктивный" (
dummy) головной узел для результирующего списка, чтобы было удобно к нему привязывать новые узлы. Указательtailуказывает на конец этого результирующего списка. - Пока хотя бы один из трех списков (
L1, L2, L3) не равенNULL:- Находим минимум из текущих узлов
L1,L2иL3. - Привязываем этот минимальный узел к
tail->nextрезультирующего списка. - Сдвигаем
tailвперед. - Сдвигаем вперед указатель того списка, откуда был взят минимум (например, если минимум был в
L2, тоL2 = L2->next).
- Находим минимум из текущих узлов
- Результатом является
dummy.next.
Код на C
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
// Узел списка
typedef struct Node {
int data;
struct Node* next;
} Node;
// Слияние трех списков
Node* MergeThreeLists(Node* L1, Node* L2, Node* L3) {
Node dummy; // Фиктивная голова для упрощения логики
dummy.next = NULL;
Node* tail = &dummy; // Указатель на конец строящегося списка
while (L1 != NULL || L2 != NULL || L3 != NULL) {
// Инициализируем значения как бесконечность
int val1 = (L1 != NULL) ? L1->data : INT_MAX;
int val2 = (L2 != NULL) ? L2->data : INT_MAX;
int val3 = (L3 != NULL) ? L3->data : INT_MAX;
// Находим минимум
int min_val = val1;
if (val2 < min_val) min_val = val2;
if (val3 < min_val) min_val = val3;
// Прикрепляем минимальный узел к результату и сдвигаем нужный указатель
if (min_val == val1) {
tail->next = L1;
L1 = L1->next;
} else if (min_val == val2) {
tail->next = L2;
L2 = L2->next;
} else {
tail->next = L3;
L3 = L3->next;
}
tail = tail->next; // Сдвигаем хвост результата
}
tail->next = NULL; // Закрываем результирующий список
return dummy.next;
}
// Вспомогательная функция вставки в конец
void Append(Node** head, int data) {
Node* n = (Node*)malloc(sizeof(Node));
n->data = data; n->next = NULL;
if (*head == NULL) { *head = n; return; }
Node* curr = *head;
while (curr->next) curr = curr->next;
curr->next = n;
}
void PrintList(Node* head) {
while (head) { printf("%d ", head->data); head = head->next; }
printf("\n");
}
int main() {
Node* L1 = NULL; Append(&L1, 1); Append(&L1, 4); Append(&L1, 7);
Node* L2 = NULL; Append(&L2, 2); Append(&L2, 5); Append(&L2, 8);
Node* L3 = NULL; Append(&L3, 3); Append(&L3, 6); Append(&L3, 9);
printf("Списки до слияния:\n");
PrintList(L1); PrintList(L2); PrintList(L3);
Node* merged = MergeThreeLists(L1, L2, L3);
printf("Объединенный список:\n");
PrintList(merged); // Ожидается: 1 2 3 4 5 6 7 8 9
return 0;
}
Задача 7. Вычитание длинных чисел, заданных списком цифр
Условие: Реализуйте вычитание двух длинных чисел (A ≥ B), которые представлены в виде односвязных списков цифр. Голова списка содержит единицы, второй узел - десятки и так далее.
Подсказка
Проходите по спискам одновременно. Если цифра уменьшаемого меньше вычитаемого, необходимо "занять" десяток из следующего узла.
Разбор алгоритма
Описание алгоритма:
Для работы с числами, которые превышают максимальный размер встроенных типов данных (например, число из 100 цифр), мы храним их в виде линейных списков. При этом самая удобная форма - хранить число в обратном порядке: то есть голова списка содержит единицы, второй узел - десятки и т.д. Это позволяет легко выполнять математические операции в столбик!
Алгоритм вычитания (считаем, что A ≥ B):
- Идем одновременно по обоим спискам
AиB. - Для каждого разряда вычисляем разность:
diff = (цифра A) - (цифра B) - заем (borrow). - Если разность получилась меньше нуля, мы "занимаем" десяток из следующего разряда:
diff += 10, и устанавливаемborrow = 1. Иначеborrow = 0. - Добавляем полученный `diff` в новый результирующий список.
- В самом конце удаляем лишние ведущие нули (которые в нашем списке находятся в хвосте).
Код на C
#include <stdio.h>
#include <stdlib.h>
// Узел для цифры (хранит число от 0 до 9)
typedef struct Node {
int digit;
struct Node* next;
} Node;
// Добавление узла в хвост списка (чтобы младшие разряды были первыми)
void Append(Node** head, int digit) {
Node* n = (Node*)malloc(sizeof(Node));
n->digit = digit; n->next = NULL;
if (!*head) { *head = n; return; }
Node* curr = *head;
while(curr->next) curr = curr->next;
curr->next = n;
}
// Вычитание списков (Предполагается, что A >= B по значению!)
Node* SubtractLists(Node* A, Node* B) {
Node* result = NULL;
int borrow = 0; // "Заем"
// Идем пока не кончится бОльшее число A
while (A != NULL) {
int valA = A->digit;
int valB = (B != NULL) ? B->digit : 0; // B короче - разряды нули
// Вычитаем в столбик
int diff = valA - valB - borrow;
// Если не хватает, занимаем 10 у следующего разряда
if (diff < 0) {
diff += 10;
borrow = 1;
} else {
borrow = 0;
}
Append(&result, diff);
A = A->next;
if (B != NULL) B = B->next;
}
// Удаление ведущих нулей (в нашем списке это узлы в конце списка)
// Эта часть кода может быть сложной, если нулей несколько,
// поэтому в упрощенном виде для студентов мы ее пропустим или
// реализуем реверсом. Здесь оставлен базовый алгоритм без обрезки нулей.
return result;
}
// Вспомогательная функция (вывод с конца, так как у нас младшие разряды впереди)
void PrintReverse(Node* head) {
if (!head) return;
PrintReverse(head->next);
printf("%d", head->digit);
}
int main() {
// Зададим число A = 205. В списке оно хранится как: 5 -> 0 -> 2
Node* A = NULL;
Append(&A, 5); Append(&A, 0); Append(&A, 2);
// Зададим число B = 18. В списке хранится как: 8 -> 1
Node* B = NULL;
Append(&B, 8); Append(&B, 1);
printf("A = "); PrintReverse(A); printf("\n");
printf("B = "); PrintReverse(B); printf("\n");
Node* res = SubtractLists(A, B);
// Ожидаем: 205 - 18 = 187
printf("A - B = "); PrintReverse(res); printf("\n");
return 0;
}
Задача 8. Умножение длинного числа на цифру
Условие: Реализуйте алгоритм умножения длинного числа, заданного списком цифр в обратном порядке (голова - единицы), на однозначное число (цифру от 0 до 9). Алгоритм должен выполняться in-place (без создания нового списка, если это возможно, изменяя исходный).
Подсказка
При умножении в столбик, перенос в старший разряд можно вычислить как деление на 10, а текущая цифра - остаток от деления на 10. Не забудьте про перенос после завершения списка.
Разбор алгоритма
Описание алгоритма:
Аналогично задаче 29, длинное число хранится в виде связного списка цифр в обратном порядке (голова списка - это единицы, второй элемент - десятки и так далее).
Необходимо умножить это длинное число A на обычную цифру D (число от 0 до 9). Алгоритм (умножение в столбик):
- Создаем переменную
carry = 0(перенос в уме). - Проходим по всему списку
A(от младших разрядов к старшим). - Для каждой цифры узла:
product = A.digit * D + carry. - Младший разряд произведения остается в узле (это
product % 10). - Старший разряд уходит "в ум" (это
carry = product / 10). - Когда список
Aзакончился, если в уме что-то осталось (carry > 0), мы создаем новый узел в хвосте списка и записываем туда этотcarry.
Код на C
#include <stdio.h>
#include <stdlib.h>
// Узел для цифры длинного числа
typedef struct Node {
int digit;
struct Node* next;
} Node;
// Вставка цифры в конец списка
void Append(Node** head, int digit) {
Node* n = (Node*)malloc(sizeof(Node));
n->digit = digit; n->next = NULL;
if (!*head) { *head = n; return; }
Node* curr = *head;
while(curr->next) curr = curr->next;
curr->next = n;
}
// Умножение длинного списка на цифру (In-place, мы меняем исходный список!)
void MultiplyListByDigit(Node* head, int D) {
if (head == NULL) return;
// Если умножаем на 0, все цифры становятся 0 (или просто оставляем один 0)
if (D == 0) {
head->digit = 0;
// Можно удалить остальные узлы, но мы опустим это для краткости
return;
}
int carry = 0;
Node* current = head;
Node* last = NULL; // последний узел: к нему потом цепляем carry
while (current != NULL) {
int product = current->digit * D + carry;
current->digit = product % 10; // Оставляем младший разряд
carry = product / 10; // Переносим старший разряд "в ум"
last = current;
current = current->next;
}
// Если после окончания числа "в уме" остался перенос,
// нужно добавить новый разряд (новый узел)
if (carry > 0) {
Node* n = (Node*)malloc(sizeof(Node));
n->digit = carry;
n->next = NULL;
last->next = n; // Прикрепляем в конец
}
}
// Рекурсивный вывод списка задом наперед
void PrintReverse(Node* head) {
if (!head) return;
PrintReverse(head->next);
printf("%d", head->digit);
}
int main() {
// Создаем длинное число 156. Хранится как: 6 -> 5 -> 1
Node* A = NULL;
Append(&A, 6);
Append(&A, 5);
Append(&A, 1);
int multiplier = 4;
printf("Число: "); PrintReverse(A); printf("\n");
printf("Умножаем на: %d\n", multiplier);
MultiplyListByDigit(A, multiplier);
// Ожидается: 156 * 4 = 624
printf("Результат: "); PrintReverse(A); printf("\n");
return 0;
}
Задача 9. Реверс линейного списка
Условие: Разверните (сделайте реверс) односвязный линейный список "на месте" (in-place), изменив направление всех связей next на противоположное без выделения новой памяти.
Подсказка
Вам понадобятся три указателя для прохода по списку: на предыдущий, текущий и следующий узлы. Как они будут меняться на каждом шаге?
Разбор алгоритма
Описание алгоритма:
Реверс (переворот) односвязного линейного списка означает, что направление всех связей next должно измениться на противоположное (первый элемент станет последним, а последний - первым).
Алгоритм должен выполняться "на месте" (In-place), без выделения новой памяти (без создания новых узлов). Для этого нам понадобятся 3 указателя:
prev- указатель на предыдущий узел (изначальноNULL, так как бывшая голова станет хвостом и должна указывать на NULL).current- указатель на текущий обрабатываемый узел (изначально равенhead).next_node- временный указатель для сохранения следующего узла (иначе мы его потеряем при перезаписи связи).
В цикле пока current != NULL:
- Сохраняем ссылку на продолжение списка:
next_node = current->next; - Разворачиваем текущую связь:
current->next = prev; - Сдвигаем указатель
prevвперед наcurrent. - Сдвигаем указатель
currentвперед наnext_node.
После окончания цикла новый корень (head) будет находиться в указателе prev.
Код на C
#include <stdio.h>
#include <stdlib.h>
// Узел односвязного списка
typedef struct Node {
int data;
struct Node* next;
} Node;
// Реверс списка: указатель на указатель, чтобы поменять сам head
void ReverseList(Node** headRef) {
Node* prev = NULL;
Node* current = *headRef;
Node* next_node = NULL;
while (current != NULL) {
// 1. Запоминаем следующий узел
next_node = current->next;
// 2. Разворачиваем стрелку назад
current->next = prev;
// 3. Сдвигаем 'prev' и 'current' на один шаг вперед
prev = current;
current = next_node;
}
// 4. По завершении 'prev' указывает на старый хвост, который стал новой головой
*headRef = prev;
}
// Вспомогательная функция вставки в конец
void Append(Node** head, int data) {
Node* n = (Node*)malloc(sizeof(Node));
n->data = data; n->next = NULL;
if (!*head) { *head = n; return; }
Node* curr = *head;
while(curr->next) curr = curr->next;
curr->next = n;
}
// Вывод списка
void PrintList(Node* head) {
while (head) {
printf("%d -> ", head->data);
head = head->next;
}
printf("NULL\n");
}
int main() {
Node* head = NULL;
Append(&head, 1);
Append(&head, 2);
Append(&head, 3);
Append(&head, 4);
Append(&head, 5);
printf("Исходный список:\n");
PrintList(head); // Ожидается: 1 -> 2 -> 3 -> 4 -> 5 -> NULL
// Переворачиваем!
ReverseList(&head);
printf("Перевернутый список:\n");
PrintList(head); // Ожидается: 5 -> 4 -> 3 -> 2 -> 1 -> NULL
return 0;
}