sortvisu.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635
  1. #!/usr/bin/env python3
  2. """
  3. Sorting algorithms visualizer using Tkinter.
  4. This module is comprised of three ``components'':
  5. - an array visualizer with methods that implement basic sorting
  6. operations (compare, swap) as well as methods for ``annotating'' the
  7. sorting algorithm (e.g. to show the pivot element);
  8. - a number of sorting algorithms (currently quicksort, insertion sort,
  9. selection sort and bubble sort, as well as a randomization function),
  10. all using the array visualizer for its basic operations and with calls
  11. to its annotation methods;
  12. - and a ``driver'' class which can be used as a Grail applet or as a
  13. stand-alone application.
  14. """
  15. from tkinter import *
  16. import random
  17. XGRID = 10
  18. YGRID = 10
  19. WIDTH = 6
  20. class Array:
  21. class Cancelled(BaseException):
  22. pass
  23. def __init__(self, master, data=None):
  24. self.master = master
  25. self.frame = Frame(self.master)
  26. self.frame.pack(fill=X)
  27. self.label = Label(self.frame)
  28. self.label.pack()
  29. self.canvas = Canvas(self.frame)
  30. self.canvas.pack()
  31. self.report = Label(self.frame)
  32. self.report.pack()
  33. self.left = self.canvas.create_line(0, 0, 0, 0)
  34. self.right = self.canvas.create_line(0, 0, 0, 0)
  35. self.pivot = self.canvas.create_line(0, 0, 0, 0)
  36. self.items = []
  37. self.size = self.maxvalue = 0
  38. if data:
  39. self.setdata(data)
  40. def setdata(self, data):
  41. olditems = self.items
  42. self.items = []
  43. for item in olditems:
  44. item.delete()
  45. self.size = len(data)
  46. self.maxvalue = max(data)
  47. self.canvas.config(width=(self.size+1)*XGRID,
  48. height=(self.maxvalue+1)*YGRID)
  49. for i in range(self.size):
  50. self.items.append(ArrayItem(self, i, data[i]))
  51. self.reset("Sort demo, size %d" % self.size)
  52. speed = "normal"
  53. def setspeed(self, speed):
  54. self.speed = speed
  55. def destroy(self):
  56. self.frame.destroy()
  57. in_mainloop = 0
  58. stop_mainloop = 0
  59. def cancel(self):
  60. self.stop_mainloop = 1
  61. if self.in_mainloop:
  62. self.master.quit()
  63. def step(self):
  64. if self.in_mainloop:
  65. self.master.quit()
  66. def wait(self, msecs):
  67. if self.speed == "fastest":
  68. msecs = 0
  69. elif self.speed == "fast":
  70. msecs = msecs//10
  71. elif self.speed == "single-step":
  72. msecs = 1000000000
  73. if not self.stop_mainloop:
  74. self.master.update()
  75. id = self.master.after(msecs, self.master.quit)
  76. self.in_mainloop = 1
  77. self.master.mainloop()
  78. self.master.after_cancel(id)
  79. self.in_mainloop = 0
  80. if self.stop_mainloop:
  81. self.stop_mainloop = 0
  82. self.message("Cancelled")
  83. raise Array.Cancelled
  84. def getsize(self):
  85. return self.size
  86. def show_partition(self, first, last):
  87. for i in range(self.size):
  88. item = self.items[i]
  89. if first <= i < last:
  90. self.canvas.itemconfig(item, fill='red')
  91. else:
  92. self.canvas.itemconfig(item, fill='orange')
  93. self.hide_left_right_pivot()
  94. def hide_partition(self):
  95. for i in range(self.size):
  96. item = self.items[i]
  97. self.canvas.itemconfig(item, fill='red')
  98. self.hide_left_right_pivot()
  99. def show_left(self, left):
  100. if not 0 <= left < self.size:
  101. self.hide_left()
  102. return
  103. x1, y1, x2, y2 = self.items[left].position()
  104. ## top, bot = HIRO
  105. self.canvas.coords(self.left, (x1 - 2, 0, x1 - 2, 9999))
  106. self.master.update()
  107. def show_right(self, right):
  108. if not 0 <= right < self.size:
  109. self.hide_right()
  110. return
  111. x1, y1, x2, y2 = self.items[right].position()
  112. self.canvas.coords(self.right, (x2 + 2, 0, x2 + 2, 9999))
  113. self.master.update()
  114. def hide_left_right_pivot(self):
  115. self.hide_left()
  116. self.hide_right()
  117. self.hide_pivot()
  118. def hide_left(self):
  119. self.canvas.coords(self.left, (0, 0, 0, 0))
  120. def hide_right(self):
  121. self.canvas.coords(self.right, (0, 0, 0, 0))
  122. def show_pivot(self, pivot):
  123. x1, y1, x2, y2 = self.items[pivot].position()
  124. self.canvas.coords(self.pivot, (0, y1 - 2, 9999, y1 - 2))
  125. def hide_pivot(self):
  126. self.canvas.coords(self.pivot, (0, 0, 0, 0))
  127. def swap(self, i, j):
  128. if i == j: return
  129. self.countswap()
  130. item = self.items[i]
  131. other = self.items[j]
  132. self.items[i], self.items[j] = other, item
  133. item.swapwith(other)
  134. def compare(self, i, j):
  135. self.countcompare()
  136. item = self.items[i]
  137. other = self.items[j]
  138. return item.compareto(other)
  139. def reset(self, msg):
  140. self.ncompares = 0
  141. self.nswaps = 0
  142. self.message(msg)
  143. self.updatereport()
  144. self.hide_partition()
  145. def message(self, msg):
  146. self.label.config(text=msg)
  147. def countswap(self):
  148. self.nswaps = self.nswaps + 1
  149. self.updatereport()
  150. def countcompare(self):
  151. self.ncompares = self.ncompares + 1
  152. self.updatereport()
  153. def updatereport(self):
  154. text = "%d cmps, %d swaps" % (self.ncompares, self.nswaps)
  155. self.report.config(text=text)
  156. class ArrayItem:
  157. def __init__(self, array, index, value):
  158. self.array = array
  159. self.index = index
  160. self.value = value
  161. self.canvas = array.canvas
  162. x1, y1, x2, y2 = self.position()
  163. self.item_id = array.canvas.create_rectangle(x1, y1, x2, y2,
  164. fill='red', outline='black', width=1)
  165. self.canvas.tag_bind(self.item_id, '<Button-1>', self.mouse_down)
  166. self.canvas.tag_bind(self.item_id, '<Button1-Motion>', self.mouse_move)
  167. self.canvas.tag_bind(self.item_id, '<ButtonRelease-1>', self.mouse_up)
  168. def delete(self):
  169. item_id = self.item_id
  170. self.array = None
  171. self.item_id = None
  172. self.canvas.delete(item_id)
  173. def mouse_down(self, event):
  174. self.lastx = event.x
  175. self.lasty = event.y
  176. self.origx = event.x
  177. self.origy = event.y
  178. self.canvas.tag_raise(self.item_id)
  179. def mouse_move(self, event):
  180. self.canvas.move(self.item_id,
  181. event.x - self.lastx, event.y - self.lasty)
  182. self.lastx = event.x
  183. self.lasty = event.y
  184. def mouse_up(self, event):
  185. i = self.nearestindex(event.x)
  186. if i >= self.array.getsize():
  187. i = self.array.getsize() - 1
  188. if i < 0:
  189. i = 0
  190. other = self.array.items[i]
  191. here = self.index
  192. self.array.items[here], self.array.items[i] = other, self
  193. self.index = i
  194. x1, y1, x2, y2 = self.position()
  195. self.canvas.coords(self.item_id, (x1, y1, x2, y2))
  196. other.setindex(here)
  197. def setindex(self, index):
  198. nsteps = steps(self.index, index)
  199. if not nsteps: return
  200. if self.array.speed == "fastest":
  201. nsteps = 0
  202. oldpts = self.position()
  203. self.index = index
  204. newpts = self.position()
  205. trajectory = interpolate(oldpts, newpts, nsteps)
  206. self.canvas.tag_raise(self.item_id)
  207. for pts in trajectory:
  208. self.canvas.coords(self.item_id, pts)
  209. self.array.wait(50)
  210. def swapwith(self, other):
  211. nsteps = steps(self.index, other.index)
  212. if not nsteps: return
  213. if self.array.speed == "fastest":
  214. nsteps = 0
  215. myoldpts = self.position()
  216. otheroldpts = other.position()
  217. self.index, other.index = other.index, self.index
  218. mynewpts = self.position()
  219. othernewpts = other.position()
  220. myfill = self.canvas.itemcget(self.item_id, 'fill')
  221. otherfill = self.canvas.itemcget(other.item_id, 'fill')
  222. self.canvas.itemconfig(self.item_id, fill='green')
  223. self.canvas.itemconfig(other.item_id, fill='yellow')
  224. self.array.master.update()
  225. if self.array.speed == "single-step":
  226. self.canvas.coords(self.item_id, mynewpts)
  227. self.canvas.coords(other.item_id, othernewpts)
  228. self.array.master.update()
  229. self.canvas.itemconfig(self.item_id, fill=myfill)
  230. self.canvas.itemconfig(other.item_id, fill=otherfill)
  231. self.array.wait(0)
  232. return
  233. mytrajectory = interpolate(myoldpts, mynewpts, nsteps)
  234. othertrajectory = interpolate(otheroldpts, othernewpts, nsteps)
  235. if self.value > other.value:
  236. self.canvas.tag_raise(self.item_id)
  237. self.canvas.tag_raise(other.item_id)
  238. else:
  239. self.canvas.tag_raise(other.item_id)
  240. self.canvas.tag_raise(self.item_id)
  241. try:
  242. for i in range(len(mytrajectory)):
  243. mypts = mytrajectory[i]
  244. otherpts = othertrajectory[i]
  245. self.canvas.coords(self.item_id, mypts)
  246. self.canvas.coords(other.item_id, otherpts)
  247. self.array.wait(50)
  248. finally:
  249. mypts = mytrajectory[-1]
  250. otherpts = othertrajectory[-1]
  251. self.canvas.coords(self.item_id, mypts)
  252. self.canvas.coords(other.item_id, otherpts)
  253. self.canvas.itemconfig(self.item_id, fill=myfill)
  254. self.canvas.itemconfig(other.item_id, fill=otherfill)
  255. def compareto(self, other):
  256. myfill = self.canvas.itemcget(self.item_id, 'fill')
  257. otherfill = self.canvas.itemcget(other.item_id, 'fill')
  258. if self.value < other.value:
  259. myflash = 'white'
  260. otherflash = 'black'
  261. outcome = -1
  262. elif self.value > other.value:
  263. myflash = 'black'
  264. otherflash = 'white'
  265. outcome = 1
  266. else:
  267. myflash = otherflash = 'grey'
  268. outcome = 0
  269. try:
  270. self.canvas.itemconfig(self.item_id, fill=myflash)
  271. self.canvas.itemconfig(other.item_id, fill=otherflash)
  272. self.array.wait(500)
  273. finally:
  274. self.canvas.itemconfig(self.item_id, fill=myfill)
  275. self.canvas.itemconfig(other.item_id, fill=otherfill)
  276. return outcome
  277. def position(self):
  278. x1 = (self.index+1)*XGRID - WIDTH//2
  279. x2 = x1+WIDTH
  280. y2 = (self.array.maxvalue+1)*YGRID
  281. y1 = y2 - (self.value)*YGRID
  282. return x1, y1, x2, y2
  283. def nearestindex(self, x):
  284. return int(round(float(x)/XGRID)) - 1
  285. # Subroutines that don't need an object
  286. def steps(here, there):
  287. nsteps = abs(here - there)
  288. if nsteps <= 3:
  289. nsteps = nsteps * 3
  290. elif nsteps <= 5:
  291. nsteps = nsteps * 2
  292. elif nsteps > 10:
  293. nsteps = 10
  294. return nsteps
  295. def interpolate(oldpts, newpts, n):
  296. if len(oldpts) != len(newpts):
  297. raise ValueError("can't interpolate arrays of different length")
  298. pts = [0]*len(oldpts)
  299. res = [tuple(oldpts)]
  300. for i in range(1, n):
  301. for k in range(len(pts)):
  302. pts[k] = oldpts[k] + (newpts[k] - oldpts[k])*i//n
  303. res.append(tuple(pts))
  304. res.append(tuple(newpts))
  305. return res
  306. # Various (un)sorting algorithms
  307. def uniform(array):
  308. size = array.getsize()
  309. array.setdata([(size+1)//2] * size)
  310. array.reset("Uniform data, size %d" % size)
  311. def distinct(array):
  312. size = array.getsize()
  313. array.setdata(range(1, size+1))
  314. array.reset("Distinct data, size %d" % size)
  315. def randomize(array):
  316. array.reset("Randomizing")
  317. n = array.getsize()
  318. for i in range(n):
  319. j = random.randint(0, n-1)
  320. array.swap(i, j)
  321. array.message("Randomized")
  322. def insertionsort(array):
  323. size = array.getsize()
  324. array.reset("Insertion sort")
  325. for i in range(1, size):
  326. j = i-1
  327. while j >= 0:
  328. if array.compare(j, j+1) <= 0:
  329. break
  330. array.swap(j, j+1)
  331. j = j-1
  332. array.message("Sorted")
  333. def selectionsort(array):
  334. size = array.getsize()
  335. array.reset("Selection sort")
  336. try:
  337. for i in range(size):
  338. array.show_partition(i, size)
  339. for j in range(i+1, size):
  340. if array.compare(i, j) > 0:
  341. array.swap(i, j)
  342. array.message("Sorted")
  343. finally:
  344. array.hide_partition()
  345. def bubblesort(array):
  346. size = array.getsize()
  347. array.reset("Bubble sort")
  348. for i in range(size):
  349. for j in range(1, size):
  350. if array.compare(j-1, j) > 0:
  351. array.swap(j-1, j)
  352. array.message("Sorted")
  353. def quicksort(array):
  354. size = array.getsize()
  355. array.reset("Quicksort")
  356. try:
  357. stack = [(0, size)]
  358. while stack:
  359. first, last = stack[-1]
  360. del stack[-1]
  361. array.show_partition(first, last)
  362. if last-first < 5:
  363. array.message("Insertion sort")
  364. for i in range(first+1, last):
  365. j = i-1
  366. while j >= first:
  367. if array.compare(j, j+1) <= 0:
  368. break
  369. array.swap(j, j+1)
  370. j = j-1
  371. continue
  372. array.message("Choosing pivot")
  373. j, i, k = first, (first+last) // 2, last-1
  374. if array.compare(k, i) < 0:
  375. array.swap(k, i)
  376. if array.compare(k, j) < 0:
  377. array.swap(k, j)
  378. if array.compare(j, i) < 0:
  379. array.swap(j, i)
  380. pivot = j
  381. array.show_pivot(pivot)
  382. array.message("Pivot at left of partition")
  383. array.wait(1000)
  384. left = first
  385. right = last
  386. while 1:
  387. array.message("Sweep right pointer")
  388. right = right-1
  389. array.show_right(right)
  390. while right > first and array.compare(right, pivot) >= 0:
  391. right = right-1
  392. array.show_right(right)
  393. array.message("Sweep left pointer")
  394. left = left+1
  395. array.show_left(left)
  396. while left < last and array.compare(left, pivot) <= 0:
  397. left = left+1
  398. array.show_left(left)
  399. if left > right:
  400. array.message("End of partition")
  401. break
  402. array.message("Swap items")
  403. array.swap(left, right)
  404. array.message("Swap pivot back")
  405. array.swap(pivot, right)
  406. n1 = right-first
  407. n2 = last-left
  408. if n1 > 1: stack.append((first, right))
  409. if n2 > 1: stack.append((left, last))
  410. array.message("Sorted")
  411. finally:
  412. array.hide_partition()
  413. def demosort(array):
  414. while 1:
  415. for alg in [quicksort, insertionsort, selectionsort, bubblesort]:
  416. randomize(array)
  417. alg(array)
  418. # Sort demo class -- usable as a Grail applet
  419. class SortDemo:
  420. def __init__(self, master, size=15):
  421. self.master = master
  422. self.size = size
  423. self.busy = 0
  424. self.array = Array(self.master)
  425. self.botframe = Frame(master)
  426. self.botframe.pack(side=BOTTOM)
  427. self.botleftframe = Frame(self.botframe)
  428. self.botleftframe.pack(side=LEFT, fill=Y)
  429. self.botrightframe = Frame(self.botframe)
  430. self.botrightframe.pack(side=RIGHT, fill=Y)
  431. self.b_qsort = Button(self.botleftframe,
  432. text="Quicksort", command=self.c_qsort)
  433. self.b_qsort.pack(fill=X)
  434. self.b_isort = Button(self.botleftframe,
  435. text="Insertion sort", command=self.c_isort)
  436. self.b_isort.pack(fill=X)
  437. self.b_ssort = Button(self.botleftframe,
  438. text="Selection sort", command=self.c_ssort)
  439. self.b_ssort.pack(fill=X)
  440. self.b_bsort = Button(self.botleftframe,
  441. text="Bubble sort", command=self.c_bsort)
  442. self.b_bsort.pack(fill=X)
  443. # Terrible hack to overcome limitation of OptionMenu...
  444. class MyIntVar(IntVar):
  445. def __init__(self, master, demo):
  446. self.demo = demo
  447. IntVar.__init__(self, master)
  448. def set(self, value):
  449. IntVar.set(self, value)
  450. if str(value) != '0':
  451. self.demo.resize(value)
  452. self.v_size = MyIntVar(self.master, self)
  453. self.v_size.set(size)
  454. sizes = [1, 2, 3, 4] + list(range(5, 55, 5))
  455. if self.size not in sizes:
  456. sizes.append(self.size)
  457. sizes.sort()
  458. self.m_size = OptionMenu(self.botleftframe, self.v_size, *sizes)
  459. self.m_size.pack(fill=X)
  460. self.v_speed = StringVar(self.master)
  461. self.v_speed.set("normal")
  462. self.m_speed = OptionMenu(self.botleftframe, self.v_speed,
  463. "single-step", "normal", "fast", "fastest")
  464. self.m_speed.pack(fill=X)
  465. self.b_step = Button(self.botleftframe,
  466. text="Step", command=self.c_step)
  467. self.b_step.pack(fill=X)
  468. self.b_randomize = Button(self.botrightframe,
  469. text="Randomize", command=self.c_randomize)
  470. self.b_randomize.pack(fill=X)
  471. self.b_uniform = Button(self.botrightframe,
  472. text="Uniform", command=self.c_uniform)
  473. self.b_uniform.pack(fill=X)
  474. self.b_distinct = Button(self.botrightframe,
  475. text="Distinct", command=self.c_distinct)
  476. self.b_distinct.pack(fill=X)
  477. self.b_demo = Button(self.botrightframe,
  478. text="Demo", command=self.c_demo)
  479. self.b_demo.pack(fill=X)
  480. self.b_cancel = Button(self.botrightframe,
  481. text="Cancel", command=self.c_cancel)
  482. self.b_cancel.pack(fill=X)
  483. self.b_cancel.config(state=DISABLED)
  484. self.b_quit = Button(self.botrightframe,
  485. text="Quit", command=self.c_quit)
  486. self.b_quit.pack(fill=X)
  487. def resize(self, newsize):
  488. if self.busy:
  489. self.master.bell()
  490. return
  491. self.size = newsize
  492. self.array.setdata(range(1, self.size+1))
  493. def c_qsort(self):
  494. self.run(quicksort)
  495. def c_isort(self):
  496. self.run(insertionsort)
  497. def c_ssort(self):
  498. self.run(selectionsort)
  499. def c_bsort(self):
  500. self.run(bubblesort)
  501. def c_demo(self):
  502. self.run(demosort)
  503. def c_randomize(self):
  504. self.run(randomize)
  505. def c_uniform(self):
  506. self.run(uniform)
  507. def c_distinct(self):
  508. self.run(distinct)
  509. def run(self, func):
  510. if self.busy:
  511. self.master.bell()
  512. return
  513. self.busy = 1
  514. self.array.setspeed(self.v_speed.get())
  515. self.b_cancel.config(state=NORMAL)
  516. try:
  517. func(self.array)
  518. except Array.Cancelled:
  519. pass
  520. self.b_cancel.config(state=DISABLED)
  521. self.busy = 0
  522. def c_cancel(self):
  523. if not self.busy:
  524. self.master.bell()
  525. return
  526. self.array.cancel()
  527. def c_step(self):
  528. if not self.busy:
  529. self.master.bell()
  530. return
  531. self.v_speed.set("single-step")
  532. self.array.setspeed("single-step")
  533. self.array.step()
  534. def c_quit(self):
  535. if self.busy:
  536. self.array.cancel()
  537. self.master.after_idle(self.master.quit)
  538. # Main program -- for stand-alone operation outside Grail
  539. def main():
  540. root = Tk()
  541. demo = SortDemo(root)
  542. root.protocol('WM_DELETE_WINDOW', demo.c_quit)
  543. root.mainloop()
  544. if __name__ == '__main__':
  545. main()