635 lines
20 KiB
Python
635 lines
20 KiB
Python
#!/usr/bin/env python3
|
|
|
|
"""
|
|
Sorting algorithms visualizer using Tkinter.
|
|
|
|
This module is comprised of three ``components'':
|
|
|
|
- an array visualizer with methods that implement basic sorting
|
|
operations (compare, swap) as well as methods for ``annotating'' the
|
|
sorting algorithm (e.g. to show the pivot element);
|
|
|
|
- a number of sorting algorithms (currently quicksort, insertion sort,
|
|
selection sort and bubble sort, as well as a randomization function),
|
|
all using the array visualizer for its basic operations and with calls
|
|
to its annotation methods;
|
|
|
|
- and a ``driver'' class which can be used as a Grail applet or as a
|
|
stand-alone application.
|
|
"""
|
|
|
|
from tkinter import *
|
|
import random
|
|
|
|
XGRID = 10
|
|
YGRID = 10
|
|
WIDTH = 6
|
|
|
|
|
|
class Array:
|
|
|
|
class Cancelled(BaseException):
|
|
pass
|
|
|
|
def __init__(self, master, data=None):
|
|
self.master = master
|
|
self.frame = Frame(self.master)
|
|
self.frame.pack(fill=X)
|
|
self.label = Label(self.frame)
|
|
self.label.pack()
|
|
self.canvas = Canvas(self.frame)
|
|
self.canvas.pack()
|
|
self.report = Label(self.frame)
|
|
self.report.pack()
|
|
self.left = self.canvas.create_line(0, 0, 0, 0)
|
|
self.right = self.canvas.create_line(0, 0, 0, 0)
|
|
self.pivot = self.canvas.create_line(0, 0, 0, 0)
|
|
self.items = []
|
|
self.size = self.maxvalue = 0
|
|
if data:
|
|
self.setdata(data)
|
|
|
|
def setdata(self, data):
|
|
olditems = self.items
|
|
self.items = []
|
|
for item in olditems:
|
|
item.delete()
|
|
self.size = len(data)
|
|
self.maxvalue = max(data)
|
|
self.canvas.config(width=(self.size+1)*XGRID,
|
|
height=(self.maxvalue+1)*YGRID)
|
|
for i in range(self.size):
|
|
self.items.append(ArrayItem(self, i, data[i]))
|
|
self.reset("Sort demo, size %d" % self.size)
|
|
|
|
speed = "normal"
|
|
|
|
def setspeed(self, speed):
|
|
self.speed = speed
|
|
|
|
def destroy(self):
|
|
self.frame.destroy()
|
|
|
|
in_mainloop = 0
|
|
stop_mainloop = 0
|
|
|
|
def cancel(self):
|
|
self.stop_mainloop = 1
|
|
if self.in_mainloop:
|
|
self.master.quit()
|
|
|
|
def step(self):
|
|
if self.in_mainloop:
|
|
self.master.quit()
|
|
|
|
def wait(self, msecs):
|
|
if self.speed == "fastest":
|
|
msecs = 0
|
|
elif self.speed == "fast":
|
|
msecs = msecs//10
|
|
elif self.speed == "single-step":
|
|
msecs = 1000000000
|
|
if not self.stop_mainloop:
|
|
self.master.update()
|
|
id = self.master.after(msecs, self.master.quit)
|
|
self.in_mainloop = 1
|
|
self.master.mainloop()
|
|
self.master.after_cancel(id)
|
|
self.in_mainloop = 0
|
|
if self.stop_mainloop:
|
|
self.stop_mainloop = 0
|
|
self.message("Cancelled")
|
|
raise Array.Cancelled
|
|
|
|
def getsize(self):
|
|
return self.size
|
|
|
|
def show_partition(self, first, last):
|
|
for i in range(self.size):
|
|
item = self.items[i]
|
|
if first <= i < last:
|
|
self.canvas.itemconfig(item, fill='red')
|
|
else:
|
|
self.canvas.itemconfig(item, fill='orange')
|
|
self.hide_left_right_pivot()
|
|
|
|
def hide_partition(self):
|
|
for i in range(self.size):
|
|
item = self.items[i]
|
|
self.canvas.itemconfig(item, fill='red')
|
|
self.hide_left_right_pivot()
|
|
|
|
def show_left(self, left):
|
|
if not 0 <= left < self.size:
|
|
self.hide_left()
|
|
return
|
|
x1, y1, x2, y2 = self.items[left].position()
|
|
## top, bot = HIRO
|
|
self.canvas.coords(self.left, (x1 - 2, 0, x1 - 2, 9999))
|
|
self.master.update()
|
|
|
|
def show_right(self, right):
|
|
if not 0 <= right < self.size:
|
|
self.hide_right()
|
|
return
|
|
x1, y1, x2, y2 = self.items[right].position()
|
|
self.canvas.coords(self.right, (x2 + 2, 0, x2 + 2, 9999))
|
|
self.master.update()
|
|
|
|
def hide_left_right_pivot(self):
|
|
self.hide_left()
|
|
self.hide_right()
|
|
self.hide_pivot()
|
|
|
|
def hide_left(self):
|
|
self.canvas.coords(self.left, (0, 0, 0, 0))
|
|
|
|
def hide_right(self):
|
|
self.canvas.coords(self.right, (0, 0, 0, 0))
|
|
|
|
def show_pivot(self, pivot):
|
|
x1, y1, x2, y2 = self.items[pivot].position()
|
|
self.canvas.coords(self.pivot, (0, y1 - 2, 9999, y1 - 2))
|
|
|
|
def hide_pivot(self):
|
|
self.canvas.coords(self.pivot, (0, 0, 0, 0))
|
|
|
|
def swap(self, i, j):
|
|
if i == j: return
|
|
self.countswap()
|
|
item = self.items[i]
|
|
other = self.items[j]
|
|
self.items[i], self.items[j] = other, item
|
|
item.swapwith(other)
|
|
|
|
def compare(self, i, j):
|
|
self.countcompare()
|
|
item = self.items[i]
|
|
other = self.items[j]
|
|
return item.compareto(other)
|
|
|
|
def reset(self, msg):
|
|
self.ncompares = 0
|
|
self.nswaps = 0
|
|
self.message(msg)
|
|
self.updatereport()
|
|
self.hide_partition()
|
|
|
|
def message(self, msg):
|
|
self.label.config(text=msg)
|
|
|
|
def countswap(self):
|
|
self.nswaps = self.nswaps + 1
|
|
self.updatereport()
|
|
|
|
def countcompare(self):
|
|
self.ncompares = self.ncompares + 1
|
|
self.updatereport()
|
|
|
|
def updatereport(self):
|
|
text = "%d cmps, %d swaps" % (self.ncompares, self.nswaps)
|
|
self.report.config(text=text)
|
|
|
|
|
|
class ArrayItem:
|
|
|
|
def __init__(self, array, index, value):
|
|
self.array = array
|
|
self.index = index
|
|
self.value = value
|
|
self.canvas = array.canvas
|
|
x1, y1, x2, y2 = self.position()
|
|
self.item_id = array.canvas.create_rectangle(x1, y1, x2, y2,
|
|
fill='red', outline='black', width=1)
|
|
self.canvas.tag_bind(self.item_id, '<Button-1>', self.mouse_down)
|
|
self.canvas.tag_bind(self.item_id, '<Button1-Motion>', self.mouse_move)
|
|
self.canvas.tag_bind(self.item_id, '<ButtonRelease-1>', self.mouse_up)
|
|
|
|
def delete(self):
|
|
item_id = self.item_id
|
|
self.array = None
|
|
self.item_id = None
|
|
self.canvas.delete(item_id)
|
|
|
|
def mouse_down(self, event):
|
|
self.lastx = event.x
|
|
self.lasty = event.y
|
|
self.origx = event.x
|
|
self.origy = event.y
|
|
self.canvas.tag_raise(self.item_id)
|
|
|
|
def mouse_move(self, event):
|
|
self.canvas.move(self.item_id,
|
|
event.x - self.lastx, event.y - self.lasty)
|
|
self.lastx = event.x
|
|
self.lasty = event.y
|
|
|
|
def mouse_up(self, event):
|
|
i = self.nearestindex(event.x)
|
|
if i >= self.array.getsize():
|
|
i = self.array.getsize() - 1
|
|
if i < 0:
|
|
i = 0
|
|
other = self.array.items[i]
|
|
here = self.index
|
|
self.array.items[here], self.array.items[i] = other, self
|
|
self.index = i
|
|
x1, y1, x2, y2 = self.position()
|
|
self.canvas.coords(self.item_id, (x1, y1, x2, y2))
|
|
other.setindex(here)
|
|
|
|
def setindex(self, index):
|
|
nsteps = steps(self.index, index)
|
|
if not nsteps: return
|
|
if self.array.speed == "fastest":
|
|
nsteps = 0
|
|
oldpts = self.position()
|
|
self.index = index
|
|
newpts = self.position()
|
|
trajectory = interpolate(oldpts, newpts, nsteps)
|
|
self.canvas.tag_raise(self.item_id)
|
|
for pts in trajectory:
|
|
self.canvas.coords(self.item_id, pts)
|
|
self.array.wait(50)
|
|
|
|
def swapwith(self, other):
|
|
nsteps = steps(self.index, other.index)
|
|
if not nsteps: return
|
|
if self.array.speed == "fastest":
|
|
nsteps = 0
|
|
myoldpts = self.position()
|
|
otheroldpts = other.position()
|
|
self.index, other.index = other.index, self.index
|
|
mynewpts = self.position()
|
|
othernewpts = other.position()
|
|
myfill = self.canvas.itemcget(self.item_id, 'fill')
|
|
otherfill = self.canvas.itemcget(other.item_id, 'fill')
|
|
self.canvas.itemconfig(self.item_id, fill='green')
|
|
self.canvas.itemconfig(other.item_id, fill='yellow')
|
|
self.array.master.update()
|
|
if self.array.speed == "single-step":
|
|
self.canvas.coords(self.item_id, mynewpts)
|
|
self.canvas.coords(other.item_id, othernewpts)
|
|
self.array.master.update()
|
|
self.canvas.itemconfig(self.item_id, fill=myfill)
|
|
self.canvas.itemconfig(other.item_id, fill=otherfill)
|
|
self.array.wait(0)
|
|
return
|
|
mytrajectory = interpolate(myoldpts, mynewpts, nsteps)
|
|
othertrajectory = interpolate(otheroldpts, othernewpts, nsteps)
|
|
if self.value > other.value:
|
|
self.canvas.tag_raise(self.item_id)
|
|
self.canvas.tag_raise(other.item_id)
|
|
else:
|
|
self.canvas.tag_raise(other.item_id)
|
|
self.canvas.tag_raise(self.item_id)
|
|
try:
|
|
for i in range(len(mytrajectory)):
|
|
mypts = mytrajectory[i]
|
|
otherpts = othertrajectory[i]
|
|
self.canvas.coords(self.item_id, mypts)
|
|
self.canvas.coords(other.item_id, otherpts)
|
|
self.array.wait(50)
|
|
finally:
|
|
mypts = mytrajectory[-1]
|
|
otherpts = othertrajectory[-1]
|
|
self.canvas.coords(self.item_id, mypts)
|
|
self.canvas.coords(other.item_id, otherpts)
|
|
self.canvas.itemconfig(self.item_id, fill=myfill)
|
|
self.canvas.itemconfig(other.item_id, fill=otherfill)
|
|
|
|
def compareto(self, other):
|
|
myfill = self.canvas.itemcget(self.item_id, 'fill')
|
|
otherfill = self.canvas.itemcget(other.item_id, 'fill')
|
|
if self.value < other.value:
|
|
myflash = 'white'
|
|
otherflash = 'black'
|
|
outcome = -1
|
|
elif self.value > other.value:
|
|
myflash = 'black'
|
|
otherflash = 'white'
|
|
outcome = 1
|
|
else:
|
|
myflash = otherflash = 'grey'
|
|
outcome = 0
|
|
try:
|
|
self.canvas.itemconfig(self.item_id, fill=myflash)
|
|
self.canvas.itemconfig(other.item_id, fill=otherflash)
|
|
self.array.wait(500)
|
|
finally:
|
|
self.canvas.itemconfig(self.item_id, fill=myfill)
|
|
self.canvas.itemconfig(other.item_id, fill=otherfill)
|
|
return outcome
|
|
|
|
def position(self):
|
|
x1 = (self.index+1)*XGRID - WIDTH//2
|
|
x2 = x1+WIDTH
|
|
y2 = (self.array.maxvalue+1)*YGRID
|
|
y1 = y2 - (self.value)*YGRID
|
|
return x1, y1, x2, y2
|
|
|
|
def nearestindex(self, x):
|
|
return int(round(float(x)/XGRID)) - 1
|
|
|
|
|
|
# Subroutines that don't need an object
|
|
|
|
def steps(here, there):
|
|
nsteps = abs(here - there)
|
|
if nsteps <= 3:
|
|
nsteps = nsteps * 3
|
|
elif nsteps <= 5:
|
|
nsteps = nsteps * 2
|
|
elif nsteps > 10:
|
|
nsteps = 10
|
|
return nsteps
|
|
|
|
def interpolate(oldpts, newpts, n):
|
|
if len(oldpts) != len(newpts):
|
|
raise ValueError("can't interpolate arrays of different length")
|
|
pts = [0]*len(oldpts)
|
|
res = [tuple(oldpts)]
|
|
for i in range(1, n):
|
|
for k in range(len(pts)):
|
|
pts[k] = oldpts[k] + (newpts[k] - oldpts[k])*i//n
|
|
res.append(tuple(pts))
|
|
res.append(tuple(newpts))
|
|
return res
|
|
|
|
|
|
# Various (un)sorting algorithms
|
|
|
|
def uniform(array):
|
|
size = array.getsize()
|
|
array.setdata([(size+1)//2] * size)
|
|
array.reset("Uniform data, size %d" % size)
|
|
|
|
def distinct(array):
|
|
size = array.getsize()
|
|
array.setdata(range(1, size+1))
|
|
array.reset("Distinct data, size %d" % size)
|
|
|
|
def randomize(array):
|
|
array.reset("Randomizing")
|
|
n = array.getsize()
|
|
for i in range(n):
|
|
j = random.randint(0, n-1)
|
|
array.swap(i, j)
|
|
array.message("Randomized")
|
|
|
|
def insertionsort(array):
|
|
size = array.getsize()
|
|
array.reset("Insertion sort")
|
|
for i in range(1, size):
|
|
j = i-1
|
|
while j >= 0:
|
|
if array.compare(j, j+1) <= 0:
|
|
break
|
|
array.swap(j, j+1)
|
|
j = j-1
|
|
array.message("Sorted")
|
|
|
|
def selectionsort(array):
|
|
size = array.getsize()
|
|
array.reset("Selection sort")
|
|
try:
|
|
for i in range(size):
|
|
array.show_partition(i, size)
|
|
for j in range(i+1, size):
|
|
if array.compare(i, j) > 0:
|
|
array.swap(i, j)
|
|
array.message("Sorted")
|
|
finally:
|
|
array.hide_partition()
|
|
|
|
def bubblesort(array):
|
|
size = array.getsize()
|
|
array.reset("Bubble sort")
|
|
for i in range(size):
|
|
for j in range(1, size):
|
|
if array.compare(j-1, j) > 0:
|
|
array.swap(j-1, j)
|
|
array.message("Sorted")
|
|
|
|
def quicksort(array):
|
|
size = array.getsize()
|
|
array.reset("Quicksort")
|
|
try:
|
|
stack = [(0, size)]
|
|
while stack:
|
|
first, last = stack[-1]
|
|
del stack[-1]
|
|
array.show_partition(first, last)
|
|
if last-first < 5:
|
|
array.message("Insertion sort")
|
|
for i in range(first+1, last):
|
|
j = i-1
|
|
while j >= first:
|
|
if array.compare(j, j+1) <= 0:
|
|
break
|
|
array.swap(j, j+1)
|
|
j = j-1
|
|
continue
|
|
array.message("Choosing pivot")
|
|
j, i, k = first, (first+last) // 2, last-1
|
|
if array.compare(k, i) < 0:
|
|
array.swap(k, i)
|
|
if array.compare(k, j) < 0:
|
|
array.swap(k, j)
|
|
if array.compare(j, i) < 0:
|
|
array.swap(j, i)
|
|
pivot = j
|
|
array.show_pivot(pivot)
|
|
array.message("Pivot at left of partition")
|
|
array.wait(1000)
|
|
left = first
|
|
right = last
|
|
while 1:
|
|
array.message("Sweep right pointer")
|
|
right = right-1
|
|
array.show_right(right)
|
|
while right > first and array.compare(right, pivot) >= 0:
|
|
right = right-1
|
|
array.show_right(right)
|
|
array.message("Sweep left pointer")
|
|
left = left+1
|
|
array.show_left(left)
|
|
while left < last and array.compare(left, pivot) <= 0:
|
|
left = left+1
|
|
array.show_left(left)
|
|
if left > right:
|
|
array.message("End of partition")
|
|
break
|
|
array.message("Swap items")
|
|
array.swap(left, right)
|
|
array.message("Swap pivot back")
|
|
array.swap(pivot, right)
|
|
n1 = right-first
|
|
n2 = last-left
|
|
if n1 > 1: stack.append((first, right))
|
|
if n2 > 1: stack.append((left, last))
|
|
array.message("Sorted")
|
|
finally:
|
|
array.hide_partition()
|
|
|
|
def demosort(array):
|
|
while 1:
|
|
for alg in [quicksort, insertionsort, selectionsort, bubblesort]:
|
|
randomize(array)
|
|
alg(array)
|
|
|
|
|
|
# Sort demo class -- usable as a Grail applet
|
|
|
|
class SortDemo:
|
|
|
|
def __init__(self, master, size=15):
|
|
self.master = master
|
|
self.size = size
|
|
self.busy = 0
|
|
self.array = Array(self.master)
|
|
|
|
self.botframe = Frame(master)
|
|
self.botframe.pack(side=BOTTOM)
|
|
self.botleftframe = Frame(self.botframe)
|
|
self.botleftframe.pack(side=LEFT, fill=Y)
|
|
self.botrightframe = Frame(self.botframe)
|
|
self.botrightframe.pack(side=RIGHT, fill=Y)
|
|
|
|
self.b_qsort = Button(self.botleftframe,
|
|
text="Quicksort", command=self.c_qsort)
|
|
self.b_qsort.pack(fill=X)
|
|
self.b_isort = Button(self.botleftframe,
|
|
text="Insertion sort", command=self.c_isort)
|
|
self.b_isort.pack(fill=X)
|
|
self.b_ssort = Button(self.botleftframe,
|
|
text="Selection sort", command=self.c_ssort)
|
|
self.b_ssort.pack(fill=X)
|
|
self.b_bsort = Button(self.botleftframe,
|
|
text="Bubble sort", command=self.c_bsort)
|
|
self.b_bsort.pack(fill=X)
|
|
|
|
# Terrible hack to overcome limitation of OptionMenu...
|
|
class MyIntVar(IntVar):
|
|
def __init__(self, master, demo):
|
|
self.demo = demo
|
|
IntVar.__init__(self, master)
|
|
def set(self, value):
|
|
IntVar.set(self, value)
|
|
if str(value) != '0':
|
|
self.demo.resize(value)
|
|
|
|
self.v_size = MyIntVar(self.master, self)
|
|
self.v_size.set(size)
|
|
sizes = [1, 2, 3, 4] + list(range(5, 55, 5))
|
|
if self.size not in sizes:
|
|
sizes.append(self.size)
|
|
sizes.sort()
|
|
self.m_size = OptionMenu(self.botleftframe, self.v_size, *sizes)
|
|
self.m_size.pack(fill=X)
|
|
|
|
self.v_speed = StringVar(self.master)
|
|
self.v_speed.set("normal")
|
|
self.m_speed = OptionMenu(self.botleftframe, self.v_speed,
|
|
"single-step", "normal", "fast", "fastest")
|
|
self.m_speed.pack(fill=X)
|
|
|
|
self.b_step = Button(self.botleftframe,
|
|
text="Step", command=self.c_step)
|
|
self.b_step.pack(fill=X)
|
|
|
|
self.b_randomize = Button(self.botrightframe,
|
|
text="Randomize", command=self.c_randomize)
|
|
self.b_randomize.pack(fill=X)
|
|
self.b_uniform = Button(self.botrightframe,
|
|
text="Uniform", command=self.c_uniform)
|
|
self.b_uniform.pack(fill=X)
|
|
self.b_distinct = Button(self.botrightframe,
|
|
text="Distinct", command=self.c_distinct)
|
|
self.b_distinct.pack(fill=X)
|
|
self.b_demo = Button(self.botrightframe,
|
|
text="Demo", command=self.c_demo)
|
|
self.b_demo.pack(fill=X)
|
|
self.b_cancel = Button(self.botrightframe,
|
|
text="Cancel", command=self.c_cancel)
|
|
self.b_cancel.pack(fill=X)
|
|
self.b_cancel.config(state=DISABLED)
|
|
self.b_quit = Button(self.botrightframe,
|
|
text="Quit", command=self.c_quit)
|
|
self.b_quit.pack(fill=X)
|
|
|
|
def resize(self, newsize):
|
|
if self.busy:
|
|
self.master.bell()
|
|
return
|
|
self.size = newsize
|
|
self.array.setdata(range(1, self.size+1))
|
|
|
|
def c_qsort(self):
|
|
self.run(quicksort)
|
|
|
|
def c_isort(self):
|
|
self.run(insertionsort)
|
|
|
|
def c_ssort(self):
|
|
self.run(selectionsort)
|
|
|
|
def c_bsort(self):
|
|
self.run(bubblesort)
|
|
|
|
def c_demo(self):
|
|
self.run(demosort)
|
|
|
|
def c_randomize(self):
|
|
self.run(randomize)
|
|
|
|
def c_uniform(self):
|
|
self.run(uniform)
|
|
|
|
def c_distinct(self):
|
|
self.run(distinct)
|
|
|
|
def run(self, func):
|
|
if self.busy:
|
|
self.master.bell()
|
|
return
|
|
self.busy = 1
|
|
self.array.setspeed(self.v_speed.get())
|
|
self.b_cancel.config(state=NORMAL)
|
|
try:
|
|
func(self.array)
|
|
except Array.Cancelled:
|
|
pass
|
|
self.b_cancel.config(state=DISABLED)
|
|
self.busy = 0
|
|
|
|
def c_cancel(self):
|
|
if not self.busy:
|
|
self.master.bell()
|
|
return
|
|
self.array.cancel()
|
|
|
|
def c_step(self):
|
|
if not self.busy:
|
|
self.master.bell()
|
|
return
|
|
self.v_speed.set("single-step")
|
|
self.array.setspeed("single-step")
|
|
self.array.step()
|
|
|
|
def c_quit(self):
|
|
if self.busy:
|
|
self.array.cancel()
|
|
self.master.after_idle(self.master.quit)
|
|
|
|
|
|
# Main program -- for stand-alone operation outside Grail
|
|
|
|
def main():
|
|
root = Tk()
|
|
demo = SortDemo(root)
|
|
root.protocol('WM_DELETE_WINDOW', demo.c_quit)
|
|
root.mainloop()
|
|
|
|
if __name__ == '__main__':
|
|
main()
|