Skip to main content

Session 1

Implementation of Simple Algorithms

This session is to implement small problems such as Euclid's Algorithm for GCD, polynomial expression evaluation through Horner's method, algorithms for evaluation of exponentiation and simple sorting algorithms. Selection sort begins by finding the least element in the array, which is moved to the first position. Then the second least element is searched for and moved to the second position in the array. This process continues until the entire array is sorted.

Objective

  • Implement Euclid's Algorithm to find GCD
  • Implement Horner's method for polynomial expression
  • Compare performance of Horner's method with brute-force method
  • Implement simple sorting algorithms
  • Implement multiplication of two matrices

1. Euclid's Algorithm

Q1: Implement Euclid's Algorithm to find GCD(15265, 15) and calculate the number of times mod and assignment operations will be required.

What is Euclid's algorithm?

Euclid's algorithm is an efficient method for computing the Greatest Common Divisor (GCD) of two integers—the largest number that divides both of them without leaving a remainder.

It is based on the principle that the GCD of two numbers also divides their difference. Specifically, the GCD of a and b is the same as the GCD of b and a mod(b).

Algorithm

The algorithm follows a simple recursive process:

  • Given two numbers a and b (where a > b).
  • Divide a by b and find the remainder r.
  • Replace a with b and b with r.
  • Repeat the process until the remainder is 0.
  • The last non-zero remainder is the GCD.

Example

GCD of 48 and 18

  • 48/18 = 2 with a remainder of 12
  • 18/12 = 1 with a remainder of 6
  • 12/6 = 2 with a remainder of 0

The GCD is 6.

Python Implementation

Euclid.py
		def gcd_with_stats(a, b):
    """
    Computes GCD and tracks modulo and assignment operations.
    """
    mod_count = 0
    assign_count = 0
 
    # Absolute values for mathematical consistency
    a, b = abs(a), abs(b)
    assign_count += 2
 
    while b:
        # Modulo operation
        remainder = a % b
        mod_count += 1
 
        # Assignment operations (a = b, b = remainder)
        a = b
        b = remainder
        assign_count += 2
 
    return a, mod_count, assign_count
 
# Input values
val_a, val_b = 15265, 15
result, mods, assigns = gcd_with_stats(val_a, val_b)
 
print(f"GCD({val_a}, {val_b}) = {result}")
print(f"Modulo Operations: {mods}")
print(f"Assignment Operations: {assigns}")
	

C Implementation

Euclid.c
		#include <stdio.h>
#include <stdlib.h>
 
/**
 * Calculates GCD and updates operation counters via pointers.
 */
long long gcd_metrics(long long a, long long b, int *mods, int *assigns) {
    *mods = 0;
    *assigns = 0;
 
    a = llabs(a);
    b = llabs(b);
    *assigns += 2;
 
    while (b != 0) {
        long long remainder = a % b;
        (*mods)++;
 
        a = b;
        b = remainder;
        *assigns += 2;
    }
 
    return a;
}
 
int main() {
    int mod_count, assign_count;
    long long a = 15265, b = 15;
 
    long long result = gcd_metrics(a, b, &mod_count, &assign_count);
 
    printf("GCD(%lld, %lld) = %lld\n", a, b, result);
    printf("Total Modulo Operations: %d\n", mod_count);
    printf("Total Assignment Operations: %d\n", assign_count);
 
    return 0;
}
	

Rust Implementation

Euclid.rs
		fn gcd_with_metrics(mut a: i64, mut b: i64) -> (i64, u32, u32) {
    let mut mod_ops = 0;
    let mut assign_ops = 0;
 
    // Initial assignments to absolute values
    a = a.abs();
    b = b.abs();
    assign_ops += 2;
 
    while b != 0 {
        let remainder = a % b;
        mod_ops += 1;
 
        a = b;
        b = remainder;
        assign_ops += 2;
    }
 
    (a, mod_ops, assign_ops)
}
 
fn main() {
    let (result, mods, assigns) = gcd_with_metrics(15265, 15);
    println!("GCD: {}", result);
    println!("Modulo Ops: {}, Assignment Ops: {}", mods, assigns);
}
	

2. (i) Horner's Rule

Q2 (i): Implement Horner's rule for evaluating polynomials P(x) = 6x^(6)+5x^(5)+4x^(4)-3x^(3)+2x^(2)+8x^(1)-7 at x=3. Calculate how may times

  • (a) Multiplications and addition operations will take
  • (b) How many times the loop will iterate

