Interview

10 JavaScript Data Structures Interview Questions and Answers

Prepare for your next interview with our guide on JavaScript data structures, featuring common questions and in-depth answers.

JavaScript is a cornerstone of modern web development, enabling dynamic and interactive user experiences. Mastery of JavaScript data structures is essential for efficiently managing and manipulating data within applications. Understanding arrays, objects, sets, and maps, among other structures, is crucial for writing optimized and maintainable code. JavaScript’s versatility and extensive ecosystem make it a valuable skill for developers across various domains.

This article provides a curated selection of interview questions focused on JavaScript data structures. Reviewing these questions will help you deepen your understanding and demonstrate your proficiency in handling data effectively during technical interviews.

JavaScript Data Structures Interview Questions and Answers

1. Explain the concept of closures and how they can be used to create private variables.

In JavaScript, a closure is created when a function is defined inside another function, allowing the inner function to access the outer function’s variables. This mechanism can be leveraged to create private variables, which are variables that cannot be accessed directly from outside the function.

Example:

function createCounter() {
    let count = 0; // private variable

    return {
        increment: function() {
            count++;
            return count;
        },
        decrement: function() {
            count--;
            return count;
        },
        getCount: function() {
            return count;
        }
    };
}

const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getCount());  // 2
console.log(counter.decrement()); // 1

In this example, the count variable is private to the createCounter function. The returned object contains methods that can manipulate and access count, but count itself is not accessible from outside the createCounter function.

2. Write a function to reverse a singly linked list.

Reversing a singly linked list involves changing the direction of the pointers so that each node points to its previous node instead of the next one. This can be achieved by iterating through the list and adjusting the pointers accordingly.

class ListNode {
    constructor(value) {
        this.value = value;
        this.next = null;
    }
}

function reverseLinkedList(head) {
    let prev = null;
    let current = head;
    let next = null;

    while (current !== null) {
        next = current.next;  // Store next node
        current.next = prev;  // Reverse current node's pointer
        prev = current;       // Move pointers one position ahead
        current = next;
    }
    return prev;  // New head of the reversed list
}

// Example usage:
let head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(3);

let reversedHead = reverseLinkedList(head);

3. How would you detect a cycle in a linked list?

To detect a cycle in a linked list, the most common algorithm used is Floyd’s Cycle-Finding Algorithm, also known as the Tortoise and Hare algorithm. This algorithm uses two pointers that move at different speeds. The slow pointer (tortoise) moves one step at a time, while the fast pointer (hare) moves two steps at a time. If there is a cycle, the fast pointer will eventually meet the slow pointer within the cycle. If there is no cycle, the fast pointer will reach the end of the list.

Example:

class ListNode {
    constructor(value) {
        this.value = value;
        this.next = null;
    }
}

function hasCycle(head) {
    let slow = head;
    let fast = head;

    while (fast !== null && fast.next !== null) {
        slow = slow.next;
        fast = fast.next.next;

        if (slow === fast) {
            return true;
        }
    }

    return false;
}

// Example usage:
let head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(3);
head.next.next.next = head.next; // Creates a cycle

console.log(hasCycle(head)); // Output: true

4. Explain the difference between deep copy and shallow copy. How would you implement each?

In JavaScript, a shallow copy of an object is a copy whose properties share the same references as those of the source object. This means that changes to nested objects in the copy will affect the original object. On the other hand, a deep copy creates a new object and recursively copies all properties, ensuring that nested objects are also copied and do not share references with the original object.

A shallow copy can be created using methods like Object.assign() or the spread operator. A deep copy can be created using JSON methods or a custom recursive function.

Example of shallow copy:

let original = { a: 1, b: { c: 2 } };
let shallowCopy = { ...original };

shallowCopy.b.c = 3;
console.log(original.b.c); // Output: 3

Example of deep copy:

let original = { a: 1, b: { c: 2 } };
let deepCopy = JSON.parse(JSON.stringify(original));

deepCopy.b.c = 3;
console.log(original.b.c); // Output: 2

5. Write a function to perform an in-order traversal of a binary tree.

In-order traversal of a binary tree is a depth-first traversal method where the nodes are recursively visited in the following order: left subtree, root node, and then right subtree. This traversal method is particularly useful when you want to retrieve the values of the nodes in a non-decreasing order.

Here is a concise implementation of in-order traversal in JavaScript:

class TreeNode {
    constructor(value) {
        this.value = value;
        this.left = null;
        this.right = null;
    }
}

function inOrderTraversal(node, result = []) {
    if (node !== null) {
        inOrderTraversal(node.left, result);
        result.push(node.value);
        inOrderTraversal(node.right, result);
    }
    return result;
}

// Example usage:
const root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);
root.left.left = new TreeNode(4);
root.left.right = new TreeNode(5);

console.log(inOrderTraversal(root)); // Output: [4, 2, 5, 1, 3]

6. How would you implement a hash table? What collision resolution method would you use?

A hash table is a data structure that maps keys to values for efficient lookup. It uses a hash function to compute an index into an array of buckets or slots, from which the desired value can be found. One common issue with hash tables is collisions, which occur when two keys hash to the same index. There are several methods to resolve collisions, with separate chaining and open addressing being the most common.

Separate chaining involves maintaining a list of all elements that hash to the same index. This can be implemented using linked lists or other data structures.

Example:

class HashTable {
    constructor(size = 53) {
        this.keyMap = new Array(size);
    }

