-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
177 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,6 +15,11 @@ | |
"name": "Joonas Pajunen", | ||
"email": "[email protected]", | ||
"role": "Developer" | ||
}, | ||
{ | ||
"name": "Martin Pärtel", | ||
"email": "[email protected]", | ||
"role": "Developer" | ||
} | ||
], | ||
"require": { | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,4 +5,4 @@ | |
wget http://getcomposer.org/composer.phar | ||
php composer.phar install --dev | ||
"); | ||
} | ||
} |