【问题标题】:N-dimensional tree, any OO languageN维树,任何OO语言
【发布时间】:2015-03-16 21:50:21
【问题描述】:

我需要从左到右填充的 N 维(每个节点限制为 N 个后代)树。

类似的东西(伪vardump):

// before
TreeNode {
  children [
    TreeNode {
      children [TreeNode {}, TreeNode {}, TreeNode {}]
    },
    TreeNode {
      children [null, null, null]
    },
    TreeNode {
      children [null, null, null]
    }
  ]
}

然后添加节点,然后我应该有:

// after
TreeNode {
  children [
    TreeNode {
      children [TreeNode {}, TreeNode {}, TreeNode {}]
    },
    TreeNode {
      children [TreeNode {}, null, null]
    },
    TreeNode {
      children [null, null, null]
    }
  ]
}

我做过的事情:

<?php

class TreeNode
{
    const NODE_CHILDREN_MAX = 3;
    const NODE_CHILD_LEVELS_MAX = 6;

    protected $id;

    private $data;

    private $parent;

    private $children = [];

    private $weight = 0;

    /**
     * Get id
     *
     * @return integer 
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set weight
     *
     * @param integer $weight
     * @return TreeNode
     */
    public function setWeight($weight)
    {
        $this->weight = $weight;

        return $this;
    }

    /**
     * Get weight
     *
     * @return integer 
     */
    public function getWeight()
    {
        return $this->weight;
    }

    public function setData($data)
    {
        $this->data = $data;
        return $this;
    }

    public function getData()
    {
        return $this->data;
    }

    /**
     * Set parent
     *
     * @param TreeNode $parent
     * @return TreeNode
     */
    public function setParent(TreeNode $parent = null)
    {
        $this->parent = $parent;

        return $this;
    }

    /**
     * Get parent
     *
     * @return TreeNode
     */
    public function getParent()
    {
        return $this->parent;
    }

    /**
     * Add children
     *
     * @param TreeNode $node
     * @return TreeNode[]
     */
    public function addChild(TrinarNode $node)
    {
        if (count($this->children) < static::NODE_CHILDREN_MAX) {
            $this->children[] = $node;
            $node->setParent($this);
            $this->weight++;
            return [$this, $node];
        } else {

            $children = $this->children;
            /** @var TreeNode[] $children */
            $children = $this->children;
            usort($children, function (TreeNode $node1, TreeNode $node2) {
                $levelDifference = $node1->subtractLevel($node2);
                $sorting = ($node1->weight - $node2->weight) % 2;
                if (abs($levelDifference) == 1) {
                    $sorting = -$sorting;
                }
                if ($sorting === 0) {
                    $sorting = ($node1->id - $node2->id) % 2;
                }
                return $sorting;
            });
            foreach ($children as $childNode) {
                $this->weight++;
                return array_merge($childNode->addChild($node), [$this]);
            }
            return [];
        }
    }

    private function subtractLevel(TreeNode $node)
    {
        return $this->getChildLevelCount() - $node->getChildLevelCount();
    }

    private function getMaxDifference()
    {
        $logs = array_map(function (TreeNode $node) {
            return floor(log($node->getWeight() === 0 ? 1 : $node->getWeight(), self::NODE_CHILDREN_MAX));
        }, $this->children);
        return pow(
            self::NODE_CHILDREN_MAX,
            min($logs) + 1
        );
    }

    public static function getLevelCount($level)
    {
        if ($level === 0) {
            return 0;
        }
        return pow(self::NODE_CHILDREN_MAX, $level) + self::getLevelCount($level - 1);
    }

    public function hasEmptyNodes()
    {
        return count($this->children) < self::NODE_CHILDREN_MAX;
    }

    /**
     * Get children
     *
     * @return TreeNode[]
     */
    public function getChildren()
    {
        return $this->children;
    }

    public function getChildLevelCount()
    {
        $weight = $this->weight;
        if (!$weight) {
            return 0;
        }
        return floor(log($weight, self::NODE_CHILDREN_MAX)) + 1;
    }

