javascriptnotesData Structures

Set

// This array has two copies of its first item
let myList = [1, 1, 2, 3, 5, 8, 13, "fibonacci"];
 
// The same thing using the Set API
let mySet = new Set();
mySet.add(1);
mySet.add(1); // this won't change mySet, since 1 is already in there
mySet.add(2);
mySet.add(3);
// ... this gets tedious
 
// An array can be turned into a set
// If you want to quickly remove all duplicates from an array, this is a good tool!
let mySet2 = new Set(myList);
 
mySet2.has(3); // true
mySet2.has(12); // false
 
// For...of loop iteration works
for (let item of mySet2) {
  console.log("mySet contains", item);
}

Map

// This object is a bird
var bird = {
  genus: "corvus",
  species: "corvax",
  commonName: "raven",
};
 
// Here is a map using the same structure
var birdMap = new Map();
birdMap.set("genus", "corvus");
birdMap.set("species", "corvax");
birdMap.set("commonName", "raven");
 
birdMap.get("genus"); // 'corvus'
 
birdMap.has("genus"); // true
birdMap.has("corvus"); // false (keys only)
 
// for...of loops work on Maps, with key and value returned
for (let value of birdMap) {
  console.log(value); // returns a series of arrays with key and value pairs
}
 
birdMap.entries(); // same as using for of
 
Object.entries(bird); // an object can be converted to a map using Object.entries method
 
birdMap2 = new Map(Object.entries(bird));

They also have entries(), keys(), and values() methods.

let myMap = new Map([
  ["a", 1],
  ["b", 2],
  ["c", 3],
]);
 
for (let [key, value] of myMap.entries()) {
  console.log(`Key: ${key}, Value: ${value}`);
}
 
for (let key of myMap.keys()) {
  console.log(`Key: ${key}`);
}
 
for (let value of myMap.values()) {
  console.log(`Value: ${value}`);
}

Map vs plain objects

Here are the main differences between a Map object and a plain object:

  1. Key types: In a plain object, keys are always strings (or symbols). In a Map, keys can be any type of value, including objects, arrays, and even other Maps.
  2. Key ordering: In a plain object, the order of keys is not guaranteed. In a Map, the order of keys is preserved, and you can iterate over them in the order they were inserted.
  3. Iteration: A Map is iterable, which means you can use for...of loops to iterate over its key-value pairs. A plain object is not iterable by default, but you can use Object.keys() or Object.entries() to iterate over its properties.
  4. Performance: Map objects are generally faster and more efficient than plain objects, especially when dealing with large datasets.
  5. Methods: A Map object provides additional methods, such as get, set, has, and delete, which make it easier to work with key-value pairs.
  6. Serialization: When serializing a Map object to JSON, it will be converted to an object but the existing Map properties might be lost in the conversion. A plain object, on the other hand, is serialized to a JSON object with the same structure.

Use a plain object (POJO) when:

  • You need a simple, lightweight object with string keys.
  • You’re working with a small dataset.
  • You need to serialize the object to JSON (e.g. to send over the network).

Use a Map object when:

  • You need to store key-value pairs with non-string keys (e.g., objects, arrays).
  • You need to preserve the order of key-value pairs.
  • You need to iterate over the key-value pairs in a specific order.
  • You’re working with a large dataset and need better performance.

In summary, while both plain objects and Map objects can be used to store key-value pairs, Map objects offer more advanced features, better performance, and additional methods, making them a better choice for more complex use cases.

Map/Set vs WeakMap/WeakSet

The primary difference between Map/Set and WeakMap/WeakSet in JavaScript lies in how they handle keys. Here’s a breakdown:

Map vs. WeakMap

Maps allows any data type (strings, numbers, objects) as keys. The key-value pairs remain in memory as long as the Map object itself is referenced. Thus they are suitable for general-purpose key-value storage where you want to maintain references to both keys and values. Common use cases include storing user data, configuration settings, or relationships between objects.

