Sorting Algorithms — Ordering Security Data
20 min In ProgressLearning Objectives
- Understand why sorting matters before searching in security tools
- Implement bubble sort, merge sort, and quicksort on security data
- Know which algorithm fits which job — and why quicksort wins in practice
Why Sorting Matters in Security
Binary search only works on sorted data. Threat intelligence feeds, blocklists, and log exports arrive unsorted. Before you can scan 1 million IPs in 20 checks instead of 1 million, you need to sort them first. Sorting is also how you surface the worst events fast — highest severity, most frequent attacker, latest timestamp. Three algorithms cover everything you'll encounter: bubble sort (understand it), merge sort (understand recursion in it), quicksort (use it).
# unsorted threat feed — useless for binary search
threats = ["192.168.1.99", "10.0.0.5", "172.16.0.3", "10.0.0.1"]
# sorted — now binary search works in O(log n)
threats.sort()
print(threats)
# ['10.0.0.1', '10.0.0.5', '172.16.0.3', '192.168.1.99']
Bubble Sort — Read It, Don't Ship It
Bubble sort compares adjacent items and swaps them if they're out of order, repeating until the list is sorted. It's the slowest — O(n²) — but the most readable. You'll see it in every algorithms course because the logic is transparent. In security context: sorting a small list of alert severities to find the worst offender. Never use it on large datasets — on 10,000 log entries it makes 100 million comparisons.
def bubble_sort(alerts):
"""Sort alerts by severity — highest first."""
n = len(alerts)
for i in range(n - 1, 0, -1):
for j in range(i):
if alerts[j]['severity'] < alerts[j + 1]['severity']:
alerts[j], alerts[j + 1] = alerts[j + 1], alerts[j]
alerts = [
{"ip": "10.0.0.3", "severity": 3, "event": "port scan"},
{"ip": "10.0.0.1", "severity": 9, "event": "brute force"},
{"ip": "10.0.0.4", "severity": 1, "event": "ping"},
{"ip": "10.0.0.2", "severity": 7, "event": "exploit attempt"},
]
bubble_sort(alerts)
for a in alerts:
print(f"[{a['severity']}] {a['ip']} — {a['event']}")
Merge Sort — Recursion in Action
Merge sort splits the list in half, sorts each half recursively, then merges them back in order. O(n log n) — much faster than bubble sort on large data. This is the algorithm where recursion actually earns its keep. Security use case: merging two sorted threat feeds from different sources into one unified sorted blocklist. The base case is a list of length 1 — already sorted, nothing to do.
def merge_sort(ip_list):
"""Sort a list of IPs — works on any comparable type."""
if len(ip_list) <= 1:
return ip_list # base case — already sorted
mid = len(ip_list) // 2
left = merge_sort(ip_list[:mid]) # recursive case
right = merge_sort(ip_list[mid:])
# merge the two sorted halves
merged = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
merged.append(left[i]); i += 1
else:
merged.append(right[j]); j += 1
merged.extend(left[i:])
merged.extend(right[j:])
return merged
# two threat feeds arriving unsorted
feed_a = ["10.0.0.9", "10.0.0.1", "172.16.0.5"]
feed_b = ["10.0.0.3", "192.168.1.2", "10.0.0.7"]
combined = merge_sort(feed_a + feed_b)
print(combined)
Quicksort — The One You Actually Use
Quicksort picks a pivot, moves everything smaller to the left and everything larger to the right, then recurses on each side. Average O(n log n) — in practice the fastest of the three. Python's built-in sorted() and list.sort() use a variant called Timsort which combines merge sort and insertion sort. Understanding quicksort tells you why sorted() is fast and when to reach for it instead of writing your own. Security use case: sorting a 50,000-entry blocklist before handing it to binary_search().
def quicksort(ip_list):
"""Sort IPs using quicksort — fast on large blocklists."""
if len(ip_list) <= 1:
return ip_list # base case
pivot = ip_list[len(ip_list) // 2]
left = [x for x in ip_list if x < pivot]
middle = [x for x in ip_list if x == pivot]
right = [x for x in ip_list if x > pivot]
return quicksort(left) + middle + quicksort(right)
blocklist = [
"10.0.0.9", "10.0.0.1", "172.16.0.5",
"10.0.0.3", "192.168.1.2", "10.0.0.7"
]
sorted_blocklist = quicksort(blocklist)
print(sorted_blocklist)
# now binary search works correctly
def binary_search(ip_list, target):
low, high = 0, len(ip_list) - 1
while low <= high:
mid = (low + high) // 2
if ip_list[mid] == target: return True
elif ip_list[mid] < target: low = mid + 1
else: high = mid - 1
return False
print(binary_search(sorted_blocklist, "172.16.0.5")) # True
print(binary_search(sorted_blocklist, "10.0.0.99")) # False
Checking If Data Is Already Sorted
Before sorting a large feed, check if it's already sorted — sorting a sorted list wastes time. is_sorted() scans pairs of adjacent items once — O(n). In a threat intel pipeline that runs every 5 minutes, skipping unnecessary sorts adds up.
def is_sorted(ip_list):
"""Return True if list is already in ascending order."""
return all(
ip_list[i] <= ip_list[i + 1]
for i in range(len(ip_list) - 1)
)
sorted_feed = ["10.0.0.1", "10.0.0.5", "192.168.1.99"]
unsorted_feed = ["10.0.0.5", "10.0.0.1", "192.168.1.99"]
print(is_sorted(sorted_feed)) # True — skip sort
print(is_sorted(unsorted_feed)) # False — sort needed
def prepare_blocklist(feed):
"""Sort only if needed, then return ready for binary search."""
return feed if is_sorted(feed) else sorted(feed)
ready = prepare_blocklist(unsorted_feed)
print(ready)
📝 Exercise: Sort and Search a Threat Feed
You receive an unsorted threat feed. Use is_sorted() to check if it needs sorting. If it does, sort it with quicksort(). Then use binary_search() to check three IPs: '10.0.0.7', '172.16.0.1', and '192.168.1.5'. Print BLOCKED or ALLOWED for each.