Last modified: December 05, 2024

This article is written in: 🇺🇸

Introduction to Data Structures & Algorithms

Data structures and algorithms are fundamental concepts in computer science that are key to building efficient software.

Data Structures

A data structure organizes and stores data in a way that allows efficient access, modification, and processing. The choice of the appropriate data structure depends on the specific use case and can significantly impact the performance of an application. Here are some common data structures:

  1. An array is like a row of lockers, each numbered in order, where each locker can hold one item of the same type. Formally, an array is a contiguous block of memory that stores a fixed-size sequence of elements of the same type. Arrays are effective for quick access to data by index, but their size cannot change once set, making them less flexible for adding or removing elements.
  2. A stack is like a stack of plates. You add new plates on top (push), and take plates from the top (pop). This follows the "Last-In, First-Out" (LIFO) principle, meaning the last item added is the first one removed. Stacks are commonly used in programming for managing function calls (function call stack) and 'undo' operations in software applications.
  3. A queue is like a line at a checkout counter. People join at the back (enqueue) and leave from the front (dequeue). This follows the "First-In, First-Out" (FIFO) principle, where the first item added is the first one removed. Queues are useful for processing items in the order they arrive, such as task scheduling or event handling in computing systems.
  4. A linked list is like a treasure hunt where each clue (node) points to the next one. Each node contains a value and a reference (pointer) to the next node. This makes linked lists dynamic and efficient for inserting and removing elements at any position in the list.
  5. A tree is like a family tree, starting with one person (root) and branching out to children (nodes), with each node possibly having its own children. Formally, a tree is a hierarchical data structure composed of nodes arranged in multiple levels. Trees are useful for representing hierarchical relationships, such as filesystem structures or organizational charts.
  6. A graph is like a network of cities connected by roads. Each city is a node, and each road is an edge connecting two nodes. Edges can be one-way (directed) or two-way (undirected). Graphs are used to model complex relationships and connections between elements, such as social networks, web pages (links), and routes between locations.

ds

Algorithms

Algorithms are step-by-step instructions to solve specific problems or perform tasks. They are everywhere in fields like computer science, mathematics, and engineering. To evaluate how good an algorithm is, we often look at its efficiency in terms of time complexity (how long it takes to run) and space complexity (how much memory it uses).

Think of an algorithm like a recipe for cooking. It consists of a series of steps to follow to achieve a specific result. Here are the key characteristics of a good algorithm:

Algorithms vs. Programs

Understanding the difference between an algorithm and a program helps clarify their roles in problem-solving:

An algorithm is like a high-level blueprint for solving a specific problem. It is abstract and language-independent, detailing a sequence of steps without any specific syntax. You can think of an algorithm as a recipe that outlines a method for solving a problem, and it can be represented in various ways, such as in plain text or as a flowchart.

For example, consider an algorithm for adding two numbers:

Step 1: Start
Step 2: Declare variables num1, num2, and sum.
Step 3: Read values into num1 and num2.
Step 4: Add num1 and num2 and store the result in sum.
Step 5: Print sum
Step 6: Stop

This algorithm can also be shown as a flowchart:

---------------------
|  Start            |
---------------------
        |
        V
-----------------------------
|  Declare num1, num2, sum  |
-----------------------------
        |
        V
------------------------
|  Read num1 and num2  |
------------------------
        |
        V
-----------------------
|  sum = num1 + num2  |
-----------------------
        |
        V
----------------------
|  Print sum         |
----------------------
        |
        V
----------------------
|  Stop              |
----------------------

On the other hand, a program is a concrete, language-dependent implementation of an algorithm. It follows the syntax rules of a particular programming language. For instance, the above algorithm can be implemented in Python as:

num1 = int(input("Enter first number: "))
num2 = int(input("Enter second number: "))
sum = num1 + num2
print("The sum is", sum)

Algorithms are abstract steps that always terminate after a finite number of steps. In contrast, some programs can run indefinitely until an external action stops them. For example, an operating system is a program designed to run continuously in a loop until the computer is turned off.

Types of Algorithms

Algorithms can be classified into various types based on the problems they solve and the strategies they use. Here are some common categories with consistent explanations and examples:

I. Sorting Algorithms arrange data in a specific order, such as ascending or descending. Examples include bubble sort, insertion sort, selection sort, and merge sort.

Example: Bubble Sort

Initial Array: [5, 3, 8, 4, 2]

Steps:
1. Compare adjacent elements and swap if needed.
2. Repeat for all elements.

After 1st Pass: [3, 5, 4, 2, 8]
After 2nd Pass: [3, 4, 2, 5, 8]
After 3rd Pass: [3, 2, 4, 5, 8]
After 4th Pass: [2, 3, 4, 5, 8] (Sorted)

II. Search Algorithms are designed to find a specific item or value within a collection of data. Examples include linear search, binary search, and depth-first search.

Example: Binary Search

Searching 33 in Sorted Array: [1, 3, 5, 7, 9, 11, 33, 45, 77, 89]

Steps:
1. Start with the middle element.
2. If the middle element is the target, return it.
3. If the target is greater, ignore the left half.
4. If the target is smaller, ignore the right half.
5. Repeat until the target is found or the subarray is empty.

Mid element at start: 9
33 > 9, so discard left half
New mid element: 45
33 < 45, so discard right half
New mid element: 11
33 > 11, so discard left half
The remaining element is 33, which is the target.

Graph Algorithms address problems related to graphs, such as finding the shortest path between nodes or determining if a graph is connected. Examples include Dijkstra's algorithm and the Floyd-Warshall algorithm.

Example: Dijkstra's Algorithm

Given a graph with weighted edges, find the shortest path from a starting node to all other nodes.

