Notes for CSC 162, 21 Jan. 2010 ff Chapter 2 of the text ---------------------------------------- stacks, queues, and deques linear structures that can be accessed only at the ends stack: insert (push) and remove (pop) only at one end queue: insert (enqueue) at one end and remove (dequeue) at the other deque: (pronounced "deck", or sometimes "deek") insert and remove at either end (but never in the middle) (exact names used for deque operations aren't well standardized) All of these are easily implemented with Python lists, though other implementations (linked list, fixed-length "circular" buffer) may be more efficient in important cases. Most of the cool stuff in this chapter is the applications. ======================================== stacks implementation with Python list << Stack.py >> class Stack: def __init__(self): self.items = [] def isEmpty(self): return len(self.items) == 0 def push(self, item): self.items.append(item) def pop(self): return self.items.pop() def peek(self): return self.items[len(self.items)-1] def size(self): return len(self.items) matching balanced parens << parChecker.py >> def parChecker(symbolString): s = Stack() for symbol in symbolString: if symbol == "(": s.push(symbol) elif symbol == ")": if s.isEmpty(): return False else: s.pop() else: # unexpected symbol return False # or maybe raise exception return s.isEmpty() That could have been done more easily with just a counter. If we extend to handle brackets and braces, too, we really need the stack: << parChecker2.py >> opens = "([{" closers = ")]}" def matches(open, close): assert open in opens if not close in closers: return False return opens.index(open) == closers.index(close) def parChecker2(symbolString): s = Stack() for symbol in symbolString: if symbol in opens: s.push(symbol) else: if s.isEmpty(): return False else: top = s.pop() if not matches(top,symbol): return False return s.isEmpty() printing numbers in various bases << toBinary.py >> def toBinary(num): remstack = Stack() while num > 0: rem = num % 2 remstack.push(rem) num = num / 2 binString = "" while not remstack.isEmpty(): binString += str(remstack.pop()) return binString << toBase.py >> def toBase(num,base): assert base <= 16 digits = "0123456789ABCDEF" remstack = Stack() while num > 0: rem = num % base remstack.push(rem) num = num / base newString = "" while not remstack.isEmpty(): newString += digits[remstack.pop()] return newString infix, prefix, postfix we're used to infix A + B we're also used to prefix, at least with parentheses: add(A, B) + A B postfix may seem a little strange A B + there's a natural correspondence infix prefix postfix A + (B * C) + A * B C A B C * + (A + B) * C * + A B C A B + C * With infix, we need parentheses to determine order of operations. (We sometimes leave them out if the order can be implied by rules for precedence: 2 + 3 * 4 == 2 + (3 * 4) ==> 14 NOT 2 + 3 * 4 == (2 + 3) * 4 ==> 20 and associativity: 10 - 4 - 3 == (10 - 4) - 3 ==> 3 NOT 10 - 4 - 3 == 10 - (4 - 3) ==> 9 Prefix and postfix have no need for parentheses; the order is manifest. Miller and Ranum point out that this is because we've effectively placed the operator in the position of the left or right paren, so it implies the grouping: infix ( A + ( B * C ) ) prefix + A * B C postfix A B C * + or infix ( ( A + B ) * C ) prefix * + A B C postfix A B + C * So what does this have to do with stacks? We can build on the balanced parens example to convert among the three representations and to evaluate expressions in any of the three notations. Compilers and interpreters (like the Python implementation) do a lot of this sort of thing. << infix2postfix.py >> Evaluates left-to-right (no precedence, left associative) import string def infix2postfix(infixexpr): opStack = Stack() postfixList = [] tokenList = infixexpr.split() for token in tokenList: if token in string.uppercase: postfixList.append(token) elif token == '(': opStack.push(token) elif token == ')': topToken = opStack.pop() while topToken != '(': postfixList.append(topToken) topToken = opStack.pop() else: # operator if not opStack.isEmpty() and opStack.peek() != '(': postfixList.append(opStack.pop()) opStack.push(token) while not opStack.isEmpty(): postfixList.append(opStack.pop()) return string.join(postfixList) << infix2postfix2.py >> From the book. Handles precedence; still implicitly left associative import string def infix2postfix2(infixexpr): prec = {} prec["*"] = 3 prec["/"] = 3 prec["+"] = 2 prec["-"] = 2 prec["("] = 1 opStack = Stack() postfixList = [] tokenList = infixexpr.split() for token in tokenList: if token in string.uppercase: postfixList.append(token) elif token == '(': opStack.push(token) elif token == ')': topToken = opStack.pop() while topToken != '(': postfixList.append(topToken) topToken = opStack.pop() else: while (not opStack.isEmpty()) and \ (prec[opStack.peek()] >= prec[token]): postfixList.append(opStack.pop()) opStack.push(token) while not opStack.isEmpty(): postfixList.append(opStack.pop()) return string.join(postfixList) Neither example handles right associativity (e.g., for exponentiation) ---------------------------------------- queues Heavily used for things that take turns "first in, first out" -- FIFO. Note that a stack is "last in, last out" -- LIFO. Examples: ready list for processes in your laptop incoming messages from the Internet (e.g. to web server) pending requests to the disk drive "natural" implementation with Python lists (rather inefficient: copies whole list on an enqueue) class Queue1: def __init__(self): self.items = [] def isEmpty(self): return self.items == [] def enqueue(self, item): self.items.insert(0, item) def dequeue(self): return self.items.pop() def size(self): return len(self.items) hotPotato game whoever has the "potato" when the "music" stops drops out simulation: person at head of queue "has the potato" dequeue and enqueue to simulate passing it one person hotPotato(["Bill", "David", "Susan", "Jane", "Kent", "Brad"], 7) cycles 7 times, producing <"David", "Susan", "Jane", "Kent", "Brad", "Bill" > then deletes David cycles 7 times, producing <"Kent", "Brad", "Bill", "Susan", "Jane" > then deletes Kent cycles 7 times, producing <"Jane", "Brad", "Bill", "Susan"> then deletes Jane cycles 7 times, producing <"Bill", "Susan", "Brad" > then deletes Bill cycles 7 times, producing <"Brad", "Susan" > then deletes Brad leaving Susan might make more sense just to have a list, cycle a cursor through it, and eliminate the "person" it points at. << hotPotato2.py >> def hotPotato2(namelist, N): group = namelist[:] # clone (deep copy) the list potato = 0 while len(group) > 1: potato = (potato + N) % len(group) del group[potato] return group[0] This suggests an alternative queue implementation: circular buffer implementation defaultQueueSize = 100 # or some other suitable constant class Queue2: def __init__(self): self.maxItems = defaultQueueSize self.numItems = 0 self.nextFree = 0 self.nextFull = 0 self.items = [None for i in range(self.maxItems)] def isEmpty(self): return self.numItems == 0 def isFull(self): return self.numItems == self.maxItems def enqueue(self, item): if self.isFull(): raise IndexError("enqueue into full queue") self.items[self.nextFree] = item self.numItems += 1 self.nextFree = (self.nextFree + 1) % self.maxItems def dequeue(self): if self.isEmpty(): raise IndexError("dequeue from empty queue") rtn = self.items[self.nextFull] self.numItems -= 1 self.nextFull = (self.nextFull + 1) % self.maxItems return rtn def size(self): return self.numItems You'll explore the performance of these two implementations in lab today. ---------------------------------------- deque Not as widely used as queues, but still handy for some important applications. The palindrome checker is cute, but not really compelling: we could simply start indices at the start and end of the string and read toward the middle. If we were reading from stdin instead of a string, we'd need to store the characters somewhere, but we could use a string just as easily as a deque. A slightly more compelling example: browser history. We mostly work at the front of the deque, but when it gets too long we delete off the back. (We never insert at the back, though.) A more compelling example: work queues in the Cilk parallel programming language. Every thread has a deque containing work to do. A thread pushes new tasks onto, and pulls tasks off of, the front of its own deque. This LIFO strategy maximizes locality, which makes caches work well. If a thread runs out of work, it steals from the tail of some other thread's deque. Using the other end avoids interfering with the other thread whenever possible. NB: this is the same access pattern as the browser history: no insertions at the back.