PriorityQueue.php 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. <?php
  2. declare(strict_types=1);
  3. namespace Laminas\Stdlib;
  4. use Countable;
  5. use IteratorAggregate;
  6. use ReturnTypeWillChange;
  7. use Serializable;
  8. use UnexpectedValueException;
  9. use function array_map;
  10. use function count;
  11. use function is_array;
  12. use function serialize;
  13. use function sprintf;
  14. use function unserialize;
  15. /**
  16. * Re-usable, serializable priority queue implementation
  17. *
  18. * SplPriorityQueue acts as a heap; on iteration, each item is removed from the
  19. * queue. If you wish to re-use such a queue, you need to clone it first. This
  20. * makes for some interesting issues if you wish to delete items from the queue,
  21. * or, as already stated, iterate over it multiple times.
  22. *
  23. * This class aggregates items for the queue itself, but also composes an
  24. * "inner" iterator in the form of an SplPriorityQueue object for performing
  25. * the actual iteration.
  26. *
  27. * @template TValue
  28. * @template TPriority of int
  29. * @implements IteratorAggregate<array-key, TValue>
  30. */
  31. class PriorityQueue implements Countable, IteratorAggregate, Serializable
  32. {
  33. public const EXTR_DATA = 0x00000001;
  34. public const EXTR_PRIORITY = 0x00000002;
  35. public const EXTR_BOTH = 0x00000003;
  36. /**
  37. * Inner queue class to use for iteration
  38. *
  39. * @var class-string<\SplPriorityQueue>
  40. */
  41. protected $queueClass = SplPriorityQueue::class;
  42. /**
  43. * Actual items aggregated in the priority queue. Each item is an array
  44. * with keys "data" and "priority".
  45. *
  46. * @var list<array{data: TValue, priority: TPriority}>
  47. */
  48. protected $items = [];
  49. /**
  50. * Inner queue object
  51. *
  52. * @var \SplPriorityQueue<TPriority, TValue>|null
  53. */
  54. protected $queue;
  55. /**
  56. * Insert an item into the queue
  57. *
  58. * Priority defaults to 1 (low priority) if none provided.
  59. *
  60. * @param TValue $data
  61. * @param TPriority $priority
  62. * @return $this
  63. */
  64. public function insert($data, $priority = 1)
  65. {
  66. /** @psalm-var TPriority $priority */
  67. $priority = (int) $priority;
  68. $this->items[] = [
  69. 'data' => $data,
  70. 'priority' => $priority,
  71. ];
  72. $this->getQueue()->insert($data, $priority);
  73. return $this;
  74. }
  75. /**
  76. * Remove an item from the queue
  77. *
  78. * This is different than {@link extract()}; its purpose is to dequeue an
  79. * item.
  80. *
  81. * This operation is potentially expensive, as it requires
  82. * re-initialization and re-population of the inner queue.
  83. *
  84. * Note: this removes the first item matching the provided item found. If
  85. * the same item has been added multiple times, it will not remove other
  86. * instances.
  87. *
  88. * @return bool False if the item was not found, true otherwise.
  89. */
  90. public function remove(mixed $datum)
  91. {
  92. $found = false;
  93. $key = null;
  94. foreach ($this->items as $key => $item) {
  95. if ($item['data'] === $datum) {
  96. $found = true;
  97. break;
  98. }
  99. }
  100. if ($found && $key !== null) {
  101. unset($this->items[$key]);
  102. $this->queue = null;
  103. if (! $this->isEmpty()) {
  104. $queue = $this->getQueue();
  105. foreach ($this->items as $item) {
  106. $queue->insert($item['data'], $item['priority']);
  107. }
  108. }
  109. return true;
  110. }
  111. return false;
  112. }
  113. /**
  114. * Is the queue empty?
  115. *
  116. * @return bool
  117. */
  118. public function isEmpty()
  119. {
  120. return 0 === $this->count();
  121. }
  122. /**
  123. * How many items are in the queue?
  124. *
  125. * @return int
  126. */
  127. #[ReturnTypeWillChange]
  128. public function count()
  129. {
  130. return count($this->items);
  131. }
  132. /**
  133. * Peek at the top node in the queue, based on priority.
  134. *
  135. * @return TValue
  136. */
  137. public function top()
  138. {
  139. $queue = clone $this->getQueue();
  140. return $queue->top();
  141. }
  142. /**
  143. * Extract a node from the inner queue and sift up
  144. *
  145. * @return TValue
  146. */
  147. public function extract()
  148. {
  149. $value = $this->getQueue()->extract();
  150. $keyToRemove = null;
  151. $highestPriority = null;
  152. foreach ($this->items as $key => $item) {
  153. if ($item['data'] !== $value) {
  154. continue;
  155. }
  156. if (null === $highestPriority) {
  157. $highestPriority = $item['priority'];
  158. $keyToRemove = $key;
  159. continue;
  160. }
  161. if ($highestPriority >= $item['priority']) {
  162. continue;
  163. }
  164. $highestPriority = $item['priority'];
  165. $keyToRemove = $key;
  166. }
  167. if ($keyToRemove !== null) {
  168. unset($this->items[$keyToRemove]);
  169. }
  170. return $value;
  171. }
  172. /**
  173. * Retrieve the inner iterator
  174. *
  175. * SplPriorityQueue acts as a heap, which typically implies that as items
  176. * are iterated, they are also removed. This does not work for situations
  177. * where the queue may be iterated multiple times. As such, this class
  178. * aggregates the values, and also injects an SplPriorityQueue. This method
  179. * retrieves the inner queue object, and clones it for purposes of
  180. * iteration.
  181. *
  182. * @return \SplPriorityQueue<TPriority, TValue>
  183. */
  184. #[ReturnTypeWillChange]
  185. public function getIterator()
  186. {
  187. $queue = $this->getQueue();
  188. return clone $queue;
  189. }
  190. /**
  191. * Serialize the data structure
  192. *
  193. * @return string
  194. */
  195. public function serialize()
  196. {
  197. return serialize($this->__serialize());
  198. }
  199. /**
  200. * Magic method used for serializing of an instance.
  201. *
  202. * @return list<array{data: TValue, priority: TPriority}>
  203. */
  204. public function __serialize()
  205. {
  206. return $this->items;
  207. }
  208. /**
  209. * Unserialize a string into a PriorityQueue object
  210. *
  211. * Serialization format is compatible with {@link SplPriorityQueue}
  212. *
  213. * @param string $data
  214. * @return void
  215. */
  216. public function unserialize($data)
  217. {
  218. $toUnserialize = unserialize($data);
  219. if (! is_array($toUnserialize)) {
  220. throw new UnexpectedValueException(sprintf(
  221. 'Cannot deserialize %s instance; corrupt serialization data',
  222. self::class
  223. ));
  224. }
  225. /** @psalm-var list<array{data: TValue, priority: TPriority}> $toUnserialize */
  226. $this->__unserialize($toUnserialize);
  227. }
  228. /**
  229. * Magic method used to rebuild an instance.
  230. *
  231. * @param list<array{data: TValue, priority: TPriority}> $data Data array.
  232. * @return void
  233. */
  234. public function __unserialize($data)
  235. {
  236. foreach ($data as $item) {
  237. $this->insert($item['data'], $item['priority']);
  238. }
  239. }
  240. /**
  241. * Serialize to an array
  242. * By default, returns only the item data, and in the order registered (not
  243. * sorted). You may provide one of the EXTR_* flags as an argument, allowing
  244. * the ability to return priorities or both data and priority.
  245. *
  246. * @param int $flag
  247. * @return array<array-key, mixed>
  248. * @psalm-return ($flag is self::EXTR_BOTH
  249. * ? list<array{data: TValue, priority: TPriority}>
  250. * : $flag is self::EXTR_PRIORITY
  251. * ? list<TPriority>
  252. * : list<TValue>
  253. * )
  254. */
  255. public function toArray($flag = self::EXTR_DATA)
  256. {
  257. return match ($flag) {
  258. self::EXTR_BOTH => $this->items,
  259. self::EXTR_PRIORITY => array_map(static fn($item): int => $item['priority'], $this->items),
  260. default => array_map(static fn($item): mixed => $item['data'], $this->items),
  261. };
  262. }
  263. /**
  264. * Specify the internal queue class
  265. *
  266. * Please see {@link getIterator()} for details on the necessity of an
  267. * internal queue class. The class provided should extend SplPriorityQueue.
  268. *
  269. * @param class-string<\SplPriorityQueue> $class
  270. * @return $this
  271. */
  272. public function setInternalQueueClass($class)
  273. {
  274. /** @psalm-suppress RedundantCastGivenDocblockType */
  275. $this->queueClass = (string) $class;
  276. return $this;
  277. }
  278. /**
  279. * Does the queue contain the given datum?
  280. *
  281. * @param TValue $datum
  282. * @return bool
  283. */
  284. public function contains($datum)
  285. {
  286. foreach ($this->items as $item) {
  287. if ($item['data'] === $datum) {
  288. return true;
  289. }
  290. }
  291. return false;
  292. }
  293. /**
  294. * Does the queue have an item with the given priority?
  295. *
  296. * @param TPriority $priority
  297. * @return bool
  298. */
  299. public function hasPriority($priority)
  300. {
  301. foreach ($this->items as $item) {
  302. if ($item['priority'] === $priority) {
  303. return true;
  304. }
  305. }
  306. return false;
  307. }
  308. /**
  309. * Get the inner priority queue instance
  310. *
  311. * @throws Exception\DomainException
  312. * @return \SplPriorityQueue<TPriority, TValue>
  313. * @psalm-assert !null $this->queue
  314. */
  315. protected function getQueue()
  316. {
  317. if (null === $this->queue) {
  318. /** @psalm-suppress UnsafeInstantiation */
  319. $queue = new $this->queueClass();
  320. /** @psalm-var \SplPriorityQueue<TPriority, TValue> $queue */
  321. $this->queue = $queue;
  322. /** @psalm-suppress DocblockTypeContradiction */
  323. if (! $this->queue instanceof \SplPriorityQueue) {
  324. throw new Exception\DomainException(sprintf(
  325. 'PriorityQueue expects an internal queue of type SplPriorityQueue; received "%s"',
  326. $queue::class
  327. ));
  328. }
  329. }
  330. return $this->queue;
  331. }
  332. /**
  333. * Add support for deep cloning
  334. *
  335. * @return void
  336. */
  337. public function __clone()
  338. {
  339. if (null !== $this->queue) {
  340. $this->queue = clone $this->queue;
  341. }
  342. }
  343. }