Как сериализовать двусвязный список на C++?

Ответ

Сериализация двусвязного списка подразумевает сохранение данных и структуры связей между узлами в линейный формат (например, файл или поток). Основная сложность — корректно сохранить и восстановить двунаправленные указатели, избежав зацикливания.

Стратегия: Вместо сохранения указателей (адресов памяти) мы сохраняем индексы узлов в последовательности обхода. При десериализации мы восстанавливаем узлы и связываем их по этим индексам.

Пример реализации сериализации и десериализации:

#include <vector>
#include <unordered_map>
#include <fstream>
#include <iostream>

struct ListNode {
    int data;
    ListNode* prev;
    ListNode* next;
    ListNode(int d) : data(d), prev(nullptr), next(nullptr) {}
};

// Сериализация в вектор целых чисел
void serialize(ListNode* head, std::vector<int>& out) {
    std::unordered_map<ListNode*, int> nodeToId;
    ListNode* current = head;
    int id = 0;

    // Первый проход: сохраняем данные и присваиваем узлам ID
    while (current) {
        nodeToId[current] = id++;
        out.push_back(current->data); // Сохраняем данные
        current = current->next;
    }

    // Второй проход: сохраняем связи в виде ID соседей
    current = head;
    while (current) {
        int prevId = (current->prev) ? nodeToId[current->prev] : -1;
        int nextId = (current->next) ? nodeToId[current->next] : -1;
        out.push_back(prevId);
        out.push_back(nextId);
        current = current->next;
    }
}

// Десериализация из вектора целых чисел
ListNode* deserialize(const std::vector<int>& in) {
    if (in.empty()) return nullptr;

    // Количество узлов = общий размер / 3 (data, prevId, nextId на узел)
    size_t nodeCount = in.size() / 3;
    std::vector<ListNode*> nodes(nodeCount, nullptr);

    // Первый проход: создаем узлы с данными
    for (size_t i = 0; i < nodeCount; ++i) {
        nodes[i] = new ListNode(in[i]);
    }

    // Второй проход: восстанавливаем связи по сохраненным ID
    for (size_t i = 0; i < nodeCount; ++i) {
        int prevId = in[nodeCount + i*2];     // Индекс prev
        int nextId = in[nodeCount + i*2 + 1]; // Индекс next

        if (prevId != -1) nodes[i]->prev = nodes[prevId];
        if (nextId != -1) nodes[i]->next = nodes[nextId];
    }

    return nodes[0]; // Возвращаем голову
}

// Вспомогательная функция для записи/чтения в/из файла
void serializeToFile(ListNode* head, const std::string& filename) {
    std::vector<int> serializedData;
    serialize(head, serializedData);

    std::ofstream file(filename, std::ios::binary);
    if (file) {
        size_t size = serializedData.size();
        file.write(reinterpret_cast<const char*>(&size), sizeof(size));
        file.write(reinterpret_cast<const char*>(serializedData.data()), size * sizeof(int));
    }
}

ListNode* deserializeFromFile(const std::string& filename) {
    std::ifstream file(filename, std::ios::binary);
    if (!file) return nullptr;

    size_t size = 0;
    file.read(reinterpret_cast<char*>(&size), sizeof(size));
    std::vector<int> serializedData(size);
    file.read(reinterpret_cast<char*>(serializedData.data()), size * sizeof(int));

    return deserialize(serializedData);
}

Альтернативные подходы:

  • Сериализация в текстовый формат (JSON, XML): Использовать библиотеки вроде nlohmann/json. Удобно для отладки и взаимодействия.
  • Бинарная сериализация с сохранением смещений: Сохранять не индексы, а смещения в файле, что эффективнее для прямого доступа.
  • Использование boost::serialization: Готовая библиотека, поддерживающая сложные графы объектов и версионность.

Ответ 18+ 🔞

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

Представь себе: у тебя есть цепочка узлов, где каждый знает, кто его сосед слева и справа. И вот тебе надо всю эту братву, со всеми их тараканами в голове (то есть данными), запихнуть в линейный файл. А потом, откуда ни возьмись, достать обратно и собрать так, чтобы они опять здоровались за ручку. Основная засада — эти ебучие двунаправленные указатели. Сохранять адреса памяти — это вообще ни в какие ворота, они после перезагрузки программы будут указывать в пизду, в рандомное место. Доверия к таким адресам — ноль ебать.

