#! /usr/bin/env python """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 * from Canvas import Line, Rectangle import random XGRID = 10 YGRID = 10 WIDTH = 6 class Array: 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 = Line(self.canvas, 0, 0, 0, 0) self.right = Line(self.canvas, 0, 0, 0, 0) self.pivot = Line(self.canvas, 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() Cancelled = "Array.Cancelled" # Exception 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: item.item.config(fill='red') else: item.item.config(fill='orange') self.hide_left_right_pivot() def hide_partition(self): for i in range(self.size): item = self.items[i] item.item.config(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.left.coords([(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.right.coords(((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.left.coords(((0, 0), (0, 0))) def hide_right(self): self.right.coords(((0, 0), (0, 0))) def show_pivot(self, pivot): x1, y1, x2, y2 = self.items[pivot].position() self.pivot.coords(((0, y1-2), (9999, y1-2))) def hide_pivot(self): self.pivot.coords(((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 x1, y1, x2, y2 = self.position() self.item = Rectangle(array.canvas, x1, y1, x2, y2, fill='red', outline='black', width=1) self.item.bind('', self.mouse_down) self.item.bind('', self.mouse_move) self.item.bind('', self.mouse_up) def delete(self): item = self.item self.array = None self.item = None item.delete() def mouse_down(self, event): self.lastx = event.x self.lasty = event.y self.origx = event.x self.origy = event.y self.item.tkraise() def mouse_move(self, event): self.item.move(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.item.coords(((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.item.tkraise() for pts in trajectory: self.item.coords((pts[:2], pts[2:])) 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.item['fill'] otherfill = other.item['fill'] self.item.config(fill='green') other.item.config(fill='yellow') self.array.master.update() if self.array.speed == "single-step": self.item.coords((mynewpts[:2], mynewpts[2:])) other.item.coords((othernewpts[:2], othernewpts[2:])) self.array.master.update() self.item.config(fill=myfill) other.item.config(fill=otherfill) self.array.wait(0) return mytrajectory = interpolate(myoldpts, mynewpts, nsteps) othertrajectory = interpolate(otheroldpts, othernewpts, nsteps) if self.value > other.value: self.item.tkraise() other.item.tkraise() else: other.item.tkraise() self.item.tkraise() try: for i in range(len(mytrajectory)): mypts = mytrajectory[i] otherpts = othertrajectory[i] self.item.coords((mypts[:2], mypts[2:])) other.item.coords((otherpts[:2], otherpts[2:])) self.array.wait(50) finally: mypts = mytrajectory[-1] otherpts = othertrajectory[-1] self.item.coords((mypts[:2], mypts[2:])) other.item.coords((otherpts[:2], otherpts[2:])) self.item.config(fill=myfill) other.item.config(fill=otherfill) def compareto(self, other): myfill = self.item['fill'] otherfill = other.item['fill'] outcome = cmp(self.value, other.value) if outcome < 0: myflash = 'white' otherflash = 'black' elif outcome > 0: myflash = 'black' otherflash = 'white' else: myflash = otherflash = 'grey' try: self.item.config(fill=myflash) other.item.config(fill=otherflash) self.array.wait(500) finally: self.item.config(fill=myfill) other.item.config(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] + range(5, 55, 5) if self.size not in sizes: sizes.append(self.size) sizes.sort() self.m_size = apply(OptionMenu, (self.botleftframe, self.v_size) + tuple(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()