import random import statistics import time import matplotlib.pyplot as plt import pandas as pd import plotly.graph_objects as go import streamlit as st import algorithms as alg st.title("Sorting Algorithm Visualizer") def render_metrics(m): if not m: return ms = m.get("seconds", 0.0) * 1000.0 comps = m.get("comparisons", 0) moves = m.get("moves", 0) c1, c2, c3 = st.columns(3) with c1: st.metric("Time (ms)", f"{ms:.2f}") with c2: st.metric("Comparisons", f"{comps:,}") with c3: st.metric("Moves", f"{moves:,}") st.subheader("Input Configuration") length = st.slider("List length", 5, 20, 8) # Input type selector input_type = st.selectbox( "Input type", ["Random", "Sorted", "Reversed", "Few unique"], index=0 ) # Generate data based on input type if input_type == "Random": data = random.sample(range(1, 30), length) # unique values elif input_type == "Sorted": data = sorted(random.sample(range(1, 30), length)) elif input_type == "Reversed": data = sorted(random.sample(range(1, 30), length), reverse=True) else: # Few unique pool = random.sample(range(1, 30), k=min(3, max(1, length // 3))) data = [random.choice(pool) for _ in range(length)] random.shuffle(data) st.write(f"Input array: {data}") st.subheader("Scaling Benchmark (n -> time)") sizes_str = st.text_input("Sizes", value="10, 20, 40, 80, 160") try: sizes = [int(x.strip()) for x in sizes_str.split(",") if x.strip()] sizes = [s for s in sizes if s > 0] except Exception: sizes = [] runs_scale = st.slider("Runs per size", 3, 50, 10, step=1) algo_options = [ "Insertion Sort", "Merge Sort", "Quick Sort", "Counting Sort", "Radix Sort (LSD)", "Heap Sort", "Shell Sort", "Bucket Sort", ] selected_algos = st.multiselect( "Algorithms to include", options=algo_options, default=[ "Insertion Sort", "Merge Sort", "Quick Sort", "Counting Sort", "Radix Sort (LSD)", "Heap Sort", "Shell Sort", "Bucket Sort", ], ) def make_data(n: int, input_type: str): if input_type == "Random": return [random.randint(1, max(30, n * 3)) for _ in range(n)] elif input_type == "Sorted": arr = [random.randint(1, max(30, n * 3)) for _ in range(n)] return sorted(arr) elif input_type == "Reversed": arr = [random.randint(1, max(30, n * 3)) for _ in range(n)] return sorted(arr, reverse=True) else: pool_size = max(2, min(10, n // 5)) pool = [random.randint(1, max(30, n * 3)) for _ in range(pool_size)] return [random.choice(pool) for _ in range(n)] ## cal algo def get_algo_fn(name: str): if name == "Insertion Sort": return lambda arr: alg.insertion_sort(arr, record_steps=False) if name == "Merge Sort": return lambda arr: alg.merge_sort(arr, record_steps=False) if name == "Quick Sort": return lambda arr: alg.quick_sort(arr, record_steps=False) if name == "Counting Sort": return lambda arr: alg.counting_sort(arr, record_steps=False) if name == "Radix Sort (LSD)": return lambda arr: alg.radix_sort_lsd(arr, base=10, record_steps=False) if name == "Heap Sort": return lambda arr: alg.heap_sort(arr, record_steps=False) if name == "Shell Sort": return lambda arr: alg.shell_sort(arr, record_steps=False) if name == "Bucket Sort": return lambda arr: alg.bucket_sort(arr, record_steps=False) raise ValueError(name) if st.button("Run Scaling Benchmark"): if not sizes: st.error("Please enter at least one valid size (e.g., 10, 20, 40).") st.stop() rows = [] for n in sizes: for algo_name in selected_algos: fn = get_algo_fn(algo_name) times = [] for _ in range(runs_scale): arr = make_data(n, input_type) _, m = fn(arr) times.append(m["seconds"] * 1000.0) # ms avg_ms = statistics.mean(times) std_ms = statistics.pstdev(times) if len(times) > 1 else 0.0 rows.append( { "n": n, "Algorithm": algo_name, "Average Time (ms)": avg_ms, "Std Dev (ms)": std_ms, } ) df_scale = pd.DataFrame(rows) fig_scale = go.Figure() for algo_name in selected_algos: sub = df_scale[df_scale["Algorithm"] == algo_name].sort_values("n") fig_scale.add_trace( go.Scatter( x=sub["n"], y=sub["Average Time (ms)"], mode="lines+markers", name=algo_name, ) ) fig_scale.update_layout( title=f"Scaling Benchmark (input_type = {input_type}, runs = {runs_scale})", xaxis_title="n (input size)", yaxis_title="Average Time (ms)", height=480, width=1000, ) st.plotly_chart(fig_scale, use_container_width=True) st.dataframe( df_scale.sort_values(["Algorithm", "n"]).style.format( { "Average Time (ms)": "{:.3f}", "Std Dev (ms)": "{:.3f}", } ), use_container_width=True, ) st.download_button( "Download Scaling CSV", data=df_scale.to_csv(index=False).encode("utf-8"), file_name="scaling_benchmark.csv", mime="text/csv", ) if st.button("Run Comparison"): data_insertion = data.copy() data_merge = data.copy() data_quick = data.copy() data_counting = data.copy() data_radix = data.copy() data_heap = data.copy() data_shell = data.copy() data_bucket = data.copy() steps_insertion, metrics_insertion = alg.insertion_sort(data_insertion) steps_merge, metrics_merge = alg.merge_sort(data_merge) steps_quick, metrics_quick = alg.quick_sort(data_quick) steps_counting, metrics_counting = alg.counting_sort(data_counting) steps_radix, metrics_radix = alg.radix_sort_lsd(data_radix, base=10) steps_heap, metrics_heap = alg.heap_sort(data_heap) steps_shell, metrics_shell = alg.shell_sort(data_shell) steps_bucket, metrics_bucket = alg.bucket_sort(data_bucket) def create_animation(steps, title, color_fn): if not steps: return go.Figure( layout=go.Layout( width=900, height=420, title=title, xaxis=dict(visible=False), yaxis=dict(visible=False), annotations=[dict(text="No steps to display", showarrow=False)], ) ) frames = [] for i, step in enumerate(steps): array = step["array"] active_index = step.get("active_index", -1) sorted_boundary = step.get("sorted_boundary", -1) colors = color_fn(len(array), active_index, sorted_boundary) frames.append( go.Frame( data=[ go.Scatter( x=list(range(len(array))), y=array, mode="markers+text", marker=dict(size=28, color=colors), text=array, textposition="middle center", ) ], name=f"Step {i+1}", ) ) initial = steps[0] initial_colors = color_fn( len(initial["array"]), initial.get("active_index", -1), initial.get("sorted_boundary", -1), ) fig = go.Figure( data=[ go.Scatter( x=list(range(len(initial["array"]))), y=initial["array"], mode="markers+text", marker=dict(size=28, color=initial_colors), text=initial["array"], textposition="middle center", ) ], layout=go.Layout( width=900, height=420, title=title, xaxis=dict(range=[-0.5, len(initial["array"]) - 0.5]), yaxis=dict(range=[0, max(max(s["array"]) for s in steps) + 5]), updatemenus=[ dict( type="buttons", buttons=[dict(label="Play", method="animate", args=[None])], showactive=False, ) ], sliders=[ { "steps": [ { "args": [ [f"Step {i+1}"], {"frame": {"duration": 500, "redraw": True}}, ], "label": f"{i+1}", "method": "animate", } for i in range(len(frames)) ], "transition": {"duration": 0}, "x": 0, "y": -0.1, "currentvalue": {"prefix": "Step: "}, } ], ), frames=frames, ) return fig def insertion_colors(length, active_index, sorted_boundary): return [ ( "red" if j == active_index else "green" if j <= sorted_boundary else "gray" ) for j in range(length) ] def merge_colors(length, active_index, sorted_boundary): return [ ( "purple" if j == active_index else "blue" if j <= sorted_boundary else "gray" ) for j in range(length) ] def quick_colors(length, active_index, sorted_boundary): return [ ( "orange" if j == active_index else "green" if j == sorted_boundary else "gray" ) for j in range(length) ] def counting_colors(length, active_index, sorted_boundary): return [ ( "purple" if j == active_index else "green" if j == sorted_boundary else "gray" ) for j in range(length) ] def radix_colors(length, active_index, sorted_boundary): return [ ( "purple" if j == active_index else "green" if j <= sorted_boundary else "gray" ) for j in range(length) ] def heap_colors(length, active_index, sorted_boundary): return [ ( "orange" if j == active_index else ( "green" if (sorted_boundary != -1 and j >= sorted_boundary) else "gray" ) ) for j in range(length) ] def shell_colors(length, active_index, sorted_boundary): return [("orange" if j == active_index else "gray") for j in range(length)] def bucket_colors(length, active_index, sorted_boundary): return [ ( "purple" if j == active_index else "green" if j <= sorted_boundary else "gray" ) for j in range(length) ] ( tab_ins, tab_mer, tab_quick, tab_count, tab_radix, tab_heap, tab_shell, tab_bucket, ) = st.tabs( [ "Insertion", "Merge", "Quick", "Counting", "Radix (LSD)", "Heap", "Shell", "Bucket", ] ) with tab_ins: st.plotly_chart( create_animation(steps_insertion, "Insertion Sort", insertion_colors), use_container_width=True, ) with tab_mer: st.plotly_chart( create_animation(steps_merge, "Merge Sort", merge_colors), use_container_width=True, ) with tab_quick: st.plotly_chart( create_animation(steps_quick, "Quick Sort", quick_colors), use_container_width=True, ) with tab_count: st.plotly_chart( create_animation(steps_counting, "Counting Sort", counting_colors), use_container_width=True, ) with tab_radix: st.plotly_chart( create_animation(steps_radix, "Radix Sort (LSD)", radix_colors), use_container_width=True, ) with tab_heap: st.plotly_chart( create_animation(steps_heap, "Heap Sort", heap_colors), use_container_width=True, ) with tab_shell: st.plotly_chart( create_animation(steps_shell, "Shell Sort", shell_colors), use_container_width=True, ) with tab_bucket: st.plotly_chart( create_animation(steps_bucket, "Bucket Sort", bucket_colors), use_container_width=True, ) df = pd.DataFrame( [ { "Algorithm": "Insertion Sort", "Time (ms)": metrics_insertion["seconds"] * 1000, "Comparisons": metrics_insertion["comparisons"], "Moves": metrics_insertion["moves"], "Frames": len(steps_insertion), "Sorted OK": steps_insertion[-1]["array"] == sorted(data), }, { "Algorithm": "Merge Sort", "Time (ms)": metrics_merge["seconds"] * 1000, "Comparisons": metrics_merge["comparisons"], "Moves": metrics_merge["moves"], "Frames": len(steps_merge), "Sorted OK": steps_merge[-1]["array"] == sorted(data), }, { "Algorithm": "Quick Sort", "Time (ms)": metrics_quick["seconds"] * 1000, "Comparisons": metrics_quick["comparisons"], "Moves": metrics_quick["moves"], "Frames": len(steps_quick), "Sorted OK": steps_quick[-1]["array"] == sorted(data), }, { "Algorithm": "Counting Sort", "Time (ms)": metrics_counting["seconds"] * 1000, "Comparisons": metrics_counting["comparisons"], "Moves": metrics_counting["moves"], "Frames": len(steps_counting), "Sorted OK": steps_counting[-1]["array"] == sorted(data), }, { "Algorithm": "Radix Sort(LSD)", "Time (ms)": metrics_radix["seconds"] * 1000, "Comparisons": metrics_radix["comparisons"], "Moves": metrics_radix["moves"], "Frames": len(steps_radix), "Sorted OK": steps_radix[-1]["array"] == sorted(data), }, { "Algorithm": "Heap Sort", "Time (ms)": metrics_heap["seconds"] * 1000, "Comparisons": metrics_heap["comparisons"], "Moves": metrics_heap["moves"], "Frames": len(steps_heap), "Sorted OK": steps_heap[-1]["array"] == sorted(data), }, { "Algorithm": "Shell Sort", "Time (ms)": metrics_shell["seconds"] * 1000, "Comparisons": metrics_shell["comparisons"], "Moves": metrics_shell["moves"], "Frames": len(steps_shell), "Sorted OK": steps_shell[-1]["array"] == sorted(data), }, { "Algorithm": "Bucket Sort", "Time (ms)": metrics_bucket["seconds"] * 1000, "Comparisons": metrics_bucket["comparisons"], "Moves": metrics_bucket["moves"], "Frames": len(steps_bucket), "Sorted OK": steps_bucket[-1]["array"] == sorted(data), }, ] ) st.subheader("Summary Table") st.dataframe(df.style.format({"Time (ms)": "{:.2f}"}), use_container_width=True) csv = df.to_csv(index=False).encode("utf-8") st.download_button( "Download CSV", data=csv, file_name="sorting_summary.csv", mime="text/csv" )