Min-Max Heap

G
using System;
using System.Collections.Generic;
using System.Linq;

namespace DataStructures.Heap;

/// <summary>
///     This class implements min-max heap.
///     It provides functionality of both min-heap and max-heap with the same time complexity.
///     Therefore it provides constant time retrieval and logarithmic time removal
///     of both the minimum and maximum elements in it.
/// </summary>
/// <typeparam name="T">Generic type.</typeparam>
public class MinMaxHeap<T>
{
    private readonly List<T> heap;

    /// <summary>
    ///     Initializes a new instance of the <see cref="MinMaxHeap{T}" /> class that contains
    ///     elements copied from a specified enumerable collection and that uses a specified comparer.
    /// </summary>
    /// <param name="collection">The enumerable collection to be copied.</param>
    /// <param name="comparer">The default comparer to use for comparing objects.</param>
    public MinMaxHeap(IEnumerable<T>? collection = null, IComparer<T>? comparer = null)
    {
        Comparer = comparer ?? Comparer<T>.Default;
        collection ??= Enumerable.Empty<T>();

        heap = collection.ToList();
        for (var i = Count / 2 - 1; i >= 0; --i)
        {
            PushDown(i);
        }
    }

    /// <summary>
    ///     Gets the  <see cref="IComparer{T}" />. object that is used to order the values in the <see cref="MinMaxHeap{T}" />.
    /// </summary>
    public IComparer<T> Comparer { get; }

    /// <summary>
    ///     Gets the number of elements in the <see cref="MinMaxHeap{T}" />.
    /// </summary>
    public int Count => heap.Count;

    /// <summary>
    ///     Adds an element to the heap.
    /// </summary>
    /// <param name="item">The element to add to the heap.</param>
    public void Add(T item)
    {
        heap.Add(item);
        PushUp(Count - 1);
    }

    /// <summary>
    ///     Removes the maximum node from the heap and returns its value.
    /// </summary>
    /// <exception cref="InvalidOperationException">Thrown if heap is empty.</exception>
    /// <returns>Value of the removed maximum node.</returns>
    public T ExtractMax()
    {
        if (Count == 0)
        {
            throw new InvalidOperationException("Heap is empty");
        }

        var max = GetMax();
        RemoveNode(GetMaxNodeIndex());
        return max;
    }

    /// <summary>
    ///     Removes the minimum node from the heap and returns its value.
    /// </summary>
    /// <exception cref="InvalidOperationException">Thrown if heap is empty.</exception>
    /// <returns>Value of the removed minimum node.</returns>
    public T ExtractMin()
    {
        if (Count == 0)
        {
            throw new InvalidOperationException("Heap is empty");
        }

        var min = GetMin();
        RemoveNode(0);
        return min;
    }

    /// <summary>
    ///     Gets the maximum value in the heap, as defined by the comparer.
    /// </summary>
    /// <exception cref="InvalidOperationException">Thrown if heap is empty.</exception>
    /// <returns>The maximum value in the heap.</returns>
    public T GetMax()
    {
        if (Count == 0)
        {
            throw new InvalidOperationException("Heap is empty");
        }

        return heap[GetMaxNodeIndex()];
    }

    /// <summary>
    ///     Gets the minimum value in the heap, as defined by the comparer.
    /// </summary>
    /// <exception cref="InvalidOperationException">Thrown if heap is empty.</exception>
    /// <returns>The minimum value in the heap.</returns>
    public T GetMin()
    {
        if (Count == 0)
        {
            throw new InvalidOperationException("Heap is empty");
        }

        return heap[0];
    }

    /// <summary>
    ///     Finds maximum value among children and grandchildren of the specified node.
    /// </summary>
    /// <param name="index">Index of the node in the Heap array.</param>
    /// <returns>Index of the maximum descendant.</returns>
    private int IndexOfMaxChildOrGrandchild(int index)
    {
        var descendants = new[]
        {
            2 * index + 1,
            2 * index + 2,
            4 * index + 3,
            4 * index + 4,
            4 * index + 5,
            4 * index + 6,
        };
        var resIndex = descendants[0];
        foreach (var descendant in descendants)
        {
            if (descendant >= Count)
            {
                break;
            }

            if (Comparer.Compare(heap[descendant], heap[resIndex]) > 0)
            {
                resIndex = descendant;
            }
        }

        return resIndex;
    }

    /// <summary>
    ///     Finds minumum value among children and grandchildren of the specified node.
    /// </summary>
    /// <param name="index">Index of the node in the Heap array.</param>
    /// <returns>Index of the minimum descendant.</returns>
    private int IndexOfMinChildOrGrandchild(int index)
    {
        var descendants = new[] { 2 * index + 1, 2 * index + 2, 4 * index + 3, 4 * index + 4, 4 * index + 5, 4 * index + 6 };
        var resIndex = descendants[0];
        foreach (var descendant in descendants)
        {
            if (descendant >= Count)
            {
                break;
            }

            if (Comparer.Compare(heap[descendant], heap[resIndex]) < 0)
            {
                resIndex = descendant;
            }
        }

        return resIndex;
    }

    private int GetMaxNodeIndex()
    {
        return Count switch
        {
            0 => throw new InvalidOperationException("Heap is empty"),
            1 => 0,
            2 => 1,
            _ => Comparer.Compare(heap[1], heap[2]) > 0 ? 1 : 2,
        };
    }

