Docsity
Docsity

Prepare for your exams
Prepare for your exams

Study with the several resources on Docsity


Earn points to download
Earn points to download

Earn points by helping other students or get them with a premium plan


Guidelines and tips
Guidelines and tips

Understanding Stacks: Concept, Applications, and Implementations, Papers of Data Structures and Algorithms

An introduction to the concept of stacks, explaining their everyday examples, operations, and applications. It covers various stack implementations, such as dynamic arrays and linked lists, and discusses real-life examples like web browser back buttons and activation record stacks. The document also includes a simple parenthesis checking application and a comparison of stack implementations.

Typology: Papers

Pre 2010

Uploaded on 08/30/2009

koofers-user-uki
koofers-user-uki 🇺🇸

10 documents

1 / 20

Toggle sidebar

Related documents


Partial preview of the text

Download Understanding Stacks: Concept, Applications, and Implementations and more Papers Data Structures and Algorithms in PDF only on Docsity! Chapter s: Stacks 1 Chapter S: Stacks You are familiar with the concept of a stack from many everyday examples. For example, you have seen a stack of books on a desk, or a stack of plates in a cafeteria. The common characteristic of these examples is that among the items in the collection, the easiest element to access is the topmost value. In the stack of plates, for instance, the first available plate is the topmost one. In a true stack abstraction that is the only item you are allowed to access. Furthermore, stack operations obey the last-in, first-out principle, or LIFO. If you add a new plate to the stack, the previous topmost plate is now inaccessible. It is only after the newly added plate is removed that the previous top of the stack once more becomes available. If you remove all the items from a stack you will access them in reverse chronological order – the first item you remove will be the item placed on the stack most recently, and the last item will be the value that has been held in the stack for the longest period of time. Stacks are used in many different types of computer applications. One example you have probably seen is in a web browser. Almost all web browsers have Back and Forward buttons that allow the user to move backwards and forwards through a series of web pages. The Back button returns the browser to the previous web page. Click the back button once more, and you return to the page before that, and so on. This works because the browser is maintaining links to web pages in a stack. Each time you click the back button it removes one link from this stack and displays the indicated page. The Stack Concept and ADT specification Suppose we wish to characterize the stack metaphor as an abstract data type. The classic definition includes the following four operations: Push (newEntry) Place a new element into the collection. The value provided becomes the new topmost item in the collection. Usually there is no output associated with this operation. Pop () Remove the topmost item from the stack. Top () Returns, but does not remove, the topmost item from the stack. isEmpty () Determines whether the stack is empty Note that the names of the operations do not specify the most important characteristic of a stack, namely the LIFO property that links how elements are added and removed. Furthermore, the names can be changed without destroying the stack-edness of an abstraction. For example, a programmer might choose to use the names add or insert rather than push, or use the names peek or inspect rather than top. Other variations are also common. For example, some implementations of the stack concept combine the pop and top operations by having the pop method return the value that has been removed from the stack. Other implementations keep these two tasks separate, so that the only access to the topmost element is through the function named top. As long as the Chapter s: Stacks 2 fundamental LIFO property is retained, all these variations can still legitimately be termed a stack. Finally, there is the question of what to do if a user attempts to apply the stack operations incorrectly. For example, what should be the result if the user tries to pop a value from an empty stack? Any useful implementation must provide some well-defined behavior in this situation. The most common implementation technique is to throw an exception or an assertion error when this occurs, which is what we will assume. However, some designers choose to return a special value, such as null. Again, this design decision is a secondary issue in the development of the stack abstraction, and whichever design choice is used will not change whether or not the collection is considered to be a stack, as long as the essential LIFO property of the collection is preserved. In a pure stack abstraction the only access is to the topmost element. An item stored deeper in the stack can only be obtained by repeatedly removing the topmost element until the value in question rises to the top. But as we will see in the discussion of implementation alternatives, often a stack is combined with other abstractions, such as a dynamic array. In this situation the data structure allows other operations, such as a search or direct access to elements. Whether or not this is a good design decision is a topic explored in one of the lessons described later in this chapter. To illustrate the workings of a stack, consider the following sequence of operations: push(“abe”) push(“amy”) push(“andy”) pop() push(“anne”) push(“alfred”) pop() pop() The following diagram illustrates the state of the stack after each of the eight operations. Applications of Stacks Back and Forward Buttons in a Web Browser In the beginning of this chapter we noted how a stack might be used to implement the Back button in a web browser. Each time the user moves to a new web page, the current Chapter s: Stacks 5 if (p < 15) return 1; else return 1 + b(p-1) Each time the function b is invoked a new activation record is created. New local variables and parameters are stored in this record. Thus there may be many copies of a local variable stored in the stack, one for each current activation of the recursive procedure. Functions, whether recursive or not, have a very simple execution sequence. If function a calls function b, the execution of function a is suspended while function b is active. Function b must return before function a can resume. If function b calls another function, say c, then this same pattern will follow. Thus, function calls work in a strict stack-like fashion. This makes the operation of the activation record stack particularly easy. Each time a function is called new area is created on the activation record stack. Each time a function returns the space on the activation record stack is popped, and the recovered space can be reused in the next function call. Question: What should (or what does) happen if there is no available space in memory for a new activation record? What condition does this most likely represent? Checking Balanced Parenthesis A simple application that will illustrate the use of the stack operations is a program to check for balanced parenthesis and brackets. By balanced we mean that every open parenthesis is matched with a corresponding close parenthesis, and parenthesis are properly nested. We will make the problem slightly more interesting by considering both parenthesis and brackets. All other characters are simply ignored. So, for example, the inputs (x(y)(z)) and a( {(b)}c) are balanced, while the inputs w)(x) and p({(q)r)} are not. To discover whether a string is balanced each character is read in turn. The character is categorized as either an opening parenthesis, a closing parenthesis, or another type of character. Values of the third category are ignored. When a value of the first category is encountered, the corresponding close parenthesis is stored on the stack. For example, when a “(“ is read, the character “)” is pushed on the stack. When a “{“ is encountered, the character pushed is “}”. The topmost element of the stack is therefore the closing value we expect to see in a well balanced expression. When a closing character is encountered, it is compared to the topmost item in the stack. If they match, the top of the stack is popped and execution continues with the next character. If they do not match an error is reported. An error is also reported if a closing character is read and the stack is Chapter s: Stacks 6 empty. If the stack is empty when the end of the expression is reached then the expression is well balanced. The following illustrates the state of the stack at various points during the processing of the expression a ( b { d e [ f ] g { h } I } j k ) l m. picture The following illustrates the detection of an error when a closing delimiter fails to match the correct opening character: picture Another error occurs when there are opening delimiters but no closing character: picture Question: Show the state of the stack after each character is read in the following expression: ( a b { c } d ( [ e [ f ] g ] ) ( j ) ) Evaluating Expressions Two standard examples that illustrate the utility of the stack expression involve the evaluation of an arithmetic expression. Normally we are used to writing arithmetic expressions in what is termed infix form. Here a binary operator is written between two arguments, as in 2 + 3 * 7. Precedence rules are used to determine which operations should be performed first, for example multiplication typically takes precedence over addition. Associativity rules apply when two operations of the same precedence occur one right after the other, as in 6 – 3 – 2. For addition, we normally perform the left most operation first, yielding in this case 3, and then the second operation, which yields the final result 1. If instead the associativity rule specified right to left evaluation we would have first performed the calculation 3 – 2, yielding 1, and then subtracted this from 6, yielding the final value 5. Parenthesis can be used to override either precedence or associativity rules when desired. For example, we could explicitly have written 6 – (3 – 2). The evaluation of infix expressions is not always easy, and so an alternative notion, termed postfix notation, is sometimes employed. In postfix notation the operator is written after the operands. The following are some examples: Infix 2 + 3 2 + 3 * 4 (2 + 3) * 4 2 + 3 + 4 2 - (3 – 4) Postfix 2 3 + 2 3 4 * + 2 3 + 4 * 2 3 + 4 + 2 3 4 - - Notice that the need for parenthesis in the postfix form is avoided, as are any rules for precedence and associativity. Chapter s: Stacks 7 We can divide the task of evaluating infix expressions into two separate steps, each of which makes use of a stack. These steps are the conversion of an infix expression into postfix, and the evaluation of a postfix expression. Conversion of infix to postfix To convert an infix expression into postfix we scan the value from left to right and divide the tokens into four categories. This is similar to the balanced parenthesis example. The categories are left and right parenthesis, operands (such as numbers or names) and operators. The actions for three of these four categories is simple: Left parenthesis Push on to stack Operand Write to output Right parenthesis Pop stack until corresponding left parenthesis is found. If stack becomes empty, report error. Otherwise write each operator to output as it is popped from stack The action for an operator is more complex. If the stack is empty or the current top of stack is a left parenthesis, then the operator is simply pushed on the stack. If neither of these conditions is true then we know that the top of stack is an operator. The precedence of the current operator is compared to the top of the stack. If the operator on the stack has higher precedence, then it is removed from the stack and written to the output, and the current operator is pushed on the stack. If the precedence of the operator on the stack is lower than the current operator, then the current operator is simply pushed on the stack. If they have the same precedence then if the operator associates left to right the actions are as in the higher precedence case, and if association is right to left the actions are as in the lower precedence case. The following diagram illustrates the state of the stack and the output as different characters in the input are read: picture Question: Using this algorithm, show the state of the stack and the output for each of the following expressions: example Evaluation of a postfix expression The advantage of postfix notation is that there are no rules for operator precedence and no parenthesis. This makes evaluating postfix expressions particularly easy. As before, the postfix expression is evaluated left to right. Operands (such as numbers) are pushed on the stack. As each operator is encountered the top two elements on the stack are Chapter s: Stacks 10 variable named count, as is the capacity. The actual values are held in a variable named data. The element type will be defined by a symbolic constant named EleType. Functions are used to initialize the array. One function, named setCapacity, must copy the values from the existing array into a new array, then change the value of the variable data to point to the new array. First write setCapacity, then write the methods that constitute the public stack interface. struct dyArray { EleType * data; int size; int capacity; }; void dyArrInit (struct dyArray *v, int initCap); void dyArrayPush (struct dyArray *v, EleType e); EleType dyArrayTop (struct dyArray *v); void dyArrayPop (struct dyArray *v); int dyArrayIsEmpty (struct dyArray *v); void dyArrayInit (struct dyArray *v, int initCap) { v->size = 0; v->data = (EleType *) malloc(initCap * sizeof(EleType)); assert(v->data != 0); v->capacity = initCap; } void dyArraySetCapacity(struct dyArray *v, int newCap) { } void dyArrayPush (struct dyArray *v, EleType e) { if (v->size >= v->capacity) dyArraySetCapacity(v, 2 * v->capacity); } EleType dyArrayTop (struct dyArray *v) { assert(! dyArrayIsEmpty(v)); } void dyArrayPop (struct dyArray *v) { assert(! dyArrayIsEmpty(v)); }; Chapter s: Stacks 11 int dyArrayIsEmpty (struct dyArray *v) { } 1. What is the algorithmic complexity of the routine top? 2. What about the routine pop? 3. If n represents the number of elements in the new array, what is the algorithmic complexity of the method setCapacity? 4. Given this, what is the worst case algorithmic complexity of the routine push? 5. Will it always take so long? 6. What is the best case execution time for this procedure? In the exercises at the end of the chapter you will explore the idea that while the worst case execution time for push is relatively slow, the worst case occurs relatively infrequently. Hence, the expectation is that in the average execution of push will be quite fast. We describe this situation by saying that the method push has constant amortized execution time. Lesson S2: Linked List Implementation of Stack As we described in earlier chapter, an alternative implementation approach is to use a linked list. Here, the container abstraction maintains a reference to a collection of elements of type Link. Each Link maintains two data fields, a value and a reference to another link. The last link in the sequence stores a null value in its link. The advantage of the linked list is that the collection can grow as large as necessary, and each new addition to the chain of links requires only a constant amount of work. Because Chapter s: Stacks 12 there are no big blocks of memory, it is never necessary to copy an entire block from place to place. It is trivial to make a linked list represent a stack. The top of the stack is the first link. To push an element on to the stack you simply add a new link. To pop a value from the stack you simply remove the first link. As with the dynamic array, your linked list stack should use an exception to check if an attempt is made to access or remove a value from an empty stack. In the ArrayStack you used the value held in the count field to determine if the stack was empty. What feature can you use to determine if a ListStack is empty? struct slink { /* single link */ EleType value; struct slink *next; }; struct listStack { struct slink *first; }; void listStackInit (struct listStack *lst) { } void listStackPush (struct listStack *lst, EleType e) { } EleType listStackTop (struct listStack *lst) { } void listStackPop (struct listStack *lst) { } int listStackIsEmpty (struct listStack *lst) { } 1. Assuming that the allocation of a new link requires constant time, what is the algorithmic execution time for the method push? Chapter s: Stacks 15 boolean test = stk.isEmpty(); // should always be true stk.push(new Integer(2)); boolean test = stk.isEmpty(); // should always be false Stack stk = new Stack(); boolean test = stk.pop(); // should always raise error 8. Using a ListStack as the implementation structure do the same analysis as in previous question. 9. Does an ArrayStack or a ListStack use less memory? Assume for this question that a data value requires 1 unit of memory, and each memory reference (such as the next field in a Link, or the firstLink field in the ListStack, or the data field in the ArrayStack) also requires 1 unit of memory. How much memory is required to store a stack of 100 values in an ArrayStack? How much memory in a ListStack? Analysis Exercises 1. When you developed the ArrayStack you were asked to determine the algorithmic execution time for the push operation. When the capacity was less than the size, the execution time was constant. But when a reallocation became necessary, execution time slowed to O(n). This might at first seem like a very negative result, since it means that the worst case execution time for pushing an item on to the stack is O(n). But the reality is not nearly so bleak. Look again at the picture that described the internal array as new elements were added to the collection. Notice that the costly reallocation of a new array occurred only once during the time that ten elements were added to the collection. If we compute the average cost, rather than the worst case cost, we will see that the ArrayStack is still a relatively efficient container. To compute the average, count 1 “unit” of cost each time a value is added to the stack without requiring a reallocation. When the reallocation occurs, count ten “units” of cost for the assignments performed as Chapter s: Stacks 16 part of the reallocation process, plus one more for placing the new element into the newly enlarged array. How many “units” are spent in the entire process of inserting these ten elements? What is the average “unit” cost for an insertion? When we can bound an “average” cost of an operation in this fashion, but not bound the worst case execution time, we call it amortized constant execution time, or average execution time. Amortized constant execution time is often written as O(1)+, the plus sign indicating it is not a guaranteed execution time bound. Do a similar analysis for 25 consecutive add operations, assuming that the internal array begins with 5 elements (as shown). What is the cost when averaged over this range? This analysis can be made into a programming assignment. Rewrite the ArrayStack class to keep track of the “unit cost” associated with each instruction, adding 1 to the cost for each simple insertion, and n for each time an array of n elements is copied. Then print out a table showing 200 consequitive insertions into a stack, and the value of the unit cost at each step. 2. The Java standard library contains a number of classes that are implemented using techniques similar to those you developed in the programming lessons described earlier. The classes Vector and ArrayList use the dynamic array approach, while the class LinkedList uses the idea of a linked list. One difference is that the names for stack operations are different from the names we have used here: Stack Vector ArrayList LinkedList Push(newValue) Add(newValue) Add(newValue) addFirst(newObject) Pop() Remove(size()-1) Remove(size()-1) removeFirst(ewObject) Top() lastElement() Get(size()-1) getFirst() IsEmpty() Size() == 0 isEmpty() isEmpty() Another difference is that the standard library classes are designed for many more tasks than simply representing stacks, and hence have a much larger interface. An important principle of modern software development is an emphasis on software reuse. Whenever possible you should leverage existing software, rather than rewriting new code that matches existing components. But there are various different techniques that can be used to achieve software reuse. In this exercise you will investigate some of these, and explore the advantages and disadvantages of each. All of these techniques leverage an existing software component in order to simplify the creation of something new. Imagine that you are a developer and are given the task of implementing a stack in Java. Part of the specifications insist that stack operations must use the push/pop/top convention. There are at least three different approaches you could use that Chapter s: Stacks 17 1. If you had access to the source code for the classes in the standard library, you could simply add new methods for these operations. The implementation of these methods can be pretty trivial, since they need do nothing more than invoke existing functions using different names. 2. You could create a new class using inheritance, and subclass from the existing class. class Stack extends Vector { … } Inheritance implies that all the functionality of the parent class is available automatically for the child class. Once more, the implementation of the methods for your stack can be very simple, since you can simply invoke the functions in the parent class. 3. The third alternative is to use composition rather than inheritance. You can create a class that maintains an internal data field of type Vector (alternatively, ArrayList or LinkedList). Again, the implementation of the methods for stack operations is very simple, since you can use methods for the vector to do most of the work. class Stack<T> { private Vector<T> data; … } Write the implementation of each of these. (For the first, just write the methods for the stack operations, not the other vector code). Then compare and contrast the three designs. Issues to consider in your analysis include readability/usability and encapsulation. By readability or usability we mean the following: how much information must be conveyed to a user of your new class before they can do their job. By encapsulation we mean: How good a job does your design do in guaranteeing the safety of the data? That is, making sure that the stack is accessed using only valid stack instructions. On the other hand, there may be reasons why you might want to allow the stack to be accessed using non-stack instructions. A common example is allowing access to all elements of the stack, not just the first. Which design makes this easier? If you are the developer for a collection class library (such as the developer for the Java collection library), do you think it is a better design choice to have a large number of classes with very small interfaces, or a very small number of classes that can each be used in a number of ways, and hence have very large interfaces? Describe the advantages and disadvantages of both approaches.
Docsity logo



Copyright © 2024 Ladybird Srl - Via Leonardo da Vinci 16, 10126, Torino, Italy - VAT 10816460017 - All rights reserved