Связанный список с несколькими родительскими и дочерними узлами

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

Структура данных:

                   ____A
                  /    |     
                 B     C    
                 |  /    \  
                 E-->  F   G
                 |     |   |
                 I     J   K

Узлы могут иметь более одного следующего узла (например, A и C) и могут иметь более одного предыдущего узла.

Текстовый файл содержит такие данные, я получаю данные из файла и превращаю их в связанный список:

                    A
                    B
                    E
                    I

                    A
                    C
                    E
                    F
                    J

                    A
                    C
                    G
                    K

Мой вопрос: Можно ли создать связанный список с узлами с более чем одним следующим или более чем с одним предыдущим узлом, если да, то как будет выглядеть структура?

Что я пробовал:

Я создал структуру, которая содержит массив из 4 целых чисел для родительского и дочернего.

struct abcd{
 char data;
 int nodeid;

 int parent[4];
 int child[4];

 struct abcd *next;

}

Таким образом, родительский массив содержит идентификатор узла самого предыдущего узла (может быть больше одного, поскольку, например, E (B и C указывают на него) -> (node-id - 1).

Дочерний массив содержит идентификатор узла мгновенного следующего узла (идентификатор узла +1).

Нет повторяющихся узлов для A или любого другого.

ВЫХОД:

1 :  A <-- 
2 :  B <-- 1
3    E <-- 2,5
4 :  I <-- 3
5 :  C <-- 1
6 :  F <-- 3
7 :  J <-- 6
8 :  G <-- 5
9 :  K <-- 8

Надеюсь, все понятно, пожалуйста, позвольте мне не знать, как мне это реализовать. С Уважением.


person KAKAK    schedule 09.08.2013    source источник
comment
вы должны называть это графиком   -  person Grijesh Chauhan    schedule 16.08.2013
comment
Вы фактически не сказали, могут ли в вашей системе быть циклы A- ›D-› A. Если нет, то это направленный ациклический граф, который важен, потому что существует множество исследований по DAG. Обычно по графам, которые гарантированно являются ациклическими, проще безопасно ориентироваться (не требуется обнаружение истории / циклов).   -  person Speed8ump    schedule 19.08.2013


Ответы (6)


Да, это называется ориентированным графом. И есть около тысячи способов реализовать это. «Правильный» способ полностью зависит от того, как вы его будете использовать, чего вы не описали. Поскольку вы, похоже, ограничили это связанными списками или двусвязными списками, я просто буду использовать только двусвязные списки.

Вперед объявите ваши типы данных

typedef struct ItemInfo_s ItemInfo;
typedef struct DoubleLinkedListNode_s DoubleLinkedListNode;

Создайте ListNode, как всегда:

struct DoubleLinkedListNode_s {
    DoubleLinkedListNode *next;
    DoubleLinkedListNode *prev;
    ItemInfo *data;
};

Затем создайте свой ItemInfo:

struct ItemInfo_s {
    DoubleLinkedListNode *children;
    DoubleLinkedListNode *parents;
    ...  /* other item data */
};

Также для здравомыслия создайте список всех созданных узлов:

DoubleLinkedListNode *items;

Я не собираюсь писать все функции управления связанными списками, но я уверен, что вы сможете понять это. По соглашению я напишу (B) как узел, указывающий на элемент B (node.data = & B). Я также укажу любые два узла, связанные вместе, с помощью '=' и '-' как несвязанную (с нулевым значением) связь узла. Я напишу цепочку элементов [- (1) = (2) = (3) -], и по соглашению указатели в цепочке элементов всегда будут указывать на первый узел в цепочке ((1) в этом примере ). Ваш данный график выглядит в памяти так:

items = [ -(A)=(B)=(C)=(E)=(F)=(G)=(I)=(J)=(K)- ]

A.children = [ -(B)=(C)- ]
A.parents = []

B.children = [ -(E)- ]
B.parents = [ -(A)- ]

C.children = [ -(E)=(G)- ]
C.parents = [ -(A)- ]

E.children = [ -(I)=(F)- ]
E.parents = [ -(B)=(C)- ]

F.children = [ -(J)- ]
F.parents = [ -(E)- ]

G.children = [ -(K)- ]
G.parents = [ -(C)- ]

I.children = []
I.parents = [ -(E)- ]

J.children = []
J.parents = [ -(F)- ]

K.children = []
K.parents = [ -(G)- ]

Всего это 9 ItemInfos и 27 DoubleLinkedListNodes. Я не могу придумать почти никаких причин, по которым я когда-либо реализовал бы это на практике, но это реализовано только с использованием списков с двойной связью. Создание двусвязных колец (соединяющих начало и конец списка вместе) могло бы упростить управление списком, но это сложнее отобразить в текстовой форме. :)

