Home Sorting & Recursion
Post
Cancel
Sorting & Recursion | SEG

Sorting & Recursion

This is part 2 of our Data Structures and Algorithms series. This post focuses on Sorting and Recursion.

What is Sorting?

Sorting is the process of rearranging elements in a particular order. It can be ascending or descending depending on the functionality we want to achieve.

The primary goal of sorting is to transform an unsorted collection of elements into a sorted sequence that satisfies a specific order, such as numerical, alphabetic, or based on a custom comparison function.

Sorting is used to make data easier to work with and to enable faster retrieval of information.

Sorting algorithms vary in terms of their efficiency, stability, memory usage, and adaptability to different types of data.

Common sorting algorithms include:

  • Bubble Sort
  • Selection Sort
  • Insertion Sort
  • Merge Sort
  • Quick Sort
  • Heap Sort
  • Counting Sort
  • Radix Sort

There’s a lot more sorting algorithms but this post focuses mainly on beginner knowledge.

What are Sorting characteristics?

Sorting algorithms have the following characteristics:

Stability - A sorting algorithm is stable if the relative order of equal elements remains unchanged after sorting. In other words, if two elements have the same key and one comes before the other in the original sequence, the same order should be maintained in the sorted sequence.
Comparison-Based - Many sorting algorithms compare elements using comparison operators (e.g., less than, greater than). Comparison-based sorting algorithms have a lower bound of O(n log n) for average and worst-case time complexity.
Adaptability - An adaptable sorting algorithm becomes more efficient when sorting partially sorted or nearly sorted data. Algorithms like Insertion Sort and Bubble Sort are adaptable since they have better performance when applied to already partially sorted data.
In-Place - It uses a constant amount of additional memory to rearrange elements. In-place sorting algorithms are memory-efficient and do not require extra space proportional to the input size.
External Sorting - Use external storage like disks to manage and merge segments of data. Sorting algorithms that handle data too large to fit into memory are called external sorting algorithms.
Time Complexity - Sorting algorithms are classified based on their average, best, and worst-case time complexities. Algorithms with lower time complexities are generally preferred for large datasets.
Space Complexity - Sorting algorithms use memory for temporary storage during the sorting process. Algorithms with lower space complexity are more memory-efficient.
Comparison Count - The number of comparisons performed by a sorting algorithm. An important metric for efficiency, especially for comparison-based algorithms.
Data Movement Count - The number of data movements (swaps or assignments). Another key efficiency metric.
Inherent Parallelism - Take advantage of multiple processing units. Some sorting algorithms have inherent parallelism, meaning they can be parallelized to take advantage of multiple processing units.
Distribution of Data - Have better performance based on data distributions. Some sorting algorithms perform better on specific types of data distributions, like partially sorted or reverse-sorted data.
Ease of Implementation - Some sorting algorithms are simpler to implement than others. This characteristic affects the development time and maintenance of code.

Sorting in action - Bubble Sort - Code example

Bubble Sort repeatedly steps through the list, compares adjacent elements, and swaps them if they are in the wrong order.

Array Animation

Here we prepared a straightforward sorting example in Java of bubble sort, let’s look at the whole thing first:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class BubbleSort {

  public static void main(String[] args) {
    int[] arr = {64, 34, 25, 12, 22, 11, 90};

    System.out.println("Original array:");
    for (int num : arr) {
      System.out.print(num + " ");
    }

    bubbleSort(arr);

    System.out.println("\nSorted array:");
    for (int num : arr) {
      System.out.print(num + " ");
    }
  }
  
  public static void bubbleSort(int[] arr) {
    int n = arr.length;
    boolean swapped;

    for (int i = 0; i < n - 1; i++) {
      swapped = false;

      for (int j = 0; j < n - i - 1; j++) {
        if (arr[j] > arr[j + 1]) {
          // Swap arr[j] and arr[j+1]
          int temp = arr[j];
          arr[j] = arr[j + 1];
          arr[j + 1] = temp;
          swapped = true;
        }
      }

      // If no two elements were swapped in the inner loop, the array is already sorted
      if (!swapped) {
        break;
      }
    }
  }
}

And now let’s try to understand it step-by-step.

Main function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  public static void main(String[] args) {
    int[] arr = {64, 34, 25, 12, 22, 11, 90};

    System.out.println("Original array:");
    for (int num : arr) {
      System.out.print(num + " ");
    }

    bubbleSort(arr);

    System.out.println("\nSorted array:");
    for (int num : arr) {
      System.out.print(num + " ");
    }
  }

This section of code is responsible for performing the following operations:

  • Declares an integer array arr with unsorted numbers.
  • Prints the original array using a for-each loop.
  • Calls the bubbleSort method to sort the array.
  • Prints the sorted array after sorting is completed.

Sorting function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  public static void bubbleSort(int[] arr) {
    int n = arr.length;
    boolean swapped;

    for (int i = 0; i < n - 1; i++) {
      swapped = false;

      for (int j = 0; j < n - i - 1; j++) {
        if (arr[j] > arr[j + 1]) {
          // Swap arr[j] and arr[j+1]
          int temp = arr[j];
          arr[j] = arr[j + 1];
          arr[j + 1] = temp;
          swapped = true;
        }
      }

      // If no two elements were swapped in the inner loop, the array is already sorted
      if (!swapped) {
        break;
      }
    }
  }