    _hash(key) {
        let total = 0;
        let WEIRD_PRIME = 31;
        for (let i = 0; i < Math.min(key.length, 100); i++) {
            let char = key[i];
            let value = char.charCodeAt(0) - 96;
            total = (total * WEIRD_PRIME + value) % this.keyMap.length;
        }
        return total;
    }

    set(key, value) {
        let index = this._hash(key);
        if (!this.keyMap[index]) {
            this.keyMap[index] = [];
        }
        this.keyMap[index].push([key, value]);
    }

    get(key) {
        let index = this._hash(key);
        if (this.keyMap[index]) {
            for (let i = 0; i < this.keyMap[index].length; i++) {
                if (this.keyMap[index][i][0] === key) {
                    return this.keyMap[index][i][1];
                }
            }
        }
        return undefined;
    }
}

let ht = new HashTable();
ht.set("hello", "world");
ht.set("goodbye", "world");
console.log(ht.get("hello")); // "world"
console.log(ht.get("goodbye")); // "world"

7. Explain the concept of memoization and provide an example of how it can be used to optimize a recursive function.

Memoization is a technique used to improve the efficiency of recursive functions by caching the results of function calls. When a function is called with the same arguments, the cached result is returned instead of recomputing the value. This can drastically reduce the time complexity of algorithms that involve repeated calculations.

Example:

function fibonacci(n, memo = {}) {
    if (n in memo) return memo[n];
    if (n <= 1) return n;
    memo[n] = fibonacci(n - 1, memo) + fibonacci(n - 2, memo);
    return memo[n];
}

console.log(fibonacci(10)); // 55
console.log(fibonacci(50)); // 12586269025

In this example, the fibonacci function calculates the nth Fibonacci number. By using a memo object to store previously computed values, the function avoids redundant calculations, making it much more efficient for large inputs.

8. Describe how you would implement a trie (prefix tree) and what applications it might have.

A trie, also known as a prefix tree, is a specialized tree-like data structure that is used to store a dynamic set of strings where the keys are usually strings. It is particularly useful for tasks that involve searching for words or prefixes efficiently.

In a trie, each node represents a single character of a string, and the path from the root to a node represents a prefix of the string. Each node can have multiple children, and a special end-of-word marker is used to indicate the end of a valid word.

Here is a simple implementation of a trie in JavaScript:

class TrieNode {
    constructor() {
        this.children = {};
        this.isEndOfWord = false;
    }
}

class Trie {
    constructor() {
        this.root = new TrieNode();
    }

    insert(word) {
        let node = this.root;
        for (let char of word) {
            if (!node.children[char]) {
                node.children[char] = new TrieNode();
            }
            node = node.children[char];
        }
        node.isEndOfWord = true;
    }

    search(word) {
        let node = this.root;
        for (let char of word) {
            if (!node.children[char]) {
                return false;
            }
            node = node.children[char];
        }
        return node.isEndOfWord;
    }

    startsWith(prefix) {
        let node = this.root;
        for (let char of prefix) {
            if (!node.children[char]) {
                return false;
            }
            node = node.children[char];
        }
        return true;
    }
}

Applications of a trie include:

  • Autocomplete: Tries are used in search engines and text editors to provide suggestions as the user types.
  • Spell Checking: Tries can be used to quickly verify if a word exists in a dictionary.
  • IP Routing: Tries are used in networking to store routing tables for IP addresses.
  • Genome Sequencing: Tries can be used to store and search DNA sequences efficiently.

9. What are WeakMap and WeakSet, and when would you use them?

WeakMap is a collection of key-value pairs where the keys are objects and the values can be arbitrary values. The key objects in a WeakMap are held weakly, meaning that if there are no other references to the key object, it can be garbage collected. This makes WeakMap useful for cases where you want to associate metadata with an object without preventing the object from being garbage collected.

Example:

let wm = new WeakMap();
let obj = {};
wm.set(obj, 'some value');
console.log(wm.get(obj)); // 'some value'
obj = null; // obj is now eligible for garbage collection

WeakSet is a collection of objects, where the objects are held weakly. This means that if there are no other references to an object stored in a WeakSet, the object can be garbage collected. WeakSet is useful for keeping track of objects without preventing their garbage collection.

Example:

let ws = new WeakSet();
let obj = {};
ws.add(obj);
console.log(ws.has(obj)); // true
obj = null; // obj is now eligible for garbage collection

10. Explain the event loop and how it handles asynchronous data operations.

The event loop in JavaScript is a mechanism that allows the language to perform non-blocking operations by offloading tasks to the system kernel whenever possible. JavaScript is single-threaded, meaning it can execute one operation at a time. However, the event loop enables it to handle asynchronous operations like I/O tasks, timers, and network requests without blocking the main thread.

When an asynchronous operation is initiated, such as a network request, it is offloaded to the web APIs provided by the browser or the Node.js runtime. Once the operation is completed, a callback function is placed in the task queue. The event loop continuously checks the call stack and the task queue. If the call stack is empty, it pushes the first task from the queue to the call stack for execution.

Here is a concise example to illustrate the event loop:

console.log('Start');

setTimeout(() => {
  console.log('Timeout');
}, 0);

console.log('End');

In this example, “Start” and “End” are logged immediately, while “Timeout” is logged after the call stack is empty, demonstrating how the event loop handles asynchronous operations.

Previous

10 Java Low Latency Interview Questions and Answers

Back to Interview
Next

10 REST Web Service Interview Questions and Answers