Introduction
As I continue my DSA study journey, this blog is the next step in my ongoing series where I document what I’m learning and hopefully make it easier for others to follow along.
If you missed the previous one, it covered Big O notation and arrays, the foundations of understanding how our code performs and stores data. You can give it a read here
In this blog, we’re going to look at three new data structures: hashmaps, stacks, and queues. These structures appear everywhere, from handling data lookup and caching to managing order in algorithmic operations. They might sound abstract at first, but they’re surprisingly intuitive once you understand their behaviour and when to use them.
By the end, you’ll not only know what each one does, but also why they matter when designing systems, optimising code, or thinking like a developer who really understands how data moves.
Hashmaps
When I first started learning about hashmaps, they sounded like something complicated like hashing a password, but in reality, we use them all the time without realising it. In JavaScript, a hashmap is simply a way to store data as key-value pairs. You give it a unique key, and it quickly gives you back the value that’s linked to it.
In plain terms, you can think of a hashmap as a super‑efficient filing system.
Imagine you’re storing folders in a cabinet, each labelled with a name like "Projects", "Invoices", or "Receipts". When you need one, you don’t check every folder in order; you go straight to the label.
That’s how a hashmap works, it uses a hashing function behind the scenes to jump directly to where the data is stored, rather than searching one by one.
In JavaScript, the simplest way to represent a hashmap is with a plain object.
Objects let us store data by key, which means we can easily access, add, or delete values.
const person = {
firstName: "Simon",
surname: "Jackson",
job: "Painter",
};
console.log(person.firstName); // Simon
console.log(person["job"]); // Painter
Here, the keys are "firstName", "surname", and "job", and each one is mapped to a string value.
When we access person.firstName, JavaScript doesn’t loop through every key like an array, it goes straight to the value through its internal hashing mechanism.
While objects are great, JavaScript also gives us a built‑in Map class that behaves like an upgraded hashmap.
The key difference is that Map allows any type of key (strings, numbers, even objects), and it remembers the insertion order of elements.
const person = new Map();
person.set("firstName", "Simon");
person.set("surname", "Jackson");
person.set("job", "Painter");
console.log(person.get("surname")); // Jackson
You can also use non‑string keys:
const person1 = { id: 101 };
const person2 = { id: 201 };
const people = new Map();
people.set(person1, "Patrick");
people.set(person2, "Greg");
console.log(people.get(person1)); // Patrick
The Map object stores each key using its own internal hashing process, giving you the same fast access pattern, no scanning, no searching.
Under the hood, a hashmap uses a hash function to convert each key into a unique index in memory (or as close as possible).
This means that instead of searching through every key, the program can access the associated value almost instantly by jumping to its hashed location.
If two different keys happen to produce the same hash (called a collision), the hashmap resolves it by storing multiple values at that index in a small list, but this is mostly abstracted away from us in JavaScript.
Insert
Adding a new entry into your object or map by setting a key-value pair. This has a time complexity of O(1) and space complexity of O(n) overall, since memory grows linearly with the number of stored key‑value pairs.
const cats = {
tina: {age:10, breed: "ragdoll"},
bella: {age:12, breed: "tabby"}
}
cats.juno = {age:12, breed: "Bombay"};
console.log(cats);
// {
// tina: { age: 10, breed: 'ragdoll' },
// bella: { age: 12, breed: 'tabby' },
// juno: { age: 12, breed: 'Bombay' }
// }
const dogs = new Map();
dogs.set("Max", {age:5, breed:"cocker spaniel"});
dogs.set("Mellow", {age:2, breed:"golden retriever"});
console.log(dogs.get("Max"));
// { age: 5, breed: 'cocker spaniel' }
Access
After we have provided the object or map with key-value pairs, we can access them by calling the keys we inserted into them. The time complexity is O(1) as it uses the hashing function to jump directly to where the data is stored and the space complexity is O(1) as you are returning the value that was stored.
let cat1 = cats.tina;
let cat2 = cats["bella"];
console.log(cat1) // {age:10, breed: "ragdoll"}
console.log(cat2) // {age:12, breed: "tabby"}
let dog1 = dogs.get("Max")
let dog2 = dogs.get("Mellow")
console.log(dog1) // {age:5, breed:"cocker spaniel"}
console.log(dog2) // {age:2, breed:"golden retriever"}
Update
Updating is very similar to inserting, as you are setting a key-value pair. But instead, we use an already used key and change its value. As it is the same as inserting, it has a time complexity of O(1) and a space complexity of O(1), as you are just updating the key's value.
let person = {
name: "Ben",
age: 34
};
person.age = 35;
console.log(person.age); // 35
const person = new Map();
person.set("name", "Ben");
person.set("age", 34);
person.set("name","Sam");
console.log(person.get("name")); // Sam
Delete
When we don't need a value in our object or map we can delete it using the delete keyword or method. Because we are just removing one key-value pair, the time and space complexity are O(1).
let task = {
place: "123 Baker Street",
time: "10.00 pm"
};
delete task.place;
console.log(task); // {time: "10.00 pm"}
let task = new Map();
task.set("place","123 Baker Street");
task.set("time","10.00 pm");
task.delete("time");
console.log(task.get("time")); // undefined
Search
What if we want to check if a key-value pair exists without accessing and returning undefined. This can be done with a method in maps, but not for objects; they can use if statements. The time and space complexity is similar to the access O(1), as we are just returning a true or false if the key exists.
const game = {
genre: "Fantasy"
}
if(game.genre !== undefined)
{
console.log(true);
}
const game = new Map();
game.set("genre", "Fantasy");
console.log(game.has("genre")); // true
console.log(game.has("name")); // false
Hashmaps are very helpful, as whenever you want a fast lookup or need to associate values to names, IDs, or keys. They’re ideal for counting occurrences of items in an array, storing cached results, tracking unique data and implementing sets or dictionaries.
In short, hashmaps are all about fast access and efficient organisation. But not every data structure prioritises speed, some focus on order. That’s where stacks and queues come in.
Stack
Before we jump into stacks, let's quickly talk about classes in JavaScript, since our example will use one.
A class is like a blueprint for creating objects with shared structure and behaviour. Instead of rewriting similar logic for each object, we can define it once inside a class and then create new instances using it.
Think of a class as a template, it describes what properties and functions its objects will have. When we create an instance with the new keyword, we’re building an individual object based on that blueprint.
class Example {
constructor(name) {
this.name = name;
}
greet() {
console.log(`Hello, my name is ${this.name}`);
}
}
const person = new Example("Alex");
person.greet(); // Hello, my name is Alex
Classes can also have private fields (marked with #), which means their properties can’t be accessed from outside the class. This helps keep internal data safe and predictable, something we’ll see in our Stack example.
What is a Stack
A Stack is a data structure that follows the LIFO principle, Last In, First Out.
This means the last item you add is the first one you take out.
A simple real‑world example is a stack of plates:
You add new plates on top of the pile, and when you remove one, you take from the top first.
Stacks are great for undo features, tracking function calls, and reversing actions, anywhere you need to go back in the reverse order things were added.
Here is a basic example of our stack class without any methods
class Stack{
#length = 0;
#data = [];
#limit = 0;
constructor(limit = 0)
{
this.#limit = limit;
}
}
In this example, we have three private fields:
length: the size of the array.
data: where we store values we add to the stack.
limit: If we need to provide the stack with a limit.
The constructor checks whether a limit was provided when the class is created and sets it for the stack instance.
Push()
push() adds a new entry to the top of the stack, in our case, to the end of the internal array.
It has a time complexity of O(1) because we’re appending a value directly to the end, and a space complexity of O(1) since we’re only adding a single element.
However, in rare cases, when the array needs to resize its underlying memory, the operation can take O(n) time.
class Stack{
#length = 0;
#data = [];
#limit = 0;
constructor(limit = 0)
{
this.#limit = limit;
}
push(value)
{
if(!this.checkLimit())
{
console.log("Limit reached")
return
}
this.#data.push(value);
this.#length++
}
}
Pop()
pop() removes the top element from the stack, in our case, the last value in the internal array.
It has a time complexity of O(1) because it simply removes and returns the final element, and a space complexity of O(1) overall, since we’re not allocating any new memory.
In the worst case, when internal reallocation happens, it may take O(n) time, but this rarely occurs.
class Stack{
#length = 0;
#data = [];
#limit = 0;
constructor(limit = 0)
{
this.#limit = limit;
}
pop()
{
if(this.#length == 0)
{
console.log("Stack empty");
return
}
this.#length--;
return this.#data.pop()
}
}
Peek()
peek() looks at the top element of the stack without removing it.
It has a time and space complexity of O(1), since it only accesses the final item in the array.
class Stack{
#length = 0;
#data = [];
#limit = 0;
constructor(limit = 0)
{
this.#limit = limit;
}
peek()
{
return this.#data[this.#length - 1];
}
}
isEmpty()
isEmpty() checks whether the stack is empty.
It’s a simple conditional check, so both its time and space complexity are O(1).
class Stack{
#length = 0;
#data = [];
#limit = 0;
constructor(limit = 0)
{
this.#limit = limit;
}
isEmpty()
{
if(this.#length !== 0 )
{
return false;
}
return true;
}
}
size()
size() returns the number of items currently in the stack.
Like the previous methods, it has a time and space complexity of O(1) because it only reads a stored property without looping or performing additional logic.
class Stack{
#length = 0;
#data = [];
#limit = 0;
constructor(limit = 0)
{
this.#limit = limit;
}
size()
{
return this.#length;
}
}
All together with the check limit method:
class Stack{
#length = 0;
#data = []
#limit = 0
constructor(limit = 0)
{
this.#limit = limit
}
push(value)
{
if(!this.checkLimit())
{
console.log("Limit reached")
return
}
this.#data.push(value);
this.#length++
}
pop()
{
if(this.#length == 0)
{
console.log("Stack empty");
return
}
this.#length--;
return this.#data.pop()
}
peek()
{
return this.#data[this.#length - 1]
}
checkLimit()
{
if(this.#limit === 0)
{
return true
}
if(this.#length === this.#limit)
{
return false
}
return true
}
isEmpty()
{
if(this.#length !== 0 )
{
return false;
}
return true;
}
size()
{
return this.#length;
}
}
Example of the stack:
const stack = new Stack(3);
stack.push("A");
stack.push("B");
stack.push("C");
console.log(stack.peek()); // C
console.log(stack.size()); // 3
stack.pop();
console.log(stack.peek()); // B
console.log(stack.isEmpty()); // false
Stacks are used everywhere in computer science and real‑world applications, like Undo/Redo systems in editors, browser history navigation and in programming when you get a stack overflow when your program crashes.
Stacks give us a way to process data in reverse order, last in, first out. But what if we need to process data in the order it arrived?
That’s where Queues come in.
Queue
A Queue is a data structure that follows the FIFO principle, First In, First Out.
This means the first item you add is the first one to be removed.
A simple real‑world example is a line of people waiting for coffee.
Whoever joins the line first gets served first, and whoever arrives later waits their turn.
Unlike stacks that handle data in reverse order, queues handle data in the order it was added.
They’re especially useful in scheduling systems, task management, and algorithmic problems like breadth‑first search (BFS).
Our Queue basic structure is similar to our stack:
class Queue {
#data = [];
#length = 0;
#limit = 0;
constructor(limit = 0) {
this.#limit = limit;
}
}
enqueue()
enqueue() adds a new item to the end of the queue.
It has a time complexity of O(1) because we append directly to the array, and space complexity of O(1) since only one value is added each time.
If the optional limit is reached, the method prevents additional insertions.
class Queue {
#data = [];
#length = 0;
#limit = 0;
constructor(limit = 0) {
this.#limit = limit;
}
enqueue(value) {
if (!this.checkLimit()) {
console.log("Limit reached");
return;
}
this.#data.push(value);
this.#length++;
}
}
dequeue()
dequeue() removes the first element from the queue, the item that’s been waiting the longest.
This operation has a time complexity of O(n) since each removal from the start of an array shifts the remaining elements one position forward.
Its space complexity remains O(1) because we’re not allocating new space.
class Queue {
#data = [];
#length = 0;
#limit = 0;
constructor(limit = 0) {
this.#limit = limit;
}
dequeue() {
if (this.#length === 0) {
console.log("Queue empty");
return;
}
this.#length--;
return this.#data.shift();
}
}
peek()
peek() looks at the first item in the queue without removing it.
It has O(1) time and space complexity since it only accesses the first element.
class Queue {
#data = [];
#length = 0;
#limit = 0;
constructor(limit = 0) {
this.#limit = limit;
}
peek() {
return this.#data[0];
}
}
isEmpty()
isEmpty() checks whether the queue currently contains any items.
Both time and space complexity are O(1).
class Queue {
#data = [];
#length = 0;
#limit = 0;
constructor(limit = 0) {
this.#limit = limit;
}
isEmpty() {
return this.#length === 0;
}
}
size()
size() returns the number of items currently in the queue.
This operation is also O(1) for both time and space.
class Queue {
#data = [];
#length = 0;
#limit = 0;
constructor(limit = 0) {
this.#limit = limit;
}
size() {
return this.#length;
}
}
Here’s the complete implementation of our Queue class with all methods combined, including the checkLimit() method to prevent overfilling.
class Queue {
#data = [];
#length = 0;
#limit = 0;
constructor(limit = 0) {
this.#limit = limit;
}
enqueue(value) {
if (!this.checkLimit()) {
console.log("Limit reached");
return;
}
this.#data.push(value);
this.#length++;
}
dequeue() {
if (this.#length === 0) {
console.log("Queue empty");
return;
}
this.#length--;
return this.#data.shift();
}
peek() {
return this.#data[0];
}
checkLimit() {
if (this.#limit === 0) return true;
return this.#length < this.#limit;
}
isEmpty() {
return this.#length === 0;
}
size() {
return this.#length;
}
}
const queue = new Queue(3);
queue.enqueue("Task 1");
queue.enqueue("Task 2");
queue.enqueue("Task 3");
console.log(queue.peek()); // Task 1
console.log(queue.size()); // 3
queue.dequeue();
console.log(queue.peek()); // Task 2
console.log(queue.isEmpty()); // false
Queues are incredibly common in real‑world programming, like handling asynchronous requests, simulating waiting lines or processes and breadth‑first search (BFS) in data structures like trees and graphs.
In short, if you ever need to process items in the order they arrive, a queue is the perfect tool.
Conclusion
We’ve now added three powerful data structures to your DSA toolkit: hashmaps, stacks, and queues.
Each one solves a different problem: hashmaps for quick lookups, stacks for working in reverse order, and queues for processing tasks in order.
If you enjoyed this post and want to keep learning, check out my other blogs in this series:
Thanks for reading and happy coding as you keep building your DSA toolkit!
Top comments (0)