【问题标题】:Determining which objects are or are not linked to the main root object确定哪些对象链接或不链接到主根对象
【发布时间】:2018-02-09 00:20:02
【问题描述】:

我正在尝试浏览一堆带有指向其他对象的链接的对象。我想从最低的 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);

我不确定是否有更好的方法来获取这些用于我的目的,因此欢迎提出建议。

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
        )

)

请注意,括号中的数字只是数组索引,而不是对象ID号,所以不要让索引把你弄丢了。

我正在尝试找到一种方法来确定哪些是链接对象,哪些是未链接对象。根据上述情况,对象应按如下方式分离:

**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 一样)。
  • 上述示例结构(在我的主要问题下方)只是一个建议,我愿意接受其他建议。
  • 免责声明:这个问题是基于另一个问题我previously asked。 在我最初的问题中,我手动创建了测试对象,但我无法以这种方式将它们从我的数据库中拉出。

【问题讨论】:

    标签: php mysql loops object


    【解决方案1】:

    这是获取所有链接 ID 的一种非常简单的方法:

    $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) 将返回一个结构化数组,其中每个对象的链接由 objectIDs 索引,如下所示:

    $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

    【讨论】:

      【解决方案2】:

      这是一个graph traversal 问题。从一个节点(根)开始,您要遍历跟踪沿途访问的每个节点的图形。一旦遍历结束,访问过的就被连接起来。广度优先搜索可以这样完成:

      //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's 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;
          }
      }
      

      还有节点类:

      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;
          }
      
      }
      

      【讨论】:

      • 当我运行它并尝试执行 print_r($linked) 或 print_r($notLinked) 时,我得到一个巨大的深度嵌套对象列表,并打印出消息“RECURSION”出去。这是我应该期待的吗?
      • $linked$notLinked 是 Node 对象的集合。根据您的应用程序,您希望它们(Node 对象)做一些有用的事情(使用它们在构造函数中提供的数据)。你也可以让一个节点只返回带有getData() 之类的数据。由你决定。关键是$linked 应该包含可以从根到达的节点,而$notLinked 应该包含图中的其余节点。
      【解决方案3】:

      将数据作为两个单独的数组处理更容易(恕我直言)。一组对象及其链接。另外,作为第一部分,我将对象转换为以 ID 作为键,这使我可以直接使用它,而不必每次都搜索 ID。

      另外为了使解决方案更简单,我在访问对象数组时创建了一个标志,这样当我再次尝试引用它时,我可以检查它是否已经被访问过。

      <?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 函数。这使用&amp;$objects,因为这意味着对函数的所有调用都将使用相同的数组。因为我想建立项目的所有用途,所以这很重要。

      buildTree 中的条件,检查它是否是我们想要的节点,它不是指同一个节点(浪费时间再看),不是 null(不知道为什么你链接到 null,但又不值得看任何进一步)并且该节点尚未被访问。如果这些条件都OK,它将下一个节点标记为已访问并进入下一个级别。

      输出是……

      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
      )
      

      【讨论】:

      • 从技术上讲,对象 8 没有链接,因此 SQL 查询的结果表明它链接到 NULL。我最初将这两个表连接在一起,但根据您和 Weltschmerz 的回答,看起来不加入它们会是更好的方法。
      • 更多的是了解某些事物的含义,如果包含它,然后适当地处理它,排除它可能更容易。只要你能满足每一种可能性,那就没问题,但是当我没有排除它时 - 我的代码最初创建了一个指向 null 新条目的链接,看起来很奇怪。
      • 我发现我可以通过使用“PDO::FETCH_GROUP|PDO::FETCH_UNIQUE”从数据库中获取数据来使您的代码更有效率。这样我就不需要转换数组并且可以消除 array_combine 行。
      • 了解如何改进您编写的任何代码总是很有用的,感谢您提供的信息 :)
      【解决方案4】:

      假设“根”是obj_id 1

      这是一个有点蛮力的算法,但它利用了 SQL 对“集合”操作的喜好。

      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
      

      输出是所有“已连接”的 ID。如果您想要未连接的:

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

      【讨论】:

      • 我认为我的偏好是在 PHP 中执行此操作,而不是在数据库中创建新表。不过感谢您的帮助!
      • 好的。 PHP 的 array_merge 可用于将数组“tables2”合并为“table1”。所以,问题实际上是关于算法,而不是 PHP 或 MySQL。
      • 我也有点困惑你指的是哪两个表。我已经在我的 SQL 选择语句中合并了 Object 表和 Object Links 表。
      • 我的错。我应该使用object_links
      猜你喜欢
      • 1970-01-01
      • 2019-02-22
      • 1970-01-01
      • 1970-01-01
      • 2020-07-26
      • 1970-01-01
      • 2021-12-14
      • 2018-11-01
      • 1970-01-01
      相关资源
      最近更新 更多