WeakMaps only allows objects as keys. However, these object keys are held weakly. This means the garbage collector can remove them from memory even if the WeakMap itself still exists, as long as there are no other references to those objects. WeakMaps are ideal for scenarios where you want to associate data with objects without preventing those objects from being garbage collected. This can be useful for things like:

  • Caching data based on objects without preventing garbage collection of the objects themselves.
  • Storing private data associated with DOM nodes without affecting their lifecycle.

Set vs. WeakSet

Similar to Map, Sets allow any data type as keys. The elements within a Set must be unique. Sets are useful for storing unique values and checking for membership efficiently. Common use cases include removing duplicates from arrays or keeping track of completed tasks.

On the other hand, WeakSet only allows objects as elements, and these object elements are held weakly, similar to WeakMap keys. WeakSets are less commonly used, but applicable when you want a collection of unique objects without affecting their garbage collection. This might be necessary for:

  • Tracking DOM nodes that have been interacted with without affecting their memory management.
  • Implementing custom object weak references for specific use cases.

Here’s a table summarizing the key differences:

FeatureMapWeakMapSetWeakSet
Key TypesAny data typeObjects (weak references)Any data type (unique)Objects (weak references, unique)
Garbage CollectionKeys and values are not garbage collectedKeys can be garbage collected if not referenced elsewhereElements are not garbage collectedElements can be garbage collected if not referenced elsewhere
Use CasesGeneral-purpose key-value storageCaching, private DOM node dataRemoving duplicates, membership checksObject weak references, custom use cases

Choosing between them

  • Use Map and Set for most scenarios where you need to store key-value pairs or unique elements and want to maintain references to both the keys/elements and the values.
  • Use WeakMap and WeakSet cautiously in specific situations where you want to associate data with objects without affecting their garbage collection. Be aware of the implications of weak references and potential memory leaks if not used correctly.

Symbol

Symbols in JavaScript are a unique and immutable data type used primarily for object property keys to avoid name collisions.

  • Uniqueness: Each Symbol value is unique, even if they have the same description.
  • Immutability: Symbol values are immutable, meaning their value cannot be changed.
  • Non-enumerable: Symbol properties are not included in for…in loops or Object.keys().
Creating `Symbol`s using the `Symbol()` function
const sym1 = Symbol();
const sym2 = Symbol("uniqueKey");
 
console.log(typeof sym1); // "symbol"
console.log(sym1 === sym2); // false, because each symbol is unique

The Symbol(..) function must be called without the new keyword.

`Symbol`s can be used to add properties to an object without risk of name collision
const obj = {};
const sym = Symbol("uniqueKey");
 
obj[sym] = "value";
console.log(obj[sym]); // "value"
`Symbol`s are not enumerable
  • Symbol properties are not included in for...in loops or Object.keys().
  • This makes them suitable for creating private/internal object state.
  • Use Object.getOwnPropertySymbols(obj) to get all symbol properties on an object.
const mySymbol = Symbol("privateProperty");
const obj = {
  name: "John",
  [mySymbol]: 42,
};
 
console.log(Object.keys(obj)); // Output: ['name']
console.log(obj[mySymbol]); // Output: 42
Global `Symbol` registry

You can create global Symbols using Symbol.for('key'), which creates a new Symbol in the global registry if it doesn’t exist, or returns the existing one. This allows you to reuse Symbols across different parts of your code base or even across different code bases.

const globalSym1 = Symbol.for("globalKey");
const globalSym2 = Symbol.for("globalKey");
 
console.log(globalSym1 === globalSym2); // true
 
const key = Symbol.keyFor(globalSym1);
console.log(key); // "globalKey"
Well-known `Symbol`

JavaScript includes several built-in Symbols, referred as well-known Symbols.

  • Symbol.iterator: Defines the default iterator for an object.
  • Symbol.toStringTag: Used to create a string description for an object.
  • Symbol.hasInstance: Used to determine if an object is an instance of a constructor.
`Symbol.iterator`
let iterable = {
  [Symbol.iterator]() {
    let step = 0;
    return {
      next() {
        step++;
        if (step <= 5) {
          return { value: step, done: false };
        }
        return { done: true };
      },
    };
  },
};
 
for (let value of iterable) {
  console.log(value); // 1, 2, 3, 4, 5
}
`Symbol.toStringTag`
let myObj = {
  [Symbol.toStringTag]: "MyCustomObject",
};
 
