Python Implementation of Fundamental Sorting Algorithms

Insertion Sort

Analysis: Maintains a sorted subarray, inserting one element at a time into this subarray while preserving order until completion. This is an in-place sorting algorithm requiring no additional memory space. Time complexity varies based on input randomness - better performance with higher randomness, worse performance with nearly reverse-sorted data.

def insertion_sort(array):
    size = len(array)
    position = 1
    while position < size:
        current_value, index = array[position], position
        while index > 0 and array[index - 1] > current_value:
            array[index] = array[index - 1]
            index -= 1
        array[index] = current_value
        position += 1

Selection Sort

Analysis: In an array of n elements, find the maximum value among the first n items and swap it with the nth position, then find the maximum among the first n-1 items and swap with the (n-1)th position, continuing until completion. Time complexity shows inferior performance compared to insertion sort with highly randomized data, but performs notably better than insertion sort with nearly reverse-sorted arrays.

def selection_sort(array):
    size = len(array)
    counter = 0
    while counter < size:
        max_pos, iterator = 0, 0
        while iterator < size - counter:
            if array[max_pos] < array[iterator]:
                max_pos = iterator
            iterator += 1
        array[size - 1 - counter], array[max_pos] = array[max_pos], array[size - counter - 1]
        counter += 1

Bubble Sort

Analysis: Starting from the first element, if an element is larger than its successor, swap them, then proceed to compare the next pair until completion. Time complexity can be optimized by setting a swap flag during each pass - if no swaps occur during a complete pass, sorting is complete. Therefore, if data is already nearly ordered, bubble sort may complete in shorter time, but in most cases, its efficiency is inferior to other sorting algorithms.

def bubble_sort(array):
    size = len(array)
    start = 0
    while start < size:
        end = size - 1
        has_swapped = False
        while end > start:
            if array[end] < array[end - 1]:
                array[end], array[end - 1] = array[end - 1], array[end]
                has_swapped = True
            end -= 1
        if not has_swapped:
            break
        start += 1

Shell Sort

Shell sort can be viewed as an improvement over insertion sort. While insertion sort compares adjacent elements, shell sort uses a gap sequence that decreases until reaching 1. In shell sort, insertion operations compare elements at intervals of x (where x belongs to the gap sequence). Shell sort performs better than both insertion and selection sorts, with time complexity approximately O(n^1.3).

def shell_sort(array):
    size = len(array)
    interval = size // 2
    while interval > 0:
        index = interval
        while index < size:
            temp_val, pos = array[index], index
            while pos >= interval and array[pos - interval] > temp_val:
                array[pos] = array[pos - interval]
                pos -= interval
            array[pos] = temp_val
            index += interval
        interval //= 2

