thi-ng/umbrella

View on GitHub
packages/heaps/src/heap.ts

Summary

Maintainability
A
35 mins
Test Coverage
import type {
    Comparator,
    IClear,
    ICopy,
    IEmpty,
    IInto,
    ILength,
    IStack,
    Maybe,
    Predicate,
    Predicate2,
} from "@thi.ng/api";
import { compare } from "@thi.ng/compare/compare";
import { equiv } from "@thi.ng/equiv";
import type { HeapOpts } from "./api.js";

/**
 * Functional syntax sugar for {@link Heap} constructor.
 *
 * @param values
 * @param opts
 */
export const defHeap = <T>(
    values?: Iterable<T> | null,
    opts?: Partial<HeapOpts<T>>
) => new Heap(values, opts);

/**
 * Generic binary heap / priority queue with customizable ordering via
 * user-supplied comparator. By default, implements min-heap ordering
 * and uses @thi.ng/compare.
 *
 * @example
 * ```ts tangle:../export/heap.ts
 * import { Heap } from "@thi.ng/heaps";
 *
 * const h = new Heap([20, 5, 10]);
 * h.push(15);
 *
 * console.log(h.pop()); // 5
 * console.log(h.pop()); // 10
 * console.log(h.pop()); // 15
 * console.log(h.pop()); // 20
 * console.log(h.pop()); // undefined
 * ```
 */
export class Heap<T>
    implements
        Iterable<T>,
        IClear,
        ICopy<Heap<T>>,
        IEmpty<Heap<T>>,
        IInto<T, Heap<T>>,
        ILength,
        IStack<T, T, Heap<T>>
{
    static parentIndex(idx: number) {
        return idx > 0 ? (idx - 1) >> 1 : -1;
    }

    static childIndex(idx: number) {
        return idx >= 0 ? (idx << 1) + 1 : -1;
    }

    values: T[];
    compare: Comparator<T>;
    equiv: Predicate2<T>;

    constructor(values?: Iterable<T> | null, opts?: Partial<HeapOpts<T>>) {
        opts = { compare, equiv, ...opts };
        this.compare = opts.compare!;
        this.equiv = opts.equiv!;
        this.values = [];
        if (values) {
            this.into(values);
        }
    }

    *[Symbol.iterator]() {
        yield* this.min();
    }

    get length() {
        return this.values.length;
    }

    copy() {
        const h = this.empty();
        h.values = this.values.slice();
        return h;
    }

    clear() {
        this.values.length = 0;
    }

    empty() {
        return new Heap<T>(null, { compare: this.compare });
    }

    peek(): Maybe<T> {
        return this.values[0];
    }

    push(val: T) {
        this.values.push(val);
        this.percolateUp(this.values.length - 1);
        return this;
    }

    pop(): Maybe<T> {
        const vals = this.values;
        const tail = vals.pop();
        let res: T;
        if (vals.length > 0) {
            res = vals[0];
            vals[0] = <T>tail;
            this.percolateDown(0);
        } else {
            res = <T>tail;
        }
        return res;
    }

    pushPop(val: T, vals = this.values): Maybe<T> {
        const head = vals[0];
        if (vals.length > 0 && this.compare(head, val) <= 0) {
            vals[0] = val;
            val = head;
            this.percolateDown(0, vals);
        }
        return val;
    }

    into(vals: Iterable<T>) {
        for (let v of vals) {
            this.push(v);
        }
        return this;
    }

    /**
     * Calls {@link Heap.pushPop} for each given value in `vals` and
     * returns last result (i.e. the smallest value in heap after
     * processing all `vals`).
     *
     * @param vals - values to insert
     */
    pushPopAll(vals: Iterable<T>): Maybe<T> {
        let res: any;
        for (let v of vals) {
            res = this.pushPop(v);
        }
        return <T>res;
    }

    replaceHead(val: T) {
        const res = this.values[0];
        this.values[0] = val;
        this.percolateDown(0);
        return res;
    }

    remove(val: T) {
        const { values, equiv } = this;
        for (let i = values.length; i-- > 0; ) {
            if (equiv(values[i], val)) {
                this.values.splice(i, 1);
                this.heapify();
                return true;
            }
        }
        return false;
    }

    find(val: T) {
        const { values, equiv } = this;
        for (let i = values.length; i-- > 0; ) {
            if (equiv(values[i], val)) {
                return values[i];
            }
        }
    }

    findWith(pred: Predicate<T>) {
        const values = this.values;
        for (let i = values.length; i-- > 0; ) {
            if (pred(values[i])) return values[i];
        }
    }

    has(val: T) {
        return this.find(val) !== undefined;
    }

    heapify(vals = this.values) {
        for (let i = (vals.length - 1) >> 1; i >= 0; i--) {
            this.percolateDown(i, vals);
        }
    }

    /**
     * Returns the largest `n` values (or less) in heap, based on
     * comparator ordering.
     *
     * @param n - number of values
     */
    max(n = this.values.length) {
        const { compare, values } = this;
        const res = values.slice(0, n);
        if (!n) {
            return res;
        }
        this.heapify(res);
        for (let m = values.length; n < m; n++) {
            this.pushPop(values[n], res);
        }
        return res.sort((a, b) => compare(b, a));
    }

    /**
     * Returns the smallest `n` values (or less) in heap, based on
     * comparator ordering.
     *
     * @param n - number of values
     */
    min(n = this.values.length) {
        const { compare, values } = this;
        const res = values.slice(0, n).sort(compare);
        if (!n) {
            return res;
        }
        let x = res[n - 1],
            y: T;
        for (let i = n, m = values.length; i < m; i++) {
            y = values[i];
            if (compare(y, x) < 0) {
                res.splice(__binarySearch(y, res, 0, n, compare), 0, y);
                res.pop();
                x = res[n - 1];
            }
        }
        return res;
    }

    parent(n: number) {
        n = Heap.parentIndex(n);
        return n >= 0 ? this.values[n] : undefined;
    }

    children(n: number) {
        n = Heap.childIndex(n);
        const vals = this.values;
        const m = vals.length;
        if (n >= m) return;
        if (n === m - 1) return [vals[n]];
        return [vals[n], vals[n + 1]];
    }

    leaves() {
        const vals = this.values;
        if (!vals.length) {
            return [];
        }
        return vals.slice(Heap.parentIndex(vals.length - 1) + 1);
    }

    protected percolateUp(i: number, vals = this.values) {
        const node = vals[i];
        const cmp = this.compare;
        while (i > 0) {
            const pi = (i - 1) >> 1;
            const parent = vals[pi];
            if (cmp(node, parent) >= 0) {
                break;
            }
            vals[pi] = node;
            vals[i] = parent;
            i = pi;
        }
    }

    protected percolateDown(i: number, vals = this.values) {
        const n = vals.length;
        const node = vals[i];
        const cmp = this.compare;
        let child = (i << 1) + 1;
        while (child < n) {
            const next = child + 1;
            if (next < n && cmp(vals[child], vals[next]) >= 0) {
                child = next;
            }
            if (cmp(vals[child], node) < 0) {
                vals[i] = vals[child];
            } else {
                break;
            }
            i = child;
            child = (i << 1) + 1;
        }
        vals[i] = node;
    }
}

/** @internal */
const __binarySearch = <T>(
    x: T,
    vals: T[],
    lo: number,
    hi: number,
    cmp: Comparator<T>
) => {
    let m;
    while (lo < hi) {
        m = (lo + hi) >>> 1;
        if (cmp(x, vals[m]) < 0) {
            hi = m;
        } else {
            lo = m + 1;
        }
    }
    return lo;
};