person Speed8ump    schedule 16.08.2013

У вас может быть такая структура:

struct abcd{
 char data;
 struct abcd *next[10];  //array of next nodes
 struct abcd *prev[10];  //array of previous nodes
}

При доступе к следующим узлам вы можете использовать node->next[i] вместо node->next, где 0<= i < 10. При выделении / создании узла сбросьте все элементы массива на NULL, чтобы не было мусора для неинициализированных узлов.

Итак, предположим, что вы добавили узел для 'A', затем вы можете добавить узлы для 'B' и 'C' как

int idx;
//find index for free element.
for(idx = 0; nodeA->next[idx] && idx < 10; idx++)
   ;
if(idx == 10)
   //dont have free space
nodeA->next[idx] = nodeB;
nodeB->prev[0] = nodeA;

//similarly add for C, you may have to check for appropriate idx!
nodeA->next[idx++]] = nodeC;
nodeC->prev[0] = nodeA;

С его помощью вы можете создать узел, который может иметь не более 10 следующих или предыдущих узлов.

Массив предназначен для простоты, вы также можете использовать struct abcd **next;, где вы можете иметь динамическое количество следующих / предыдущих узлов. Однако вам придется правильно распределить память.

person Rohan    schedule 09.08.2013

Связанные и двусвязные списки - это особая разновидность направленных графов, которые можно оптимизировать до знакомой вам структуры head/tail, data/next/prev. Поскольку вы расширяете его возможности, вы теряете эту специфичность и хотите вернуться к общей структуре ориентированного графа и работать оттуда.

Ориентированный граф проще всего описать с помощью списка смежности: изображение графов со списками смежности

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

typedef struct _BPNode { // "Back-Pointing Node"
    void *data;
    struct _BPNode *nexts[];
    struct _BPNode *prevs[];
} Node;

typedef struct _BPGraph { // "Back-Pointing Graph"
    Node **allNodes;
} BPGraph;

Или что-то подобное. Отказ от ответственности: я не тестировал это в компиляторе. И на всякий случай вот руководство о том, как читать некоторые из содержащихся там деклараций .

В качестве альтернативы вы можете создать два ориентированных графа: один движется вперед, а другой - назад. Однако для этого потребуется больше памяти, чем для этого «обратного» графика. Он также будет работать медленнее (больше промахов в кеш-памяти процессора), будет менее интуитивно понятным и будет более проблематичным для освобождения памяти.

person Nobbynob Littlun    schedule 18.08.2013

Можно ли создать связанный список с узлами с более чем одним следующим или более чем одним предыдущим узлом, если да, то как будет выглядеть структура?

Да, это возможно. Вы должны задать себе вопрос: «как мне сохранить достаточно большой объем данных?», а краткий ответ: «вы должны использовать ADT» . Напомним, что ADT - это математическая модель для сбора данных.

Вы можете реализовать его с любым ADT, выбор конкретного ADT зависит от операций, которые вы планируете использовать наиболее часто. В моем примере я буду использовать динамический массив. Структура будет объявлена ​​следующим образом (без конкретных полей для узла):

struct llnode {
  int item;
  struct llnode *children;
  int length;
  int capacity;
};

... где элемент - это код ASCII для 'A', 'B', 'C' и т. д., а children - это указатель на массив struct llnodes. Однако вы можете создать отдельную структуру для динамического массива, чтобы он был менее беспорядочным, но это полностью зависит от вас. Та же идея применима к родительским узлам.

person Jacob Pollack    schedule 09.08.2013

