Skip to content

Commit

Permalink
Added Topological sort.
Browse files Browse the repository at this point in the history
  • Loading branch information
mpartel committed May 6, 2013
1 parent 4d868aa commit 2500cbe
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 1 deletion.
5 changes: 5 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
"name": "Joonas Pajunen",
"email": "[email protected]",
"role": "Developer"
},
{
"name": "Martin Pärtel",
"email": "[email protected]",
"role": "Developer"
}
],
"require": {
Expand Down
76 changes: 76 additions & 0 deletions library/Xi/Algorithm/TopologicalSort.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php
namespace Xi\Algorithm;

/**
* Implements topological sorting.
*
* Topological sorting means sorting the nodes in a directed acyclic graph into a list such
* that if a node X points to a node Y then Y appears before X in the list.
* This is useful for things like resolving dependencies.
*
* http://en.wikipedia.org/wiki/Topological_sorting
*/
class TopologicalSort
{
/**
* @param array $edges An array of scalars to arrays of scalars, representing the edges in the graph.
* @return array An array of nodes in the graph.
* @throws \InvalidArgumentException if the graph has a cycle or $deps is in an invalid format
*/
public static function apply(array $edges)
{
$allNodes = self::allNodes($edges);

// We do a depth-first search.
// A node is marked with 1 when first seen and with 2 when recursion returns from it.
// If we meet a node marked 0 then we recursively add its descendants to the list before adding it to the list.
// If we meet a node marked 1 then we've found a cycle, which is an error.
// If we meet a node marked 2 then it's already on the list and we return.
$unmarkedNodes = array_fill_keys($allNodes, null);
$marks = array_fill_keys($allNodes, 0);
$result = [];

$visit = function ($node) use ($edges, &$visit, &$marks, &$unmarkedNodes, &$result) {
$mark = $marks[$node];
if ($mark === 0) {
$marks[$node] = 1;
unset($unmarkedNodes[$node]);

foreach ((isset($edges[$node]) ? $edges[$node] : []) as $next) {
$visit($next);
}

$marks[$node] = 2;
$result[] = $node;
} elseif ($mark === 1) {
throw new \InvalidArgumentException("The graph has a cycle involving node $node");
}
};

// We try each node as a starting point since we don't know which node (if any) is the root node.
// (if the graph has unconnected subgraphs then their order in the list is undefined).
while (!empty($unmarkedNodes)) {
$node = key($unmarkedNodes);
unset($unmarkedNodes[$node]);
$visit($node);
}

return $result;
}

private static function allNodes(array $deps)
{
$allNodes = [];
foreach ($deps as $node => $others) {
if (!is_array($others)) {
throw new \InvalidArgumentException('Dependencies should be given as arrays, not single elements.');
}
$allNodes[] = $node;
foreach ($others as $other) {
$allNodes[] = $other;
}
}

return array_unique($allNodes);
}
}
95 changes: 95 additions & 0 deletions tests/Xi/Tests/Algorithm/TopologicalSortTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php
namespace Xi\Algorithm;

class TopologicalSortTest extends \PHPUnit_Framework_TestCase
{
/**
* @test
*/
public function simpleCase()
{
$edges = [
'B' => ['C', 'D'],
'A' => ['B'],
'C' => ['D'],
];

for ($i = 0; $i < 100; ++$i) {
$edges = self::shuffleGraphSpec($edges);
$this->assertEquals(['D', 'C', 'B', 'A'], TopologicalSort::apply($edges));
}
}

/**
* @test
* @dataProvider provider
*/
public function test(array $edges)
{
for ($i = 0; $i < 100; ++$i) {
$edges = self::shuffleGraphSpec($edges);
$sorted = TopologicalSort::apply($edges);
$this->verify($sorted, $edges);
}
}

public function provider()
{
return [
[[
'B' => ['C', 'D'],
'A' => ['B'],
'C' => ['D'],
]],

// Wikipedia's example
[[
7 => [11, 8],
5 => [11],
3 => [8, 10],
11 => [2, 9, 10],
8 => [9]
]]
];
}

/**
* @test
* @expectedException \InvalidArgumentException
*/
public function throwsOnCycle()
{
$edges = [
'B' => ['C', 'D'],
'A' => ['B'],
'C' => ['D'],
'D' => ['A']
];

TopologicalSort::apply($edges);
}

private function shuffleGraphSpec(array $array)
{
$keys = array_keys($array);
shuffle($keys);
$result = [];
foreach ($keys as $key) {
$result[$key] = $array[$key];
shuffle($result[$key]);
}

return $result;
}

private function verify(array $sorted, array $edges)
{
foreach ($edges as $left => $rights) {
foreach ($rights as $right) {
$leftIndex = array_search($left, $sorted);
$rightIndex = array_search($right, $sorted);
$this->assertLessThan($leftIndex, $rightIndex);
}
}
}
}
2 changes: 1 addition & 1 deletion tests/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
wget http://getcomposer.org/composer.phar
php composer.phar install --dev
");
}
}

0 comments on commit 2500cbe

Please sign in to comment.