Click here to load reader
Upload
vuongthien
View
220
Download
0
Embed Size (px)
Citation preview
Linked Lists (cont'd)
Singly Linked List Runtimes Worst Case Expected
Case Best Case
size() O(1) set(int, E) O(n) O(n) O(1) (near the front / last*) E get(int) O(n) O(n) O(1) (near the front / last*) add(E) O(n) O(n) O(1) * add(int, E) O(n) O(n) O(1) (near the front / last*) remove(E) O(n) O(n) O(1) ) (near the front) remove(int, E) O(n) O(n) O(1) (near the front) * Can get last, set last, add after last in O(1) time if you also maintain a reference to the last node (aka, tail), in addition to the reference to the first node (aka, head). How is the space requirement for a singly linked list compare to a dynamic array? Each node in a singly linked list has two references, one to the data and the other to the next node. A dynamic array has only one reference to the data. But immediately after doubling the array, the space is the same as the singly linked list. The worst-‐case space is the same. When should you create a new node? Only create a new node when you want to add a new element to the list. You should reuse nodes when possible (eg, reuse dominoes). Writing the following is wasteful:
Node<E> temp = new Node<E>(null, null); temp = head;
It creates a new node and then immediately throws the new node away. Creating the new node is a relatively expensive operation compared to an assignment. Simply declare the variable as a Node<E> without using new:
Node<E> temp; temp = head;
Circular Linked Lists: The last node in the list points the first node in the list. You still have a reference to the head.
• Useful when you want to be able reach every node starting from any node. E.g., When an operating system wants to make it appear as though several processes
are running concurrently, it will cycle through all the processes repeatedly and run each one for a short time. It can use a circular linked list so it can remove and add new processes to the list quickly, while cycling from one process to the next.
Doubly Linked Lists: Each node contains the reference to the data, a reference to the next node, and a reference to the previous node.
class Node<E> { E data; Node<E> prev; Node<E> next; }
What does the last node's next field contain? null What does the first node's prev field contain? null In addition to keeping a reference to the first (head), keep a reference to the last (tail) node. When the list is empty, to what should the head and tail refer? null Advantages: • You can traverse the linked list in both directions. • If you have a direct reference to a node, you can remove the node or insert a new node before it.
What would you have to do if the list was singly linked? Start at the first node and traverse the linked list until you find the previous node. To remove a node of a doubly linked list you would include code such as
node.prev.next = node.next; node.next.prev = node.prev;
• You can remove the last node in O(1) time. What is the runtime for singly linked lists? O(n). Even if you have a reference to the last node, or to the second to last node, you can't continue to remove the last node, as you cannot get to the previous node in O(1) time.
• Provides redundancy. E.g., Used by the IBM 360 to maintain directory structures. If the system was modifying the linked list and the system crashed the linked list could be corrupted. If a node had a bad next pointer, the system could traverse the linked list backwards and if it found a previous pointer to the node, it could fix the next pointer:
Disadvantages: • Uses more space than a singly-‐linked list.
• More complicated code to maintain. Java's LinkedList class uses a doubly-linked list.
Stacks & Queues Abstract Data Type (ADT): A collection of data and associated operations that can be preformed on the data independent of the implementations. Specifies WHAT the operations can do, but not HOW it does it. IDEA: Define abstract data types that have limited operations that are fast (i.e., easy to build in software or hardware.) Stacks and queues are two such data types with limited operations. From a computer scientist or mathematical point of view, it is interesting to determine just how expressive these simple data types are.
Stacks
Operations: push(E) -‐ Put an element on top of the stack. E pop() -‐ Remove the element at top of the stack. boolean isEmpty() -‐ Return true if the stack has no element. Optional operations: E peek() -‐ Get element at top of the stack without removing it. int size()-‐ Get the number of elements on the stack.
Properties: • Elements are stored in order of insertion. (But you cannot refer to them with an index, though.)
• Elements removed in reverse order from which they were added. • Last-in First-out (LIFO) The last
Applications of a stack: • Reversing order of data • Tracking function calls (system stack). • Or to mimic recursion by using an explicit stack, instead of the (implicit) system stack.
• Nested parentheses matching
You cannot loop over a stack using an index. Typically, you would write the following coding pattern:
while (!stack.isEmpty()) { … x = stack.pop(); … }
Example: How would you find the maximum on the stack? Need to examine every value. The look at a value below the top, you need to pop each value off the stack. // BAD public static void max(Stack<Integer> s) { int maxValue = s.pop(); while (!s.isEmpty()) { int value = s.pop(); if (value > max) max = value; } return max; } What is wrong? The contents of the stack are lost. You need to save the contents of the stack as you examine the stack contents. Then push the saved data back onto the stack. // Good public static void max(Stack<Integer> s) { Stack<Integer> save = new ArrayStack<Integer>(); int maxValue = s.pop(); save.push(maxValue); while (!s.isEmpty()) { int value = s.pop(); save.push(value); if (value > max) max = value; } return max; }
Example: Check whether a string of parenthesis, braces, or brackets are properly nested. Used to check whether programs have properly nested braces and mathematical expressions have properly next parenthesis. eg, {[()]()} is properly nested. [{(})] is not properly nested, because the pair {} contains a single ( and the pair () contains a single }. We will refer to the symbols {[( as "opening braces" and }]) as "closing braces" Algorithm: • Read each symbol left to right
• If the next symbol is an opening brace, push it on stack • If it is a closing brace, pop the last opening "brace" off the stack
o If the two braces are not a matching type -‐ error • Repeat until the is stack empty or reach end of the expression
• If you run out of symbols before stack is empty -‐ error • If the stack is empty before the end of the expression – error • Otherwise the expression has properly nested braces. Below the stack contents is shown left-‐to-‐right with the rightmost symbol on the top: Eg, {[()]()} Stack contents each round: - empty stack { read { push {[ read [ push {[( read ( push {[ read ) pop off ( { read ] pop off [ {( read ( push { read ) pop off ( - read } pop off { Eg, [{(})] Stack contents each round: - [ read [ push [{ read { push [{( read ( push {[ read } pop off ( -‐ error, mismatch Exercise: Give an example when the stack is empty before the end of the expression and another when the stack is not empty but all of the expression has been read.