Определение того, какие объекты связаны или не связаны с основным корневым объектом

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

Таблицы в моей базе данных выглядят следующим образом:

Таблица объектов:

+----+---------+
| id | title   |
+----+---------+
|  1 | Apple   |
|  3 | Carrot  |
|  4 | Dill    |
|  5 | Egg     |
|  6 | Fred    |
|  7 | Goat    |
|  8 | Harry   |
|  9 | Igloo   |
| 10 | Jason   |
| 11 | Klaus   |
| 12 | Banana  |
| 15 | Oyster1 |
| 16 | Oyster2 |
+----+---------+

Таблица Object_Links:

+----+---------+--------------+
| id |  obj_id |  obj_link_id |
+----+---------+--------------+
|  1 |       1 |           12 |
|  2 |       1 |            5 |
|  3 |       3 |            1 |
|  4 |       3 |           12 |
|  5 |       3 |            3 |
|  6 |       4 |            1 |
|  7 |       4 |            5 |
|  8 |       5 |            6 |
|  9 |       6 |            7 |
| 10 |       7 |            7 |
| 11 |       7 |            8 |
| 12 |       9 |           12 |
| 13 |       9 |            5 |
| 14 |      10 |            1 |
| 15 |      10 |            5 |
| 16 |      10 |            8 |
| 17 |      11 |            1 |
| 18 |      11 |            5 |
| 19 |      11 |           10 |
| 20 |      12 |            3 |
| 21 |      15 |           16 |
| 22 |      16 |           15 |
+----+---------+--------------+

Итак, из таблицы вы можете видеть, что объект 1 имеет ссылки на оба объекта 12 и 5.

Мой SQL-запрос выглядит следующим образом:

select  object.id, title, obj_link_id
    from  object
    left join  object_links  ON object.id = object_links.object_id
    order by  object.id 

который дает таблицу:

+----+---------+--------------+
| id | title   |  obj_link_id |
+----+---------+--------------+
|  1 | Apple   |           12 |
|  1 | Apple   |            5 |
|  3 | Carrot  |            1 |
|  3 | Carrot  |           12 |
|  3 | Carrot  |            3 |
|  4 | Dill    |            1 |
|  4 | Dill    |            5 |
|  5 | Egg     |            6 |
|  6 | Fred    |            7 |
|  7 | Goat    |            7 |
|  7 | Goat    |            8 |
|  8 | Harry   |         NULL |
|  9 | Igloo   |           12 |
|  9 | Igloo   |            5 |
| 10 | Jason   |            1 |
| 10 | Jason   |            5 |
| 10 | Jason   |            8 |
| 11 | Klaus   |            1 |
| 11 | Klaus   |            5 |
| 11 | Klaus   |           10 |
| 12 | Banana  |            3 |
| 15 | Oyster1 |           16 |
| 16 | Oyster2 |           15 |
+----+---------+--------------+

В PHP я использую:

$objects = $stmt->fetchAll(PDO::FETCH_CLASS);

Я не был уверен, есть ли лучший способ получить их для моих целей, поэтому я открыт для предложений.

A print_r($objects) дает:

