Skip to content

Commit a432bdc

Browse files
Merge pull request #1728 from ruhelamahi7-code/feature/merge-heap-sort-visualizer
Add: Merge Sort and Heap Sort to Multi-Algorithm Sorting Visualizer
2 parents b46abbc + d949304 commit a432bdc

3 files changed

Lines changed: 315 additions & 2 deletions

File tree

math/Heap-Sort/Heap-Sort.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import sys
2+
import os
3+
4+
# Add project root to sys.path
5+
if "__file__" in globals():
6+
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
7+
else:
8+
sys.path.append(os.path.abspath(os.getcwd()))
9+
10+
from utils.validation import get_choice, get_int_list
11+
12+
13+
def _sift_down(arr: list[int], start: int, end: int, reverse: bool) -> None:
14+
"""Restores the heap property for the subtree rooted at `start`.
15+
16+
Args:
17+
arr: The list being heapified (mutated in place).
18+
start: Index of the subtree root to sift down from.
19+
end: Last valid index of the active heap region.
20+
reverse: If True, maintains a min-heap (for descending sort).
21+
Otherwise, maintains a max-heap (for ascending sort).
22+
"""
23+
root = start
24+
25+
while True:
26+
child = 2 * root + 1
27+
if child > end:
28+
break
29+
30+
# Pick the child that should be closer to the root
31+
if child + 1 <= end:
32+
if not reverse and arr[child] < arr[child + 1]:
33+
child += 1
34+
elif reverse and arr[child] > arr[child + 1]:
35+
child += 1
36+
37+
should_swap = (not reverse and arr[root] < arr[child]) or (reverse and arr[root] > arr[child])
38+
39+
if should_swap:
40+
arr[root], arr[child] = arr[child], arr[root]
41+
root = child
42+
else:
43+
break
44+
45+
46+
def heap_sort(arr: list[int], reverse: bool = False) -> list[int]:
47+
"""Sorts a list of integers using the Heap Sort algorithm.
48+
49+
Args:
50+
arr: The list of integers to sort.
51+
reverse: If True, sorts in descending order. Otherwise, ascending.
52+
53+
Returns:
54+
A new sorted list.
55+
"""
56+
result = arr.copy()
57+
n = len(result)
58+
59+
# Phase 1: Build a max-heap (ascending) or min-heap (descending)
60+
for start in range(n // 2 - 1, -1, -1):
61+
_sift_down(result, start, n - 1, reverse)
62+
63+
# Phase 2: Repeatedly extract the root and shrink the heap
64+
for end in range(n - 1, 0, -1):
65+
result[0], result[end] = result[end], result[0]
66+
_sift_down(result, 0, end - 1, reverse)
67+
68+
return result
69+
70+
71+
def main() -> None:
72+
print("=" * 50)
73+
print("🏔️ HEAP SORT INTERACTIVE TOOL 🏔️")
74+
print("=" * 50)
75+
print("Sort a list of numbers in Ascending or Descending order.\n")
76+
77+
while True:
78+
print("=" * 50)
79+
80+
arr = get_int_list(
81+
prompt="➡️ Enter numbers to sort separated by spaces (e.g., 64 34 25): ",
82+
error_empty="❌ Error: Input cannot be empty!",
83+
error_invalid="❌ Error: Please enter valid integers only."
84+
)
85+
86+
print("\nChoose sorting order:")
87+
print("1️⃣ Ascending")
88+
print("2️⃣ Descending")
89+
90+
order_choice = get_choice(
91+
prompt="🎯 Enter your choice (1 or 2): ",
92+
choices=["1", "2"],
93+
error_invalid="❌ Invalid sorting choice! Please select 1 or 2."
94+
)
95+
96+
reverse = (order_choice == "2")
97+
98+
sorted_arr = heap_sort(arr, reverse)
99+
100+
print(f"\n📊 Original list: {arr}")
101+
if reverse:
102+
print(f"✅ Sorted list (Descending): {sorted_arr}")
103+
else:
104+
print(f"✅ Sorted list (Ascending): {sorted_arr}")
105+
106+
again = input("\n🔄 Do you want to sort another list? (y/n): ").strip().lower()
107+
if again != 'y':
108+
print("\n👋 Thanks for using Heap Sort Tool! Goodbye!\n")
109+
break
110+
111+
112+
if __name__ == "__main__":
113+
main()

tests/test_heap_sort.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import unittest
2+
from unittest.mock import patch
3+
import io
4+
import os
5+
import importlib.util
6+
7+
# Absolute path to Heap-Sort.py
8+
file_path = os.path.join(
9+
os.path.dirname(__file__), "..",
10+
"math", "Heap-Sort", "Heap-Sort.py"
11+
)
12+
file_path = os.path.abspath(file_path)
13+
14+
# Load module dynamically
15+
spec = importlib.util.spec_from_file_location("heap_sort_module", file_path)
16+
heap_sort_module = importlib.util.module_from_spec(spec)
17+
spec.loader.exec_module(heap_sort_module)
18+
19+
heap_sort = heap_sort_module.heap_sort
20+
main = heap_sort_module.main
21+
22+
23+
class TestHeapSort(unittest.TestCase):
24+
25+
def test_heap_sort_ascending(self):
26+
self.assertEqual(heap_sort([64, 34, 25, 12, 22, 11, 90]), [11, 12, 22, 25, 34, 64, 90])
27+
self.assertEqual(heap_sort([5, 1, 4, 2, 8]), [1, 2, 4, 5, 8])
28+
29+
def test_heap_sort_descending(self):
30+
self.assertEqual(heap_sort([64, 34, 25, 12, 22, 11, 90], reverse=True), [90, 64, 34, 25, 22, 12, 11])
31+
self.assertEqual(heap_sort([5, 1, 4, 2, 8], reverse=True), [8, 5, 4, 2, 1])
32+
33+
def test_heap_sort_empty_and_single_item(self):
34+
self.assertEqual(heap_sort([]), [])
35+
self.assertEqual(heap_sort([42]), [42])
36+
37+
def test_heap_sort_negative_integers(self):
38+
self.assertEqual(heap_sort([-5, -1, -10, 0, 5]), [-10, -5, -1, 0, 5])
39+
self.assertEqual(heap_sort([-5, -1, -10, 0, 5], reverse=True), [5, 0, -1, -5, -10])
40+
41+
def test_heap_sort_already_sorted(self):
42+
self.assertEqual(heap_sort([1, 2, 3, 4, 5]), [1, 2, 3, 4, 5])
43+
self.assertEqual(heap_sort([5, 4, 3, 2, 1], reverse=True), [5, 4, 3, 2, 1])
44+
45+
def test_heap_sort_with_duplicates(self):
46+
self.assertEqual(heap_sort([5, 3, 8, 3, 9, 1, 5]), [1, 3, 3, 5, 5, 8, 9])
47+
48+
def test_heap_sort_does_not_mutate_original(self):
49+
original = [5, 3, 1, 4, 2]
50+
original_copy = original.copy()
51+
heap_sort(original)
52+
self.assertEqual(original, original_copy)
53+
54+
@patch('builtins.input')
55+
@patch('sys.stdout', new_callable=io.StringIO)
56+
def test_main_flow_ascending(self, mock_stdout, mock_input):
57+
mock_input.side_effect = ["64 34 25", "1", "n"]
58+
main()
59+
output = mock_stdout.getvalue()
60+
self.assertIn("Original list: [64, 34, 25]", output)
61+
self.assertIn("Sorted list (Ascending): [25, 34, 64]", output)
62+
63+
@patch('builtins.input')
64+
@patch('sys.stdout', new_callable=io.StringIO)
65+
def test_main_flow_invalid_inputs(self, mock_stdout, mock_input):
66+
mock_input.side_effect = ["", "64 abc 25", "64 34 25", "3", "64 34 25", "1", "y", "12 11", "2", "n"]
67+
main()
68+
output = mock_stdout.getvalue()
69+
self.assertIn("Error: Input cannot be empty!", output)
70+
self.assertIn("Error: Please enter valid integers only.", output)
71+
self.assertIn("Invalid sorting choice! Please select 1 or 2.", output)
72+
self.assertIn("Sorted list (Ascending): [25, 34, 64]", output)
73+
self.assertIn("Sorted list (Descending): [12, 11]", output)
74+
75+
76+
if __name__ == '__main__':
77+
unittest.main()

web-app/js/projects/sorting-visualizer.js

Lines changed: 125 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ function getSortingVisualizerHTML() {
1212
<option value="selection">Selection Sort</option>
1313
<option value="insertion">Insertion Sort</option>
1414
<option value="quick">Quick Sort</option>
15+
<option value="merge">Merge Sort</option>
16+
<option value="heap">Heap Sort</option>
1517
</select>
1618
</div>
1719
@@ -289,7 +291,9 @@ function initSortingVisualizer() {
289291
bubble: { time: "O(N²)", space: "O(1)" },
290292
selection: { time: "O(N²)", space: "O(1)" },
291293
insertion: { time: "O(N²)", space: "O(1)" },
292-
quick: { time: "O(N log N) avg / O(N²) worst", space: "O(log N)" }
294+
quick: { time: "O(N log N) avg / O(N²) worst", space: "O(log N)" },
295+
merge: { time: "O(N log N)", space: "O(N)" },
296+
heap: { time: "O(N log N)", space: "O(1)" }
293297
};
294298

295299
// Update complexities when algorithm changes
@@ -507,6 +511,121 @@ function initSortingVisualizer() {
507511
renderBars(arr, [], [], Array.from({length: arr.length}, (_, idx) => idx));
508512
}
509513

514+
async function mergeSortVisual(arr) {
515+
const sortedIndices = [];
516+
517+
async function mergeHelper(left, right) {
518+
if (left >= right) {
519+
if (!sortedIndices.includes(left)) sortedIndices.push(left);
520+
return;
521+
}
522+
if (cancelSorting) return;
523+
524+
const mid = Math.floor((left + right) / 2);
525+
await mergeHelper(left, mid);
526+
await mergeHelper(mid + 1, right);
527+
if (cancelSorting) return;
528+
529+
const leftArr = arr.slice(left, mid + 1);
530+
const rightArr = arr.slice(mid + 1, right + 1);
531+
let i = 0, j = 0, k = left;
532+
const range = Array.from({ length: right - left + 1 }, (_, idx) => left + idx);
533+
534+
while (i < leftArr.length && j < rightArr.length) {
535+
if (cancelSorting) return;
536+
comparisons++;
537+
renderBars(arr, range, [], sortedIndices);
538+
await sleep(delay());
539+
540+
const shouldPickLeft = isAscending ? leftArr[i] <= rightArr[j] : leftArr[i] >= rightArr[j];
541+
arr[k] = shouldPickLeft ? leftArr[i++] : rightArr[j++];
542+
swaps++;
543+
renderBars(arr, [], [k], sortedIndices);
544+
await sleep(delay());
545+
k++;
546+
}
547+
while (i < leftArr.length) {
548+
arr[k] = leftArr[i++];
549+
swaps++;
550+
renderBars(arr, [], [k], sortedIndices);
551+
await sleep(delay());
552+
k++;
553+
}
554+
while (j < rightArr.length) {
555+
arr[k] = rightArr[j++];
556+
swaps++;
557+
renderBars(arr, [], [k], sortedIndices);
558+
await sleep(delay());
559+
k++;
560+
}
561+
562+
for (let idx = left; idx <= right; idx++) {
563+
if (!sortedIndices.includes(idx)) sortedIndices.push(idx);
564+
}
565+
renderBars(arr, [], [], sortedIndices);
566+
await sleep(delay());
567+
}
568+
569+
await mergeHelper(0, arr.length - 1);
570+
renderBars(arr, [], [], Array.from({ length: arr.length }, (_, idx) => idx));
571+
}
572+
573+
async function heapSortVisual(arr) {
574+
const n = arr.length;
575+
const sortedIndices = [];
576+
577+
async function siftDown(root, end) {
578+
while (true) {
579+
if (cancelSorting) return;
580+
let child = 2 * root + 1;
581+
if (child > end) break;
582+
583+
comparisons++;
584+
renderBars(arr, [root, child], [], sortedIndices);
585+
await sleep(delay());
586+
587+
if (child + 1 <= end) {
588+
comparisons++;
589+
renderBars(arr, [root, child, child + 1], [], sortedIndices);
590+
await sleep(delay());
591+
const rightIsNext = isAscending ? arr[child + 1] > arr[child] : arr[child + 1] < arr[child];
592+
if (rightIsNext) child++;
593+
}
594+
595+
const shouldSwap = isAscending ? arr[root] < arr[child] : arr[root] > arr[child];
596+
if (shouldSwap) {
597+
swaps++;
598+
renderBars(arr, [], [root, child], sortedIndices);
599+
await sleep(delay());
600+
[arr[root], arr[child]] = [arr[child], arr[root]];
601+
root = child;
602+
} else {
603+
break;
604+
}
605+
}
606+
}
607+
608+
for (let start = Math.floor(n / 2) - 1; start >= 0; start--) {
609+
if (cancelSorting) return;
610+
await siftDown(start, n - 1);
611+
}
612+
613+
for (let end = n - 1; end > 0; end--) {
614+
if (cancelSorting) return;
615+
swaps++;
616+
renderBars(arr, [], [0, end], sortedIndices);
617+
await sleep(delay());
618+
[arr[0], arr[end]] = [arr[end], arr[0]];
619+
sortedIndices.push(end);
620+
renderBars(arr, [], [], sortedIndices);
621+
await sleep(delay());
622+
await siftDown(0, end - 1);
623+
}
624+
625+
if (!sortedIndices.includes(0)) sortedIndices.push(0);
626+
renderBars(arr, [], [], sortedIndices);
627+
}
628+
510629
randomSort.addEventListener('click', () => {
511630
if (isSorting) return;
512631
generateRandomArray();
@@ -563,6 +682,10 @@ function initSortingVisualizer() {
563682
await insertionSort(workingArr);
564683
} else if (algo === 'quick') {
565684
await quickSortVisual(workingArr);
685+
} else if (algo === 'merge') {
686+
await mergeSortVisual(workingArr);
687+
} else if (algo === 'heap') {
688+
await heapSortVisual(workingArr);
566689
}
567690

568691
if (cancelSorting) {
@@ -590,4 +713,4 @@ function initSortingVisualizer() {
590713

591714
// Initial render
592715
generateRandomArray();
593-
}
716+
}

0 commit comments

Comments
 (0)