Full or Empty: Telling Them Apart
Chapter Forty-Five
Syllabus topic Module 1, "Queues: Circular Queue operations"
Pages 137 to 139 of 411
In one line
In a circular queue with only front and rear, both an empty queue and a full one satisfy front == rear, and the three ways out are to keep a count, to waste one cell, or to keep a flag.
The ambiguity
Take a circular queue holding only front and rear, with rear being the next cell to write.
Empty. Nothing has been added, or everything has been removed. front and rear are at the same place, so front == rear.
Full. Every cell holds an item. rear has advanced all the way round and caught up with front, so front == rear.
The two states produce identical values of both markers. No test on front and rear alone can distinguish them, because the information is genuinely not there.
Run, so it is not merely asserted
class AmbiguousQueue:
"""front and rear only. No count, no flag, one cell per capacity."""
def __init__(self, capacity):
self.capacity = capacity
self.cells = [None] * capacity
self.front = 0
self.rear = 0
def enqueue(self, item):
self.cells[self.rear] = item
self.rear = (self.rear + 1) % self.capacity
def dequeue(self):
item = self.cells[self.front]
self.cells[self.front] = None
self.front = (self.front + 1) % self.capacity
return item
def markers(self):
return "front=%d rear=%d front==rear is %s" % (
self.front, self.rear, self.front == self.rear)
empty = AmbiguousQueue(4)
print("a queue with nothing in it :", empty.markers())
print(" its cells :", empty.cells)
full = AmbiguousQueue(4)
for item in ("A", "B", "C", "D"):
full.enqueue(item)
print()
print("a queue holding four items :", full.markers())
print(" its cells :", full.cells)
print()
print("the markers are identical :",
(empty.front, empty.rear) == (full.front, full.rear))
print("the queues are not :", empty.cells != full.cells)
print()
print("so no test on front and rear alone can tell these two apart.")a queue with nothing in it : front=0 rear=0 front==rear is True
its cells : [None, None, None, None]
a queue holding four items : front=0 rear=0 front==rear is True
its cells : ['A', 'B', 'C', 'D']
the markers are identical : True
the queues are not : True
so no test on front and rear alone can tell these two apart.One queue holds nothing, the other holds four items, and their markers are indistinguishable.
The three standard answers
1. Keep a count
Hold an integer of how many items are in the queue.
empty: count == 0
full : count == capacity
For: all cells are usable, both tests are obvious, and size is free. Against: one more field, which every operation must update.
This is what chapter 44 uses, and it is what this book recommends, because size is wanted anyway and a count that is maintained in exactly two places is hard to get wrong.
Full or Empty: Telling Them Apart
2. Sacrifice one cell
Never let the queue hold more than capacity - 1 items. Then a full queue always leaves one gap, so the markers never coincide when full.
empty: front == rear
full : (rear + 1) mod capacity == front
For: no extra field at all. Against: one cell of the array is never used, and the tests are less obvious. A queue declared with capacity 5 holds 4.
This is the version many textbooks print, so it must be recognised, and the full test is the line to memorise: (rear + 1) mod capacity == front.
3. Keep a flag
Hold a boolean saying whether the last operation was an enqueue. If the markers coincide and the last operation was an enqueue, it is full; if it was a dequeue, it is empty.
For: all cells usable, only one bit. Against: the flag has to be correct after every operation, and reasoning about it is harder than either of the others. Rarely used.
All three, run side by side
class CountQueue:
def __init__(self, capacity):
self.capacity, self.cells = capacity, [None] * capacity
self.front = self.rear = self.count = 0
def is_empty(self):
return self.count == 0
def is_full(self):
return self.count == self.capacity
def enqueue(self, item):
if self.is_full():
return False
self.cells[self.rear] = item
self.rear = (self.rear + 1) % self.capacity
self.count += 1
return True
class SacrificeQueue:
def __init__(self, capacity):
self.capacity, self.cells = capacity, [None] * capacity
self.front = self.rear = 0
def is_empty(self):
return self.front == self.rear
def is_full(self):
return (self.rear + 1) % self.capacity == self.front
def enqueue(self, item):
if self.is_full():
return False
self.cells[self.rear] = item
self.rear = (self.rear + 1) % self.capacity
return True
for cls in (CountQueue, SacrificeQueue):
q = cls(5)
accepted = 0
while q.enqueue(accepted):
accepted += 1
print("%-16s capacity 5 accepted %d items before reporting full"
% (cls.__name__, accepted))CountQueue capacity 5 accepted 5 items before reporting full
SacrificeQueue capacity 5 accepted 4 items before reporting fullThere is the cost of the second method, measured: a five cell array holding four items.
Which to write in an examination
Either of the first two, stated clearly. The marks are for:
- Explaining why the ambiguity exists: both states give
front == rear. - Naming the method chosen.
- Giving the correct full and empty tests for that method, which is where answers go wrong by mixing
the count method's empty with the sacrifice method's full.
Mixing them is the error to avoid. If you keep a count, use count == 0 and count == capacity. If you sacrifice a cell, use front == rear and (rear + 1) mod capacity == front.
Quick revision
- In a circular queue with only front and rear, empty and full both give
front == rear. - Three answers: keep a count; sacrifice one cell; keep a flag.
- Count:
emptyiscount == 0,fulliscount == capacity. All cells usable, one field to maintain. - Sacrifice:
emptyisfront == rear,fullis(rear + 1) mod capacity == front. No extra field,
Full or Empty: Telling Them Apart
one cell lost, so capacity 5 holds 4.
- Flag: a boolean recording whether the last operation was an enqueue. Rarely used.
- In an answer: say why the ambiguity exists, name the method, and give that method's own two tests
without mixing them.
Test yourself
1. Why can front and rear alone not distinguish full from empty? Because in both states the two markers hold the same value: an empty queue has never advanced rear past front, and a full one has advanced it all the way round to meet front again.
2. Give the full and empty tests for the count method. Empty is count == 0; full is count == capacity.
3. Give the full and empty tests for the sacrifice method. Empty is front == rear; full is (rear + 1) mod capacity == front.
4. What does the sacrifice method cost, exactly? One cell of the array. A queue declared with capacity 5 can hold only 4 items, as the run showed.
5. Describe the flag method and say why it is rare. Keep a boolean recording whether the last operation was an enqueue; when the markers coincide it distinguishes full from empty. It is rare because the flag must be maintained correctly by every operation and is harder to reason about than a count.
6. What is the standard error in answering this question? Mixing the methods: using the count method's empty test with the sacrifice method's full test, or the reverse. Each method has its own pair and they must be used together.
The rest of this subject
These notes are cut from the University's printed syllabus. Open the syllabus itself, or the past papers, for the same subject.