What is Horner's Rule?

Horner’s Rule is an efficient algorithm for evaluating polynomials that reduces the number of multiplications required by nesting the terms. Instead of calculating each power of x individually, it rewrites the polynomial in a recursive format.

Mathematical Logic

  • For a polynomial P(x) = a_n x^n + a_(n-1) x^(n-1) + ... + a_1 x + a_0, Horner's Rule transforms it into:
		P(x) = (...((a_n x + a_(n-1))x + a_(n-2))x + ... + a_1)x + a_0
	
  • Given our polynomial:
		P(x) = 6x^6 + 5x^5 + 4x^4 - 3x^3 + 2x^2 + 8x - 7
	
  • The nested form at x=3 is:
		P(3) = (((((6 * 3 + 5) * 3 + 4) * 3 - 3) * 3 + 2) * 3 + 8) * 3 - 7
	

Algorithm

  • Initialize result as the coefficient of the highest degree term a_n
  • For each subsequent coefficient from a_(n-1) down to a_0:
    • Multiply the current result by x.
    • Add the next coefficient to the result.
  • The final result is the value of the polynomial.

Example

Step Operation Current Result
Start Initial value (a​n) 6
1 (6×2)+2 14
2 (14×2)+5 33
3 (33×2)+(−7) 59

Python Implementation

horners-method.py
		def horner_rule_analysis(coeffs, x):
    # n is the degree (length of coeffs - 1)
    # result starts as the first coefficient (6 in this case)
    result = coeffs[0]
 
    mult_count = 0
    add_count = 0
    loop_count = 0
 
    # The loop starts from the second coefficient to the last
    for i in range(1, len(coeffs)):
        loop_count += 1
 
        # Every step involves 1 multiplication and 1 addition
        result = (result * x) + coeffs[i]
 
        mult_count += 1
        add_count += 1
 
    return result, mult_count, add_count, loop_count
 
# P(x) = 6x^6 + 5x^5 + 4x^4 - 3x^3 + 2x^2 + 8x - 7
coeffs = [6, 5, 4, -3, 2, 8, -7]
x_val = 3
 
res, m, a, lp = horner_rule_analysis(coeffs, x_val)
 
print(f"Result: {res}")
print(f"(a) Multiplications: {m}, Additions: {a}")
print(f"(b) Loop Iterations: {lp}")
	

C Implementation

horners-method.c
		#include <stdio.h>
 
void evaluate_horner(int coeffs[], int n, int x) {
    long long result = coeffs[0];
    int mults = 0, adds = 0, iterations = 0;
 
    // The loop iterates from 1 to n-1 (total iterations = degree)
    for (int i = 1; i < n; i++) {
        iterations++;
 
        result = (result * x) + coeffs[i];
 
        mults++; // One multiplication per step
        adds++;  // One addition per step
    }
 
    printf("Result: %lld\n", result);
    printf("(a) Multiplications: %d, Additions: %d\n", mults, adds);
    printf("(b) Loop Iterations: %d\n", iterations);
}
 
int main() {
    int coeffs[] = {6, 5, 4, -3, 2, 8, -7};
    int n = sizeof(coeffs) / sizeof(coeffs[0]);
    evaluate_horner(coeffs, n, 3);
    return 0;
}
	

Rust Implementation

horners-method.rs
		struct Analysis {
    result: i64,
    multiplications: usize,
    additions: usize,
    iterations: usize,
}
 
fn horner_analysis(coeffs: &[i64], x: i64) -> Analysis {
    let mut res = coeffs[0];
    let mut m = 0;
    let mut a = 0;
    let mut i_count = 0;
 
    // Skipping the first element as it's our starting 'res'
    for &c in coeffs.iter().skip(1) {
        i_count += 1;
        res = (res * x) + c;
        m += 1;
        a += 1;
    }
 
    Analysis {
        result: res,
        multiplications: m,
        additions: a,
        iterations: i_count,
    }
}
 
fn main() {
    let coeffs = [6, 5, 4, -3, 2, 8, -7];
    let x = 3;
    let stats = horner_analysis(&coeffs, x);
 
    println!("Result: {}", stats.result);
    println!(
        "(a) Multiplications: {}, Additions: {}",
        stats.multiplications, stats.additions
    );
    println!("(b) Loop Iterations: {}", stats.iterations);
}
	

2. (ii) Brute Force Method