    private bool HasChild(int index) => index * 2 + 1 < Count;

    private bool IsGrandchild(int node, int grandchild) => grandchild > 2 && Grandparent(grandchild) == node;

    /// <summary>
    ///     Checks if node at index belongs to Min or Max level of the heap.
    ///     Root node belongs to Min level, its children - Max level,
    ///     its grandchildren - Min level, and so on.
    /// </summary>
    /// <param name="index">Index to check.</param>
    /// <returns>true if index is at Min level; false if it is at Max Level.</returns>
    private bool IsMinLevelIndex(int index)
    {
        // For all Min levels, value (index + 1) has the leftmost bit set to '1' at even position.
        const uint minLevelsBits = 0x55555555;
        const uint maxLevelsBits = 0xAAAAAAAA;
        return ((index + 1) & minLevelsBits) > ((index + 1) & maxLevelsBits);
    }

    private int Parent(int index) => (index - 1) / 2;

    private int Grandparent(int index) => ((index - 1) / 2 - 1) / 2;

    /// <summary>
    ///     Assuming that children sub-trees are valid heaps, pushes node to lower levels
    ///     to make heap valid.
    /// </summary>
    /// <param name="index">Node index.</param>
    private void PushDown(int index)
    {
        if (IsMinLevelIndex(index))
        {
            PushDownMin(index);
        }
        else
        {
            PushDownMax(index);
        }
    }

    private void PushDownMax(int index)
    {
        if (!HasChild(index))
        {
            return;
        }

        var maxIndex = IndexOfMaxChildOrGrandchild(index);

        // If smaller element are put at min level (as result of swaping), it doesn't affect sub-tree validity.
        // If smaller element are put at max level, PushDownMax() should be called for that node.
        if (IsGrandchild(index, maxIndex))
        {
            if (Comparer.Compare(heap[maxIndex], heap[index]) > 0)
            {
                SwapNodes(maxIndex, index);
                if (Comparer.Compare(heap[maxIndex], heap[Parent(maxIndex)]) < 0)
                {
                    SwapNodes(maxIndex, Parent(maxIndex));
                }

                PushDownMax(maxIndex);
            }
        }
        else
        {
            if (Comparer.Compare(heap[maxIndex], heap[index]) > 0)
            {
                SwapNodes(maxIndex, index);
            }
        }
    }

    private void PushDownMin(int index)
    {
        if (!HasChild(index))
        {
            return;
        }

        var minIndex = IndexOfMinChildOrGrandchild(index);

        // If bigger element are put at max level (as result of swaping), it doesn't affect sub-tree validity.
        // If bigger element are put at min level, PushDownMin() should be called for that node.
        if (IsGrandchild(index, minIndex))
        {
            if (Comparer.Compare(heap[minIndex], heap[index]) < 0)
            {
                SwapNodes(minIndex, index);
                if (Comparer.Compare(heap[minIndex], heap[Parent(minIndex)]) > 0)
                {
                    SwapNodes(minIndex, Parent(minIndex));
                }

                PushDownMin(minIndex);
            }
        }
        else
        {
            if (Comparer.Compare(heap[minIndex], heap[index]) < 0)
            {
                SwapNodes(minIndex, index);
            }
        }
    }

    /// <summary>
    ///     Having a new node in the heap, swaps this node with its ancestors to make heap valid.
    ///     For node at min level. If new node is less than its parent, then it is surely less then
    ///     all other nodes on max levels on path to the root of the heap. So node are pushed up, by
    ///     swaping with its grandparent, until they are ordered correctly.
    ///     For node at max level algorithm is analogical.
    /// </summary>
    /// <param name="index">Index of the new node.</param>
    private void PushUp(int index)
    {
        if (index == 0)
        {
            return;
        }

        var parent = Parent(index);

        if (IsMinLevelIndex(index))
        {
            if (Comparer.Compare(heap[index], heap[parent]) > 0)
            {
                SwapNodes(index, parent);
                PushUpMax(parent);
            }
            else
            {
                PushUpMin(index);
            }
        }
        else
        {
            if (Comparer.Compare(heap[index], heap[parent]) < 0)
            {
                SwapNodes(index, parent);
                PushUpMin(parent);
            }
            else
            {
                PushUpMax(index);
            }
        }
    }

    private void PushUpMax(int index)
    {
        if (index > 2)
        {
            var grandparent = Grandparent(index);
            if (Comparer.Compare(heap[index], heap[grandparent]) > 0)
            {
                SwapNodes(index, grandparent);
                PushUpMax(grandparent);
            }
        }
    }

    private void PushUpMin(int index)
    {
        if (index > 2)
        {
            var grandparent = Grandparent(index);
            if (Comparer.Compare(heap[index], heap[grandparent]) < 0)
            {
                SwapNodes(index, grandparent);
                PushUpMin(grandparent);
            }
        }
    }

    private void RemoveNode(int index)
    {
        SwapNodes(index, Count - 1);
        heap.RemoveAt(Count - 1);
        if (Count != 0)
        {
            PushDown(index);
        }
    }

    private void SwapNodes(int i, int j)
    {
        var temp = heap[i];
        heap[i] = heap[j];
        heap[j] = temp;
    }
}