console.log(Object.prototype.toString.call(myObj)); // "[object MyCustomObject]"

Array methods

  1. print a list of users from an object

    // works
    // btw, forEach doesn't return anything
    const names = [];
    users.forEach((user) => {
      names.push(user.name);
    });
     
    // best way
    const names = users.map((user) => user.name);
  2. filtering

    // conditions
    const names = users.filter((user) => user.isActive).map((user) => user.name);
  3. sorting

    // alphabetically
    users.sort((a, b) => a.firstname.localeCompare(b.firstname));
  4. write a function which gets an array and an element and returns an array with this element at the end

    const numbers = [1, 2];
    const append = (arr, el) => {
      // don't use arr.push(el) because push is not safe (it doesn't create a new array)
      return [...arr, el]; // this is pure because it will return the same result every time, and it doesn't modify any variables outside of the function
    };
  5. write a function which can concatenate 2 arrays

    const mergeArrays = (arr1, arr2) => {
      // WRONG: push mutates array
      arr1.push(...arr2);
      return arr1;
     
      // RIGHT: concat doesn't mutate the array
      return arr1.concat(...arr2);
      // concat can even take multiple arguments
      return arr1.concat(...arr2, ...arr3);
      // OR
      return [...arr1, ...arr2];
    };
  6. check if an object exists

    const isNameExists = (name, arr) => arr.some((el) => el.name === name);
     
    const isNameExists = (name, arr) => {
      const el = arr.find((el) => el.name === name);
      return Boolean(el);
    };
  7. remove all duplicates

    const uniqueArr = (arr) => {
      return [...new Set(arr)];
    };
     
    const uniqueArr = (arr) => {
      const result = [];
      arr.forEach((item) => {
        if (!result.includes(item));
        result.push(item);
      });
      return result;
    };
     
    // more functional
    const uniqueArr = (arr) => {
      return arr.reduce((acc, el) => {
        return acc.includes(el) ? acc : [...acc, el];
      }, []);
    };
  8. sorting an array

    // mutates arr
    arr.sort((a, b) => a - b);
     
    // for string
    books.sort((book1, book2) => {
      const authorLasttName1 = book1.author.split(" ")[1];
      const authorLastName2 = book2.author.split(" ")[1];
      return authorLastName1 < authorLastName2 ? -1 : 1;
    });
  9. array reduce function

    1. syntax:

      array.reduce(
        callback(accumulator, currentValue, currentIndex, array),
        initialValue
      );
    2. sum of array elements:

      const numbers = [1, 2, 3, 4, 5];
      const sum = numbers.reduce((accumulator, currentValue) => {
        return accumulator + currentValue;
      }, 0);
       
      console.log(sum); // Output: 15
    3. flattening an array of arrays:

      const nestedArray = [
        [1, 2],
        [3, 4],
        [5, 6],
      ];
      const flattenedArray = nestedArray.reduce((accumulator, currentValue) => {
        return accumulator.concat(currentValue);
      }, []);
       
      console.log(flattenedArray); // Output: [1, 2, 3, 4, 5, 6]
    4. counting instances of values in an object:

      const fruits = [
        "apple",
        "banana",
        "orange",
        "apple",
        "orange",
        "banana",
        "banana",
      ];
      const countFruits = fruits.reduce((accumulator, currentValue) => {
        if (!accumulator[currentValue]) {
          accumulator[currentValue] = 0;
        }
        accumulator[currentValue]++;
        return accumulator;
      }, {});
       
      console.log(countFruits); // Output: { apple: 2, banana: 3, orange: 2 }
  10. check if user with given name exists

    function userExists(users, name) {
      return users.some((user) => user.name === name);
    }
     
    // Example usage
    const users = [
      { id: 1, name: "Alice" },
      { id: 2, name: "Bob" },
      { id: 3, name: "Charlie" },
    ];
     
    console.log(userExists(users, "Alice")); // Output: true
    console.log(userExists(users, "David")); // Output: false
  11. Find the number of occurrences of minimum value in list

    function countMinOccurrences(arr) {
      if (arr.length === 0) {
        return 0; // Return 0 if the array is empty
      }
     
      const minValue = Math.min(...arr);
      const minOccurrences = arr.filter((num) => num === minValue).length;
     
      return minOccurrences;
    }
     
    // Example usage
    const numbers = [3, 1, 4, 1, 5, 1, 2, 1];
    const occurrences = countMinOccurrences(numbers);
    console.log(occurrences); // Output: 4
  12. Initialize a static array

    With five elements, each initialized to its index + 1

    const staticArray = Array.from({ length: 5 }, (value, index) => index + 1);
    console.log(staticArray); // Output: [1, 2, 3, 4, 5]

    With 5 elements, each initialized to 0

    const staticArray = Array.from({ length: 5 }, () => 0);