Q2 (ii): Apply a brute force method to implement the above polynomial expression from 2(i) and compare it with Horner's method in terms of number of multiplication operations.

What is Brute Force Method?

In Brute Force, for each term a_k * x^k, you calculate x multiplied by itself k times, then multiply by the coefficient a_k.

Algorithm

  • Initialize total_sum = 0.
  • For each term a_k x^k:
    • Calculate power_val = x^k (using a loop or power function).
    • Calculate term_val = a_k * power_val.
    • Add term_val to total_sum.
  • Return total_sum.

Example

Polynomial: 3x^2 + 2x + 5 at x=4

  • Term 1 (3x^2): 4 * 4 = 16; 16 * 3 = 48 (2 mults)
  • Term 2 (2x^1): 2 * 4 = 8 (1 mult)
  • Term 3 (5): 5 (0 mults)
  • Sum: 48 + 8 + 5 = 61.

Total Multiplications: 2 + 1 = 3. (Horner would only use 2).

Python Implementation

horners-method.py
		def brute_force_analysis(coeffs, x):
    n = len(coeffs) - 1 # The degree of the polynomial (6 in this case)
    total_sum = 0
    mult_count = 0
    add_count = 0
 
    # Loop through each coefficient and its index
    for i, coeff in enumerate(coeffs):
        # Calculate current power: e.g., for the first term (index 0), power is 6
        power = n - i
 
        # Calculate x^power by multiplying x, 'power' number of times
        current_pow_val = 1
        for _ in range(power):
            current_pow_val *= x
            mult_count += 1 # Tracking multiplications for powers
 
        # Multiply coefficient by the calculated power
        if power > 0:
            total_sum += coeff * current_pow_val
            mult_count += 1 # Tracking multiplication by coefficient
        else:
            # Constant term (x^0) has no power or coefficient multiplication
            total_sum += coeff
 
        # Every coefficient after the first one involves an addition operation
        if i > 0:
            add_count += 1
 
    return total_sum, mult_count, add_count
 
# P(x) = 6x^6 + 5x^5 + 4x^4 - 3x^3 + 2x^2 + 8x - 7
coeffs = [6, 5, 4, -3, 2, 8, -7]
res, m, a = brute_force_analysis(coeffs, 3)
 
print(f"Brute Force Result: {res}")
print(f"Total Multiplications: {m}")
print(f"Total Additions: {a}")
	

C Implementation

horners-method.c
		#include <stdio.h>
 
void brute_force_eval(int coeffs[], int n, int x) {
    long long total_sum = 0;
    int mult_count = 0, add_count = 0;
    int degree = n - 1;
 
    // Iterate through the array of coefficients
    for (int i = 0; i < n; i++) {
        int power = degree - i;
        long long current_pow_val = 1;
 
        // Inner loop: Calculate x to the power of 'power'
        // This is the source of extra multiplications compared to Horner
        for (int j = 0; j < power; j++) {
            current_pow_val *= x;
            mult_count++;
        }
 
        // Multiply the resulting power by the coefficient
        if (power > 0) {
            total_sum += (long long)coeffs[i] * current_pow_val;
            mult_count++;
        } else {
            // Constant term (last element)
            total_sum += coeffs[i];
        }
 
        // Add to the running total (Addition operation)
        if (i > 0) add_count++;
    }
 
    printf("Brute Force Result: %lld\n", total_sum);
    printf("Multiplications: %d, Additions: %d\n", mult_count, add_count);
}
 
int main() {
    // Array representing 6x^6 + 5x^5 + 4x^4 - 3x^3 + 2x^2 + 8x - 7
    int coeffs[] = {6, 5, 4, -3, 2, 8, -7};
    brute_force_eval(coeffs, 7, 3);
    return 0;
}
	

Rust Implementation

horners-method.rs
		fn brute_force(coeffs: &[i64], x: i64) {
    let mut total_sum = 0;
    let mut mults = 0;
    let mut adds = 0;
    let n = coeffs.len() - 1; // Highest degree
 
    // Loop through coefficients from highest degree to constant
    for (i, &coeff) in coeffs.iter().enumerate() {
        let power = n - i;
        let mut current_pow_val = 1;
 
        // Manually compute power to track multiplication overhead
        for _ in 0..power {
            current_pow_val *= x;
            mults += 1;
        }
 
        // Perform: coefficient * (x^power)
        if power > 0 {
            total_sum += coeff * current_pow_val;
            mults += 1;
        } else {
            // Constant term (x^0)
            total_sum += coeff;
        }
 
        // Perform the addition to the total sum
        if i > 0 {
            adds += 1;
        }
    }
 
    println!("Brute Force Result: {}", total_sum);
    println!("Multiplications: {}, Additions: {}", mults, adds);
}
 
