Nano Hash - криптовалюты, майнинг, программирование

Реализация списка смежности в C++

Я ищу краткое и точное представление списка смежности графа в С++. Мои узлы - это просто идентификаторы узлов. Вот как я это сделал. Просто хочется узнать, что об этом думают специалисты. Есть ли способ лучше?

Это реализация класса (ничего особенного, сейчас мне все равно на публичные/приватные методы)

#include <iostream>
#include <vector>
#include <fstream>
#include <sstream>

using namespace std;

class adjList {
public:
    int head;
    vector<int> listOfNodes;
    void print();
};

void adjList :: print() {
    for (int i=0; i<listOfNodes.size(); ++i) {
        cout << head << "-->" << listOfNodes.at(i) << endl;
    }
}

class graph {
public:
    vector<adjList> list;
    void print();
};

void graph :: print() {
    for (int i=0; i<list.size(); ++i) {
        list.at(i).print();
        cout << endl;
    }
}

Моя основная функция анализирует входной файл построчно. Где каждая строка интерпретируется следующим образом:

<source_node> <node1_connected_to_source_node> <node2_connected_to_source_node <node3_connected_to_source_node> <...>

Вот основное:

int main()
    {
        fstream file("graph.txt", ios::in);
        string line;
        graph g;
        while (getline(file, line)) {
            int source;
            stringstream str(line);
            str >> source;
            int node2;
            adjList l;
            l.head = source;
            while (str >> node2) {
                l.listOfNodes.push_back(node2);
            }
            g.list.push_back(l);
        }
        file.close();
        g.print();
        getchar();
        return 0;
    }

Я знаю, что должен добавить функцию addEdge() в класс adjList вместо того, чтобы напрямую изменять ее переменную из main(), однако сейчас я просто думаю о лучшей структуре.

EDIT: В моем подходе есть один недостаток. Для сложного графа с большим количеством узлов node действительно будет структурой/классом, и в этом случае я буду дублировать значения, сохраняя весь объект. В этом случае я думаю, что должен использовать указатели. Например, для неориентированного графа я буду хранить копии объектов узла в adjList (соединение между узлами 1 и 2 означает, что в списке смежности 1 будет 2, и наоборот). Я могу избежать этого, сохраняя указатели объектов узлов в adjList, а не весь объект. Проверьте реализацию dfs, которая извлекает выгоду из этого подхода. Там мне нужно убедиться, что каждый узел посещается только один раз. Наличие нескольких копий одного и того же узла усложнит мне жизнь. нет?

В этом случае мои определения классов изменятся следующим образом:

#include <iostream>
#include <vector>
#include <fstream>
#include <sstream>
#include <map>

using namespace std;

class node {
public:
    node() {}
    node(int id, bool _dirty): node_id(id), dirty(_dirty) {}
    int node_id;
    bool dirty;
};

class adjList {
public:
    node *head;
    vector<node*> listOfNodes;
    void print();
    ~adjList() { delete head;}
};

void adjList :: print() {
    for (int i=0; i<listOfNodes.size(); ++i) {
        cout << head->node_id << "-->" << listOfNodes.at(i)->node_id << endl;
    }
}

class graph {
public:
    vector<adjList> list;
    void print();
    void dfs(node *startNode);
};

void graph::dfs(node *startNode) {
    startNode->dirty = true;
    for(int i=0; i<list.size(); ++i) {
        node *stNode = list.at(i).head;
        if (stNode->node_id != startNode->node_id) { continue;}
        for (int j=0; j<list.at(i).listOfNodes.size(); ++j) {
            if (!list.at(i).listOfNodes.at(j)->dirty) {
                dfs(list.at(i).listOfNodes.at(j));
            }
        }
    }
    cout << "Node: "<<startNode->node_id << endl;
}

void graph :: print() {
    for (int i=0; i<list.size(); ++i) {
        list.at(i).print();
        cout << endl;
    }
}

И вот как я реализовал функцию main(). Я использую карту‹>, чтобы избежать дублирования объектов. Создание нового объекта только в том случае, если он не был определен ранее. Проверка существования объекта по его id.

int main()
{
    fstream file("graph.txt", ios::in);
    string line;
    graph g;
    node *startNode;
    map<int, node*> nodeMap;
    while (getline(file, line)) {
        int source;
        stringstream str(line);
        str >> source;
        int node2;
        node *sourceNode;
        // Create new node only if a node does not already exist
        if (nodeMap.find(source) == nodeMap.end()) {
                sourceNode = new node(source, false);
                nodeMap[source] = sourceNode;
        } else {
                sourceNode = nodeMap[source];
        }
        adjList l;
        l.head = sourceNode;
        nodeMap[source] = sourceNode;
        while (str >> node2) {
            // Create new node only if a node does not already exist
            node *secNode;
            if (nodeMap.find(node2) == nodeMap.end()) {
                secNode = new node(node2, false);
                nodeMap[node2] = secNode;
            } else {
                secNode = nodeMap[node2];
            }
            l.listOfNodes.push_back(secNode);
        }
        g.list.push_back(l);
        startNode = sourceNode;
    }
    file.close();
    g.print();
    g.dfs(startNode);
    getchar();
    return 0;
}

