Вступление
Связанные списки — это одна очень распространенная и удобная структура данных, которая может использоваться во многих случаях практического программирования. В этом посте мы предполагаем, что речь идет об односвязном списке. Это означает, что каждый элемент указывает на свой предыдущий элемент и, в свою очередь, указывает на следующий элемент. В этом сценарии первый элемент списка, его голова, не имеет предка, а последний элемент не имеет преемника.
Иногда из-за ошибок, плохой архитектуры или сложности приложений у нас могут возникнуть проблемы со списками. Одной из типичных проблем является наличие цикла, который вкратце означает, что некоторые элементы списка указываются дважды, как показано на рисунке ниже.
Итак, в первую очередь мы должны быть уверены, что есть петля. Тогда мы должны спросить: как мы можем сломать это?
Есть несколько алгоритмов нахождения цикла, но вот очень простой. Он известен как алгоритм Флойда или «алгоритм черепахи и зайца».
обзор
Алгоритм Флойда опирается на одну очень простую идею. Первоначально мы установили два указателя на заголовок списка.
На каждом шаге мы увеличиваем оба указателя. Но заяц увеличивается на два элемента, в то время как черепаха ходит по каждому предмету, как показано на изображениях ниже.
Черепаха, как видно из рисунка ниже, намного медленнее.
В результате, если у нас нет петли в списке, заяц попадет в конец списка; но если у нас есть петля, заяц поймает черепаху внутри петли.
Код
Предполагая типичное представление односвязного списка, вот пример кода на PHP.
Класс предмета
class Item { protected $_name = ''; protected $_key = ''; protected $_next = null; public function __construct($key, $name) { $this->_key = $key; $this->_name = $name; } public function setNext(&$next) { $this->_next = $next; } public function &getNext() { return $this->_next; } public function setKey($key) { $this->_key = $key; } public function getKey() { return $this->_key; } public function setName($name) { $this->_name = $name; } public function getName() { return $this->_name; } public function __toString() { return $this->_key . ' ' . $this->_name . "\n"; } }
Класс списка
class Linked_List { protected $_head = null; public function insert($item) { if ($this->_head == null) { $this->_head = $item; return; } $current = $this->_head; while ($current->getNext()) { $current = $current->getNext(); } $current->setNext($item); } public function detectLoop() { $tortoise = $hare = $this->_head; if ($hare->getNext() != null) { $hare = $hare->getNext()->getNext(); } else { return FALSE; } while ($hare) { if ($hare == $tortoise) { return $hare; } if ($hare->getNext()) { $hare = $hare->getNext()->getNext(); } else { return FALSE; } $tortoise = $tortoise->getNext(); } return FALSE; } protected function _isReachable($a, $b) { $current = $b; while ($current->getNext() != $b) { if ($current->getNext() == $a) { return $current; } $current = $current->getNext(); } return FALSE; } public function breakLoop() { if (FALSE === ($loop = $this->detectLoop())) { return; } $startOfTheLoop = $this->_head; while (FALSE === ($lastLoopItem = $this->_isReachable($startOfTheLoop, $loop))) { $startOfTheLoop = $startOfTheLoop->getNext(); } $pseudoNext = null; $lastLoopItem->setNext($pseudoNext); } public function __toString() { $current = $this->_head; $output = ''; while ($current) { $output .= $current->getKey() . ' ' . $current->getName() . "\n"; $current = $current->getNext(); } return $output; } }
инициализация
$items = array();
$ll = new Linked_List();
for ($i = 0; $i < 100; $i++) {
$items[$i] = new Item($i, 'Item #' . $i);
$ll->insert($items[$i]);
}
// 0 Item #0
// 1 Item #1
// ...
// 99 Item #99
echo $ll;
$items = array(); $ll = new Linked_List(); for ($i = 0; $i < 100; $i++) { $items[$i] = new Item($i, 'Item #' . $i); $ll->insert($items[$i]); } // 0 Item #0 // 1 Item #1 // ... // 99 Item #99 echo $ll;
Разорвать петлю
Чтобы разорвать цикл, нам нужно сначала проверить, доступен ли элемент из цикла. Как только мы узнаем это, мы должны вернуть последний элемент цикла, чтобы улучшить производительность следующего метода — breakLoop. Это делается с помощью следующего метода:
protected function _isReachable($a, $b) { $current = $b; while ($current->getNext() != $b) { if ($current->getNext() == $a) { return $current; } $current = $current->getNext(); } return FALSE; }
Теперь мы можем разорвать цикл, удалив двойную ссылку на первый элемент цикла.
// member of Linked_List public function breakLoop() { if (FALSE === ($loop = $this->detectLoop())) { return; } $startOfTheLoop = $this->_head; while (FALSE === ($lastLoopItem = $this->_isReachable($startOfTheLoop, $loop))) { $startOfTheLoop = $startOfTheLoop->getNext(); } $pseudoNext = null; $lastLoopItem->setNext($pseudoNext); }
сложность
Предполагая, что длина списка равна N, а длина цикла равна M, мы разорвем цикл в O ((NM) * M) по сложности времени только после того, как мы нашли цикл. Вопрос в том, как быстро мы можем найти цикл с алгоритмом выше? Хорошо, заяц довольно быстр и может проходить через некоторые элементы петли несколько раз, но черепаха последовательно проходит по всем элементам. Вопрос в том, догонит ли заяц черепаху до того, как последний доберется до конца цикла? Ответ — да, и мы можем думать об одном связанном списке без петель, где два указателя начинаются с начала списка. Тот же вопрос можно перевести на: достигнет ли более быстрый указатель конца до того, как более медленный достигнет середины списка? Ну да.
Наконец, ответом O (N) является ответ на вопрос, существует ли петля, и O (M * (NM)) прерывает ее.
Однако, хорошая новость заключается в том, что когда мы работаем со связанными списками, мы часто работаем только с указателями (как в нашем случае в этом посте). Таким образом, нам не нужна дополнительная память, что делает их очень полезными. На самом деле, мы увидим то же преимущество в следующем алгоритме — реверсирование связанного списка (более эффективное, чем реверсирование массива).