fn main() {
    let coeffs = [6, 5, 4, -3, 2, 8, -7];
    brute_force(&coeffs, 3);
}
	

Complexity Comparison

Metric Horner's Method Brute Force Method
Multiplications n (6) 2n(n+1)​+n (21)
Additions n (6) n (6)
Time Complexity O(n) O(n2)

Brute Force method is significantly more expensive because the inner loop recalculates powers from scratch for every single term, whereas Horner's Rule reuses the result of the previous multiplication.

Comparison (Honer vs Brute)

Brute Force Multiplications:

  • To calculate x^6, you do 5 multiplications.
  • Then 1 more to multiply by the coefficient 6.
  • Totaling k multiplications for a term of degree k.
  • For degree 6: 6+5+4+3+2+1 = 21 multiplications.

Horner's Method Multiplications:

  • As shown previously, this only requires 6 multiplications.

3. Matrix Multiplication

Q3: Implement matrix multiplication of two matrices A[4,4] & B[4,4] and calculate:
(i) how many times the inner-most and outer-most loops will run
(ii) total number of multiplications and additions in computing the multiplication

Explanation

Matrix multiplication computes a resultant matrix C from two input matrices A and B such that:

		C[i][j] = Σ (A[i][k] × B[k][j])   for k = 0 to n-1
	

For n×n matrices, this requires three nested loops:

  • Outer loop (i): Iterates over rows of A
  • Middle loop (j): Iterates over columns of B
  • Inner-most loop (k): Computes the dot product between the i-th row of A and j-th column of B

Each inner iteration performs one multiplication (A[i][k] * B[k][j]) and one addition (+= to accumulate into C[i][j]). The total time complexity is O(n³) and space complexity is O(n²) for the result matrix.

Algorithm & Pseudocode

		INPUT: A[4][4], B[4][4]
OUTPUT: C[4][4]
 
Initialize C[4][4] with zeros
For i from 0 to 3:
    For j from 0 to 3:
        For k from 0 to 3:
            C[i][j] += A[i][k] * B[k][j]
Return C
	

Answer

(i) Loop Execution Count

Loop Position Iteration Count Reason
Outer-most (i) 4 times Runs once per row of A
Inner-most (k) 64 times Executes for every (i, j) pair: 4 × 4 × 4 = 64

(ii) Total Arithmetic Operations

Operation Count Explanation
Multiplications 64 1 per inner iteration → n³ = 4³ = 64
Additions 64 Each C[i][j] += ... performs 1 accumulation addition per inner step

Execution Trace (Conceptual)

For n=4:

  • i=0: j runs 0..3 → k runs 4 times each → 16 inner executions
  • i=1: same → 16 inner executions
  • i=2: same → 16 inner executions
  • i=3: same → 16 inner executions
  • Total inner executions = 16 × 4 = 64
  • Each inner step: 1 mul + 1 add64 mul + 64 add

Python Implementation

matrix-multiplation.py
		def matrix_multiply_4x4(A, B):
    C = [[0]*4 for _ in range(4)]
    outer_count = inner_count = mul_count = add_count = 0
 
    for i in range(4):
        outer_count += 1
        for j in range(4):
            for k in range(4):
                C[i][j] += A[i][k] * B[k][j]
                inner_count += 1
                mul_count += 1
                add_count += 1
 
    return C, outer_count, inner_count, mul_count, add_count
 
A = [[1,2,3,4], [5,6,7,8], [9,10,11,12], [13,14,15,16]]
B = [[1,0,0,1], [0,1,0,1], [0,0,1,1], [1,1,1,0]]
 
C, outer, inner, mul, add = matrix_multiply_4x4(A, B)
 
print("Result Matrix C:")
for row in C:
    print([f"{x:4}" for x in row])
print(f"\nOuter-most loop runs: {outer} times")
print(f"Inner-most loop runs: {inner} times")
print(f"Total Multiplications: {mul}")
print(f"Total Additions: {add}")
	

C Implementation

matrix-multiplation.c
		#include <stdio.h>
 