This section of code is responsible for performing the following operations:

  • Finds the length (n) of the array.
  • Outer loop (i loop) runs n-1 times to control sorting passes.
  • Inner loop (j loop) compares adjacent elements and swaps them if needed.
  • Applies optimization:
    • Uses a swapped flag to check if any swaps were made.
    • If no swaps occur in a pass, the array is already sorted, and the loop exits early.

What kind of problems can be solved with sorting?

Sorting is a fundamental operation in computer science, and it helps solve a variety of problems across different domains.

Below are some key problem types that can be efficiently solved using sorting.

Search Optimization - Sorted data allows for efficient binary search. Reduces the number of comparisons needed to find a specific value in a dataset.
Data Analysis - Sorting enables data to be presented in an organized manner. Making it easier to identify patterns, trends, and outliers.
Duplicate Removal - Sorting simplifies the process. Sorting simplifies the process of identifying and removing duplicates from a dataset, improving data quality.
Ranking and Leaderboards - Sorting is used to rank items. Sorting is used to rank items based on specific criteria, such as scores, ratings, or popularity, creating leaderboards or top-N lists.
Database Querying - Sorting improves query performance. Sorted indices in databases improve query performance by allowing faster retrieval and range queries.
Median and Quartile Calculation - Sorting simplifies statistical analysis calculation. Sorting simplifies the calculation of median and quartiles for statistical analysis.
Data Transformation - Sorting facilitates data processing. Sorting can be used to reorganize data into a different format or structure, facilitating further processing.
Merge and Intersection Operations - Sorting facilitates operations. Sorted data sets can be efficiently merged or intersected, enabling operations like merging multiple lists or finding common elements between sets.
Frequency Counting - Sorting facilitates counting the frequency. Sorting aids in counting the frequency of occurrences of different elements in a dataset.
Data Visualization - Sorting enhances data visualization. Sorted data enhances data visualization, making it easier to create histograms, charts, and graphs.
Range Queries - Sorting allows for efficient range queries. In applications like databases or geographic information systems, sorting allows for efficient range queries.
Load Balancing - Sorting can help balance the load. In distributed systems, sorting can help balance the load by evenly distributing data based on a certain key.
Event Scheduling - Sorting can help schedule events in chronological order. Sorting can be used to schedule events in chronological order, such as appointments, tasks, or events on a timeline.
Optimal Solutions - Sorting can help finding the optimal arrangement of items. Some problems require finding the optimal arrangement of items, which can be aided by sorting. For example, the "closest pair of points" problem involves sorting points by their coordinates to enable efficient searching for close pairs.
Network Routing - Sorting can help optimizing routes. Sorting can help in optimizing routes and paths in network routing problems.
Data Reduction - Sorting simplify data reduction techniques. Sorted data can simplify data reduction techniques, like summarizing large datasets by extracting important points or trends.
Data Cleaning - Sorting can assist with anomalies. Sorting can assist in identifying and correcting anomalies or outliers in data.
Finding Modes - Sorting can assist in find modes. Sorting makes it easier to find modes in a dataset. For example, find the most frequently occurring values.

What is Recursion?

Recursion is a programming technique where a function calls itself to solve a problem.

In other words, a recursive function is one that includes calls to itself within its own definition.

Recursion provides an elegant way to solve problems that can be broken down into smaller instances of the same problem.

There is a long list of commonly used techniques like backtracking, or brute force. These advance concepts will be explored in the future.

What are Recursion characteristics?

Recursion has the following characteristics:

Base Case - The termination condition for the recursion. Every recursive function must have a base case, which defines the simplest scenario where the function does not call itself. It acts as the termination condition for the recursion and prevents infinite recursion.
Recursive Case - The step that breaks down the problem into subproblems. The recursive case defines how the function should call itself with smaller or simpler inputs to solve a larger problem. This is the step that breaks down the problem into smaller subproblems.
Divide and Conquer - Complex problems divided into smaller subproblems. Recursion often involves dividing a complex problem into smaller subproblems, solving each subproblem recursively, and then combining the results to obtain the final solution.
Function Stack - The data structure that stores information about active subroutines. Recursive calls are managed through a call stack. The call stack is the data structure that stores information about the active subroutines of a computer program. Each call creates a new instance of the function with its own set of parameters and local variables. As recursive calls complete, they are popped off the stack.
Memory Usage - Caused by function call stack. Recursion can consume significant memory, especially for deep levels of recursion, due to the function call stack. This can lead to a stack overflow exceeding the stack limit if not managed properly.
Tail Recursion - The last operation in the function before returning a value. A recursive call is tail recursive if it's the last operation in the function before returning a value. Tail recursion can be optimized by some programming languages to use less memory.
Indirect Recursion - A group of function calling each other. When a group of functions call each other in a circular manner, it's called indirect recursion.

Recursion in action - Code example

Here we prepared a straightforward recursion example in Python:

1
2
3
4
5
6
7
8
9
def factorial(n):
    if n == 0:
        return 1
    else:
        return n * factorial(n - 1)

result = factorial(5)

print(result)

This Python code defines a recursive function to calculate the factorial of a given number.

For factorial(5), the function executes as follows:

1
2
3
4
5
6
factorial(5) = 5 * factorial(4)
factorial(4) = 4 * factorial(3)
factorial(3) = 3 * factorial(2)
factorial(2) = 2 * factorial(1)
factorial(1) = 1 * factorial(0)
factorial(0) = 1  (Base Case)

Now, resolving from the base case upwards:

1
2
3
4
5
factorial(1) = 1 * 1 = 1
factorial(2) = 2 * 1 = 2
factorial(3) = 3 * 2 = 6
factorial(4) = 4 * 6 = 24
factorial(5) = 5 * 24 = 120

This means factorial(5) computes to 5! = 5 × 4 × 3 × 2 × 1 = 120.

Let’s break down each section of the code one by one.

Function Definition

1
def factorial(n):

This defines a function named factorial that takes one argument, n.

Base Case

1
2
if n == 0:
    return 1

If n is 0, the function returns 1. This is the base case of the recursion.

Recursive Case

1
2
else:
    return n * factorial(n - 1)

If n is not 0, the function recursively calls itself with n - 1.

Each recursive call multiplies n by the factorial of n-1.

Calling the Function

1
result = factorial(5)

Calls the factorial function with 5 and stores the result in the result variable.

Printing the Result

1
print(result)

Prints the calculated factorial of 5.

What kind of problems can be solved with recursion?

Recursion is an important concept in computer science, and it helps solve a variety of problems across different domains.

Below are some key problem types that can be efficiently solved using recursion.

Mathematical Problems - Permutations and combinations problems. Calculating factorials, Fibonacci numbers, and exponentiation. Solving problems involving permutations and combinations.
Data Structures - Navigating data structures. Traversing trees. Navigating graphs using depth-first or breadth-first search. BFS: one node is selected at a time, uses queue, slower than DFS. DFS: edge-based, uses stack.
Searching and Sorting - Implementing strategies. Binary search in sorted arrays. Implementing sorting algorithms with divide-and-conquer strategies.
Dynamic Programming - Problems that can be broken into overlapping subproblems. Solving problems that can be broken down into overlapping subproblems, such as the knapsack problem or the longest common subsequence problem.
Backtracking - Problems with constraint-based search strategy. Solving problems with a constraint-based search strategy, like the N-Queens problem or Sudoku.
Recursive Algorithms - Implementation. Implementing recursive algorithms like the Tower of Hanoi or quicksort.
String Manipulation - Involves string reversal and pattern matching. Solving problems involving string reversal, palindrome checking, and pattern matching using recursion.
Combinatorial Problems - Generating all possible combinations. Solving problems related to generating all possible combinations, permutations, or subsets of a set.
Parsing and Expression Evaluation - Using recursive descent parsing. Parsing and evaluating mathematical expressions using recursive descent parsing.
Graph Problems - Finding paths or cycles. Traversing and searching through graphs, such as finding paths or cycles.
Simulation and Gaming - Implementing mechanics. Implementing game mechanics, simulations, and artificial intelligence in games.
Image Processing - Related to image manipulation. Solving problems related to image manipulation, region filling, and contour tracing.
Language Processing - For domain-specific languages. Implementing parsers and interpreters for programming languages and domain-specific languages.
Fractals and Graphics - Generating patterns. Generating fractal patterns and graphics using recursive algorithms.
Tree-Based Algorithms - Implementing algorithms. Implementing algorithms based on trees, such as Huffman coding for data compression.
Dynamic Data Structures - Implementing data structures. Implementing dynamic data structures like linked lists, stacks, and queues.
Simulation and Modeling - Simulating complex systems. Simulating complex systems, natural phenomena, or physical processes using recursion.

These are just a few examples of the many types of problems that can be solved using recursion.

Conclusion

Sorting and recursion are fundamental concepts in computer science that play a crucial role in data organization, algorithm design, and problem-solving.

Sorting helps in arranging data efficiently, leading to faster retrieval and improved performance in various applications. With different sorting algorithms available, each has its strengths and weaknesses, making it essential to choose the most appropriate one based on efficiency, stability, and adaptability.

Recursion, on the other hand, provides a powerful way to break down complex problems into smaller, more manageable subproblems. It is widely used in solving mathematical problems, navigating data structures, implementing search and sorting techniques, and various other computational challenges. Understanding the characteristics and applications of recursion allows developers to write more elegant and efficient code.

By mastering sorting and recursion, programmers can enhance their problem-solving abilities and optimize algorithms for various real-world applications. As these concepts serve as the building blocks for more advanced techniques like dynamic programming and backtracking, a strong grasp of them lays a solid foundation for tackling more complex computational challenges in the future.

Live discussion

Want to see a live discussion on the topic? Check out our YouTube recording:

Stay tuned!

Our journey in the Data Structures and Algorithms world doesn’t end here! In the next installment, we’ll delve into the Big O notation. Stay tuned!

This post is licensed under CC BY 4.0 by the author.