Skip to content

Commit 5675528

Browse files
committed
Merge pull request #7 from xi-project/topological-sort
Topological sort
2 parents 8428daa + c569080 commit 5675528

File tree

7 files changed

+190
-2
lines changed

7 files changed

+190
-2
lines changed

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
composer.phar
22
composer.lock
3-
tests/phpunit.xml
43
vendor
54
.idea

composer.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,19 @@
1515
"name": "Joonas Pajunen",
1616
"email": "[email protected]",
1717
"role": "Developer"
18+
},
19+
{
20+
"name": "Martin Pärtel",
21+
"email": "[email protected]",
22+
"role": "Developer"
1823
}
1924
],
2025
"require": {
2126
"php": ">=5.3.3"
2227
},
28+
"require-dev": {
29+
"phpunit/phpunit": "3.7.*"
30+
},
2331
"autoload": {
2432
"psr-0": {
2533
"Xi\\Algorithm": "library/"
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
namespace Xi\Algorithm;
3+
4+
/**
5+
* Implements topological sorting.
6+
*
7+
* Topological sorting means sorting the nodes in a directed acyclic graph into a list such
8+
* that if a node X points to a node Y then Y appears before X in the list.
9+
* This is useful for things like resolving dependencies.
10+
*
11+
* http://en.wikipedia.org/wiki/Topological_sorting
12+
*/
13+
class TopologicalSort
14+
{
15+
/**
16+
* @param array $edges An array of scalars to arrays of scalars, representing the edges in the graph.
17+
* @return array An array of nodes in the graph.
18+
* @throws \InvalidArgumentException if the graph has a cycle or $deps is in an invalid format
19+
*/
20+
public static function apply(array $edges)
21+
{
22+
$allNodes = self::allNodes($edges);
23+
24+
// We do a depth-first search.
25+
// A node is marked with 1 when first seen and with 2 when recursion returns from it.
26+
// If we meet a node marked 0 then we recursively add its descendants to the list before adding it to the list.
27+
// If we meet a node marked 1 then we've found a cycle, which is an error.
28+
// If we meet a node marked 2 then it's already on the list and we return.
29+
$unmarkedNodes = array_fill_keys($allNodes, null);
30+
$marks = array_fill_keys($allNodes, 0);
31+
$result = array();
32+
33+
$visit = function ($node) use ($edges, &$visit, &$marks, &$unmarkedNodes, &$result) {
34+
$mark = $marks[$node];
35+
if ($mark === 0) {
36+
$marks[$node] = 1;
37+
unset($unmarkedNodes[$node]);
38+
39+
foreach ((isset($edges[$node]) ? $edges[$node] : array()) as $next) {
40+
$visit($next);
41+
}
42+
43+
$marks[$node] = 2;
44+
$result[] = $node;
45+
} elseif ($mark === 1) {
46+
throw new \InvalidArgumentException("The graph has a cycle involving node $node");
47+
}
48+
};
49+
50+
// We try each node as a starting point since we don't know which node (if any) is the root node.
51+
// (if the graph has unconnected subgraphs then their order in the list is undefined).
52+
while (!empty($unmarkedNodes)) {
53+
$node = key($unmarkedNodes);
54+
unset($unmarkedNodes[$node]);
55+
$visit($node);
56+
}
57+
58+
return $result;
59+
}
60+
61+
private static function allNodes(array $deps)
62+
{
63+
$allNodes = array();
64+
foreach ($deps as $node => $others) {
65+
if (!is_array($others)) {
66+
throw new \InvalidArgumentException('Dependencies should be given as arrays, not single elements.');
67+
}
68+
$allNodes[] = $node;
69+
foreach ($others as $other) {
70+
$allNodes[] = $other;
71+
}
72+
}
73+
74+
return array_unique($allNodes);
75+
}
76+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<?php
2+
namespace Xi\Algorithm;
3+
4+
class TopologicalSortTest extends \PHPUnit_Framework_TestCase
5+
{
6+
/**
7+
* @test
8+
*/
9+
public function simpleCase()
10+
{
11+
$edges = array(
12+
'B' => array('C', 'D'),
13+
'A' => array('B'),
14+
'C' => array('D'),
15+
);
16+
17+
for ($i = 0; $i < 100; ++$i) {
18+
$edges = self::shuffleGraphSpec($edges);
19+
$this->assertEquals(array('D', 'C', 'B', 'A'), TopologicalSort::apply($edges));
20+
}
21+
}
22+
23+
/**
24+
* @test
25+
* @dataProvider provider
26+
*/
27+
public function test(array $edges)
28+
{
29+
for ($i = 0; $i < 100; ++$i) {
30+
$edges = self::shuffleGraphSpec($edges);
31+
$sorted = TopologicalSort::apply($edges);
32+
$this->verify($sorted, $edges);
33+
}
34+
}
35+
36+
public function provider()
37+
{
38+
return array(
39+
array(array(
40+
'B' => array('C', 'D'),
41+
'A' => array('B'),
42+
'C' => array('D'),
43+
)),
44+
45+
// Wikipedia's example
46+
array(array(
47+
7 => array(11, 8),
48+
5 => array(11),
49+
3 => array(8, 10),
50+
11 => array(2, 9, 10),
51+
8 => array(9)
52+
))
53+
);
54+
}
55+
56+
/**
57+
* @test
58+
* @expectedException \InvalidArgumentException
59+
*/
60+
public function throwsOnCycle()
61+
{
62+
$edges = array(
63+
'B' => array('C', 'D'),
64+
'A' => array('B'),
65+
'C' => array('D'),
66+
'D' => array('A')
67+
);
68+
69+
TopologicalSort::apply($edges);
70+
}
71+
72+
private function shuffleGraphSpec(array $array)
73+
{
74+
$keys = array_keys($array);
75+
shuffle($keys);
76+
$result = array();
77+
foreach ($keys as $key) {
78+
$result[$key] = $array[$key];
79+
shuffle($result[$key]);
80+
}
81+
82+
return $result;
83+
}
84+
85+
private function verify(array $sorted, array $edges)
86+
{
87+
foreach ($edges as $left => $rights) {
88+
foreach ($rights as $right) {
89+
$leftIndex = array_search($left, $sorted);
90+
$rightIndex = array_search($right, $sorted);
91+
$this->assertLessThan($leftIndex, $rightIndex);
92+
}
93+
}
94+
}
95+
}

tests/bootstrap.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@
55
wget http://getcomposer.org/composer.phar
66
php composer.phar install --dev
77
");
8-
}
8+
}
File renamed without changes.

tests/watchr

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#!/usr/bin/env watchr
2+
def run_phpunit
3+
system('echo; echo; echo; echo; cd tests/ ; ../vendor/bin/phpunit .')
4+
end
5+
6+
Dir.chdir(File.dirname(File.dirname(__FILE__)))
7+
8+
watch('.*') { run_phpunit }
9+
10+
run_phpunit

0 commit comments

Comments
 (0)