ВТОРОЕ РЕДАКТИРОВАНИЕ После предложения Ульриха Экхардта поместить список смежности в класс узла, вот что я думаю, это лучшая структура данных для хранения графа и выполнения dfs(), dijkstra( ) виды операций. Обратите внимание, что список смежности объединен в классе узла.

#include <iostream>
#include <vector>
#include <fstream>
#include <sstream>
#include <map>

using namespace std;

class node {
public:
    node() {
    }
    node(int id, bool _dirty): node_id(id), dirty(_dirty) {
        //cout << "In overloaded const\n";
    }
    int node_id;
    bool dirty;
    vector<node*> listOfNodes;
};

class graph {
public:
    vector<node*> myGraph;
    void dfs(node* startNode);
};

void graph::dfs(node* startNode) {
    startNode->dirty = true;
    for (int j=0; j<startNode->listOfNodes.size(); ++j) {
            if (!startNode->listOfNodes.at(j)->dirty) {
                dfs(startNode->listOfNodes.at(j));
            }
        }

    cout << "Node: "<<startNode->node_id << endl;
}

Можем ли мы сделать лучше, чем это?


  • В конечном итоге это зависит от того, что вам нужно делать с этим графиком, но то, что у вас есть, кажется довольно разумным и простым. 19.05.2013
  • правда... но есть одна проблема... для неориентированного графа я буду хранить копии объектов узлов в adjList (соединение между узлами 1 и 2 означает, что в списке смежности 1 будет 2 и наоборот). Я могу избежать этого, сохраняя указатели объектов узлов в adjList, а не весь объект. Но это приводит к другому обсуждению кучи и стека. Поэтому хочу знать, что люди обычно используют. 19.05.2013
  • Разве adjList не хранит только индексы узлов? Между прочим, вам не нужно где-то определять класс узла? 19.05.2013
  • да... после определения класса узла... спасибо! Я пропустил это... 19.05.2013
  • Я думаю, что ваш подход работает, но он ужасно неэффективен. В dfs() вы выполняете линейный просмотр вектора списков смежности, чтобы найти тот, который соответствует текущему узлу, то есть O (узлы). Вы делаете это для каждого узла, делая его O (узлы * узлы). Проблема в вашей структуре данных, которая должна включать в себя список смежности в узле: struct node{ vector<node*> adjacency_list; }; Тогда ваш граф становится простым map<int, node> nodes;. Тогда обход соседних узлов представляет собой простой цикл по adjacency_list без каких-либо дополнительных затрат. 19.05.2013
  • Мне просто пришло в голову, что использование map<int, node> даже устраняет хак с reinterpret_cast ниже, и это не медленнее, так что это путь. И не беспокойтесь об избыточном хранении ребер в неориентированном графе. Если у вас так много ребер, вам все равно следует рассмотреть сжатую матрицу смежности, но это также меняет сложность итерации по всем ребрам узла. Идите по легкому пути, пока не увидите реальную необходимость пойти другим путем. 19.05.2013
  • @UlrichEckhardt да, dfs() ужасно медленный из-за моей неэффективной реализации. Ваше решение - очень хорошее наблюдение. Спасибо! Можете ли вы отредактировать свой ответ, чтобы включить этот комментарий? Затем я могу отметить это как принятый ответ... 19.05.2013

Ответы:


1

