Algorithm exercises: brute-force#

Learning objectives

By the end of this lab, you will be able to:

  • Trace and modify brute-force algorithms (linear search, insertion sort)

  • Implement efficient in-place sorting algorithms using swap and shift techniques


Part 1: Brute-force exercises#

In Brute-force algorithms, you learned about brute-force algorithms that solve problems by examining all possible solutions. The two main algorithms introduced were linear search and insertion sort. In this section, you will first warm up with some linear search exercises, and then tackle the more challenging task of implementing efficient variants of insertion sort.


Exercise 1.2: Find elements above threshold#

Implement a function find_above_threshold(values, threshold) that returns a list of all values greater than the given threshold.

print(find_above_threshold([15, 22, 8, 31, 17], 20))     # Expected: [22, 31]
print(find_above_threshold([1, 2, 3, 4, 5], 10))        # Expected: []
print(find_above_threshold([100, 50, 75, 25], 50))      # Expected: [100, 75]

Reviewing insertion sort#

Do you remember the insertion sort algorithm from Brute-force algorithms? Here is the implementation presented in the textbook:

def insertion_sort(input_list):
    result = []
    for item in input_list:
        insert_position = len(result)
        for prev_position in reversed(range(insert_position)):
            if item < result[prev_position]:
                insert_position = prev_position
        result.insert(insert_position, item)
    return result

print(insertion_sort([5, 3, 8, 1]))  # [1, 3, 5, 8]
[1, 3, 5, 8]

This implementation is intuitive and was useful for introducing several Python features: the insert() method, the range() function, and the reversed() function. However, it has some inefficiencies:

  1. Extra memory: It creates a new list (result) to store the sorted elements, using additional memory proportional to the input size.

  2. Hidden cost of insert(): The list.insert() method looks simple, but internally it must shift all elements after the insertion point to make room for the new element. For a list with many elements, this shifting operation can be expensive.

For example, if we insert an element at position 0 of a list with 100 elements, Python must move all 100 elements one position to the right. This happens “behind the scenes” but still takes time.

In the following exercises, you will implement more efficient versions that modify the list in place (without creating a new list) and avoid the hidden cost of insert().


Exercise 1.3: In-place insertion sort with swap#

Implement a function insertion_sort_swap(items) that sorts a list in place using element swapping.

Requirements:

  • Do not use the list.insert() method

  • Do not create a second list (modify the input list directly)

  • Use element swapping to move elements to their correct positions

numbers = [5, 3, 8, 1]
insertion_sort_swap(numbers)
print(numbers)  # Expected: [1, 3, 5, 8]

words = ["banana", "apple", "cherry"]
insertion_sort_swap(words)
print(words)  # Expected: ["apple", "banana", "cherry"]

Exercise 1.4: In-place insertion sort with shift#

The swap-based approach works, but each swap requires 3 operations. When moving an element several positions, we can do better.

Consider moving the element 1 from position 3 to position 0 in [3, 5, 8, 1]:

  • With swap: 3 swaps, each requiring 3 operations = 9 operations total

  • With shift: Save 1 once (1 operation), shift 3 elements right (3 operations), place 1 (1 operation) = 5 operations total

Implement a function insertion_sort_shift(items) that uses shifting instead of swapping.

The idea: Instead of swapping adjacent elements repeatedly, save the current element in a variable, shift all larger elements one position to the right, then place the saved element in the correct position.


Exercise 1.5: Sort by string length#

Modify the insertion sort algorithm to sort strings by their length (shortest first) instead of alphabetically.

Implement sort_by_length(strings).

print(sort_by_length(["cat", "elephant", "dog", "a"]))  # Expected: ["a", "cat", "dog", "elephant"]
print(sort_by_length(["ab", "abc", "a"]))              # Expected: ["a", "ab", "abc"]
print(sort_by_length(["same", "size", "word"]))        # Expected: ["same", "size", "word"] (same length, original order)

Exercise 1.6: Find second largest#

Implement a function find_second_largest(numbers) that returns the second largest value in a list with at least two distinct elements.

print(find_second_largest([3, 1, 4, 1, 5, 9, 2, 6]))  # Expected: 6
print(find_second_largest([10, 20]))                   # Expected: 10
print(find_second_largest([5, 5, 5, 3, 3]))           # Expected: 3