int main() {
    int A[4][4] = {{1,2,3,4}, {5,6,7,8}, {9,10,11,12}, {13,14,15,16}};
    int B[4][4] = {{1,0,0,1}, {0,1,0,1}, {0,0,1,1}, {1,1,1,0}};
    int C[4][4] = {0};
 
    int outer_count = 0, inner_count = 0;
    int mul_count = 0, add_count = 0;
 
    for (int i = 0; i < 4; i++) {
        outer_count++;
        for (int j = 0; j < 4; j++) {
            for (int k = 0; k < 4; k++) {
                C[i][j] += A[i][k] * B[k][j];
                inner_count++;
                mul_count++;
                add_count++; // accumulation
            }
        }
    }
 
    printf("Result Matrix C:\n");
    for (int i = 0; i < 4; i++) {
        for (int j = 0; j < 4; j++) printf("%4d ", C[i][j]);
        printf("\n");
    }
    printf("\nOuter-most loop runs: %d times\n", outer_count);
    printf("Inner-most loop runs: %d times\n", inner_count);
    printf("Total Multiplications: %d\n", mul_count);
    printf("Total Additions: %d\n", add_count);
    return 0;
}
	

Rust Implementation

matrix-multiplation.rs
		fn main() {
    let a: [[i32; 4]; 4] = [
        [1, 2, 3, 4],
        [5, 6, 7, 8],
        [9, 10, 11, 12],
        [13, 14, 15, 16],
    ];
    let b: [[i32; 4]; 4] = [[1, 0, 0, 1], [0, 1, 0, 1], [0, 0, 1, 1], [1, 1, 1, 0]];
 
    let mut c = [[0i32; 4]; 4];
    let (mut outer, mut inner, mut mul, mut add) = (0, 0, 0, 0);
 
    for i in 0..4 {
        outer += 1;
        for j in 0..4 {
            for k in 0..4 {
                c[i][j] += a[i][k] * b[k][j];
                inner += 1;
                mul += 1;
                add += 1;
            }
        }
    }
 
    println!("Result Matrix C:");
    for row in &c {
        println!("{:4} {:4} {:4} {:4}", row[0], row[1], row[2], row[3]);
    }
    println!("\nOuter-most loop runs: {} times", outer);
    println!("Inner-most loop runs: {} times", inner);
    println!("Total Multiplications: {}", mul);
    println!("Total Additions: {}", add);
}
	

4. Binary Exponentiation

Q4: Implement left to right and right to left binary exponentiation methods for the following problems:
(i) 4^512
(ii) 3^31
Calculate the followings for problems (i) and(ii)

  • How many times the loops will be executed?
  • How many times the multiplication and additions operations will be executed?

Explanation

Binary exponentiation, also known as exponentiation by squaring, is an efficient algorithm for computing large powers of a number. It reduces the number of multiplications needed by using the properties of exponents.

The algorithm can be implemented in two ways: left-to-right and right-to-left.

  • Left-to-Right: Start from the highest bit of the exponent and work downwards.
  • Right-to-Left: Start from the lowest bit of the exponent and work upwards.
  • Both methods have a time complexity of O(log n) and require O(1) space.
  • The number of multiplications depends on the number of bits in the exponent and the number of 1s in the binary representation of the exponent.
  • For example, for 4^512, the exponent 512 in binary is 1000000000, which has 1 bit set to 1. For 3^31, the exponent 31 in binary is 11111, which has 5 bits set to 1.

Python Implementation

binary-exponentiation.py
		def right_to_left(base, exp):
    """Computes base^exp using Right-to-Left binary exponentiation."""
    result = 1
    running_square = base
    loops, multiplications = 0, 0
 
    temp_exp = exp
    while temp_exp > 0:
        loops += 1
        # If the bit is 1, multiply the result by current square
        if temp_exp % 2 == 1:
            result *= running_square
            multiplications += 1
 
        # Square the base for the next bit
        if temp_exp > 1:
            running_square *= running_square
            multiplications += 1
 
        temp_exp //= 2
    print(f"After processing bit: result={result}")
    return loops, multiplications
 
 
def left_to_right(base, exp):
    """Computes base^exp using Left-to-Right binary exponentiation."""
    result = 1
    loops, multiplications = 0, 0
 
    # Process bits from the Most Significant Bit to the Least
    binary_str = bin(exp)[2:]
    for bit in binary_str:
        loops += 1
        # Always square the result
        result *= result
        multiplications += 1
 
        # If the bit is 1, multiply by the original base
        if bit == "1":
            result *= base
            multiplications += 1
    print(f"After processing bit: result={result}")
    return loops, multiplications
 
 
