{"cells":[{"cell_type":"markdown","metadata":{},"source":"Stacks and Queues\n=================\n\n"},{"cell_type":"markdown","metadata":{},"source":["## Agenda\n\n"]},{"cell_type":"markdown","metadata":{},"source":["1. Stacks\n - … for delimiter pairing\n - … for postfix expression evaluation\n - … for tracking execution and *backtracking*\n\n2. Queues\n - … for tracking execution and *backtracking*\n - … for fair scheduling (aka \"round-robin\" scheduling)\n - … for apportioning work\n\n3. Run-time analysis\n\n"]},{"cell_type":"markdown","metadata":{},"source":["## Overview\n\n"]},{"cell_type":"markdown","metadata":{},"source":["While the list data structure is incredibly useful, both implementations\nwe explored (array-backed and linked) have operations that run in $O(N)$\ntime, which make them non-ideal for use with large, growing collections\nof data.\n\nBy further restricting the list API, however — in particular, by\n*isolating points of access to either the front or end of the data set*\n— we can create data structures whose operations are all $O(1)$, and\nremain very useful in their own right.\n\n"]},{"cell_type":"markdown","metadata":{},"source":["## 1. Stacks\n\n"]},{"cell_type":"markdown","metadata":{},"source":["Stacks are linear data structures which only permit access to one \"end\"\nof the data collection. We can only append (\"push\") items onto the tail\nend (a.k.a. the \"top\") of a stack, and only the most recently added item\ncan be removed (\"popped\"). The last item to be pushed onto a stack is\ntherefore the first one to be popped off, which is why we refer to\nstacks as last-in, first out (LIFO) structures.\n\n"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["# array-backed implementation\n\nclass Stack:\n def __init__(self):\n self.data = []\n\n def push(self, val):\n pass\n\n def pop(self):\n assert not self.empty()\n pass\n\n def peek(self):\n assert not self.empty()\n pass\n\n def empty(self):\n pass\n\n def __bool__(self):\n return not self.empty()"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["s = Stack()\nfor x in range(10):\n s.push(x)"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["while s:\n print(s.pop())"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["# linked implementation\n\nclass Stack:\n class Node:\n def __init__(self, val, next=None):\n self.val = val\n self.next = next\n\n def __init__(self):\n self.top = None\n\n def push(self, val):\n pass\n\n def pop(self):\n assert not self.empty()\n pass\n\n def peek(self):\n assert not self.empty()\n pass\n\n def empty(self):\n pass\n\n def __bool__(self):\n return not self.empty()"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["s = Stack()\nfor x in range(10):\n s.push(x)"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["while s:\n print(s.pop())"]},{"cell_type":"markdown","metadata":{},"source":["### … for delimiter pairing\n\n"]},{"cell_type":"markdown","metadata":{},"source":["Stacks are used by parsers to decide if delimited expressions are\nwell-formed.\n\ne.g., `'(1 + 2 * (3 - (4 / 2) + 5) - (6 + 1))'`\n\n"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["def check_parens(expr):\n s = Stack()\n for c in expr:\n pass\n return False"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["check_parens('()')"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["check_parens('((()))')"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["check_parens('()(()()(()))')"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["check_parens('(')"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["check_parens('())')"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["check_parens('(1 + 2 * (3 - (4 / 2) + 5) - (6 + 1))')"]},{"cell_type":"markdown","metadata":{},"source":["### … for postfix expression (aka \"reverse polish notation\")\n\n"]},{"cell_type":"markdown","metadata":{},"source":["evaluation\nStacks are used for the evaluation of postfix arithmetic expressions\n(which can be easily converted back and forth between the more common\ninfix expressions).\n\ne.g., `'(1 + 2) * 5'` $\\rightarrow$ `'1 2 + 5 *'`\n\n"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["def eval_postfix(expr):\n s = Stack()\n toks = expr.split()\n for t in toks:\n pass\n return 0"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["eval_postfix('1 2 + 5 *')"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["eval_postfix('1 2 5 * +')"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["# ((1 + 2) * (3 + 2)) * 10\neval_postfix('1 2 + 3 2 + * 10 *')"]},{"cell_type":"markdown","metadata":{},"source":["### … for tracking execution and backtracking\n\n"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["maze_str = \"\"\"######\n I #\n # ## #\n # ####\n # O\n ######\"\"\"\n\ndef parse_maze(maze_str):\n '''Parses a string representing a maze into a 2D array.'''\n grid = []\n for line in maze_str.split('\\n'):\n grid.append(['# IO'.index(c) for c in line.strip()])\n return grid\n\ndef print_maze(grid):\n '''Takes a 2D array maze representation and pretty-prints it.\n The contents of the 2D maze are in the range 0-5, which are interpreted as:\n\n 0: a wall\n 1: an unvisited (i.e., not previously traversed) path\n 2: the maze entrance\n 3: the maze exit\n 4: a discovered but unvisited path\n 5: a visited path\n '''\n for r in grid:\n print(''.join('# IO!+'[c] for c in r))"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["parse_maze(maze_str)"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["print_maze(parse_maze(maze_str))"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["maze = parse_maze(maze_str)\nmaze[1][0] = maze[1][1] = 5\nmaze[1][2] = maze[2][1] = 4\nprint_maze(maze)"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["class Move:\n '''Represents a move in the maze between orthogonally adjacent locations\n `frm` and `to`, which are both (row,col) tuples.'''\n def __init__(self, frm, to):\n self.frm = frm\n self.to = to\n\n def __repr__(self):\n return '({},{}) -> ({},{})'.format(self.frm[0], self.frm[1],\n self.to[0], self.to[1])\n\ndef moves(maze, loc):\n '''Returns all possible moves within a maze from the provide location.'''\n moves = [Move(loc, (loc[0]+d[0], loc[1]+d[1]))\n for d in ((-1, 0), (1, 0), (0, -1), (0, 1))\n if loc[0]+d[0] in range(len(maze)) and\n loc[1]+d[1] in range(len(maze[0])) and\n maze[loc[0]+d[0]][loc[1]+d[1]] in (1, 2, 3)]\n return moves"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["maze = parse_maze(maze_str)\nprint_maze(maze)"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["moves(maze, (1, 0))"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["moves(maze, (1, 1))"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["maze[1][0] = 5\nmoves(maze, (1, 1))"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["from time import sleep\nfrom IPython.display import clear_output\n\ndef mark(maze, loc):\n '''Marks a loc in the maze as having been discovered'''\n if maze[loc[0]][loc[1]] != 3:\n maze[loc[0]][loc[1]] = 4\n\ndef visit(maze, loc):\n '''Marks a loc in the maze as having been visited'''\n maze[loc[0]][loc[1]] = 5\n\ndef display(maze):\n '''Prints out the maze after clearing the cell -- useful for animation.'''\n clear_output(True)\n print_maze(maze)\n sleep(0.5)"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["def solve_maze(maze, entry):\n '''Searches for the exit in a maze starting from the given entry point.\n\n The algorithm works as follows:\n\n 1. Visit the entry point and save all possible moves from that location.\n 2. Remove and consider one of the saved moves. If it is the exit, we are done,\n otherwise visit the destination and save all possible moves from there.\n 3. If we run out of saved moves, we can't find an exit.\n\n When we save a move, we also mark it as \"discovered\" in the maze.\n\n The data structure used to save moves plays a critical role in how maze\n exploration proceeds!\n '''\n for m in moves(maze, entry):\n save_move(m)\n visit(maze, entry)\n while not out_of_moves():\n move = next_move()\n if maze[move.to[0]][move.to[1]] == 3:\n break\n display(maze)\n visit(maze, move.to)\n for m in moves(maze, move.to):\n mark(maze, m.to)\n save_move(m)\n display(maze)"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["move_stack = Stack()\n\ndef save_move(move):\n pass\n\ndef next_move():\n pass\n\ndef out_of_moves():\n pass"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["maze_str = \"\"\"######\n I #\n # ## #\n # ####\n # O\n ######\"\"\"\nsolve_maze(parse_maze(maze_str), (1, 0))"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["maze_str = \"\"\"#################\n I # # #\n # ##### # # # # #\n # # # # # # #\n # ### ### # # ###\n # # # O\n #################\"\"\"\n\nsolve_maze(parse_maze(maze_str), (1, 0))"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["maze_str = \"\"\"#################\n I #\n # # # # # # # # #\n # # # # # # # # #\n # ###############\n # O\n #################\"\"\"\n\nsolve_maze(parse_maze(maze_str), (1, 0))"]},{"cell_type":"markdown","metadata":{},"source":["Intuitively, because the stack is a last-in, first-out data structure,\nit keeps following moves down the most recently discovered path until it\neither reaches the exit or reaches a dead end. It then picks up from the\npreviously discovered path. We call this type of exploration\n*depth-first traversal*.\n\n"]},{"cell_type":"markdown","metadata":{},"source":["## 1. Queues\n\n"]},{"cell_type":"markdown","metadata":{},"source":["Queues are linear data structures wherein we are only permitted to\nappend (\"enqueue\") items onto the rear, and remove (\"dequeue\") items\nfrom the front. The oldest item still in a queue is therefore the next\none to be dequeued, which is why we refer to a queue as a first-in,\nfirst-out (FIFO) structure. It is helpful to think of a queue as being\nthe model for a line at a typical supermarket checkout aisle (first\ncustomer in, first customer to be checked out).\n\n"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["# array-backed implementation\n\nclass Queue:\n def __init__(self):\n self.data = []\n\n def enqueue(self, val):\n pass\n\n def dequeue(self):\n assert not self.empty()\n pass\n\n def empty(self):\n pass\n\n def __bool__(self):\n return not self.empty()"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["q = Queue()\nfor x in range(10):\n q.enqueue(x)"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["while q:\n print(q.dequeue())"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["# circular array-backed implementation (partial)\n\nclass Queue:\n def __init__(self, size):\n self.data = [None] * size\n self.head = self.tail = -1\n\n def enqueue(self, val):\n pass\n\n def dequeue(self):\n pass"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["q = Queue(10)\nfor x in range(6):\n q.enqueue(x)"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["q.data"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["for x in range(5):\n print(q.dequeue())"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["q.data"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["for x in range(6, 12):\n q.enqueue(x)"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["q.data"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["# linked implementation\n\nclass Queue:\n class Node:\n def __init__(self, val, next=None):\n self.val = val\n self.next = next\n\n def __init__(self):\n self.head = self.tail = None\n\n def enqueue(self, val):\n pass\n\n def dequeue(self):\n assert not self.empty()\n pass\n\n def empty(self):\n pass\n\n def __bool__(self):\n return not self.empty()"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["q = Queue()\nfor x in range(10):\n q.enqueue(x)"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["while q:\n print(q.dequeue())"]},{"cell_type":"markdown","metadata":{},"source":["### … for tracking execution and backtracking\n\n"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["move_queue = Queue()\n\ndef save_move(move):\n pass\n\ndef next_move():\n pass\n\ndef out_of_moves():\n pass"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["maze_str = \"\"\"######\n I #\n # ## #\n # ####\n # O\n ######\"\"\"\n\nsolve_maze(parse_maze(maze_str), (1, 0))"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["maze_str = \"\"\"#################\n I # # #\n # ##### # # # # #\n # # # # # # #\n # ### ### # # ###\n # # # O\n #################\"\"\"\n\nsolve_maze(parse_maze(maze_str), (1, 0))"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["maze_str = \"\"\"#################\n I #\n # # # # # # # # #\n # # # # # # # # #\n # ###############\n # O\n #################\"\"\"\n\nsolve_maze(parse_maze(maze_str), (1, 0))"]},{"cell_type":"markdown","metadata":{},"source":["Intuitively, because the queue is a first-in, first-out – i.e., *fair*\n– data structure, it keeps rotating through all the paths which haven't\nyet dead-ended, making just one move further down each time. We call\nthis type of exploration *breadth-first traversal*.\n\nAre there types of mazes which might be more suitably tackled using one\napproach over the other (i.e., depth-first vs. breadth-first)?\n\n"]},{"cell_type":"markdown","metadata":{},"source":["### … for fair scheduling (aka \"round-robin\" scheduling)\n\n"]},{"cell_type":"markdown","metadata":{},"source":["Queues are often used to either carry out or simulate the fair\nallocation of resources. Here we implement a \"round-robin\" scheduler for\npermitting different tasks to run for small, fixed periods of time until\nthey complete:\n\n"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["from random import randint\nfrom time import sleep\n\ntask_queue = Queue()\nfor i in range(3):\n task_queue.enqueue(('Job {}'.format(i), randint(3, 6)))"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["n = task_queue.head\nwhile n:\n print(n.val)\n n = n.next"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["while not task_queue.empty():\n job, time_left = task_queue.dequeue()\n print('Running', job)\n sleep(1)\n time_left -= 1\n if time_left > 0:\n print('Re-queueuing', job, 'with remaining time =', time_left)\n task_queue.enqueue((job, time_left))\n else:\n print('*', job, 'done')"]},{"cell_type":"markdown","metadata":{},"source":["### … for doling out work\n\n"]},{"cell_type":"markdown","metadata":{},"source":["Queues are also frequently used as a sort of conveyer belt for multiple\nworker functions to draw from. Here we implement a \"work queue\" pattern\nused by multiple threads of execution:\n\n"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["from threading import Thread, Lock\nfrom time import sleep\nimport random\n\nlock = Lock()\ndef worker_fn(cid, q):\n while True:\n try:\n with lock:\n work = q.dequeue()\n except: # queue is empty\n sleep(1)\n continue\n if work == 'Stop':\n print('Consumer', cid, 'stopping.')\n break\n else:\n print('Consumer', cid, 'processing', work)\n sleep(random.random())\n\nwork_queue = Queue()\nfor i in range(5):\n Thread(target=worker_fn, args=(i, work_queue)).start()"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["import threading\nthreading.active_count()"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["for i in range(10):\n with lock:\n work_queue.enqueue(i)"]},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[],"source":["for i in range(5):\n with lock:\n work_queue.enqueue('Stop')"]},{"cell_type":"markdown","metadata":{},"source":["## 1. Run-time analysis\n\n"]},{"cell_type":"markdown","metadata":{},"source":["Stack & Queue implementations:\n\n- Insertion (push and enqueue) = $O(1)$\n- Deletion (pop and dequeue) = $O(1)$\n\n"]}],"metadata":{"org":null,"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.5.2"}},"nbformat":4,"nbformat_minor":0}