Skip to content

Commit e35a612

Browse files
authored
New: Implement Mergesort (#1)
* New: Implement merge sort * test * make mergesort animation logic work * add mergesort to readme
1 parent d705fda commit e35a612

File tree

3 files changed

+208
-0
lines changed

3 files changed

+208
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ Tool to visualize sorting algorithms
44
### Currently supported sort algorithms
55
- Bubblesort
66
- Selectionsort
7+
- Mergesort
78

89
### Livedemo
910
- https://agonyz.github.io/sort-algorithm-visualizer/

src/App.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { SelectionsortVisualizer } from './components/SelectionsortVisualizer.tsx';
22
import { BubblesortVisualizer } from './components/BubblesortVisualizer.tsx';
3+
import { MergeSortVisualizer } from './components/MergesortVisualizer.tsx';
34
import { useState } from 'react';
45
import { Container, Form, Nav, Navbar } from 'react-bootstrap';
56
import { Github } from 'react-bootstrap-icons';
@@ -14,6 +15,8 @@ export const App = () => {
1415
return <BubblesortVisualizer />;
1516
case 'selection':
1617
return <SelectionsortVisualizer />;
18+
case 'merge':
19+
return <MergeSortVisualizer />;
1720
default:
1821
return <BubblesortVisualizer />;
1922
}
@@ -39,6 +42,7 @@ export const App = () => {
3942
>
4043
<option value="bubble">Bubblesort</option>
4144
<option value="selection">Selectionsort</option>
45+
<option value="merge">Mergesort</option>
4246
</Form.Control>
4347
</Form.Group>
4448
</Nav>
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import { useEffect, useRef, useState } from 'react';
2+
import { useGSAP } from '@gsap/react';
3+
import gsap from 'gsap';
4+
import { Button, Col, Container, Form, Row } from 'react-bootstrap';
5+
import { delay, generateRandomArray } from '../utils';
6+
7+
export const MergeSortVisualizer = () => {
8+
const [array, setArray] = useState<number[]>([]);
9+
const [isSorting, setIsSorting] = useState<boolean>(false);
10+
const [arraySize, setArraySize] = useState<number>(10);
11+
const [sortDelay, setSortDelay] = useState<number>(500);
12+
13+
const boxRefs = useRef<(HTMLDivElement | null)[]>([]);
14+
const container = useRef(null);
15+
const { contextSafe } = useGSAP({ scope: container });
16+
const tl = gsap.timeline(); // todo: find better way than using global gsap timeline element
17+
18+
useEffect(() => {
19+
resetComponent();
20+
}, [arraySize]);
21+
22+
const resetComponent = () => {
23+
const array = generateRandomArray(arraySize);
24+
setArray(array);
25+
};
26+
27+
const mergeSort = async (array: number[], left = 0): Promise<number[]> => {
28+
if (array.length <= 1) {
29+
return array;
30+
}
31+
32+
const m = Math.floor(array.length / 2);
33+
const l: number[] = array.slice(0, m);
34+
const r: number[] = array.slice(m);
35+
36+
const sortedLeft = await mergeSort(l, left);
37+
const sortedRight = await mergeSort(r, left + m);
38+
39+
return await merge(sortedLeft, sortedRight, left);
40+
};
41+
42+
const merge = async (
43+
l: number[],
44+
r: number[],
45+
left: number,
46+
): Promise<number[]> => {
47+
const result: number[] = [];
48+
let leftIndex = 0;
49+
let rightIndex = 0;
50+
51+
while (leftIndex < l.length && rightIndex < r.length) {
52+
if (l[leftIndex] < r[rightIndex]) {
53+
result.push(l[leftIndex]);
54+
leftIndex++;
55+
} else {
56+
result.push(r[rightIndex]);
57+
rightIndex++;
58+
}
59+
}
60+
61+
// concatenate remaining elements from left or right array
62+
const merged = result
63+
.concat(l.slice(leftIndex))
64+
.concat(r.slice(rightIndex));
65+
66+
// visualize the merge process
67+
await visualizeSort(merged, left);
68+
69+
return merged;
70+
};
71+
72+
const visualizeSort = contextSafe(
73+
async (currentArray: number[], startIdx: number) => {
74+
// move all boxes up before merging
75+
currentArray.forEach((_, index) => {
76+
const box = boxRefs.current[startIdx + index];
77+
if (box) {
78+
tl.to(box, {
79+
y: -50,
80+
duration: 0.2,
81+
backgroundColor: '#f39c12',
82+
});
83+
}
84+
});
85+
86+
await tl.play();
87+
88+
// calculate the new positions and animate the final merge
89+
const counts: Record<number, number> = {}; // keep count of occurrences of each value
90+
currentArray.forEach((_, index) => {
91+
const box = boxRefs.current[startIdx + index];
92+
if (box) {
93+
const boxValue = parseInt(box.innerText);
94+
if (!(boxValue in counts)) {
95+
counts[boxValue] = 0;
96+
}
97+
const offset = counts[boxValue]; // offset for the same values
98+
counts[boxValue]++; // increment count for next occurrence
99+
100+
const newIndex = currentArray.indexOf(boxValue) + offset;
101+
tl.to(box, {
102+
x: (newIndex - index) * box.getBoundingClientRect().width, // move horizontally to new position
103+
duration: 0.5,
104+
});
105+
}
106+
});
107+
108+
await tl.play();
109+
110+
// move all boxes back down to their original vertical positions
111+
currentArray.forEach((_, index) => {
112+
const box = boxRefs.current[startIdx + index];
113+
if (box) {
114+
tl.to(box, {
115+
y: 0,
116+
duration: 0.2,
117+
backgroundColor: '#3498db',
118+
});
119+
}
120+
});
121+
122+
await tl.play();
123+
},
124+
);
125+
126+
const startSort = async () => {
127+
setIsSorting(true);
128+
const sortedArray = await mergeSort([...array]);
129+
setArray(sortedArray);
130+
tl.revert();
131+
setIsSorting(false);
132+
};
133+
134+
return (
135+
<>
136+
<h1 className="mt-5 text-center">Merge Sort</h1>
137+
<Container ref={container} className="mt-5">
138+
<Row>
139+
<Col>
140+
<Form.Group controlId="formArraySize">
141+
<Form.Label>Array Size: {arraySize}</Form.Label>
142+
<Form.Range
143+
value={arraySize}
144+
onChange={(e) => setArraySize(parseInt(e.target.value))}
145+
min={5}
146+
max={15}
147+
disabled={isSorting}
148+
/>
149+
</Form.Group>
150+
</Col>
151+
<Col>
152+
<Form.Group controlId="formSortDelay">
153+
<Form.Label>Sort Delay (ms): {sortDelay}</Form.Label>
154+
<Form.Range
155+
value={sortDelay}
156+
onChange={(e) => setSortDelay(parseInt(e.target.value))}
157+
min={1}
158+
max={5000}
159+
disabled={isSorting}
160+
/>
161+
</Form.Group>
162+
</Col>
163+
</Row>
164+
165+
<Row className="mt-5">
166+
{array.map((v, i) => (
167+
<Col
168+
ref={(el: HTMLDivElement) => (boxRefs.current[i] = el)}
169+
className="border d-flex justify-content-center align-items-center"
170+
style={{
171+
height: 50,
172+
width: 50,
173+
}}
174+
key={i}
175+
>
176+
{v}
177+
</Col>
178+
))}
179+
</Row>
180+
<Row className="justify-content-center mt-3">
181+
<Col className="d-flex justify-content-center">
182+
<Button
183+
className="mt-5 mx-3"
184+
onClick={() => resetComponent()}
185+
disabled={isSorting}
186+
>
187+
Generate
188+
</Button>
189+
</Col>
190+
<Col className="d-flex justify-content-center">
191+
<Button
192+
className="mt-5"
193+
onClick={() => startSort()}
194+
disabled={isSorting}
195+
>
196+
Sort
197+
</Button>
198+
</Col>
199+
</Row>
200+
</Container>
201+
</>
202+
);
203+
};

0 commit comments

Comments
 (0)