Skip to Content
Course content

121: Implementing a Linked List in Swift

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

A few years ago, I was reviewing a pull request for a junior dev who had built a music playback queue. He was using a standard Swift Array to store the songs. Every time a song finished, he called songs.remove(at: 0) to move to the next track. It worked fine during testing with five songs, but when we tested it with a user's 500-song playlist, the app hitched. That's because removing the first element of an array forces Swift to shift every single remaining element one position to the left. It's an O(n) operation that kills performance as the list grows.

This is the classic moment where you stop reaching for an array and start thinking about a Linked List. Unlike arrays, which occupy a contiguous block of memory, a linked list is a series of independent nodes scattered in memory, each holding a reference to the next one. To remove the first item, you don't shift anything; you just tell the list that the second item is now the first. It's an O(1) operation, and it's incredibly efficient for queues.

Defining the Atomic Node

To get this working in Swift, we first need a Node. Because a node needs to reference another node of the same type, we have to use a class. Structs won't work here because they are value types and cannot be recursive without indirect pointers.

class Node<T> {
    var value: T
    var next: Node?

    init(value: T, next: Node? = nil) {
        self.value = value
        self.next = next
    }
}

I used a generic T here because a linked list shouldn't care if it's storing Strings, Ints, or Song objects. The next property is optional because the very last node in your list—the tail—will always point to nil.

Wrapping it in a Management Class

You don't want your API users manually messing with Node objects; that's an implementation detail. Instead, we wrap everything in a LinkedList class that manages the head (the start) and the tail (the end). Keeping a reference to the tail is a pro move—it allows us to append items to the end of the list instantly without having to loop through the entire chain first.

class LinkedList<T> {
    private var head: Node<T&gt>?
    private var tail: Node<T&gt>?

    var isEmpty: Bool {
        return head == nil
    }

    func append(_ value: T) {
        let newNode = Node(value: value)
        if let tailNode = tail {
            tailNode.next = newNode
        } else {
            head = newNode
        }
        tail = newNode
    }

    func popFirst() -> T? {
        guard let headNode = head else { return nil }
        
        head = headNode.next
        if head == nil {
            tail = nil
        }
        return headNode.value
    }
}

Notice how popFirst handles the edge case where the list becomes empty. If the head becomes nil, we have to clear the tail as well, otherwise, we'd be holding onto a reference to a node that is no longer part of the list.

Walking the Chain

The one downside of a linked list is that you can't jump to a specific index like you can with array[42]. You have to start at the head and "walk" through the nodes. I usually implement this by making the list conform to Sequence, but for a basic implementation, a simple while loop does the trick.

func printAll() {
    var current = head
    while let node = current {
        print(node.value)
        current = node.next
    }
}

It's a trade-off. You lose random access, but you gain the ability to grow and shrink your list at the boundaries without the memory overhead of shifting elements. In the music queue example, this change takes the playback transition from a noticeable stutter to a near-instant operation.




📋 Practical Task

Implementing a Music Playlist Queue

Your task is to complete the PlaylistQueue implementation. You need to build a linked list that manages Song objects. Each Song has a title and an artist.

Implement the following in a new Swift file:

  • A Song struct with title and artist properties.
  • A Node class to hold the Song and the reference to the next node.
  • A PlaylistQueue class that provides:
    • An enqueue(song:) method to add a song to the end of the playlist.
    • A dequeue() method that removes and returns the song at the front of the playlist.
    • A peek() method that returns the song at the front without removing it.

Test your implementation by adding three different songs and calling dequeue() twice to ensure the songs are returned in the exact order they were added (First-In, First-Out).

Rating
0 0

There are no comments for now.

to be the first to leave a comment.