def run_all():
    problems = [(4, 512), (3, 31)]
    for b, e in problems:
        print(f"--- Problem: {b}^{e} ---")
 
        r_loops, r_mults = right_to_left(b, e)
        print(f"R2L Method: Loops={r_loops}, Multiplications={r_mults}")
 
        l_loops, l_mults = left_to_right(b, e)
        print(f"L2R Method: Loops={l_loops}, Multiplications={l_mults}\n")
 
 
if __name__ == "__main__":
    run_all()
	

C Implementation

binary-exponentiation.c
		#include <stdio.h>
 
// Struct to hold our operation metrics
typedef struct {
    int loops;
    int mults;
} Metrics;
 
Metrics right_to_left(unsigned long long base, unsigned int exp) {
    Metrics m = {0, 0};
    unsigned long long res = 1;
    while (exp > 0) {
        m.loops++;
        if (exp & 1) {
            res *= base;
            m.mults++;
        }
        if (exp > 1) {
            base *= base;
            m.mults++;
        }
        exp >>= 1;
    }
    return m;
}
 
Metrics left_to_right(unsigned long long base, unsigned int exp) {
    Metrics m = {0, 0};
    unsigned long long res = 1;
    int msb = 0;
 
    // Find highest bit position
    for (int i = 31; i >= 0; i--) {
        if ((exp >> i) & 1) {
            msb = i;
            break;
        }
    }
 
    for (int i = msb; i >= 0; i--) {
        m.loops++;
        res *= res;
        m.mults++;
        if ((exp >> i) & 1) {
            res *= base;
            m.mults++;
        }
    }
    return m;
}
 
int main() {
    unsigned long long bases[] = {4, 3};
    unsigned int exps[] = {512, 31};
 
    for (int i = 0; i < 2; i++) {
        printf("--- Problem: %llu^%u ---\n", bases[i], exps[i]);
 
        Metrics r = right_to_left(bases[i], exps[i]);
        printf("R2L Method: Loops=%d, Multiplications=%d\n", r.loops, r.mults);
 
        Metrics l = left_to_right(bases[i], exps[i]);
        printf("L2R Method: Loops=%d, Multiplications=%d\n\n", l.loops, l.mults);
    }
    return 0;
}
	

Rust Implementation

binary-exponentiation.rs
		fn right_to_left(mut base: u128, mut exp: u32) -> (u32, u32) {
    let (mut res, mut loops, mut mults) = (1u128, 0, 0);
    while exp > 0 {
        loops += 1;
        if exp % 2 == 1 {
            res = res.wrapping_mul(base);
            mults += 1;
        }
        if exp > 1 {
            base = base.wrapping_mul(base);
            mults += 1;
        }
        exp /= 2;
    }
    (loops, mults)
}
 
fn left_to_right(base: u128, exp: u32) -> (u32, u32) {
    let (mut res, mut loops, mut mults) = (1u128, 0, 0);
    let bit_len = 32 - exp.leading_zeros();
 
    for i in (0..bit_len).rev() {
        loops += 1;
        res = res.wrapping_mul(res); // Square
        mults += 1;
        if (exp >> i) & 1 == 1 {
            res = res.wrapping_mul(base); // Multiply by original base
            mults += 1;
        }
    }
    (loops, mults)
}
 
fn main() {
    let problems = [(4, 512), (3, 31)];
    for (b, e) in problems {
        println!("--- Problem: {}^{} ---", b, e);
        let r = right_to_left(b, e);
        let l = left_to_right(b, e as u32);
        println!("R2L: Loops={}, Multiplications={}", r.0, r.1);
        println!("L2R: Loops={}, Multiplications={}\n", l.0, l.1);
    }
}
	

5. Bubble Sort

Q5: Implement Bubble Sort algorithm for the following list of numbers:
55 25 15 40 60 35 17 65 75 10
Calculate
(i) a number of exchange operations
(ii) a number oftimes comparison operations
(iii) a number of times the inner and outer loops will iterate?

Explanation