Нормальная стратегия, без выёбонов: Вместо адресов мы сохраняем индексы. Проходим по списку, каждому узлу как на перекличке в армии присваиваем порядковый номер (ID). Потом сохраняем данные каждого, а следом — номера его соседей (предыдущего и следующего). Если соседа нет (например, голова или хвост), пишем -1. Всё, схема проще, чем три копейки.

Вот, смотри, как это в коде выглядит, ядрёна вошь:

#include <vector>
#include <unordered_map>
#include <fstream>
#include <iostream>

struct ListNode {
    int data;
    ListNode* prev;
    ListNode* next;
    ListNode(int d) : data(d), prev(nullptr), next(nullptr) {}
};

// Сериализация в вектор целых чисел
void serialize(ListNode* head, std::vector<int>& out) {
    std::unordered_map<ListNode*, int> nodeToId;
    ListNode* current = head;
    int id = 0;

    // Первый проход: сохраняем данные и присваиваем узлам ID
    while (current) {
        nodeToId[current] = id++;
        out.push_back(current->data); // Сохраняем данные
        current = current->next;
    }

    // Второй проход: сохраняем связи в виде ID соседей
    current = head;
    while (current) {
        int prevId = (current->prev) ? nodeToId[current->prev] : -1;
        int nextId = (current->next) ? nodeToId[current->next] : -1;
        out.push_back(prevId);
        out.push_back(nextId);
        current = current->next;
    }
}

// Десериализация из вектора целых чисел
ListNode* deserialize(const std::vector<int>& in) {
    if (in.empty()) return nullptr;

    // Количество узлов = общий размер / 3 (data, prevId, nextId на узел)
    size_t nodeCount = in.size() / 3;
    std::vector<ListNode*> nodes(nodeCount, nullptr);

    // Первый проход: создаем узлы с данными
    for (size_t i = 0; i < nodeCount; ++i) {
        nodes[i] = new ListNode(in[i]);
    }

    // Второй проход: восстанавливаем связи по сохраненным ID
    for (size_t i = 0; i < nodeCount; ++i) {
        int prevId = in[nodeCount + i*2];     // Индекс prev
        int nextId = in[nodeCount + i*2 + 1]; // Индекс next

        if (prevId != -1) nodes[i]->prev = nodes[prevId];
        if (nextId != -1) nodes[i]->next = nodes[nextId];
    }

    return nodes[0]; // Возвращаем голову
}

// Вспомогательная функция для записи/чтения в/из файла
void serializeToFile(ListNode* head, const std::string& filename) {
    std::vector<int> serializedData;
    serialize(head, serializedData);

    std::ofstream file(filename, std::ios::binary);
    if (file) {
        size_t size = serializedData.size();
        file.write(reinterpret_cast<const char*>(&size), sizeof(size));
        file.write(reinterpret_cast<const char*>(serializedData.data()), size * sizeof(int));
    }
}

ListNode* deserializeFromFile(const std::string& filename) {
    std::ifstream file(filename, std::ios::binary);
    if (!file) return nullptr;

    size_t size = 0;
    file.read(reinterpret_cast<char*>(&size), sizeof(size));
    std::vector<int> serializedData(size);
    file.read(reinterpret_cast<char*>(serializedData.data()), size * sizeof(int));

    return deserialize(serializedData);
}

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

  • Текстовый формат (JSON, XML): Взять какую-нибудь библиотечку, типа nlohmann/json. Получается человекочитаемо, для отладки — просто песня. Но весит, бля, овердохуища по сравнению с бинарником.
  • Бинарная сериализация со смещениями: Вместо индексов в массиве можно сохранять смещения прямо в файле. Чуть более мозголомно, но зато можно прыгать по файлу, почти как по массиву в памяти.
  • Забить и использовать boost::serialization: Это как вызвать спецназ. Библиотека сама умеет сериализовать графы объектов любой сложности, следит за версиями и не даёт тебе прострелить себе ногу. Но это уже тяжёлая артиллерия, не для школьных проектов.