    private function getLevelBreakCount()
    {
        return count(array_unique(array_map(function (TreeNode $node) {
            return $node->getChildLevelCount();
        }, $this->children)));
    }
}

当前的问题是我不知道在添加时应该选择哪个节点...在以前的版本中,节点是根据权重添加的(应该选择最小的权重),但这不是我真正需要的。我需要从左到右添加节点,而“级别”未结束,如果“级别”已满,则创建新节点。其中“级别” - 与根节点距离相同的节点组。

以前的版本(为了清楚起见):

// before
TreeNode {
  children [
    TreeNode {
      children [TreeNode {}, null, null]
    },
    TreeNode {
      children [null, null, null]
    },
    TreeNode {
      children [null, null, null]
    }
  ]
}

然后添加节点:

// after
TreeNode {
  children [
    TreeNode {
      children [TreeNode {}, null, null]
    },
    TreeNode {
      children [TreeNode {}, null, null]
    },
    TreeNode {
      children [null, null, null]
    }
  ]
}

并且单元测试TreeNode 应该通过(PHPUnit 需要):

<?php

class TestableTreeNode extends TreeNode
{
    private $name;

    public function setId($id)
    {
        $this->id = $id;
        return $this;
    }

    /**
     * @return mixed
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * @param mixed $name
     * @return $this
     */
    public function setName($name)
    {
        $this->name = $name;
        return $this;
    }
}

class TreeNodeTest extends \PHPUnit_Framework_TestCase
{
    private $flatStorage = [];

    public function test()
    {
        $this->iniSet('memory_limit', -1);
        $data = $this->generateTestData(3);
        $testable = $this->flatten($data->real);
        $this->dump('expected.dump', $data->flat);
        $this->dump('real.dump', $data->real);
        $this->dump('converted.dump', $testable);
        foreach ($data->flat as $levelNumber => $level) {
            $this->assertArrayHasKey($levelNumber, $testable, 'Searched level doesn\'t exists!');
            $testableLevel = $testable[$levelNumber];
            foreach ($level as $nodeId => $node) {
                $this->assertArrayHasKey($nodeId, $testableLevel, 'Searched node doesn\'t exists!');
                $testableNode = $testableLevel[$nodeId];
                $this->assertEquals($node->getName(), $testableNode->getName(), 'Tree is not valid!');
            }
        }
    }

    protected function generateTestData($levels = TestableTreeNode::NODE_CHILD_LEVELS_MAX)
    {
        $tree = new TestableTreeNode();
        $flatTree = [];
        $id = 0;
        for ($level = 0; $level < $levels; $level++) {
            $treeLevel = [];
            $max = pow(TestableTreeNode::NODE_CHILDREN_MAX, $level+1);
            for ($nodeId = 0; $nodeId < $max; $nodeId++) {
                $treeNode = (new TestableTreeNode())
                    ->setId($id++)
                    ->setName($level . ':' . $nodeId);
                $tree->addChild($treeNode);
                $treeLevel[] = $treeNode;
            }
            $flatTree[] = $treeLevel;
        }
        return (object)['flat' => $flatTree, 'real' => $tree];
    }

    protected function flatten(TestableTreeNode $treeNode, $level = 0)
    {
        if (!$level) {
            $this->flatStorage = [];
        }
        foreach ($treeNode->getChildren() as $child) {
            $this->setRendered($level, $child);
            $this->flatten($child, $level+1);
        }
        return $this->flatStorage;
    }

    protected function setRendered($level, $child)
    {
        if (!isset($this->flatStorage[$level])) {
            $this->flatStorage[$level] = [];
        }
        $this->flatStorage[$level][] = $child;
    }

    private function dump($fileName, $data)
    {
        ob_start();
        var_dump($data);
        file_put_contents($fileName, ob_get_contents());
        ob_end_clean();
    }

}

Code in action(with unit-test)

