Data Structures & Algorithms
CS 305 & 503

Quiz 2, 23 March 2010


  1. A stack can be manipulated to behave like a queue. Assuming n is the number of elements in the (simulated) queue, and counting the number of stack operations required, compare the worst-case estimates of queue enqueue and dequeue operations implemented on a stack to those provided by a proper queue. Justify your comparison. You should assume that you can use only stacks having the standard stack interface (push() and pop()), and that you cannot access the stack implementation. Do not worry about errors.


    dequeue(): A dequeue() operation removes the value at the queue head, and so does pop(), which means a dequeue() operation can be implemented with an O(1) number of pop() operations.

    enqueue(): An enqueue() operation adds a value to the queue tail, which is inconveniently placed at the bottom of a stack. However, if the stack elements could be reversed, then the stack tail would become the stack head, and adding the new value requires one push. Fortunately, stacks are made for reversing order, and the enqueue can be implemented as

    simulatedEnqueue(V v)
    
      Stack auxiliaryStack = new Stack()
    
      while not stack.empty()
        auxiliaryStack.push(stack.pop())
    
      stack.push(v)
    
      while not auxiliaryStack.empty()
        stack.push(auxiliaryStack.pop())
    

    Each of the while loops has a body that does a constant number of stack operations (two) per iteration, and evaluating the guard also performs a constant number of stack operations (one). For each loop the number of iterations is bounded by the number of elements in the stack (n). The estimate for each loop is

    O(n)*(O(1) + O(1)) = O(n)*(O(max(1, 1))) = O(n)*O(1) = O(n*1) = O(n)
    The remaining basic statements execute a constant number of stack operations, and the final estimate for a simulated enqueue operation is
    O(1) + O(n) + O(1) + O(n) = O(max(1, n, 1, n)) = O(n)

    The answers, in no particular order and approximately verbatim:
    1. For a proper queue, the worst-case estimate of enqueue | dequeue operations are just O(1) because they require no manipulation or extra loops in order to run. They can just run as is.1 However, in order to implement queue operations a with a stack you need to manipulate the stack and be able to access the end of it. Since queues work as LIFO behavior & stacks work in FIFO behavior. In order to do this you would need to implement a for | while loop in order to go through the stack and flip it to act like a queue. This would therefore raise the estimate2 to O(n) (because of the loop), making a standard queue more time-efficient than a stack that is manipulated to act like a queue.

      1: These two sentences don't have anything to do with the question, or the answer; you could have just as well left them out.

      2: On which queue operations?

    2. Because queues operate with first in first out, the worst case scenario would be having to reach a value that forces you to go through the entire queue to get to it.3 This is done using a loop that touches each value until you reach the end of the queue (where the value you want is stored) The same thing happens with a stack except the stack is last in first out. The worst case is having to go through the whole queue or stack.4

       
      for i = 0 to q.length 
        if (i == q.length) 
          dequeue i; 
      

      3: What circumstances would require that you go through the the queue?

      4: How would you quantify the worst case in terms of the queue size?

    3. The number of worst estimates5 on a stack is O(n2) because in order to make it work like a queue you would have to enqueue and dequeue twice in order for elements to be in the correct order.6 A proper queue would only enqueue and dequeue once for each element. making that estimation O(n). (O(n) + O(n) = O(n) enqueues + dequeues).

      5: Worst case estimates I'm guessing.

      6: Is that for both enqueue and dequeue operations?

    4. Enqueue creates a node and assigns it a value and a next value.7 This does not touch other values so it is constant.8 Enqueue for a stack would involve push(). Push does the same thing so it would also be constant. Dequeue just removes the last node reference so it would be constant; however, for a stack we would need to pop() the first node (not the last), and this would require us to go through every node to get the first so dequeue for a stack would be O(n).

      7: True, but the important point is where does does enque() create this node?

      8: True again, but the question's about simulating queue operations with stack operations, so an estimate for enque() isn't necessary.

    5. I am lost on this question. I am going to guess and say that in order to make a stack behave like a queue, you will need more procedures to enable the stack to do what a queue does already.9 I couldn't even make a guess as to what each implementation would entail. It is safe to say I will be reading up on my stacks & queues in the near future - as most of the knowledge I retained from the first section is on linked lists.

      9: That's correct. Now the question is what code do the new procedures contain, and what is the worst-case upper bound on the number of stack operations performed when the procedures are called?

    6. The main difference between a stack and a queue is that a queue allows10 removal from the bottom of a stack instead of the top. This causes FIFO instead of LIFO. So the queue operation would add an element to the top of the queue just like push would add to the top of a stack. So queue in a stack would be the same as just one push statement.11 For enqueue it is also the same as just one push statement. However, with dequeue it is taking the element from the end of the queue which is not possible in a stack since it is the element on the bottom.12 In order to implement a dequeue in a stack the stack must do a pop() for every element in the stack. This is O(n) since it has to do a stack operation for every element where the number of elements is n.

      Queue and enque are O(1) because they are adding to the queue or stack at the top so it is independent of the number of elements already in the stack/queue.

      10: You can be a bit stronger than “allows:” a queue requires that elements be added to the tail.

      11: And the worst-case upper bound estimate for that is...?

      12: Right, so a stack would need to use more than one stack operation when simulating enqueue(). Now the question is how many more?

    7. Due to the differences in order between stacks and queues, queue operations implemented on a stack likely have a higher worst-case estimate than those in an actual queue.13 Enqueue would likely implement pop() and would require a case statement to determine where the value taken into pop() would be after it has been passed through enqueue.14 Compared to the worst-case estimate of a normal queue, the worst-case estimate would be less reliable.15 The same applies to the dequeue operation implemented by push(). It would be required to contain a similar loop, thus pushing it towards an estimate that would be worse than the worst-case estimate of a proper stack's dequeue.

      13: That's true, but trivially so: you're estimating the number of stack operations used, and a queue would implement its operations without using any stack operations.

      14: What? Why a case statement? And what does it mean to pass through enqueue?

      15: What's making the worst-case estimate unreliable? What does it mean for a worst-case estimate to be unreliable?

    8. The enqueue operation for the simulated and proper queues should be the same estimate if the ordering is not important.16 The difference would be where the element is added, whether at the top in a proper queue or the bottom in a simulated queue. The difference in estimates comes in the simulated dequeue because the element that is removed must be the most recently added element.17 The simulated queue would need to loop n - 1 push() operations to bring the needed element to the top. This would be (n) more times than a proper queue. To re-establish the stack, another loop would need to push() the elements back in. This is another (n), resulting in O(n2) estimate.18 The simulated queue is much slower than a proper queue.

      16: But ordering is important, so this sentence is irrelevant.

      17: A queue removes the least-recently added element.

      18: These loops occur in succession; they're not nested.

    9. The worst case estimate of a queue enqueue depends solely on if the queue is full and the code to add to a full queue.19 Worst case of enqueue is O(n2) because it has to run through the entire stack looking for an empty space.20 If it has one then the value is inputted otherwise the stack must be increased as to change the # of elements. The worst case of a dequeue is onsq() because all it has to do is run through the stack and remove the first object inputted which means depending on its point of entry it may have to touch every element worst case both enqueue and dequeue is that you touch all the elements, so that takes into play the # of operations required.

      19: The point is to simulate queue operations using a stack. Why does it matter how the queue is implemented?

      20: Running through every element in an array is linear, not O(n2). The point is to count the number of stack operations required, not the number of internal operations performed by each stack operation.

    10. Stacks follow LIFO order whereas queues follow FIFO so implementing a stack would help with traversal issues.21 Stacks use push and pop which adds or removes the last value in the stack but enqueue and dequeue take the first element and either add one onto the front of the queue or remove the first element. So best case22 is if you need to access the first element in the stack, worst case is if you have to access the last value in the stack.23

      21: What? Where is traversal even mentioned in the question?

      22: The question doesn't have anything to do with best cases.

      23: Would anything ever require such access?

    11. Since a stack has FILO behavior and queue exhibits FIFO behavior, the worst case scenario of the dequeue operation is O(n) when the item being dequeued was the first element pushed into the stack.

      The worst case scenario for the enqueue operation occurs when the stack is full, and a new stack needs to be created to fix up the values (assume dynamic implementation).24 This operation would become O(n + 1)25 because you are accessing all the elements,26 and then adding another.

      Therefore, in the worst case enqueue() tl(O)(n + 1) and dequeue() O(n)

      24: How the stack is implemented doesn't matter.

      25: Which is equivalent to O(n).

      26: But that's not what you should be counting; you should be counting the number of push and pop operations required to implement enqueue().

  2. Suppose the code

    total = 0
    for (i = 0; i < a.size(); i++)
      total += a[i]
    

    executes in time proportional to O(n2), n the number of elements in the array. What would you estimate the execution time for the statement

    avg = total/a.size()
    

    to be? Justify your answer.


    The problem is figuring out how a singly-nested loop could have O(n2) execution time. It is unreasonable to assume that the expressions total = 0, i = 0, and i++ execute in anything other than constant time. Reasonably assuming a correct array implementation, the number of loop iterations is bounded by n, leaving i < a.size() and total += a[i]. Assuming Java, total += a[i] executes in constant time, leaving i < a.size(), which must execute in O(n) time to get the specified O(n2) estimate. The comparison < executes in constant time, leaving a.size() as the O(n) operation.

    With a.size() an O(n) operation, avg = total/a.size() is an O(n) operation too.

    The answers, in no particular order and approximately verbatim:
    1. O(n3) because the first code is going through the elements twice1 whereas in average you have to go through it 3 times.2 Once to add up the total, once to divide by the size and, once to store it back into total.

      1: Why twice?

      2: Why three times? Why do you have to go through the elements at all?

    2. If the top code executes in O(n2), the a.size() operation must take O(n) time,3 coupled with the O(n) for-loop, to give O(n2). If the a.size() takes O(n) time, than the avg = total/a.size() operation will take O(n) time as well.

      3: Why is that?

    3. The execution time of this statement would be O(1). This is just a statement not inside a loop or a choice. It is only iterated once. While the answer it gives depends on the size of array a the time it takes to execute the code does not, making the statement execute in constant time.4

      4: This answer ignores two-thirds of the question. Even if you think the question asker is incompetent and habitually asks questions containing mostly irrelevancies, you should at least go through the mental exercise of convincing yourself that the parts you didn't use really are irrelevant.

    4. The execution time would be O(n) because worst case size() has to go through the list n times to count the size.5

      5: size()'s worst case behavior can be made arbitrarily bad by using more and more nested loops to repeatedly count the available elements.

    5. I would estimate that the statement will execute in time proportional to O(log n) because every time it iterates the loop, it divides the total by the array size, thus making that part of the code run at O(log n).6 grade(75, 57)

      6: The average computation is not part of the loop. But even if it was, avg is not part of the loop guard, so its value has no control over how many iterations occur. And even if the loop iterated O(log n) times, the question asks for a estimate of the execution time for the statement itself, not the number of times its executed.

    6. The statement is going to execute in O(1) [constant] time. The statement is simply basic division without accessing the array.7 total is an O(1) statement, and a.size() by itself is O(1). The division is8 also O(1).

      7: Is that interpretation consistent with the rest of the question.

      8: ?

    7. The above statement is O(n) because the average variable is entirely dependent of the number of elements that the array holds.9

      9: That's true; avg does depend on the array size, but the question asks about execution time.

    8. Being that O(n2) is generally the worst case in regards to time as compared to O(n),10 and the variable total is dependent on the case statement,11 the execution time for avg would be proportional to O(n2) as well.

      10: O(n2) is an upper bound to n, but then a lot of functions are upper bounds to n. Why is O(n2) in particular important?

      11: Case statement? Where?

    9. If the first program is of n2 order, it seems that what is being counted is the number of times the array is touched,12 and the size method accesses all of the elements in the array.13 If that is the case I would say that the 2nd set of code would be n.

      12: The problem states execution time's being counted, although that's equivalent to counting array accesses.

      13: Why is that so?

    10. The execution time would be O(1) because it is not a loop14 and does not depend directly on the array elements. Also it only divides once, not constantly which would have been O(log n).15

      14: It's not obviously a loop, but that doesn't mean a loop can't be hidden somewhere in the statement itself.

      15: Only if the value being divided were controlling the loop iterations.

    11. The execution time for the statement “avg = total/a.size()” would be O(log n) because the number is being divided each time the program runs through it,16 therefore it is iterated a logarithmic amount of times, giving the statement a O(log n) execution time.

      16: No, the statement's separate from the loop. But even if it weren't, the question wants to know how long it takes to execute the statement, not how many times the statement is executed.

  3. Is it possible to devise an algorithm that three-sorts an array in worst-case time proportional to O(log n)? Justify your answer.


    No, it's not. Suppose A is an algorithm that three-sorts an array in worst-case O(log n) time. Because it executes in sub-linear time, A cannot touch every element in the array to be sorted, otherwise it would run in linear time. If A sorts an array in which every element is out of place (the array [ 3, 3, 1, 1, 2, 2 ] for example), it will fail to sort correctly because some of the elements will be untouched and so be out of place.

    The answers, in no particular order and approximately verbatim:
    1. No, I don't believe there is because to see all the elements in the array you must go completely through it once. I did this as a for loop in my code and it came out to be O(n). Since all elements must be seen to sort it, no it could not be order log n. (Only log n is constantly dividing)1

      1: Repeated division is one way to get logarithmic behavior, but not the only one.

    2. It is possible to devise an algorithm that three-sorts an array in worst-case time proportional to O(log n). By using O(log n) each iteration of the algorithm will subsequently search for and organize parts of the array2 by dividing up the array into pieces using the functions of a binary search.3

      2: But how many array parts are there? Are there no more than O(log n)?

      3: A binary search is only interested in one particular element in an array (assuming it's in the array). A three-sort has to be concerned with all parts of the array.

    3. Yes. A binary search4 could be implemented to achieve worst-case time estimation of O(log n). This is possible because rather than touching every element like a linear search, binary search constantly narrows the range, creating O(log n) behavior.

      4: The question's asking about sorting, not searching.

    4. You could have the idea of a binary search, which is O(log n) and make it a sort instead. So you would divide the array into 2 parts until you got down to 2 values that you would compare and if necessary switch. Then you would move on to the next pair5 and do it constantly until it was sorted.6

      5: How many times would the algorithm have to move on to the next pair?

      6: This is a good way to develop a O(n log n) algorithm, but the question asked about a O(log n) algorithm.

    5. O(log n) is a better asymptotic curve than O(n), which means it would need to be a better algorithm than the O(n) three-sort. The only reason the O(n) three-sort is O(n) is because it must iterate n times through a for-loop. To be O(log n) it would need to behave similar to binary search in that each iteration is half as long as the last.7 This works for a search, but for a sort you would need to pass through the array completely each time. Therefore, since the array must be completely iterated to ensure a complete sort, O(log n) behavior is not possible. grade(95, 79)

      7: Each binary-search iteration throws away half the array.

    6. Yes, I think it is possible to devise an algorithm that three-sorts an array in O(log n) time. Instead of using a linear search to go through the array, a binary search could be used and this would make the execution time O(log n). You would just have to add more lines of code into the standard binary search method (if statements) to test if one element in the array is grater than the element after it and swap them if so, and you would have to do this as you go through the array.8

      8: Does this result in a three-sort that always works?

    7. Considering we have written 2 of the same programs at better times;9 I would have to say if it is usually possible to make a program run slower.10 By adding an additional loop nested within an existing loop, the program will be slower. By developing a program that creates additional steps within existing steps I would imagine you could reach O(log n).

      9: Do you mean better than O(log n)? If you mean the assignment, are O(n) and O(n2) better than O(log n)?

      10: Yes, easily. But the question's asking about an algorithm that runs faster.

    8. It is possible to devise an algorithm that three-sorts an array if your algorithm implements the code that is used in binary search.11 Binary searches occur in O(log n) time, so if you searched through the array using binary search logic it would cut down the time.12

      11: The algorithm should be a sort, not a search.

      12: But would the result be a correct three-sort?

    9. I believe that it is possible to make a worst-case time proportional to O(log n) because in order to get an O(log n) time there will be an algorithm that continuously cuts the array in a set amount, for example every time the array goes through the loop it could be cut in half, thus giving us a logarithmic notation.13

      13: But would that algorithm implement a three-sort?

    10. No, it is not possible to sort an array of numbers as in the three sort. You must compare at the very least every number to another number. That is at least O(n). If you do not compare every number then the numbers that were not compared are most likely not in the right place making the array not sorted correctly, and the algorithm didn't work.14

      14: Good.

    11. No, the searching through an array could have O(log n) but not three-sorts algorithm because the sorting has to be more than O(log n).15

      15: True, but why is that so?

  4. Two art-loving philanthropists have agreed to support an artists colony, one by buying rolls of canvas for the painters and the other by buying blocks of stone for the sculptors. However, a question has come up as to whether this plan evenly distributes costs between the two philanthropists. Knowing your skill in algorithm analysis, the philanthropists come to you for an answer and an explanation. What do you say to them? You may (unrealistically) assume that canvas and stone have the same per-unit costs, and the painters and sculptors turn out approximately the same number of works.


    You should point out that the plan is unfair because it's biased in favor of the philanthropist buying the canvas. As the artists make larger pieces, the costs of buying canvas increases by O(n2), while the costs of buying stone increases by O(n3): the philanthropist buying stone has to buy more, and so pay more, than the philanthropist buying canvas. (Alternatively, you could assume artists are making smaller pieces and reverse the bias. Or you could assume no change in size, which is inconsistent with asymptotic analysis, and observe that an n-inch painting costs less than an n-inch sculpture.)

    The answers, in no particular order and approximately verbatim:
    1. First of all it would all come down to time. If both sculptors and painters took the same amount of time to produce their art then yes, it would be even, and the worst case scenario would be O(1).1 This may not be the case through because how would it be possible to know how long each individual would take.2 Therefore I would explain that there is no way to know, but the estimation is O(n).3 (Sculptors do not depend on painters). O(n) + O(n) = O(n)

      1: What does n represent, and what does f(n) represent?

      2: You could measure the time, or make an estimate.

      3: How did you go from O(1) to O(n)?

    2. You would first need to know how many painters : sculptors and then you would need to know how much time a painting and a sculpture takes.4 Using that knowledge you can devise an algorithm that would tell you if it is even or not by taking price, # of painters/sculptors, and average time to finish a painting/sculpture.5 If there are the same amount of people and it takes approximately the same time, then the 2 philanthropists have evenly distributed the costs.

      4: You can assume they produce the same amount of work over time, so why does producing them slower or faster have any bearing?

      5: And what might that algorithm be?

    3. You can assume the number of canvas sheets and blocks of stone you both purchase are both constant in proportion to the amount of artists in the colony.6 Since both the painter and sculptor turn out the same number of works, and each item costs the same per unit, you will both be paying the same amount of money.7

      6: Why can we assume this? Is it even true?

    4. Assuming that the works turned out will fill an array, we would have a painters array and a sculptors array. Each array would be the same size and each element would be the same cost.8 To find the total cost of each array we could perform.

      for (int i = 0; i < size of painters array; ++i) { O(n)
        int cost += array[i];
        }
      
      for (int i = 0; i < size of sculptors array; ++i) { O(n)
        int cost += array[i];
        }
      

      Both algorithms give the same cost estimate, therefore, the cost is evenly distribute between the two philanthropists.

      7: Is that an accurate representation of the costs?

    5. I would tell them that they are distributing costs evenly between the two philanthropists. I would explain to them that the total cost spent on each side is not affected by the cost of the materials9 or the number of work put out because they are the same. The total cost that is being incurred is (pieces of art × cost per unit) × 2.10 If one of the art forms had an additional cost per unit, the situation would change.

      8: Why not? The only cost mentioned is the material costs.

      9: That's the question, though. Is that the correct interpretation of costs?

    6. What I would say to them is which is the more cost effective for them? Since the canvas and stone have the same per-unit cost and the artists produce the same amount of work, then the costs would be evenly distributed.11 But if its harder to buy blocks of stone for the sculpture than it is to by light weight canvas, the one buying the heavy stone has more cost.12

      10: How did you reach that conclusion?

      11: Wouldn't that be factored into the unit costs?

    7. Although canvas and stone are the same price13 and the amount of work is presumably the same, there may be a slight deviation between the costs. There are many factors kept out of the loop that would not only affect the costs but the deviation as well. As n (presumably the number of rolls of canvas and blocks of stone) increases, the standard deviation14 increases as well, thus further unevenly distributing the costs.

      12: Careful: they have the same unit costs, but does that translate to the same overall costs (or the same costs per work, if you like)?

      13: Standard deviation of what?

    8. Well, they want to pay the same amount (let's say per month), and the pre-unit costs are the same, as well as the number of works turned out so this now is a question of how many units each worker needs per work. If the painter needs p units per 1 work and the sculptor needs s units per 1 work then you multiply p and s and that's how many units of stone and canvas should be bought.15 This way the painter can nave s number of works and the sculptor can make p number of works with the supplies.16

      14: s and p are both ratios (units per work), and multiplying them together results in the square of ratios.

      15: But the question is interested in fairness. Is this fair?

    9. If the canvas & the stone are the same price per unit and the same # of works are being make, the only way that the 2 philanthropists costs would be distributed unevenly is if there was some sort of manipulation in one of the factors17 (i.e., less | more painters and | or less | more sculptors to buy product for).18 Otherwise this algorithm:
      cost × # of items = total cost.
      says that if cost & amt. of material is the same, the total cost for each philanthropist should be the same.

      16: True, if you've covered all the relevant factors contributing to cost.

      17: Which is it?

    10. Though there are similar costs from both the canvas and the stone blocks, you have to take into consideration how much you are actually getting19 and factor in errors from either side. Though the same amount of works are submitted that does not take into consideration how many attempts were made. Worst case scenario, each artists messes up at least n times so therefore you must replace supplies.20 So the only way to evenly distribute cost is to have a set value to give no matter what the excess cost or # of artists is that way they both give equally.

      18: How much of what are you actually getting?

      19: That's true, but that only raises the per-work cost of materials. You'd have to consider if sculptors and painters have different error rates to determine if the overall costs are different.

    11. Assuming that each work by the artists use only 1 canvas or 1 block of stone or each work uses the same number of canvases to number of blocks of stone, yes the philanthropists would be splitting the costs evenly. Since the painters & sculptors turn out the same number of works, labeled X, and the cost of the materials for each work (canvas | stone) is the same,21 Y, each philanthropist is paying the cost XY to support the artist colony.22 Since each is paying the same cost the plan is evenly distributing the cost.

      20: That's the question: are the material costs the same? They have the same unit costs, but does that mean they have the same overall costs?

      21: XY is the total materials costs; if each philanthropist is paying XY, they're paying twice as much as they need.


This page last modified on 25 March 2010.