Array
(
    [0] => stdClass Object
        (
            [id] => 1
            [title] => Apple
            [obj_link_id] => 12
        )

    [1] => stdClass Object
        (
            [id] => 1
            [title] => Apple
            [obj_link_id] => 5
        )

    [2] => stdClass Object
        (
            [id] => 3
            [title] => Carrot
            [obj_link_id] => 1
        )

    [3] => stdClass Object
        (
            [id] => 3
            [title] => Carrot
            [obj_link_id] => 12
        )

    [4] => stdClass Object
        (
            [id] => 3
            [title] => Carrot
            [obj_link_id] => 3
        )

    [5] => stdClass Object
        (
            [id] => 4
            [title] => Dill
            [obj_link_id] => 1
        )

    [6] => stdClass Object
        (
            [id] => 4
            [title] => Dill
            [obj_link_id] => 5
        )

    [7] => stdClass Object
        (
            [id] => 5
            [title] => Egg
            [obj_link_id] => 6
        )

    [8] => stdClass Object
        (
            [id] => 6
            [title] => Fred
            [obj_link_id] => 7
        )

    [9] => stdClass Object
        (
            [id] => 7
            [title] => Goat
            [obj_link_id] => 7
        )

    [10] => stdClass Object
        (
            [id] => 7
            [title] => Goat
            [obj_link_id] => 8
        )

    [11] => stdClass Object
        (
            [id] => 8
            [title] => Harry
            [obj_link_id] =>
        )

    [12] => stdClass Object
        (
            [id] => 9
            [title] => Igloo
            [obj_link_id] => 12
        )

    [13] => stdClass Object
        (
            [id] => 9
            [title] => Igloo
            [obj_link_id] => 5
        )

    [14] => stdClass Object
        (
            [id] => 10
            [title] => Jason
            [obj_link_id] => 1
        )

    [15] => stdClass Object
        (
            [id] => 10
            [title] => Jason
            [obj_link_id] => 5
        )

    [16] => stdClass Object
        (
            [id] => 10
            [title] => Jason
            [obj_link_id] => 8
        )

    [17] => stdClass Object
        (
            [id] => 11
            [title] => Klaus
            [obj_link_id] => 1
        )

    [18] => stdClass Object
        (
            [id] => 11
            [title] => Klaus
            [obj_link_id] => 5
        )

    [19] => stdClass Object
        (
            [id] => 11
            [title] => Klaus
            [obj_link_id] => 10
        )

    [20] => stdClass Object
        (
            [id] => 12
            [title] => Banana
            [obj_link_id] => 3
        )

    [21] => stdClass Object
        (
            [id] => 15
            [title] => Oyster1
            [obj_link_id] => 16
        )

    [22] => stdClass Object
        (
            [id] => 16
            [title] => Oyster2
            [obj_link_id] => 15
        )

)

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

Я пытаюсь найти способ определить, какие из них связаны и которые являются несвязанными объектами. Исходя из вышеприведенного сценария, объекты должны быть разделены следующим образом:

**Linked:**

    Apple
    Banana
    Carrot
    Egg
    Fred
    Goat
    Harry

**Not Linked:**

    Dill
    Igloo
    Jason
    Klaus
    Oyster1
    Oyster2

Мой главный вопрос:

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

stdClass Object
(
  [LinkedElements] => stdClass Object
    (
      [1] => stdClass Object
        (
          [id] => 1
          [name] => Apple
          [link] => Array
            (
              [0] => 14
              [1] => 5
            )

        )

      [14] => stdClass Object
        (
          [id] => 14
          [name] => Banana
          [link] => Array
            (
              [0] => 3
            )

        )

      [3] => stdClass Object
        (
          [id] => 3
          [name] => Carrot
          [link] => Array
            (
              [0] => 1
              [1] => 14
              [2] => 3
            )

        )

      [5] => stdClass Object
        (
          [id] => 5
          [name] => Egg
          [link] => Array
            (
              [0] => 6
            )

        )

      [6] => stdClass Object
        (
          [id] => 6
          [name] => Fred
          [link] => Array
            (
              [0] => 7
            )

        )

      [7] => stdClass Object
        (
          [id] => 7
          [name] => Goat
          [link] => Array
            (
              [0] => 7
              [1] => 8
            )

        )

      [8] => stdClass Object
        (
          [id] => 8
          [name] => Harry
        )

    )

  [UnLinkedElements] => stdClass Object
    (
      [4] => stdClass Object
        (
          [id] => 4
          [name] => Dill
          [link] => Array
            (
              [0] => 1
              [1] => 5
            )

        )

      [9] => stdClass Object
        (
          [id] => 9
          [name] => Igloo
          [link] => Array
            (
              [0] => 14
              [1] => 5
            )

        )

      [10] => stdClass Object
        (
          [id] => 10
          [name] => Jason
          [link] => Array
            (
              [0] => 1
              [1] => 5
              [2] => 8
            )

        )

      [11] => stdClass Object
        (
          [id] => 11
          [name] => Klaus
          [link] => Array
            (
              [0] => 1
              [1] => 5
              [2] => 10
            )

        )

      [15] => stdClass Object
        (
          [id] => 15
          [name] => Oyster1
          [link] => Array
            (
              [0] => 16
            )

        )

      [16] => stdClass Object
        (
          [id] => 16
          [name] => Oyster2
          [link] => Array
            (
              [0] => 15
            )

        )

    )

)