【问题讨论】:

  • 树有什么作用?这是 Doctrine 的嵌套集插件吗?决定“添加时应该选择哪个节点”是什么意思?你上面的代码示例能达到你想要的程度吗?
  • @halfer 不,这不是教义,树只是树,我需要在其中存储一些数据。我的意思是:我应该选择哪个子节点并在其上运行TreeNode#addChild,因为如果我选择第一个树不会是树,它就变成了列表。所以我需要找到一种算法,基于它我将能够控制递归。目前我的结果很差:(以前根据权重添加节点(一些内部变量来观察树的大小):将节点添加到最小的节点。
  • 在你的二维例子中,我猜你得到了所有项目(2-1)级别的计数,并将项目添加到没有 MAX 项目的第一个节点。我不确定如何有效地做到这一点,除了可能单独持有对下一个添加位置的引用。
  • 我不知道你的总体目的,但是 Doctrine 有一个层次结构插件可以简化这个吗?
  • @halfer 它不是二维的,它是无限的,学说插件不合适,导致其中的添加是完全手动的,并且最多限制为 2 个子节点。我认为加载所有表至少是个坏主意......保存对下一个合适节点的引用的问题是:它变化太频繁,无论如何都应该以某种方式计算(它是问题:))

标签: php algorithm oop recursion tree


【解决方案1】:

找到解决方案,它不是最优雅的(我认为),但它有效。很快:它基于每个级别允许的最大节点数。还更新了 runnable 中的代码,代码正在通过单元测试。

<?php

class TreeNode
{
    const NODE_CHILDREN_MAX = 3;
    const NODE_CHILD_LEVELS_MAX = 6;

    protected $id;

    private $parent;

    private $children = [];

    private $weight = 0;

    /**
     * Get id
     *
     * @return integer
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set weight
     *
     * @param integer $weight
     * @return TreeNode
     */
    public function setWeight($weight)
    {
        $this->weight = $weight;

        return $this;
    }

    /**
     * Get weight
     *
     * @return integer
     */
    public function getWeight()
    {
        return $this->weight;
    }

    /**
     * Set parent
     *
     * @param TreeNode $parent
     * @return TreeNode
     */
    public function setParent(TreeNode $parent = null)
    {
        $this->parent = $parent;

        return $this;
    }

    /**
     * Get parent
     *
     * @return TreeNode
     */
    public function getParent()
    {
        return $this->parent;
    }

    /**
     * Add children
     *
     * @param TreeNode $node
     * @return TreeNode[]
     */
    public function addChild(TreeNode $node)
    {
        if (count($this->children) < static::NODE_CHILDREN_MAX) {
            $this->children[] = $node;
            $node->setParent($this);
            $this->weight++;
            return [$this, $node];
        } else {
            $children = $this->children;
            $targetWeight = $this->getTargetChildWeight();
            $children = array_filter($children, function (TreeNode $node) use ($targetWeight) {
                return $node->weight < $targetWeight;
            });
            usort($children, function (TreeNode $node1, TreeNode $node2) use ($targetWeight) {
                $sorting = (($targetWeight - $node1->weight) - ($targetWeight - $node2->weight)) % 2;
                if (!$sorting) {
                    $sorting = ($node1->weight - $node2->weight) % 2;
                    if (!$sorting) {
                        $sorting = ($node1->id - $node2->id) % 2;
                    }
                }
                return $sorting;
            });
            foreach ($children as $childNode) {
                $this->weight++;
                return array_merge($childNode->addChild($node), [$this]);
            }
            return [];
        }
    }

    private function getTargetChildWeight()
    {
        $levels = array_map(function (TreeNode $node) {
            return $node->getChildLevelCount();
        }, $this->children);
        return static::weightOf(min($levels));
    }

    /**
     * Get children
     *
     * @return TreeNode[]
     */
    public function getChildren()
    {
        return $this->children;
    }

    public function getChildLevelCount()
    {
        return static::levelOf($this->weight);
    }

    public static function weightOf($level)
    {
        if (!$level) {
            return 0;
        }
        return pow(self::NODE_CHILDREN_MAX, $level) + static::weightOf($level-1);
    }

    public static function levelOf($weight)
    {
        $level = 0;
        while (static::weightOf($level) < $weight) {
            $level++;
        }
        if (static::weightOf($level) == $weight) {
            $level++;
        }
        return $level;
    }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-11-28
    • 1970-01-01
    • 1970-01-01
    • 2014-02-12
    • 2019-07-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多