Skip to Content
Course content

241: Implementing Data Structures from Scratch

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Look, you've probably been using ArrayList and HashMap since day one of this course. They're great. But there's a specific kind of anxiety that hits when you're in a technical interview or optimizing a high-throughput system and you realize you don't actually know how the machinery under the hood works. You're treating the data structure as a magic box.

I want to break that box open. Instead of just reading a definition, let's try to build something. Let's say we're building a simple task queue for a background processor. We want to add tasks to the end and pull them off the front.

The Performance Wall

My first instinct—the "lazy" engineer approach—is to just use an ArrayList. Let's see what happens when we do that.

List<String> queue = new ArrayList<>();
queue.add("Task 1");
queue.add("Task 2");
queue.add("Task 3");

// Removing from the front
String first = queue.remove(0); 

It works. But here's the problem: every time I call remove(0), Java has to shift every single remaining element one position to the left to fill the gap. If I have a million tasks in there, that's a million operations just to get one item. That's $O(n)$ time complexity, and in a production system, that's a performance nightmare. I need something where removing the head is nearly instantaneous.

Wiring Nodes by Hand

To fix this, I need a structure where elements aren't sitting side-by-side in a contiguous block of memory. I need "nodes" that point to each other. I'll start by defining a tiny helper class. I'm keeping this as a static inner class because it doesn't need to exist outside the context of our list.

class Node {
    String data;
    Node next;

    Node(String data) {
        this.data = data;
        this.next = null;
    }
}

Now, if I just manually link these in a main method, it looks like this:

Node head = new Node("Task 1");
head.next = new Node("Task 2");
head.next.next = new Node("Task 3");

This is a Singly Linked List. It's elegant, but managing these .next.next chains manually in my business logic is a recipe for a headache. I need a wrapper class to handle the pointers so I can just call add() and poll().

Wrapping it in a Manager

I'll create a MyQueue class. I need to keep track of the head (where I take things out) and the tail (where I put things in). If I only tracked the head, adding to the end would require me to loop through the entire list every single time.

class MyQueue {
    private Node head;
    private Node tail;

    public void enqueue(String value) {
        Node newNode = new Node(value);
        if (tail == null) {
            head = tail = newNode;
            return;
        }
        tail.next = newNode;
        tail = newNode;
    }

    public String dequeue() {
        if (head == null) return null;
        
        String data = head.data;
        head = head.next;
        
        if (head == null) {
            tail = null;
        }
        return data;
    }
}

I've just turned an $O(n)$ operation into an $O(1)$ operation. I'm not shifting elements anymore; I'm just moving a reference pointer. It's a massive win.

The "Empty List" Trap

I tried running this with a few dequeue calls, and I immediately hit a problem. I forgot to handle the case where the list becomes empty. If I call dequeue when head is null, I'll get a NullPointerException if I'm not careful.

Wait, I actually handled it with if (head == null) return null;, but what if the user of my class expects an exception instead of a null? In a real-world scenario, returning null can lead to bugs further down the line. I should probably throw a NoSuchElementException to be explicit. It's better to crash early and loudly than to let a null slip through and cause a mysterious failure ten methods later.

This process—trying a built-in, identifying the bottleneck, sketching a raw pointer solution, and then encapsulating it—is exactly how you implement any data structure. Whether it's a Stack, a Doubly Linked List, or a Binary Search Tree, the pattern is the same: manage your references, handle your edge cases (especially the empty and single-element states), and always keep the Big O complexity in mind.




📋 Practical Task

Build a Doubly Linked List for a Music Playlist

A singly linked list is great for a queue, but a music playlist needs to go both directions (Previous and Next). Your task is to implement a Playlist data structure from scratch.

  • Create a SongNode class that contains a String songName and two references: Node next and Node prev.
  • Implement a Playlist class with the following methods:
    • void addSong(String name): Adds a song to the end of the playlist.
    • void playNext(): Moves a "currentSong" pointer to the next node and prints the song name.
    • void playPrevious(): Moves the "currentSong" pointer to the previous node and prints the song name.
  • Ensure that playNext() and playPrevious() handle the boundaries of the list (the start and end) without throwing a NullPointerException.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.