Обратите внимание:

  • Навигация осуществляется от объекта к ссылке, а не наоборот.
  • Достаточно иметь объектную точку для себя (как это делает объект 7).
  • Вышеприведенная структура выборки (под моим основным вопросом) является только предложением, и я открыт для других предложений.
  • Отказ от ответственности: этот вопрос основан на другом вопросе я ранее заданном. В моем первоначальном вопросе я вручную создал тестовые объекты, но я не смог вытащить их из своей базы данных таким образом.

Ответ 1

Легче (IMHO) обрабатывать данные как два отдельных массива. Набор объектов и их ссылки. Кроме того, в качестве первой части я конвертирую объект, чтобы иметь идентификатор в качестве ключа, это позволяет мне использовать его напрямую, а не искать идентификатор каждый раз.

Также, чтобы сделать решение намного проще, я создал флаг в массиве объектов, когда я его посещаю, поэтому, когда я пытаюсь его снова ссылаться, я могу проверить, был ли он уже посещен.

<?php 
error_reporting ( E_ALL );
ini_set ( 'display_errors', 1 );

$objects =[[1,'apple'],
        [3, 'Carrot'],
        [4, 'Dill'],
        [5, 'Egg '],
        [6, 'Fred'],
        [7, 'Goat'],
        [8, 'Harry'],
        [9, 'Igloo'],
        [10, 'Jason'],
        [11, 'Klaus'],
        [12, 'Banana'],
        [15, 'Oyster1'],
        [16, 'Oyster2' ]];
$links =[[1,12],
        [1,5],
        [3,1],
        [3,12],
        [3,3],
        [4,1],
        [4,5],
        [5,6],
        [6,7],
        [7,7],
        [7,8],
        [8,null],
        [9,12],
        [9,5],
        [10,1],
        [10,5],
        [10,8],
        [11,1],
        [11,5],
        [11,10],
        [12,3],
        [15,16],
        [16,15 ]];

function buildTree ( $id, &$objects, $links )   {
    foreach ( $links as $testNode )    {
        if ( $testNode[0] == $id && 
                $testNode[1] != $id &&
                $testNode[1] != null &&
                !isset($objects[$testNode[1]]['visited']) )    {
            $objects[$testNode[1]]['visited'] = true;
            buildTree ( $testNode[1], $objects, $links);
        }
    }
}

// Convert array to have the ID as key
$objects = array_combine(array_column($objects, 0), $objects);
// Fetch ID of first item
reset($objects);
$startID = key($objects);
// Mark as visited
$objects[$startID]['visited'] = true;
// Process
buildTree ( $startID, $objects, $links);

$linked = [];
$notLinked = [];
foreach ( $objects as $object)  {
    if ( isset($object['visited']) )    {
        $linked[] = $object[1];
    }
    else    {
        $notLinked[] = $object[1];
    }
}
echo "Linked...".PHP_EOL;
print_r($linked);

echo "Not linked...".PHP_EOL;
print_r($notLinked);

Как вы можете видеть, ядром является рекурсивная функция buildTree. Это использует &$objects, так как это означает, что все вызовы функции будут использовать один и тот же массив. Поскольку я хочу создать все виды использования элементов, это важно.

Условие в buildTree проверяет, хочет ли он node, это не относится к тому же node (трату времени, которое вы ищете больше), а не null (не знаете, почему вы ссылаетесь на null, но снова не стоит смотреть дальше) и что node еще не был посещен. Если эти условия в порядке, он отмечает следующий node по мере посещения и переходит на следующий уровень.

Выход...

Linked...
Array
(
    [0] => apple
    [1] => Carrot
    [2] => Egg 
    [3] => Fred
    [4] => Goat
    [5] => Harry
    [6] => Banana
)
Not linked...
Array
(
    [0] => Dill
    [1] => Igloo
    [2] => Jason
    [3] => Klaus
    [4] => Oyster1
    [5] => Oyster2
)