Вы можете попытаться отделить данные от структуры данных, реализовав списки указателей на объекты данных:

struct data_item {
    unsigned char data;
    unsigned char id;
    unsigned int  count;
    // Whatever other data you want.
};

struct list_node {
    struct data_item *item;
    struct list_node *next;
}

Теперь, когда мы встречаем символы в файле, мы вставляем их в структуру данных «репозиторий». В этом примере я буду использовать простую таблицу, но вы можете использовать список, если хотите сэкономить место, или дерево, если хотите сэкономить место, сохраняя при этом высокую скорость поиска и т. Д.

data_item data_table[UCHAR_MAX + 1] = {0};
...

unsigned char data = read_character_from_file();
struct data_item *di = data_table[data];

if (di == NULL)
    di = new_data_item(data);
else
    ++di->count;

И прикрепите их к текущему списку:

struct list_node *list;
if (first_item_in_list())
    list = new_list(di)
else
    list - add_list(list, di);

Теперь у вас может быть сколько угодно таких списков (даже список списков, если вы не знаете количество списков заранее).

person idoby    schedule 16.08.2013
comment
Видимо кое-что забыл; Просто показывает, что вы всегда должны корректировать то, что публикуете в internetz! - person idoby; 16.08.2013

Вы описываете график.

(Двойной) связанный список на самом деле всего лишь одномерный список и не подходит для обозначения того, что вы хотите.

Есть два основных способа реализации графа:

  • Списки смежности. Каждый узел / вершина имеет список входящих и исходящих ребер. Как тот, который вы описываете.
  • Матрица смежности. Матрица n умноженная на n (где n - количество узлов / вершин) с записью в [a][b], если узел a имеет край до b.

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

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

Ваша реализация могла бы выглядеть так:

char adj_matrix[0x80][0x80]; // I am assuming that you only have ASCII letters
memset(adj_matrix, 0, sizeof(adj_matrix)); // initialise empty

Вставка элементов будет выглядеть так:

adj_matrix['A']['C'] = 1; // edge from A --> C

Чтобы определить все входящие ребра для 'A', вам нужно будет выполнить итерацию по матрице:

for (i = 'A'; i <= 'Z'; i++)
    if (adj_matrix[i]['A'])
        // A has an incoming edge from i

для исходящего наоборот

for (i = 'A'; i <= 'Z'; i++)
    if (adj_matrix['E'][i])
        // E has an outgoing edge to i

Как уже говорилось, вы можете значительно повысить производительность как в пространстве, так и во времени, используя битовые поля и инструкции битового сканирования (например, gcc __builtin_clzll, icc _bit_scan_reverse).

person Sergey L.    schedule 19.08.2013
comment
Это, несомненно, самое простое и элегантное решение, которое, вероятно, будет самым быстрым на большинстве машин. Единственным недостатком будет то, что память жестко ограничена, а данные имеют тенденцию быть разреженными. Но если предположить, что это обычное приложение и современный компьютер (а не специализированное устройство), я определенно сделал бы это именно так. - person Carey Gregory; 19.08.2013
comment
Это быстро и умно, если вы абсолютно уверены, что это будет только латинский алфавит. Но для обычного приложения разница в производительности между этим и другими ответами незначительна. Я чувствую, что что-то более переносимое в другую кодировку - лучший и более надежный ответ. Насколько хорошо этот код будет стоять, если ему нужно будет выйти на международный уровень? UTF-8 содержит тысячи символов, то есть миллионы чрезвычайно редких перестановок. - person Nobbynob Littlun; 28.08.2014
comment
@AndrewK На заданный вопрос мое решение - одно из самых простых в реализации и поддержке. На практике матричные представления ориентированных графов не редкость, в первую очередь из-за их повышенной производительности и разумных требований к памяти для небольших наборов вершин. Если вам требуется больше вершин, вам будет лучше с представлением списка смежности или фильтром цветения или даже с их комбинацией. Как и любой алгоритм, это зависит от вашего варианта использования. Знаете ли вы, что пузырьковая сортировка быстрее, чем любой другой алгоритм, менее чем на 16 элементах? - person Sergey L.; 28.08.2014