Iterators and generators

In JavaScript, iterators and generators are powerful tools for managing sequences of data and controlling the flow of execution in a more flexible way.

Iterators are objects that define a sequence and potentially a return value upon its termination. It adheres to a specific interface:

  • An iterator object must implement a next() method.
  • The next() method returns an object with two properties:
    • value: The next value in the sequence.
    • done: A boolean that is true if the iterator has finished its sequence, otherwise false.
Example
const iterator = {
  current: 0,
  last: 5,
  next() {
    if (this.current <= this.last) {
      return { value: this.current++, done: false };
    } else {
      return { value: undefined, done: true };
    }
  },
};
 
let result = iterator.next();
while (!result.done) {
  console.log(result.value); // Logs 0, 1, 2, 3, 4, 5
  result = iterator.next();
}

Generators are a special functions that can pause execution and resume at a later point. It uses the function* syntax and the yield keyword to control the flow of execution. When you call a generator function, it doesn’t execute completely like a regular function. Instead, it returns an iterator object. Calling the next() method on the returned iterator advances the generator to the next yield statement, and the value after yield becomes the return value of next().

Example
function* numberGenerator() {
  let num = 0;
  while (num <= 5) {
    yield num++;
  }
}
 
const gen = numberGenerator();
console.log(gen.next()); // { value: 0, done: false }
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: 4, done: false }
console.log(gen.next()); // { value: 5, done: false }
console.log(gen.next()); // { value: undefined, done: true }

Generators are powerful for creating iterators on-demand, especially for infinite sequences or complex iteration logic. They can be used for:

  • Lazy evaluation – processing elements only when needed, improving memory efficiency for large datasets.
  • Implementing iterators for custom data structures.
  • Creating asynchronous iterators for handling data streams.

Priority Queue / Heap

A Priority Queue is a data structure where each element has a priority. Elements with higher priority are dequeued before elements with lower priority. If two elements have the same priority, they are dequeued according to their order in the queue. JavaScript does not have a built-in Priority Queue, so you need to implement it manually or use a third-party library.

const { PriorityQueue } = require("@datastructures-js/priority-queue");

Creating a Min-Heap (smallest element at the top)

const minHeap = new PriorityQueue({ compare: (a, b) => a - b });

Creating a Max-Heap (largest element at the top)

const maxHeap = new PriorityQueue({ compare: (a, b) => b - a });

enqueue(value): Add value to the heap.

minHeap.enqueue(10); // Adds 10 to minHeap

dequeue(): Remove and return the top element.

minHeap.dequeue(); // Removes and returns smallest element

front(): Get the top element without removing it.

minHeap.front(); // Returns smallest element without removing

size(): Get the number of elements in the heap.

minHeap.size(); // Returns number of elements in minHeap

isEmpty(): Check if the heap is empty.

minHeap.isEmpty(); // Returns true if heap is empty
Example usage
const minHeap = new PriorityQueue({ compare: (a, b) => a - b });
minHeap.enqueue(10);
minHeap.enqueue(5);
minHeap.enqueue(20);
console.log(minHeap.front()); // Output: 5
console.log(minHeap.size()); // Output: 3
minHeap.dequeue(); // Removes 5
console.log(minHeap.front()); // Output: 10
OperationTime Complexity
InsertO(logn)O(\log n)
Extract MinO(logn)O(\log n)
PeekO(1)O(1)