Bubble Sort is a simple sorting algorithm that repeatedly steps through the list, compares adjacent elements and swaps them if they are in the wrong order. The process is repeated until the list is sorted. The algorithm gets its name because smaller elements "bubble" to the top of the list.

  • The outer loop runs n-1 times, where n is the number of elements in the list.
  • The inner loop runs n-i-1 times for each iteration of the outer loop, where i is the current iteration of the outer loop.
  • The number of comparisons is the sum of the inner loop iterations, which is n(n-1)/2 in the worst case.
  • The number of exchanges depends on the initial order of the elements. In the worst case (when the list is in reverse order), the number of exchanges is also n(n-1)/2.

Python Implementation

bubble-sort.py
		def bubble_sort(numbers):
    n = len(numbers)
    # Counters for the requested metrics
    exchanges = 0
    comparisons = 0
    outer_iterations = 0
    inner_iterations = 0
 
    # The outer loop runs 'n' times (once for each element)
    for i in range(n):
        outer_iterations += 1
 
        # The inner loop runs from the start to the 'unsorted' portion.
        # We subtract 'i' because the last 'i' elements are already sorted.
        for j in range(0, n - i - 1):
            inner_iterations += 1
            comparisons += 1
 
            # Comparison: Is the current element larger than the next?
            if numbers[j] > numbers[j + 1]:
                # Exchange: Swap the elements
                numbers[j], numbers[j + 1] = numbers[j + 1], numbers[j]
                exchanges += 1
 
    return exchanges, comparisons, outer_iterations, inner_iterations
 
 
# Input data
data = [55, 25, 15, 40, 60, 35, 17, 65, 75, 10]
ex, comp, out, inn = bubble_sort(data)
 
print(f"Sorted: {data}")
print(f"Exchanges: {ex}, Comparisons: {comp}, Outer: {out}, Inner: {inn}")
	

C Implementation

bubble-sort.c
		#include <stdio.h>
 
void bubble_sort(int arr[], int n) {
    int exchanges = 0, comparisons = 0;
    int outer_iters = 0, inner_iters = 0;
 
    // Outer loop: Each pass ensures the i-th largest element is in place
    for (int i = 0; i < n; i++) {
        outer_iters++;
 
        // Inner loop: Reduces range as elements are sorted at the end
        for (int j = 0; j < n - i - 1; j++) {
            inner_iters++;
            comparisons++;
 
            // Comparison
            if (arr[j] > arr[j + 1]) {
                // Exchange logic using a temporary variable
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
                exchanges++;
            }
        }
    }
 
    printf("Metrics for Bubble Sort:\n");
    printf("(i)   Exchanges: %d\n", exchanges);
    printf("(ii)  Comparisons: %d\n", comparisons);
    printf("(iii) Outer Iterations: %d, Inner Iterations: %d\n", outer_iters, inner_iters);
}
 
int main() {
    int data[] = {55, 25, 15, 40, 60, 35, 17, 65, 75, 10};
    int size = sizeof(data) / sizeof(data[0]);
    bubble_sort(data, size);
    return 0;
}
	

Rust Implementation

bubble-sort.rs
		fn bubble_sort(arr: &mut [i32]) {
    let n = arr.len();
    let (mut exchanges, mut comparisons) = (0, 0);
    let (mut outer_iters, mut inner_iters) = (0, 0);
 
    // Outer loop: Tracks how many elements are already placed at the end
    for i in 0..n {
        outer_iters += 1;
 
        // Inner loop: Comparison limit decreases as 'i' increases
        for j in 0..n - i - 1 {
            inner_iters += 1;
            comparisons += 1;
 
            if arr[j] > arr[j + 1] {
                // Exchange using Rust's safe swap method
                arr.swap(j, j + 1);
                exchanges += 1;
            }
        }
    }
 
    println!("Exchanges: {}, Comparisons: {}", exchanges, comparisons);
    println!(
        "Outer Loop Iters: {}, Inner Loop Iters: {}",
        outer_iters, inner_iters
    );
}
 
fn main() {
    let mut nums = [55, 25, 15, 40, 60, 35, 17, 65, 75, 10];
    bubble_sort(&mut nums);
}
	

6. Selection Sort

Q6: Implement Selection Sort algorithm on a similar set of data as in Q5 and
(i)perform similar operations on the above example
(ii) make a comparison between the two algorithms in terms of best case and worst case complexities.

Explanation

