From Clumsy to Clever, How Penguins Learn to Line Up

gayatri kumargayatri kumar
11 min read

Ever seen a bunch of penguins trying to line up from shortest to tallest? At first, it seems like chaos—they shuffle around, bumping into each other, occasionally swapping places, until finally, they all stand in the correct order. That's Bubble Sort for you: simple, clumsy, and a great way to learn the basics of sorting. But just like the penguins can learn to line up better, so can Bubble Sort be optimized and refined.

In this article, we'll explore Bubble Sort in depth, starting from the basics to more optimized approaches. We’ll take a peek at its efficiency, examine code examples in Python, JavaScript, Java, and C++, and learn why it may not always be the best way to get things sorted.

The Simple Stuff - Understanding Bubble Sort

Imagine a line of penguins, all of different heights, and your goal is to sort them from the shortest to the tallest. Initially, the penguins don't know how to line up efficiently. So, they decide to use a simple strategy: compare their heights one by one, and swap places if they're in the wrong order. The biggest penguin gradually "bubbles" to the end of the line, hence the name Bubble Sort.

Algorithm Step-by-Step

1. Pair Comparison: Start from the beginning of the line. Compare each penguin with the next one in line.

2. Swap if Needed: If the left penguin is taller than the right one, they swap places.

3. Bubble to the End: After the first full pass, the tallest penguin will have moved to the end of the line.

4. Repeat & Reduce: Start over from the beginning, ignoring the last sorted penguin(s), until all are in order.


Coding Bubble Sort (Basic Version)

def bubble_sort(arr):
    n = len(arr)
    # Traverse through all array elements
    for i in range(n):
        # Last i elements are already sorted
        for j in range(0, n-i-1):
            # Compare and swap if elements are in the wrong order
            if arr[j] > arr[j+1]:
                arr[j], arr[j+1] = arr[j+1], arr[j]
    return arr

penguin_heights = [3, 1, 5, 2, 4]
print("Unsorted:", penguin_heights)
print("Sorted:", bubble_sort(penguin_heights))

The code iteratively goes through the list, comparing each pair of adjacent elements, and swaps them if they're in the wrong order.