Ответ 2

Это обход трассировки. Начиная с node (root), вы хотите пересечь график, отслеживающий каждую посещаемую node по пути. Как только обход закончился, посещенные связаны. Первый поиск по белому может быть выполнен следующим образом:

//To form a graph fetch all objects from the database (sorted by id) and 
//index them in a hash map
$objects = $stmt->fetchAll(PDO::FETCH_OBJ);
$nodes = [];
foreach ($objects as $object) {
    $nodes[$object->id] = new Node($object);
}

//fetch all connections from the database and link the objects
$links = $stmt->fetchAll(PDO::FETCH_OBJ);
foreach ($links as $link) {
    $nodes[$link->obj_id]->addLink($nodes[$link->obj_link_id]);
}

//let assume root is the first node (sorted by id), 
//traverse the graph starting from root
$root = reset($nodes);
$root->traverse();

//now if a node can be reached by the root it is marked as visited
$linked = [];
$notLinked = [];
foreach ($nodes as $node) {
    if ($node->isVisited()) {
        $linked[] = $node;
    } else {
        $notLinked[] = $node;
    }
}

И класс node:

class Node
{

    /**
     * List of neighbor nodes.
     *
     * @var Node[]
     */
    private $links = [];

    /**
     * id, title info
     *
     * @var array
     */
    private $data = [];

    /**
     * To track visited nodes.
     *
     * @var bool
     */
    private $visited = false;

    /**
     * Node constructor.
     * @param array $data
     */
    public function __construct($data)
    {
        $this->data = $data;
    }


    /**
     * Add a link to this node.
     *
     * @param Node $node
     * @return void
     */
    public function addLink(Node $node)
    {
        $this->links[] = $node;
    }

    /**
     * Traverse the graph in a Breadth-First-Search fashion marking
     * every visited node.
     * @return void
     */
    public function traverse()
    {
        //initialize queue
        $q = new SplQueue();

        //add root to queue and mark as visited
        $q->enqueue($this);
        $this->visited = true;

        while (!$q->isEmpty()) {
            /** @var Node $cur */
            $cur = $q->dequeue();

            foreach ($cur->links as $link) {

                //if link not visited already add it to queue and mark visited
                if (!$link->visited) {
                    $link->visited = true;
                    $q->enqueue($link);
                }
            }
        }
    }

    /**
     * Checks if node has been visited.
     *
     * @return bool
     */
    public function isVisited()
    {
        return $this->visited;
    }

}

Ответ 3

Скажем, что "корень" obj_id 1.

Вот несколько алгоритм грубой силы, но он использует преимущества SQL-операций "set".

Insert into table1 the root (1)
Loop
    Create table2 with all nodes linked to any node in table1
    Exit if number of rows in table2 = num rows in table1
    table1 := table2

Ближе к SQL:

# Initialize:
CREATE TABLE table1 (
    obj_id ...
    PRIMARY KEY(obj_id)
)
    SELECT 1;   # assuming root is 1

start of loop:
CREATE TABLE table2 (
    obj_id ...
    PRIMARY KEY(obj_id)
)
    SELECT DISTINCT obj_link_id
        FROM table1
        JOIN object_links USING(obj_id);

SELECT @fini := ( SELECT COUNT(*) FROM table1 ) = 
                ( SELECT COUNT(*) FROM table2 )   # will give true/false
DROP TABLE table1;
RENAME TABLE table2 TO table1;
loop if @fini=0

Выходные данные - это все "подключенные" идентификаторы. Если вам нужны несвязные:

SELECT obj_id
    FROM object_links
    LEFT JOIN table1 USING(obj_id)
    WHERE table1.obj_id IS NULL;   # that is, missing from table1

Ответ 4

Вот довольно короткий способ получить все связанные идентификаторы:

$pdo = new PDO('mysql:host=localhost;dbname=test_obj_link', 'testread', 'testread');