Selection Sort is a simple comparison-based sorting algorithm. It divides the input list into two parts: the sorted part at the left end and the unsorted part at the right end. Initially, the sorted part is empty and the unsorted part is the entire list. The algorithm repeatedly selects the smallest (or largest, depending on sorting order) element from the unsorted part and moves it to the end of the sorted part.

  • The outer loop runs n-1 times, where n is the number of elements in the list.
  • The inner loop runs n-i-1 times for each iteration of the outer loop, where i is the current iteration of the outer loop.
  • The number of comparisons is the same as Bubble Sort, which is n(n-1)/2 in the worst case.
  • The number of exchanges is n-1 in the worst case, as each element is moved at most once.

Python Implementation

selection-sort.py
		def selection_sort_detailed(arr):
    n = len(arr)
    # Metric trackers
    exchanges, comparisons = 0, 0
    outer_iters, inner_iters = 0, 0
 
    # Outer loop: Moves the boundary of the unsorted subarray
    for i in range(n):
        outer_iters += 1
 
        # Step 1: Assume the first unsorted element is the smallest
        min_index = i
 
        # Step 2: Inner loop to find the actual minimum in the remaining unsorted list
        for j in range(i + 1, n):
            inner_iters += 1
            comparisons += 1
            if arr[j] < arr[min_index]:
                # We found a new minimum; just update the index (no swap yet!)
                min_index = j
 
        # Step 3: Swap the found minimum with the first unsorted element
        # This happens ONLY once per outer loop iteration.
        if min_index != i:
            arr[i], arr[min_index] = arr[min_index], arr[i]
            exchanges += 1
 
    return exchanges, comparisons, outer_iters, inner_iters
 
 
# Run the analysis
data = [55, 25, 15, 40, 60, 35, 17, 65, 75, 10]
ex, comp, out, inn = selection_sort_detailed(data)
print(f"Exchanges: {ex}, Comparisons: {comp}")
	

C Implementation

selection-sort.c
		#include <stdio.h>
 
void selection_sort(int arr[], int n) {
    int exchanges = 0, comparisons = 0, inner_loops = 0;
 
    // Loop through each position in the array
    for (int i = 0; i < n - 1; i++) {
        int min_idx = i;
 
        // Search the unsorted part of the array for the smallest value
        for (int j = i + 1; j < n; j++) {
            inner_loops++;
            comparisons++;
            if (arr[j] < arr[min_idx]) {
                min_idx = j;
            }
        }
 
        // If the smallest value is not in its correct place, swap it
        if (min_idx != i) {
            int temp = arr[min_idx];
            arr[min_idx] = arr[i];
            arr[i] = temp;
            exchanges++;
        }
    }
 
    printf("Result -> Exchanges: %d, Comparisons: %d, Inner Loops: %d\n",
            exchanges, comparisons, inner_loops);
}
	

Rust Implementation

selection-sort.rs
		fn selection_sort_analysis(arr: &mut [i32]) {
    let n = arr.len();
    let (mut ex, mut comp) = (0, 0);
    let (mut out, mut inn) = (0, 0);
 
    // Iterating through the array
    for i in 0..n {
        out += 1;
        let mut min_idx = i;
 
        // The inner loop scans the right side of the current index 'i'
        for j in i + 1..n {
            inn += 1;
            comp += 1;
            // Update min_idx if a smaller value is found
            if arr[j] < arr[min_idx] {
                min_idx = j;
            }
        }
 
        // Only swap if a new minimum was actually found at a different position
        if min_idx != i {
            arr.swap(i, min_idx);
            ex += 1;
        }
    }
    println!(
        "Exchanges: {}, Comparisons: {}, Inner Loops: {}",
        ex, comp, inn
    );
}
	

Comparison of Bubble Sort and Selection Sort

Metric Bubble Sort Selection Sort
Best Case Time Complexity O(n) O(n^2)
Worst Case Time Complexity O(n^2) O(n^2)
Number of Comparisons O(n^2) O(n^2)
Number of Exchanges O(n^2) O(n)
  • Bubble Sort can be optimized to stop if no swaps are made, which gives it a best case time complexity of O(n) when the list is already sorted. Selection Sort, on the other hand, always requires O(n^2) comparisons regardless of the initial order of the elements. However, Selection Sort performs fewer exchanges than Bubble Sort, making it more efficient in terms of the number of swaps, especially for large lists.
  • In summary, Bubble Sort is generally more efficient for small or nearly sorted lists, while Selection Sort can be more efficient for larger lists where the cost of swapping is high.
  • For the given list of numbers, both algorithms will perform the same number of comparisons, but Bubble Sort may perform more exchanges than Selection Sort depending on the initial order of the elements.