Abstract Data Types Queue + Dequeue Amortized analysis
500 likes | 636 Views
This document delves into the implementation of a queue using two stack data structures, providing an in-depth examination of the algorithmic approach and its amortized analysis. We explore the mechanics of enqueueing (injecting) and dequeueing (popping) operations, detailing how elements transition between the two stacks while maintaining the queue's integrity. The analysis reveals that each element undergoes a limited number of movements, allowing for efficient execution of a series of operations, all in O(1) amortized time under the right conditions.
Abstract Data Types Queue + Dequeue Amortized analysis
E N D
Presentation Transcript
5 4 17 21 Q 5 4 17 21 Can one Implement A Queue with stacks? • You are given the STACK ABSTRACT data structure (1, 2 .. as many as you want) • Can you use it to implement a queue S2 S1
Implementation of Queue with stacks S2 S1 13 5 4 17 21 size=5 inject(x,Q): push(x,S2); size ← size + 1 inject(2,Q)
Implementation with stacks S2 S1 13 5 4 17 21 2 size=5 inject(x,Q): push(x,S2); size ← size + 1 inject(2,Q)
Implementation of a Queue with stacks S2 S1 13 5 4 17 21 2 size=6 inject(x,Q): push(x,S2); size ← size + 1 inject(2,Q)
Pop S2 S1 13 5 4 17 21 2 size=6 pop(Q): if empty?(Q) error if empty?(S1) then move(S2, S1) pop( S1); size ← size -1 pop(Q)
Pop S2 S1 5 4 17 21 2 size=6 pop(Q): if empty?(Q) error if empty?(S1) then move(S2, S1) pop( S1); size ← size -1 pop(Q)
Pop S2 S1 5 4 17 21 2 size=5 pop(Q): if empty?(Q) error if empty?(S1) then move(S2, S1) pop( S1); size ← size -1 pop(Q) pop(Q)
Pop S2 S1 2 5 4 17 21 size=5 pop(Q): if empty?(Q) error if empty?(S1) then move(S2, S1) pop( S1); size ← size -1 pop(Q) pop(Q)
Pop S2 S1 2 5 4 17 21 size=5 pop(Q): if empty?(Q) error if empty?(S1) then move(S2, S1) pop( S1); size ← size -1 pop(Q) pop(Q)
Pop S2 S1 2 17 5 4 21 size=5 pop(Q): if empty?(Q) error if empty?(S1) then move(S2, S1) pop( S1); size ← size -1 pop(Q) pop(Q)
Pop S2 S1 4 2 17 5 21 size=5 pop(Q): if empty?(Q) error if empty?(S1) then move(S2, S1) pop( S1); size ← size -1 pop(Q) pop(Q)
Pop S2 S1 4 2 5 17 21 size=5 pop(Q): if empty?(Q) error if empty?(S1) then move(S2, S1) pop( S1); size ← size -1 pop(Q) pop(Q)
Pop S2 S1 4 2 17 21 size=4 pop(Q): if empty?(Q) error if empty?(S1) then move(S2, S1) pop( S1); size ← size -1 pop(Q) pop(Q)
move(S2, S1) while not empty?(S2) do x ←pop(S2) push(x,S1)
Analysis • O(n) worst case time per operation
Amortized Analysis • How long it takes to perform m operations on the worst case ? • O(nm) • Is this tight ?
Key Observation • An expensive operation cannot occur too often !
2 7 5 4 21 S1 S2 Amortized complexity THM: If we start with an empty queue and perform m operations then it takes O(m) time Proof: • No element moves from S2 to S1 • Entrance at S1, exit at S2. • Every element: • Enters S1 exactly once • Moves from S1 to S2 at most once • Exits S2 at most once • #ops per element ≤ 3 • m operations #elements ≤ m work ≤ 3 m
Potential based Proof (on your own) Consider Think of Φ as accumulation of easy operations covering for future potential “damage” Recall that: Amortized(op) = actual(op) + ΔΦ This is O(1) if a move does not occur Say we move S2: Then the actual time is |S2| + O(1) ΔΦ = -|S2| So the amortized time is O(1)
Double ended queue (deque) • Push(x,D) : Insert x as the first in D • Pop(D) : Delete the first element of D • Inject(x,D): Insert x as the last in D • Eject(D): Delete the last element of D • Size(D) • Empty?(D) • Make-deque()
x x.next x.prev x.element Implementation with doubly linked lists head tail size=2 13 5
Empty list head tail size=0 We use two sentinels here to make the code simpler
Push head tail size=1 5 push(x,D): n = new node n.element ←x n.next ← head.next (head.next).prev ← n head.next ← n n.prev← head size ← size + 1
4 head tail size=1 5 push(x,D): n = new node n.element ←x n.next ← head.next (head.next).prev ← n head.next ← n n.prev← head size ← size + 1 push(4,D)
4 head tail size=1 5 push(x,D): n = new node n.element ←x n.next ← head.next (head.next).prev ← n head.next ← n n.prev← head size ← size + 1 push(4,D)
4 head tail size=1 5 push(x,D): n = new node n.element ←x n.next ← head.next (head.next).prev ← n head.next ← n n.prev← head size ← size + 1 push(4,D)
4 head tail size=2 5 push(x,D): n = new node n.element ←x n.next ← head.next (head.next).prev ← n head.next ← n n.prev← head size ← size + 1 push(4,D)
Implementation with stacks S2 S1 13 5 4 17 21 size=5 push(x,D): push(x,S1) push(2,D)
Implementation with stacks S2 S1 2 13 5 4 17 21 size=6 push(x,D): push(x,S1) push(2,D)
Pop S2 S1 2 13 5 4 17 21 size=6 pop(D): if empty?(D) error if empty?(S1) then split(S2, S1) pop( S1) pop(D)
Pop S2 S1 13 5 4 17 21 size=5 pop(D): if empty?(D) error if empty?(S1) then split(S2, S1) pop( S1) pop(D) pop(D)
Pop S2 S1 5 4 17 21 size=4 pop(D): if empty?(D) error if empty?(S1) then split(S2, S1) pop( S1) pop(D) pop(D)
Pop S2 S1 5 4 17 21 size=4 pop(D): if empty?(D) error if empty?(S1) then split(S2, S1) pop( S1) pop(D)
Pop S2 S1 5 4 17 21 size=4 pop(D): if empty?(D) error if empty?(S1) then split(S2, S1) pop( S1) pop(D)
Pop S2 5 4 S1 17 21 size=4 pop(D): if empty?(D) error if empty?(S1) then split(S2, S1) pop( S1) pop(D)
Pop S2 S1 4 17 21 size=4 pop(D): if empty?(D) error if empty?(S1) then split(S2, S1) pop( S1) pop(D)
Pop S2 S1 4 17 21 size=3 pop(D): if empty?(D) error if empty?(S1) then split(S2, S1) pop( S1) pop(D)
Split S2 S1 5 4 17 21 S3
Split S2 S1 5 4 17 S3 21
Split S2 S1 5 4 S3 17 21
Split S2 S1 4 5 S3 17 21
Split S2 S1 5 4 S3 17 21
Split S2 S1 17 5 4 S3 21
Split S2 S1 17 21 5 4 S3
Split (same thing in reverse) S2 S1 5 4 S3
split(S2, S1) S3←make-stack() d ←size(S2) while (i ≤⌊d/2⌋) do x ←pop(S2) push(x,S3) i ← i+1 while (i ≤⌈d/2⌉) do x ←pop(S2) push(x,S1) i ← i+1 while (i ≤⌊d/2⌋) do x ←pop(S3) push(x,S2) i ← i+1
Analysis • O(n) worst case time per operation
Thm: If we start with an empty deque and perform m operations then it takes O(m) time
A better bound Consider Think of Φ as accumulation of easy operations covering for future potential “damage” Recall that: Amortized(op) = actual(op) + ΔΦ This is O(1) if no splitting occurs Say we split S1: Then the actual time is |S1| + O(1) ΔΦ = -|S1| (S2 empty) So the amortized time is O(1)