DEV Community

loading...
Cover image for Writing some JavaScript classes for interviews (Heap, Trie, etc)

Writing some JavaScript classes for interviews (Heap, Trie, etc)

braeden profile image Braeden Smith ・4 min read

JavaScript is a fantastic language for writing leetcode-style solutions. But unlike Python, C++, etc, it's missing a few critical data structures.

During a recent interview, I found myself scrambling to find a JS implementation of a min-heap online. As I was explaining to the interviewer the correct solution, I had to explain my interview language of choice did not have the native data structure I needed.

I never wanted to find myself in the same situation in the future.

And I didn't love any of the existing implementations online. I felt like they were either too complex to quickly drop into an interview or too simple and missing basic features.

GitHub logo braeden / interview.js

A few useful JS classes (Heap, Disjoint Set, Trie) for drop-in use during interviews

Requirements:

  • Every class needed to execute in Node v10, but should use ES6+ practices
    • This means no private functions, no nullish coalescing etc.
  • Each class should be easy to read and understand by an interviewer
  • Include the minimum viable function set but otherwise keep it short

Building a heap class

The heap was the most critical since it's a fairly common occurrence in problems and has some complexity.

We will use a zero-indexed array as the heap.

Constructor:

All arguments should be optional.

  • A comparator should be an input to decide heap type and heapify custom objects (like .sort()).
  • An input array which can be O(n) heapified should also be optional.
constructor(comparator = (a, b) => a - b, input = [])
Enter fullscreen mode Exit fullscreen mode

We heapify down each node from the parent node to the root of the heap.

class Heap {
    constructor(comparator = (a, b) => a - b, input = []) {
        this.arr = input;
        this.comparator = comparator;
        if (this.size() > 1) {
            for (let i = Heap.parent(this.size() - 1); i >= 0; i--)
                this.heapifyDown(i);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Push, peek, size, pop

The easiest functions to implement:

size(): returns the length of the internal array.

peek(): returns 0th element if it exists, otherwise it returns null

push(): pushes like usual to the end of the array, and then calls heapifyUp on the last element to maintain the heap invariant.

pop(): swaps the first and last elements of the heap, pops() from the array (previously the highest priority element). And then heapifyDown() from index 0 to maintain the heap invariant.

push(elem) {
    this.arr.push(elem);
    this.heapifyUp(this.size() - 1);
}

peek() {
    return this.size() > 0 ? this.arr[0] : null;
}

pop() {
    if (this.size() === 0)
        return null;
    Heap.swap(this.arr, 0, this.size() - 1);
    const result = this.arr.pop();
    this.heapifyDown(0);
    return result;
}

size() {
    return this.arr.length;
}
Enter fullscreen mode Exit fullscreen mode

heapifyUp and heapifyDown

These are recursive internal functions that are used to swap heap elements to keep the heap valid.

heapifyUp(): Base case = heapifying up from the root (0).
Otherwise, we grab the parent of element we're heapifying, and if the parent is lower priority then the current element, we swap them and recurse on the parent index.

heapifyUp(idx) {
    if (idx === 0)
        return;
    const parentIdx = Heap.parent(idx);
    if (this.comparator(this.arr[idx], this.arr[parentIdx]) < 0) {
        Heap.swap(this.arr, parentIdx, idx);
        this.heapifyUp(parentIdx);
    }
}
Enter fullscreen mode Exit fullscreen mode

heapifyDown(): Base case there are no children nodes for our index (no where to heapifyDown into).
We grab the child with the maxmimum priority from our current location, and swap with our current index if that child is of higher priority. And then we recurse on the child index.

heapifyDown(idx) {
    if (Heap.leftChild(idx) >= this.size())
        return;
    const childIdx = this.maxPriorityChild(idx);
    if (this.comparator(this.arr[childIdx], this.arr[idx]) < 0) {
        Heap.swap(this.arr, childIdx, idx);
        this.heapifyDown(childIdx);
    }
}
Enter fullscreen mode Exit fullscreen mode

This is the gist of our heap class, with a few static functions to move indices from parent to child and vice-versa!

The full class can be found here.


Building a dead-simple Trie class

A Trie is a super awesome data structure that I find myself using all the time in coding challenges.

The idea is that it's a tree of characters for various words, in our case, we'll be using standard JS objects to allow O(1) access to check characters at each level in the Trie.

We need three functions:

  • The ability to insert into the Trie
  • The ability to check if a full word exists in the Trie
  • The ability to check if a prefix of a given word exists in the Trie

These last two can be combined with an optional argument in the function.

Insert

Given a base object, we want to walk the tree with each character, creating an empty object, walking into it and then inserting the next character. But we also don't want to override existing data at the same level in the tree, ex: help, hello.

{"h":{"e":{"l":{"l":{"o":{"end":true}}}}}}
Enter fullscreen mode Exit fullscreen mode

We can use the spread operator to make a shallow copy of existing object data, otherwise it's undefined and will create the empty object we want.

insert(word) {
    let temp = this.o;
    word.split('').forEach(e => {
        temp[e] = { ...temp[e] };
        temp = temp[e];
    });
    temp.end = true;
}
Enter fullscreen mode Exit fullscreen mode

Find

The find function is very similar, we just walk down the object, and if at any point, the character we're looking at next doesn't exist, we return false.

If the user wants the full word match only, we will return the status of the .end property on the final node. Otherwise, once we've exhausted the character walk, the prefix find is true.

find(word, full = true) {
    let temp = this.o;
    let arr = word.split('');
    for (let i = 0; i < word.length; i++) {
        if (!temp[arr[i]])
            return false;
        temp = temp[arr[i]];
    }
    return full ? !!temp.end : true;
}
Enter fullscreen mode Exit fullscreen mode

The full class can be found here.

I won't delve into the details, but I also included a disjoint set class that also comes in handy!

Hopefully this helps anyone else in the process of interviewing that needs some quick JS classes!

GitHub logo braeden / interview.js

A few useful JS classes (Heap, Disjoint Set, Trie) for drop-in use during interviews

Discussion (2)

pic
Editor guide
Collapse
jonathangetahun profile image
Jonathan Getahun

Just curious what made you focus on javascript as an interview language over python, like the specific positions you're applying for or something else?

Currently considering picking up python over JS for interviews.

Collapse
braeden profile image
Braeden Smith Author

Yeah absolutely! I've worked w/ Python a decent amount. I just am so engrained with C style loops, blocking, etc, because I'm often switching between C, C++, & JS. So JS is a great mix of dynamic typing + C style language.

Plus I use a lot of JS for projects, so I'm generally more comfortable iterating fast (as long as I have access to a heap class).

My suggestion is to do a handful of leetcode easy/medium problems. Try in JS and try in Python, whatever makes you work the fastest with clean code is the language I'd recommend! Hopefully that helps!