Есть несколько вещей, которые можно было бы улучшить, но в целом ваш подход разумен. Заметки:

  • Вы используете int в качестве индекса в контейнере, что выдаст вам предупреждение от некоторых компиляторов, поскольку размер контейнера может превышать размер, представляемый как int. Вместо этого используйте size_t.
  • Перепишите for (int i=0; i<list.size(); ++i) на for(size_t i=0, size=list.size(); i!=size; ++i). Использование != вместо < будет работать с итераторами. Однократное чтение и сохранение размера упрощает отладку и, возможно, делает ее более эффективной.
  • Внутри цикла для печати у вас есть list.at(i).print();. list.at(i) проверит, что индекс действителен, и вызовет исключение, если это не так. В этом очень простом случае я уверен, что индекс действителен, поэтому использование вместо него list[i] будет быстрее. Кроме того, он неявно документирует, что индекс действителен, а не то, что вы ожидаете, что он будет недействительным.
  • Функции print() должны быть постоянными.
  • Я не понимаю, что такое int head. Это какой-то идентификатор узла? И не является ли идентификатор просто индексом внутри graph::list? Если это индекс, вы можете вычислить его по запросу, используя адрес элемента минус адрес первого элемента, поэтому нет необходимости хранить его избыточно. Кроме того, рассмотрите возможность проверки этого индекса при чтении, чтобы у вас не было ребер, ведущих к несуществующей вершине.
  • Если вы не заботитесь об инкапсуляции на уровне узла (что разумно!), вы также можете сделать это структурой, что экономит часть ввода.
  • Хранение указателей вместо индексов сложно, но может повысить скорость. Проблема в том, что для чтения может понадобиться указатель на несуществующую вершину. Есть хак, который позволяет сделать это без использования дополнительного хранилища, он требует сначала сохранить индексы в значениях указателя (используя reinterpret_cast), а после чтения сделать второй проход по данным, где вы подгоняете эти значения к фактическим адресам. Конечно, вы также можете использовать второй проход для проверки того, что у вас нет ребер, ведущих к вершинам, которых вообще не существует (это место, где функция at(i) становится полезной), поэтому этот второй проход для проверки некоторых гарантий в любом случае это хорошо.

По явному запросу вот пример того, как сохранить индекс в указателе:

// read file
for(...) {
    size_t id = read_id_from_file();
    node* node_ptr = reinterpret_cast<node*>(id);
    adjacency_list.push_back(node_ptr);
}

/* Note that at this point, you do have node* that don't contain
valid addresses but just the IDs of the nodes they should finally
point to, so you must not use these pointers! */

// make another pass over all nodes after reading the file
for(size_t i=0, size=adjacency_list.size(); i!=size; ++i) {
    // read ID from adjacency list
    node* node_ptr = adjacency_list[i];
    size_t id = reinterpret_cast<size_t>(node_ptr);
    // convert ID to actual address
    node_ptr = lookup_node_by_id(id);
    if(!node_ptr)
        throw std::runtime_error("unknown node ID in adjacency list");
    // store actual node address in adjacency list
    adjacency_list[i] = node_ptr;
}

Я почти уверен, что это работает в целом, хотя я не уверен на 100%, что это гарантированно сработает, поэтому я не хочу публиковать это здесь. Однако я надеюсь, что это также проясняет, почему я спрашиваю, что такое «голова». Если это действительно просто индекс в контейнере, в нем мало нужды ни внутри файла, ни в памяти. Если это какое-то имя или идентификатор для узла, который вы извлекли из файла, то он вам абсолютно необходим, но тогда вы не можете использовать его как индекс, значения там также могут начинать свои идентификаторы с 1 или 1000, который вы должны поймать и справиться без сбоев!

19.05.2013
  • Большое спасибо за ваш подробный ответ. Что касается вашего 4-го пункта, в голове хранится идентификатор исходного узла. Теперь я тоже вижу его излишним. Головной узел вполне может быть первым узлом в adjList. Я не понял вашего последнего пункта полностью. Может быть, если у вас есть время, вы можете привести короткий пример. Для сложных графов с большим количеством узлов узел действительно будет структурой/классом, и в этом случае я буду дублировать значения, сохраняя весь объект. В этом случае я думаю, что должен использовать указатели. Смотрите редактирование моего вопроса. 19.05.2013
  • Новые материалы

    Кластеризация: более глубокий взгляд
    Кластеризация — это метод обучения без учителя, в котором мы пытаемся найти группы в наборе данных на основе некоторых известных или неизвестных свойств, которые могут существовать. Независимо от..

    Как написать эффективное резюме
    Предложения по дизайну и макету, чтобы представить себя профессионально Вам не позвонили на собеседование после того, как вы несколько раз подали заявку на работу своей мечты? У вас может..

    Частный метод Python: улучшение инкапсуляции и безопасности
    Введение Python — универсальный и мощный язык программирования, известный своей простотой и удобством использования. Одной из ключевых особенностей, отличающих Python от других языков, является..

    Как я автоматизирую тестирование с помощью Jest
    Шутка для победы, когда дело касается автоматизации тестирования Одной очень важной частью разработки программного обеспечения является автоматизация тестирования, поскольку она создает..

    Работа с векторными символическими архитектурами, часть 4 (искусственный интеллект)
    Hyperseed: неконтролируемое обучение с векторными символическими архитектурами (arXiv) Автор: Евгений Осипов , Сачин Кахавала , Диланта Хапутантри , Тимал Кемпития , Дасвин Де Сильва ,..

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

    Обеспечение масштабируемости LLM: облачный анализ с помощью AWS Fargate и Copilot
    В динамичной области искусственного интеллекта все большее распространение получают модели больших языков (LLM). Они жизненно важны для различных приложений, таких как интеллектуальные..