# frozen_string_literal: true # Released under the MIT License. # Copyright, 2021, by Wander Hillen. # Copyright, 2021-2022, by Samuel Williams. module Timers # A priority queue implementation using a standard binary minheap. It uses straight comparison # of its contents to determine priority. This works because a Handle from Timers::Events implements # the '<' operation by comparing the expiry time. # See for explanations of the main methods. class PriorityHeap def initialize # The heap is represented with an array containing a binary tree. See # https://en.wikipedia.org/wiki/Binary_heap#Heap_implementation for how this array # is built up. @contents = [] end # Returns the earliest timer or nil if the heap is empty. def peek @contents[0] end # Returns the number of elements in the heap def size @contents.size end # Returns the earliest timer if the heap is non-empty and removes it from the heap. # Returns nil if the heap is empty. (and doesn't change the heap in that case) def pop # If the heap is empty: if @contents.empty? return nil end # If we have only one item, no swapping is required: if @contents.size == 1 return @contents.pop end # Take the root of the tree: value = @contents[0] # Remove the last item in the tree: last = @contents.pop # Overwrite the root of the tree with the item: @contents[0] = last # Bubble it down into place: bubble_down(0) # validate! return value end # Inserts a new timer into the heap, then rearranges elements until the heap invariant is true again. def push(element) # Insert the item at the end of the heap: @contents.push(element) # Bubble it up into position: bubble_up(@contents.size - 1) # validate! return self end # Empties out the heap, discarding all elements def clear! @contents = [] end # Validate the heap invariant. Every element except the root must not be smaller than # its parent element. Note that it MAY be equal. def valid? # notice we skip index 0 on purpose, because it has no parent (1..(@contents.size - 1)).all? { |e| @contents[e] >= @contents[(e - 1) / 2] } end private def swap(i, j) @contents[i], @contents[j] = @contents[j], @contents[i] end def bubble_up(index) parent_index = (index - 1) / 2 # watch out, integer division! while index > 0 && @contents[index] < @contents[parent_index] # if the node has a smaller value than its parent, swap these nodes # to uphold the minheap invariant and update the index of the 'current' # node. If the node is already at index 0, we can also stop because that # is the root of the heap. # swap(index, parent_index) @contents[index], @contents[parent_index] = @contents[parent_index], @contents[index] index = parent_index parent_index = (index - 1) / 2 # watch out, integer division! end end def bubble_down(index) swap_value = 0 swap_index = nil while true left_index = (2 * index) + 1 left_value = @contents[left_index] if left_value.nil? # This node has no children so it can't bubble down any further. # We're done here! return end # Determine which of the child nodes has the smallest value: right_index = left_index + 1 right_value = @contents[right_index] if right_value.nil? or right_value > left_value swap_value = left_value swap_index = left_index else swap_value = right_value swap_index = right_index end if @contents[index] < swap_value # No need to swap, the minheap invariant is already satisfied: return else # At least one of the child node has a smaller value than the current node, swap current node with that child and update current node for if it might need to bubble down even further: # swap(index, swap_index) @contents[index], @contents[swap_index] = @contents[swap_index], @contents[index] index = swap_index end end end end end