The gap sequence used here is [size//2, size//4, ..., 1].

Quick Sort

Quick sort embodies the divide-and-conquer approach. First, select a pivot element from the array, place smaller elements to the left of the pivot and larger elements to the right, then recursively apply quick sort to the left and right subarrays until completion. Quick sort's efficiency depends on pivot selection - if the median is consistently chosen, performance is excellent, but if always choosing maximum or minimum values, performance degrades. Quick sort outperforms shell sort with average time complexity of O(n log n). This implementation always selects the first array element as pivot, performing well with highly randomized data but poorly with reverse-sorted data (potentially causing stack overflow).

def quick_sort(array):
    def partition(arr, low_idx, high_idx):
        pivot_element = arr[low_idx]
        while low_idx < high_idx:
            while low_idx < high_idx and arr[high_idx] >= pivot_element:
                high_idx -= 1
            arr[low_idx], arr[high_idx] = arr[high_idx], arr[low_idx]
            while low_idx < high_idx and arr[low_idx] <= pivot_element:
                low_idx += 1
            arr[low_idx], arr[high_idx] = arr[high_idx], arr[low_idx]
        return low_idx

    def _recursive_quick_sort(arr, low_idx, high_idx):
        if low_idx < high_idx:
            pivot_location = partition(arr, low_idx, high_idx)
            _recursive_quick_sort(arr, low_idx, pivot_location - 1)
            _recursive_quick_sort(arr, pivot_location + 1, high_idx)

    _recursive_quick_sort(array, 0, len(array) - 1)

To prevent stack overflow, set the maximum recursion limit:

import sys
sys.setrecursionlimit(3000)

Merge Sort

Merge sort achieves efficiency comparable to quick sort. While quick sort partitions arrays by element values, merge sort partitions by indices, always splitting arrays in half and merging sorted halves. For each partition, recursively call merge sort. Merge sort maintains stable performance with good results for both highly randomized and reverse-sorted data, with time complexity O(n log n).

def merge_sort(array):
    def _combine(left_arr, right_arr):
        i, j, result = 0, 0, []
        while i < len(left_arr) and j < len(right_arr):
            if left_arr[i] < right_arr[j]:
                result.append(left_arr[i])
                i += 1
            else:
                result.append(right_arr[j])
                j += 1
        result.extend(left_arr[i:])
        result.extend(right_arr[j:])
        return result
    
    if len(array) <= 1:
        return array
    
    mid_point = len(array) // 2
    left_half = merge_sort(array[:mid_point])
    right_half = merge_sort(array[mid_point:])
    
    return _combine(left_half, right_half)

Heap Sort

Heap sort utilizes heap data structure. Since Python's standard library heapq includes heap sort algorithms, the implementation can be concise. Heap sort maintains a binary heap (either min or max heap), repeatedly extracting the smallest element. Heap sort remains stable with good performance in both high randomness and reverse-sorted scenarios, with time complexity O(n log n).

Import the module first:

from heapq import *

def python_heap_sort(array):
    heapify(array)
    return [heappop(array) for i in range(len(array))]

A binary heap is a complete binary tree where root elements are always smaller than child elements (min heap). Building heap from array takes linear time complexity, while extracting elements takes logarithmic time. Complete binary trees can be stored using arrays.

class BinaryHeap:
    def __init__(self):
        self.storage = [0]
        self.count = 0

    def bubble_up(self, idx):
        while idx // 2 > 0:
            if self.storage[idx] < self.storage[idx // 2]:
                self.storage[idx], self.storage[idx // 2] = self.storage[idx // 2], self.storage[idx]
            idx //= 2

    def find_min_child(self, idx):
        if idx * 2 > self.count:
            return None
        elif idx * 2 + 1 > self.count:
            return idx * 2
        else:
            if self.storage[idx * 2] <= self.storage[idx * 2 + 1]:
                return idx * 2
            else:
                return idx * 2 + 1

    def bubble_down(self, idx):
        while True:
            min_child = self.find_min_child(idx)
            if not min_child:
                break
            if self.storage[idx] > self.storage[min_child]:
                self.storage[idx], self.storage[min_child] = self.storage[min_child], self.storage[idx]
            idx = min_child

    def add(self, value):
        self.storage.append(value)
        self.count += 1
        self.bubble_up(self.count)

    def extract(self):
        if self.count == 0:
            raise IndexError('The heap is empty')
        item = self.storage[1]
        self.remove(item)
        return item

    def remove(self, value):
        pos = self.storage.index(value)
        self.storage[pos] = self.storage[self.count]
        self.storage.pop()
        self.count -= 1
        self.bubble_down(pos)

    def build_heap(self, arr):
        self.storage = [0] + arr[:]
        self.count = len(arr)
        index = self.count // 2
        while index > 0:
            self.bubble_down(index)
            index -= 1

    def display(self):
        print(self.storage[1:])

    def __bool__(self):
        return bool(self.count)

def custom_heap_sort(array):
    heap_obj, output = BinaryHeap(), []
    heap_obj.build_heap(array)
    while heap_obj:
        output.append(heap_obj.extract())
    return output

These are common sorting algorithms. Let's test their performance.

import random
from time import time

num_elements = 20000
test_list = [random.random() for i in range(num_elements)]
random.shuffle(test_list)

functions = [list.sort, python_heap_sort, quick_sort, merge_sort, custom_heap_sort, shell_sort, insertion_sort, selection_sort, bubble_sort]

for function in functions:
    start_time = time()
    function(test_list.copy())
    end_time = time()
    print(f"{function.__name__}: {end_time - start_time}")

With high rnadomness, results show:

python_heap_sort: 0.01575779914855957
quick_sort: 0.024766206741333008
merge_sort: 0.02457451820373535
custom_heap_sort: 0.06661224365234375
shell_sort: 6.078854084014893
insertion_sort: 14.36155891418457
selection_sort: 33.40423130989075
bubble_sort: 48.74769401550293

With reverse-sorted data:

num_elements = 20000
test_list = [random.random() for i in range(num_elements)]
test_list.sort(reverse=True)

functions = [list.sort, python_heap_sort, quick_sort, merge_sort, custom_heap_sort, shell_sort, insertion_sort, selection_sort, bubble_sort]

for function in functions:
    start_time = time()
    function(test_list.copy())
    end_time = time()
    print(f"{function.__name__}: {end_time - start_time}")

python_heap_sort: 0.003007173538208008
quick_sort: 5.107363939285278
merge_sort: 0.014906883239746094
custom_heap_sort: 0.05798840522766113
shell_sort: 9.991580724716187
insertion_sort: 17.121063709259033
selection_sort: 12.812097787857056
bubble_sort: 25.45111322402954

Tags: python sorting-algorithms insertion-sort selection-sort bubble-sort

Posted on Sat, 01 Aug 2026 16:46:37 +0000 by abhishekphp6