function bubbleSort(arr) {
    let n = arr.length;
    // Traverse through all array elements
    for (let i = 0; i < n; i++) {
        // Last i elements are already sorted
        for (let j = 0; j < n - i - 1; j++) {
            // Compare and swap if elements are in the wrong order
            if (arr[j] > arr[j + 1]) {
                [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
            }
        }
    }
    return arr;
}
let penguinHeights = [3, 1, 5, 2, 4];
console.log("Unsorted:", penguinHeights);
console.log("Sorted:", bubbleSort(penguinHeights));
#include <iostream>
using namespace std;

void bubbleSort(int arr[], int n) {
    // Traverse through all array elements
    for (int i = 0; i < n; i++) {
        // Last i elements are already sorted
        for (int j = 0; j < n - i - 1; j++) {
            // Compare and swap if elements are in the wrong order
            if (arr[j] > arr[j + 1]) {
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

int main() {
    int penguinHeights[] = {3, 1, 5, 2, 4};
    int n = sizeof(penguinHeights) / sizeof(penguinHeights[0]);
    cout << "Unsorted: ";
    for (int i = 0; i < n; i++) cout << penguinHeights[i] << " ";
    cout << endl;
    bubbleSort(penguinHeights, n);
    cout << "Sorted: ";
    for (int i = 0; i < n; i++) cout << penguinHeights[i] << " ";
    cout << endl;
}

Time Complexity, Space Complexity, Advantages & Drawbacks

What Is Time Complexity?

Think of time complexity as a way to measure how long an algorithm takes to finish its task. Imagine our penguins lining up in height order. If you have only 3 penguins, they can compare heights pretty quickly. But what if you had 100 penguins? Or 1,000? The more penguins you have, the longer it takes for all of them to line up correctly.

In Bubble Sort, time complexity tells us how much effort (in terms of comparisons and swaps) it will take to sort an array as its size (number of elements) increases.

Calculating Time Complexity for Bubble Sort

- Worst Case (O(n²)): Let’s break it down simply. If there are n penguins in line, each penguin has to compare itself with the one next to it, all the way to the end of the line. This means, for the first penguin, there are n-1 comparisons, for the second one n-2, and so on. When you add these up, the total comparisons end up being around n × n, which is why the time complexity is called O(n²) (pronounced "O of n squared"). It means that the time taken grows roughly as the square of the number of penguins (or elements).

- Best Case (O(n)): If the penguins are already standing in the correct order, Bubble Sort makes one full pass, realizes there are no swaps needed, and then stops. This means it only takes n comparisons to confirm everything is in order, resulting in a best-case time complexity of O(n).

Summary of Time Complexity

- Worst and Average Case: O(n²) - If the penguins are in random order or even in reverse order, it will take a lot of comparisons and swaps.

- Best Case: O(n) - If the penguins are already lined up correctly, it only takes one pass to confirm they are sorted.

What Is Space Complexity?

Think of space complexity as the amount of extra space (memory) an algorithm needs to get its job done. Imagine that the penguins need not only to line up but also to carry bags for their food. If the penguins can do all their comparisons and swaps without needing to set down their bags or use any extra space, then we say that the space complexity is O(1).

Calculating Space Complexity for Bubble Sort

- In-Place Sorting: Since Bubble Sort does all its work by swapping the penguins (or elements) in their current positions, it doesn’t need any extra storage or "bags." This makes its space complexity O(1), meaning it uses a constant amount of extra space regardless of how many elements (penguins) are being sorted.

Putting It All Together

Time Complexity is like how long it takes for a group of penguins to line up based on their height. The larger the group, the longer it takes—potentially squaring the time needed for each comparison. Space Complexity, on the other hand, is about how much extra "room" or "memory" you need to sort them. In the case of Bubble Sort, the penguins don't need any extra space beyond their current positions, making it efficient in terms of space but not always in terms of time.


Optimizing Bubble Sort

Optimization 1: Early Exit (Smarter Penguins)

In the basic version, even if the list is already sorted, the algorithm continues making passes. To optimize, add a flag to track if any swaps were made during a pass. If no swaps occur, the list is already sorted, and we can break early.

def bubble_sort_optimized(arr):
    n = len(arr)
    for i in range(n):
        swapped = False
        for j in range(0, n-i-1):
            if arr[j] > arr[j+1]:
                arr[j], arr[j+1] = arr[j+1], arr[j]
                swapped = True
        # If no swaps were made, break out early
        if not swapped:
            break
    return arr

penguin_heights = [3, 1, 5, 2, 4]
print("Sorted (Optimized):", bubble_sort_optimized(penguin_heights))

Step-by-Step Explanation

Let’s start with the Early Exit Optimization. In this version, we use a swap flag to check if any swaps happened during a pass. If no swaps occur, the list is already sorted, and we can stop early without going through unnecessary passes.

Let’s take this list of penguin heights: [5, 3, 2, 4, 1].

  1. First Pass (i = 0):

    • Compare 5 and 3: Since 5 is greater than 3, swap them.
      List becomes: [3, 5, 2, 4, 1]

    • Compare 5 and 2: Swap them.
      List becomes: [3, 2, 5, 4, 1]

    • Compare 5 and 4: Swap them.
      List becomes: [3, 2, 4, 5, 1]

    • Compare 5 and 1: Swap them.
      List becomes: [3, 2, 4, 1, 5]

Result after first pass: The largest penguin (5) is now in its correct position. Swap flag is true, meaning there were swaps, so we move on to the next pass.

  1. Second Pass (i = 1):

    • Compare 3 and 2: Swap them.
      List becomes: [2, 3, 4, 1, 5]

    • Compare 3 and 4: No swap needed.

    • Compare 4 and 1: Swap them.
      List becomes: [2, 3, 1, 4, 5]

Result after second pass: The second-largest penguin (4) is now in its correct position. Swap flag is true, so we proceed to the next pass.

  1. Third Pass (i = 2):

    • Compare 2 and 3: No swap needed.

    • Compare 3 and 1: Swap them.
      List becomes: [2, 1, 3, 4, 5]

Result after third pass: The third-largest penguin (3) is now in its correct position. Swap flag is true, so continue.

  1. Fourth Pass (i = 3):

    • Compare 2 and 1: Swap them.
      List becomes: [1, 2, 3, 4, 5]

Result after fourth pass: The second-smallest penguin (2) is now in place, and no further swaps are needed, as the list is fully sorted.

Since no swaps happened in the next pass, we can break early, stopping the algorithm and avoiding unnecessary comparisons.

Effect of Optimization: Reduces the number of unnecessary passes, improving performance in already sorted or nearly sorted lists.


Optimization 2: Bidirectional Bubble Sort (Cocktail Shaker Sort)

Instead of only passing from left to right, also pass from right to left. This way, both the smallest and largest elements find their places faster.

def cocktail_shaker_sort(arr):
    n = len(arr)
    start = 0
    end = n - 1
    while start < end:
        swapped = False
        # Traverse from left to right
        for i in range(start, end):
            if arr[i] > arr[i+1]:
                arr[i], arr[i+1] = arr[i+1], arr[i]
                swapped = True
        end -= 1
        # Traverse from right to left
        for i in range(end, start, -1):
            if arr[i] < arr[i-1]:
                arr[i], arr[i-1] = arr[i-1], arr[i]
                swapped = True
        start += 1
        if not swapped:
            break
    return arr

penguin_heights = [3, 1, 5, 2, 4]
print("Sorted (Cocktail Shaker):", cocktail_shaker_sort(penguin_heights))

Now, let’s understand Cocktail Shaker Sort, which is a bidirectional version of Bubble Sort. It sorts in both directions, first left to right, then right to left, making the sorting process a bit faster.

Let’s take a small list: [5, 3, 4, 2, 1].

  1. First Pass (Left to Right):

  • Compare 5 and 3: Swap them.
    List becomes: [3, 5, 4, 2, 1]

  • Compare 5 and 4: Swap them.
    List becomes: [3, 4, 5, 2, 1]

  • Compare 5 and 2: Swap them.
    List becomes: [3, 4, 2, 5, 1]

  • Compare 5 and 1: Swap them.
    List becomes: [3, 4, 2, 1, 5]

After this pass, the largest element (5) is in its correct position at the end of the list.

  1. Second Pass (Right to Left):

  • Compare 1 and 2: Swap them.
    List becomes: [3, 4, 1, 2, 5]

  • Compare 1 and 4: Swap them.
    List becomes: [3, 1, 4, 2, 5]

  • Compare 1 and 3: Swap them.
    List becomes: [1, 3, 4, 2, 5]

After this pass, the smallest element (1) is now correctly positioned at the start of the list.

  1. Third Pass (Left to Right):

  • Compare 3 and 4: No swap needed.

  • Compare 4 and 2: Swap them.
    List becomes: [1, 3, 2, 4, 5]

Now, the second-largest element (4) is in place.

  1. Fourth Pass (Right to Left):

  • Compare 2 and 3: Swap them.
    List becomes: [1, 2, 3, 4, 5]

The list is now fully sorted.


Drawbacks and Real-World Applications

When (and When Not) to Use Bubble Sort

  • Inefficiency: Bubble Sort is inefficient for large datasets due to its O(n²) time complexity in the worst case.

  • Repeated Passes: It often makes unnecessary passes through the list, even when the list is nearly sorted.

  • Practicality: For practical, large-scale applications, Bubble Sort is rarely used because faster sorting algorithms like Quick Sort or Merge Sort are much more efficient.

Applications of Bubble Sort

  • Educational Tools: Bubble Sort is commonly used in teaching sorting algorithms because of its simplicity and clear swapping process.

  • Small Data Sets: It can work well for small or almost sorted datasets where its inefficiencies won’t have much impact.

  • Visualization: Bubble Sort’s step-by-step approach makes it ideal for animations and visual representations in sorting algorithm lessons.


Innovative Challenges – Test Your Skills!

  1. Challenge: Sorting Game Scores
    You’re building a game leaderboard and need to sort the players’ scores from lowest to highest. Try using Bubble Sort to organize the scores: [20, 15, 30, 10, 25].

  2. Challenge: Alphabetical Order
    You have a list of book titles: [“Zoo”, “Apple”, “Orange”, “Banana”]. Use Bubble Sort to arrange them in alphabetical order!

  3. Challenge: Birthday Sorting
    A class has students with birthdays in different months. Use Bubble Sort to arrange the months: [“March”, “January”, “December”, “July”, “April”].

Try out the challenges and share your solutions in the comments!


Your Sorting Adventure is Complete!

Bubble Sort may not be the fastest swimmer in the ocean of sorting algorithms, but it's a great introduction to understanding the basics of sorting. From simple pair comparisons to clever optimizations, you’ve learned how to get those penguins in line efficiently. And while there are more efficient ways to sort data (more on that in future articles!), Bubble Sort gives you a strong foundation to build upon.

Ready for the next sorting adventure? Subscribe to stay updated on Insertion Sort!

50
Subscribe to my newsletter

Read articles from gayatri kumar directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

gayatri kumar
gayatri kumar