3 """Sorting algorithms visualizer using Tkinter.
5 This module is comprised of three ``components'':
7 - an array visualizer with methods that implement basic sorting
8 operations (compare, swap) as well as methods for ``annotating'' the
9 sorting algorithm (e.g. to show the pivot element);
11 - a number of sorting algorithms (currently quicksort, insertion sort,
12 selection sort and bubble sort, as well as a randomization function),
13 all using the array visualizer for its basic operations and with calls
14 to its annotation methods;
16 - and a ``driver'' class which can be used as a Grail applet or as a
17 stand-alone application.
23 from Canvas
import Line
, Rectangle
34 def __init__(self
, master
, data
=None):
36 self
.frame
= Frame(self
.master
)
37 self
.frame
.pack(fill
=X
)
38 self
.label
= Label(self
.frame
)
40 self
.canvas
= Canvas(self
.frame
)
42 self
.report
= Label(self
.frame
)
44 self
.left
= Line(self
.canvas
, 0, 0, 0, 0)
45 self
.right
= Line(self
.canvas
, 0, 0, 0, 0)
46 self
.pivot
= Line(self
.canvas
, 0, 0, 0, 0)
48 self
.size
= self
.maxvalue
= 0
52 def setdata(self
, data
):
58 self
.maxvalue
= max(data
)
59 self
.canvas
.config(width
=(self
.size
+1)*XGRID
,
60 height
=(self
.maxvalue
+1)*YGRID
)
61 for i
in range(self
.size
):
62 self
.items
.append(ArrayItem(self
, i
, data
[i
]))
63 self
.reset("Sort demo, size %d" % self
.size
)
67 def setspeed(self
, speed
):
77 self
.stop_mainloop
= 1
85 Cancelled
= "Array.Cancelled" # Exception
87 def wait(self
, msecs
):
88 if self
.speed
== "fastest":
90 elif self
.speed
== "fast":
92 elif self
.speed
== "single-step":
94 if not self
.stop_mainloop
:
96 id = self
.master
.after(msecs
, self
.master
.quit
)
98 self
.master
.mainloop()
99 self
.master
.after_cancel(id)
101 if self
.stop_mainloop
:
102 self
.stop_mainloop
= 0
103 self
.message("Cancelled")
104 raise Array
.Cancelled
109 def show_partition(self
, first
, last
):
110 for i
in range(self
.size
):
112 if first
<= i
< last
:
113 item
.item
.config(fill
='red')
115 item
.item
.config(fill
='orange')
116 self
.hide_left_right_pivot()
118 def hide_partition(self
):
119 for i
in range(self
.size
):
121 item
.item
.config(fill
='red')
122 self
.hide_left_right_pivot()
124 def show_left(self
, left
):
125 if not 0 <= left
< self
.size
:
128 x1
, y1
, x2
, y2
= self
.items
[left
].position()
130 self
.left
.coords([(x1
-2, 0), (x1
-2, 9999)])
133 def show_right(self
, right
):
134 if not 0 <= right
< self
.size
:
137 x1
, y1
, x2
, y2
= self
.items
[right
].position()
138 self
.right
.coords(((x2
+2, 0), (x2
+2, 9999)))
141 def hide_left_right_pivot(self
):
147 self
.left
.coords(((0, 0), (0, 0)))
149 def hide_right(self
):
150 self
.right
.coords(((0, 0), (0, 0)))
152 def show_pivot(self
, pivot
):
153 x1
, y1
, x2
, y2
= self
.items
[pivot
].position()
154 self
.pivot
.coords(((0, y1
-2), (9999, y1
-2)))
156 def hide_pivot(self
):
157 self
.pivot
.coords(((0, 0), (0, 0)))
159 def swap(self
, i
, j
):
163 other
= self
.items
[j
]
164 self
.items
[i
], self
.items
[j
] = other
, item
167 def compare(self
, i
, j
):
170 other
= self
.items
[j
]
171 return item
.compareto(other
)
173 def reset(self
, msg
):
178 self
.hide_partition()
180 def message(self
, msg
):
181 self
.label
.config(text
=msg
)
184 self
.nswaps
= self
.nswaps
+ 1
187 def countcompare(self
):
188 self
.ncompares
= self
.ncompares
+ 1
191 def updatereport(self
):
192 text
= "%d cmps, %d swaps" % (self
.ncompares
, self
.nswaps
)
193 self
.report
.config(text
=text
)
198 def __init__(self
, array
, index
, value
):
202 x1
, y1
, x2
, y2
= self
.position()
203 self
.item
= Rectangle(array
.canvas
, x1
, y1
, x2
, y2
,
204 fill
='red', outline
='black', width
=1)
205 self
.item
.bind('<Button-1>', self
.mouse_down
)
206 self
.item
.bind('<Button1-Motion>', self
.mouse_move
)
207 self
.item
.bind('<ButtonRelease-1>', self
.mouse_up
)
215 def mouse_down(self
, event
):
222 def mouse_move(self
, event
):
223 self
.item
.move(event
.x
- self
.lastx
, event
.y
- self
.lasty
)
227 def mouse_up(self
, event
):
228 i
= self
.nearestindex(event
.x
)
229 if i
>= self
.array
.getsize():
230 i
= self
.array
.getsize() - 1
233 other
= self
.array
.items
[i
]
235 self
.array
.items
[here
], self
.array
.items
[i
] = other
, self
237 x1
, y1
, x2
, y2
= self
.position()
238 self
.item
.coords(((x1
, y1
), (x2
, y2
)))
241 def setindex(self
, index
):
242 nsteps
= steps(self
.index
, index
)
243 if not nsteps
: return
244 if self
.array
.speed
== "fastest":
246 oldpts
= self
.position()
248 newpts
= self
.position()
249 trajectory
= interpolate(oldpts
, newpts
, nsteps
)
251 for pts
in trajectory
:
252 self
.item
.coords((pts
[:2], pts
[2:]))
255 def swapwith(self
, other
):
256 nsteps
= steps(self
.index
, other
.index
)
257 if not nsteps
: return
258 if self
.array
.speed
== "fastest":
260 myoldpts
= self
.position()
261 otheroldpts
= other
.position()
262 self
.index
, other
.index
= other
.index
, self
.index
263 mynewpts
= self
.position()
264 othernewpts
= other
.position()
265 myfill
= self
.item
['fill']
266 otherfill
= other
.item
['fill']
267 self
.item
.config(fill
='green')
268 other
.item
.config(fill
='yellow')
269 self
.array
.master
.update()
270 if self
.array
.speed
== "single-step":
271 self
.item
.coords((mynewpts
[:2], mynewpts
[2:]))
272 other
.item
.coords((othernewpts
[:2], othernewpts
[2:]))
273 self
.array
.master
.update()
274 self
.item
.config(fill
=myfill
)
275 other
.item
.config(fill
=otherfill
)
278 mytrajectory
= interpolate(myoldpts
, mynewpts
, nsteps
)
279 othertrajectory
= interpolate(otheroldpts
, othernewpts
, nsteps
)
280 if self
.value
> other
.value
:
287 for i
in range(len(mytrajectory
)):
288 mypts
= mytrajectory
[i
]
289 otherpts
= othertrajectory
[i
]
290 self
.item
.coords((mypts
[:2], mypts
[2:]))
291 other
.item
.coords((otherpts
[:2], otherpts
[2:]))
294 mypts
= mytrajectory
[-1]
295 otherpts
= othertrajectory
[-1]
296 self
.item
.coords((mypts
[:2], mypts
[2:]))
297 other
.item
.coords((otherpts
[:2], otherpts
[2:]))
298 self
.item
.config(fill
=myfill
)
299 other
.item
.config(fill
=otherfill
)
301 def compareto(self
, other
):
302 myfill
= self
.item
['fill']
303 otherfill
= other
.item
['fill']
304 outcome
= cmp(self
.value
, other
.value
)
312 myflash
= otherflash
= 'grey'
314 self
.item
.config(fill
=myflash
)
315 other
.item
.config(fill
=otherflash
)
318 self
.item
.config(fill
=myfill
)
319 other
.item
.config(fill
=otherfill
)
323 x1
= (self
.index
+1)*XGRID
- WIDTH
//2
325 y2
= (self
.array
.maxvalue
+1)*YGRID
326 y1
= y2
- (self
.value
)*YGRID
327 return x1
, y1
, x2
, y2
329 def nearestindex(self
, x
):
330 return int(round(float(x
)/XGRID
)) - 1
333 # Subroutines that don't need an object
335 def steps(here
, there
):
336 nsteps
= abs(here
- there
)
345 def interpolate(oldpts
, newpts
, n
):
346 if len(oldpts
) != len(newpts
):
347 raise ValueError, "can't interpolate arrays of different length"
348 pts
= [0]*len(oldpts
)
349 res
= [tuple(oldpts
)]
350 for i
in range(1, n
):
351 for k
in range(len(pts
)):
352 pts
[k
] = oldpts
[k
] + (newpts
[k
] - oldpts
[k
])*i
//n
353 res
.append(tuple(pts
))
354 res
.append(tuple(newpts
))
358 # Various (un)sorting algorithms
361 size
= array
.getsize()
362 array
.setdata([(size
+1)//2] * size
)
363 array
.reset("Uniform data, size %d" % size
)
366 size
= array
.getsize()
367 array
.setdata(range(1, size
+1))
368 array
.reset("Distinct data, size %d" % size
)
370 def randomize(array
):
371 array
.reset("Randomizing")
374 j
= random
.randint(0, n
-1)
376 array
.message("Randomized")
378 def insertionsort(array
):
379 size
= array
.getsize()
380 array
.reset("Insertion sort")
381 for i
in range(1, size
):
384 if array
.compare(j
, j
+1) <= 0:
388 array
.message("Sorted")
390 def selectionsort(array
):
391 size
= array
.getsize()
392 array
.reset("Selection sort")
394 for i
in range(size
):
395 array
.show_partition(i
, size
)
396 for j
in range(i
+1, size
):
397 if array
.compare(i
, j
) > 0:
399 array
.message("Sorted")
401 array
.hide_partition()
403 def bubblesort(array
):
404 size
= array
.getsize()
405 array
.reset("Bubble sort")
406 for i
in range(size
):
407 for j
in range(1, size
):
408 if array
.compare(j
-1, j
) > 0:
410 array
.message("Sorted")
412 def quicksort(array
):
413 size
= array
.getsize()
414 array
.reset("Quicksort")
418 first
, last
= stack
[-1]
420 array
.show_partition(first
, last
)
422 array
.message("Insertion sort")
423 for i
in range(first
+1, last
):
426 if array
.compare(j
, j
+1) <= 0:
431 array
.message("Choosing pivot")
432 j
, i
, k
= first
, (first
+last
)//2, last
-1
433 if array
.compare(k
, i
) < 0:
435 if array
.compare(k
, j
) < 0:
437 if array
.compare(j
, i
) < 0:
440 array
.show_pivot(pivot
)
441 array
.message("Pivot at left of partition")
446 array
.message("Sweep right pointer")
448 array
.show_right(right
)
449 while right
> first
and array
.compare(right
, pivot
) >= 0:
451 array
.show_right(right
)
452 array
.message("Sweep left pointer")
454 array
.show_left(left
)
455 while left
< last
and array
.compare(left
, pivot
) <= 0:
457 array
.show_left(left
)
459 array
.message("End of partition")
461 array
.message("Swap items")
462 array
.swap(left
, right
)
463 array
.message("Swap pivot back")
464 array
.swap(pivot
, right
)
467 if n1
> 1: stack
.append((first
, right
))
468 if n2
> 1: stack
.append((left
, last
))
469 array
.message("Sorted")
471 array
.hide_partition()
475 for alg
in [quicksort
, insertionsort
, selectionsort
, bubblesort
]:
480 # Sort demo class -- usable as a Grail applet
484 def __init__(self
, master
, size
=15):
488 self
.array
= Array(self
.master
)
490 self
.botframe
= Frame(master
)
491 self
.botframe
.pack(side
=BOTTOM
)
492 self
.botleftframe
= Frame(self
.botframe
)
493 self
.botleftframe
.pack(side
=LEFT
, fill
=Y
)
494 self
.botrightframe
= Frame(self
.botframe
)
495 self
.botrightframe
.pack(side
=RIGHT
, fill
=Y
)
497 self
.b_qsort
= Button(self
.botleftframe
,
498 text
="Quicksort", command
=self
.c_qsort
)
499 self
.b_qsort
.pack(fill
=X
)
500 self
.b_isort
= Button(self
.botleftframe
,
501 text
="Insertion sort", command
=self
.c_isort
)
502 self
.b_isort
.pack(fill
=X
)
503 self
.b_ssort
= Button(self
.botleftframe
,
504 text
="Selection sort", command
=self
.c_ssort
)
505 self
.b_ssort
.pack(fill
=X
)
506 self
.b_bsort
= Button(self
.botleftframe
,
507 text
="Bubble sort", command
=self
.c_bsort
)
508 self
.b_bsort
.pack(fill
=X
)
510 # Terrible hack to overcome limitation of OptionMenu...
511 class MyIntVar(IntVar
):
512 def __init__(self
, master
, demo
):
514 IntVar
.__init
__(self
, master
)
515 def set(self
, value
):
516 IntVar
.set(self
, value
)
517 if str(value
) != '0':
518 self
.demo
.resize(value
)
520 self
.v_size
= MyIntVar(self
.master
, self
)
521 self
.v_size
.set(size
)
522 sizes
= [1, 2, 3, 4] + range(5, 55, 5)
523 if self
.size
not in sizes
:
524 sizes
.append(self
.size
)
526 self
.m_size
= apply(OptionMenu
,
527 (self
.botleftframe
, self
.v_size
) + tuple(sizes
))
528 self
.m_size
.pack(fill
=X
)
530 self
.v_speed
= StringVar(self
.master
)
531 self
.v_speed
.set("normal")
532 self
.m_speed
= OptionMenu(self
.botleftframe
, self
.v_speed
,
533 "single-step", "normal", "fast", "fastest")
534 self
.m_speed
.pack(fill
=X
)
536 self
.b_step
= Button(self
.botleftframe
,
537 text
="Step", command
=self
.c_step
)
538 self
.b_step
.pack(fill
=X
)
540 self
.b_randomize
= Button(self
.botrightframe
,
541 text
="Randomize", command
=self
.c_randomize
)
542 self
.b_randomize
.pack(fill
=X
)
543 self
.b_uniform
= Button(self
.botrightframe
,
544 text
="Uniform", command
=self
.c_uniform
)
545 self
.b_uniform
.pack(fill
=X
)
546 self
.b_distinct
= Button(self
.botrightframe
,
547 text
="Distinct", command
=self
.c_distinct
)
548 self
.b_distinct
.pack(fill
=X
)
549 self
.b_demo
= Button(self
.botrightframe
,
550 text
="Demo", command
=self
.c_demo
)
551 self
.b_demo
.pack(fill
=X
)
552 self
.b_cancel
= Button(self
.botrightframe
,
553 text
="Cancel", command
=self
.c_cancel
)
554 self
.b_cancel
.pack(fill
=X
)
555 self
.b_cancel
.config(state
=DISABLED
)
556 self
.b_quit
= Button(self
.botrightframe
,
557 text
="Quit", command
=self
.c_quit
)
558 self
.b_quit
.pack(fill
=X
)
560 def resize(self
, newsize
):
565 self
.array
.setdata(range(1, self
.size
+1))
571 self
.run(insertionsort
)
574 self
.run(selectionsort
)
582 def c_randomize(self
):
588 def c_distinct(self
):
596 self
.array
.setspeed(self
.v_speed
.get())
597 self
.b_cancel
.config(state
=NORMAL
)
600 except Array
.Cancelled
:
602 self
.b_cancel
.config(state
=DISABLED
)
615 self
.v_speed
.set("single-step")
616 self
.array
.setspeed("single-step")
622 self
.master
.after_idle(self
.master
.quit
)
625 # Main program -- for stand-alone operation outside Grail
629 demo
= SortDemo(root
)
630 root
.protocol('WM_DELETE_WINDOW', demo
.c_quit
)
633 if __name__
== '__main__':