Steps:
1. Initialize the starting node with a distance of 0 and all other nodes with infinity.
2. Visit the unvisited node with the smallest known distance.
3. Update the distances of its neighboring nodes.
4. Repeat until all nodes have been visited.

Example Graph:
A -> B (1)
A -> C (4)
B -> C (2)
B -> D (5)
C -> D (1)

Starting from A:
- Shortest path to B: A -> B (1)
- Shortest path to C: A -> B -> C (3)
- Shortest path to D: A -> B -> C -> D (4)

String Algorithms deal with problems related to strings, such as finding patterns or matching sequences. Examples include the Knuth-Morris-Pratt (KMP) algorithm and the Boyer-Moore algorithm.

Example: Boyer-Moore Algorithm

Text:    "ABABDABACDABABCABAB"
Pattern: "ABABCABAB"

Steps:
1. Compare the pattern from right to left.
2. If a mismatch occurs, use the bad character and good suffix heuristics to skip alignments.
3. Repeat until the pattern is found or the text is exhausted.

Pattern matched starting at index 10 in the text.

Important Algorithms for Software Engineers

Understanding Algorithmic Complexity

Algorithmic complexity helps us understand the computational resources (time or space) an algorithm needs as the input size increases. Here’s a breakdown of different types of complexity:

Analyzing Algorithm Growth Rates

Understanding how the running time or space complexity of an algorithm scales with increasing input size is pivotal in algorithm analysis. To describe this rate of growth, we employ several mathematical notations that offer insights into the algorithm's efficiency under different conditions.

Big O Notation (O-notation)

The Big O notation represents an asymptotic upper bound, indicating the worst-case scenario for an algorithm's time or space complexity. Essentially, it signifies an upper limit on the growth of a function.

If we designate $f(n)$ as the actual complexity and $g(n)$ as the function in Big O notation, stating $f(n) = O(g(n))$ implies that $f(n)$, the time or space complexity of the algorithm, grows no faster than $g(n)$.

For instance, if an algorithm has a time complexity of $O(n)$, it signifies that the algorithm's running time does not grow more rapidly than a linear function of the input size, in the worst-case scenario.

Big Omega Notation (Ω-notation)

The Big Omega notation provides an asymptotic lower bound that expresses the best-case scenario for the time or space complexity of an algorithm.

If $f(n) = Ω(g(n))$, this means that $f(n)$ grows at a rate that is at least as fast as $g(n)$. In other words, $f(n)$ does not grow slower than $g(n)$.

For example, if an algorithm has a time complexity of $Ω(n)$, it implies that the running time is at the bare minimum proportional to the input size in the best-case scenario.

Theta Notation (Θ-notation)

Theta notation offers a representation of the average-case scenario for an algorithm's time or space complexity. It sets an asymptotically tight bound, implying that the function grows neither more rapidly nor slower than the bound.

Stating $f(n) = Θ(g(n))$ signifies that $f(n)$ grows at the same rate as $g(n)$ under average circumstances. This indicates the time or space complexity is both at most and at least a linear function of the input size.

Remember, these notations primarily address the growth rate as the input size becomes significantly large. While they offer a high-level comprehension of an algorithm's performance, the actual running time in practice can differ based on various factors, such as the specific input data, the hardware or environment where the algorithm is operating, and the precise way the algorithm is implemented in the code.

Diving into Big O Notation Examples

Big O notation is a practical tool for comparing the worst-case scenario of algorithm complexities. Here are examples of various complexities:

The graph below illustrates the growth of these different time complexities:

big_o

The choice of an algorithm significantly impacts the application's performance, making the understanding of time complexity crucial.

Interpreting Big O Notation: Key Rules

Can every problem have an O(1) algorithm?

When do algorithms have O(logn) or O(nlogn) complexity?

The exact time complexity of an algorithm usually stems from how the size of the input affects the execution flow of the algorithm—particularly the loop iterations.

Consider four example algorithms with differing complexities:

I. First Algorithm $O(n)$: Here, the running time is directly proportional to the input size ($n$), as each loop iteration reduces $n$ by 1. Hence, the number of iterations equals the initial value of $n$.

WHILE n > 0:
    n = n - 1

II. Second Algorithm $O(log(n))$: In this case, the running time is proportional to the number of times the loop can iterate before $n$ reduces to 0. Each loop iteration halves the value of $n$. This equals the number of times you can halve $n$ before it becomes 0, which also corresponds to $log(n)$.

WHILE n > 0:
   n = n / 2

III. Third Algorithm $O(nlog(n))$: Here, the outer loop iterates $n$ times, and the inner loop iterates $log(n)$ times for each outer loop iteration. Hence, the total number of iterations is $n * log(n)$.

m = n
WHILE m > 0:
   k = n
   WHILE k > 0:
      k = k / 2
   m = m - 1

IV. Fourth Algorithm $O(log^2(n))$: In this scenario, the outer loop iterates $log(n)$ times, and the inner loop also iterates $log(n)$ times for each outer loop iteration. Consequently, the total number of iterations equals $log^2(n)$.

m = n
WHILE m > 0:
   k = n
   WHILE k > 0:
      k = k / 2
   m = m / 2

Misconceptions

Table of Contents

    Introduction to Data Structures & Algorithms
    1. Data Structures
    2. Algorithms
      1. Algorithms vs. Programs
      2. Types of Algorithms
      3. Important Algorithms for Software Engineers
    3. Understanding Algorithmic Complexity
      1. Analyzing Algorithm Growth Rates
      2. Diving into Big O Notation Examples
      3. Interpreting Big O Notation: Key Rules
      4. Can every problem have an O(1) algorithm?
      5. When do algorithms have O(logn) or O(nlogn) complexity?
    4. Misconceptions