$links = $pdo
    ->query('select obj_id, obj_link_id from object_links')
    ->fetchAll(PDO::FETCH_GROUP|PDO::FETCH_COLUMN);

function getLinks($objId, $links,  $indirectNodes = []) {
    $directNodes = isset($links[$objId]) ? $links[$objId] : [];
    foreach($directNodes as $linkedNode) {
        if (!in_array($linkedNode, $indirectNodes)) {
            $indirectNodes[] = $linkedNode;
            $indirectNodes = array_unique(array_merge(
                $indirectNodes,
                getLinks($linkedNode, $links, $indirectNodes)
            ));
        }
    }
    return $indirectNodes;
}

$linkedObjectIds = getLinks(1, $links);

fetchAll(PDO::FETCH_GROUP|PDO::FETCH_COLUMN) вернет структурированный массив со ссылками на объект, индексированный объектными идентификаторами, который выглядит так:

$links = [
    1  => ['5', '12'],
    3  => ['1', '3', '12'],
    4  => ['1', '5'],
    5  => ['6'],
    6  => ['7'],
    7  => ['7', '8'],
    9  => ['5', '12'],
    10 => ['1', '5', '8'],
    11 => ['1', '5', '10'],
    12 => ['3'],
    15 => ['16'],
    16 => ['15'],
];

Функция getLinks будет "ходить" в массиве $links рекурсивно и объединить все дочерние массивы, найденные в пути. Поскольку PHP, похоже, не использует функцию array_union - array_unique(array_merge(..)).

Результат:

$linkedObjectIds = array (
  0 => '5',
  1 => '6',
  2 => '7',
  3 => '8',
  4 => '12',
  10 => '3',
  11 => '1',
)

Обратите внимание, что индексы здесь не имеют значения.

Чтобы получить соответствующие объекты, вы можете сделать следующее:

$objects = $pdo
    ->query('select id, title from object')
    ->fetchAll(PDO::FETCH_KEY_PAIR);
$linkedObjects = array_intersect_key($objects, array_flip($linkedObjectIds));
$notLinkedObjects = array_diff_key($objects, $linkedObjects);

Переменные будут содержать:

$objects = array (
  1 => 'Apple',
  3 => 'Carrot',
  4 => 'Dill',
  5 => 'Egg',
  6 => 'Fred',
  7 => 'Goat',
  8 => 'Harry',
  9 => 'Igloo',
  10 => 'Jason',
  11 => 'Klaus',
  12 => 'Banana',
  15 => 'Oyster1',
  16 => 'Oyster2',
);
$linkedObjects = array (
  1 => 'Apple',
  3 => 'Carrot',
  5 => 'Egg',
  6 => 'Fred',
  7 => 'Goat',
  8 => 'Harry',
  12 => 'Banana',
);
$notLinkedObjects = array (
  4 => 'Dill',
  9 => 'Igloo',
  10 => 'Jason',
  11 => 'Klaus',
  15 => 'Oyster1',
  16 => 'Oyster2',
);

Демо: http://rextester.com/ZQQGE35352

Обратите внимание, что in_array() и array_unique(), вероятно, медленны, поскольку они должны искать значения, которые не сортируются. Это может привести к проблемам с производительностью некоторых наборов данных. Предполагая, что PHP может быстрее искать ключи, мы можем использовать array_key_exists() вместо in_array() и оператора массива + (объединение по ключам) вместо array_unique(array_merge()). И код будет даже немного короче:

function getLinks($objId, $links,  $indirectNodes = []) {
    $directNodes = isset($links[$objId]) ? $links[$objId] : [];
    foreach($directNodes as $linkedNode) {
        if (!array_key_exists($linkedNode, $indirectNodes)) {
            $indirectNodes[$linkedNode] = 0;
            $indirectNodes = $indirectNodes + getLinks($linkedNode, $links, $indirectNodes);
        }
    }
    return $indirectNodes;
}

Однако, поскольку функция теперь возвращает необходимый результат в виде ключей, нам нужно будет использовать array_keys() для их извлечения:

$linkedObjectIds = array_keys(getLinks(1, $links));

Демо: http://rextester.com/GHO7179