N维树,任何OO语言

时间:2015-01-18 22:04:58

标签: php algorithm oop recursion tree

我需要从左到右填充的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)

1 个答案:

答案 0 :(得分:0)

找到解决方案,它不是最优雅的(我认为),但它的工作原理。简而言之:它基于每个级别允许的最大节点数。还在 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;
    }
}