dsa_rust/sequences/queues/vec_circ_queue.rs
1/*! A circular Vec-based queue
2
3# About
4This simple, safe, Vec-based circular queue is mostly just a fun experiment, but can be used for situations in which you need a fixed-sized buffer with FIFO logic. The circular queue can be used to provide a solution to the [Josephus problem](https://en.wikipedia.org/wiki/Josephus_problem).
5
6# Design
7Rust takes its types (and its type safety) seriously. Array's must have a known size at compile time, forcing any array-backed structure's size hard coded. `[T; N]`
8
9This example illustrates the circular queue logic. The example provides a queue postcondition for enqueue/dequeue operations to illustrate the state of the list after each operation. Remember that `enqueue()` adds to the _back_, and `dequeue()` removes from the _front_ of the queue.
10```rust
11
12 use dsa_rust::sequences::queues::vec_circ_queue::CircularQueue;
13
14 // The queue is a fixed size
15 let mut q: CircularQueue<char> = CircularQueue::new(3);
16
17 // enqueue/dequeue return Result because adding/removing on a
18 // full/empty queue is an error
19 // Postcondition
20 //
21 // <-> a <-> b <-> c <->
22 // ^ ^
23 // front back
24 q.enqueue('a');
25 q.enqueue('b');
26 q.enqueue('c');
27
28 // The queue is at capacity, and the queue hasn't shifted
29 assert_eq!(q.peek().unwrap(), &'a');
30 assert_eq!(q.size(), q.capacity());
31 assert_eq!(q.front(), 0);
32 assert_eq!(q.back(), 2);
33
34 // The queue cannot take additional elements
35 assert!(q.enqueue_with_check('d').is_err());
36
37 // Remove the head node and check changes:
38 // Postcondition:
39 // - There is only one item in the queue
40 // - The front and back of the queue point to the same index
41 // Postcondition
42 //
43 // <-> None <-> b <-> c <->
44 // ^ ^
45 // front back
46 assert_eq!(q.dequeue().unwrap(), 'a');
47
48 // Postcondition
49 //
50 // <-> None <-> Non <-> c <->
51 // ^
52 // front/back
53 assert_eq!(q.dequeue().unwrap(), 'b'); // queue: 0:None, 1:None, 2:c (fr/ba)
54 assert_eq!(2, q.capacity() - q.size()); // Remaining capacity
55 assert_eq!(q.front(), 2);
56 assert_eq!(q.back(), 2);
57 assert_eq!(q.size(), 1);
58
59 // Adding new items wraps the queue, hence circular queue
60 // Postcondition
61 //
62 // <-> d <-> None <-> c <->
63 // ^ ^
64 // back front
65 q.enqueue('d');
66
67 // Postcondition
68 //
69 // <-> d <-> e <-> c <->
70 // ^ ^
71 // back front
72 q.enqueue('e');
73 assert_eq!(q.front(), 2);
74 assert_eq!(q.back(), 1);
75 assert_eq!(q.size(), 3);
76
77 // Removes two more elements and checks that there is just one element left
78 // Postcondition
79 //
80 // <-> d <-> e <-> None <->
81 // ^ ^
82 // front back
83 assert_eq!(q.dequeue().unwrap(), 'c');
84
85 // Postcondition
86 //
87 // <-> None <-> e <-> None <->
88 // ^
89 // front/back
90 assert_eq!(q.dequeue().unwrap(), 'd');
91 assert_eq!(2, q.capacity() - q.size()); // Remaining capacity
92 assert_eq!(q.front(), 1);
93 assert_eq!(q.back(), 1);
94 assert_eq!(q.size(), 1);
95 assert_eq!(q.peek().unwrap(), &'e');
96```
97*/
98
99/// # About
100/// All functions operate in `O(1)` time unless otherwise noted.
101///
102/// See the [module-level documentation](`crate::lists::queues::vec_circ_queue`) for more information.
103pub struct CircularQueue<T> {
104 data: Vec<Option<T>>, // Store elements as `Option` to allow reusing slots
105 front: usize,
106 back: usize,
107 size: usize,
108 capacity: usize,
109}
110impl<T> CircularQueue<T> {
111 /// Creates a queue that contains (at least) `capacity` number of elements,
112 /// and initializes all positions to `None` in `O(n)` time.
113 pub fn new(capacity: usize) -> CircularQueue<T> {
114 let mut data = Vec::with_capacity(capacity);
115 // Fills the vec with None
116 //data.resize_with(capacity, Default::default); // Clean and generic
117 //data.resize_with(capacity, || None); // More explicit and slightly more efficient
118 // Somehow the fastest initializiation option
119 for _ in 0..capacity {
120 data.push(None)
121 }
122 CircularQueue {
123 data,
124 front: 0,
125 back: 0,
126 size: 0,
127 capacity,
128 }
129 }
130
131 /// Returns the index representing the front of the queue.
132 pub fn front(&self) -> usize {
133 self.front
134 }
135
136 /// Returns the index representing the back of the queue.
137 pub fn back(&self) -> usize {
138 self.back
139 }
140
141 /// Returns the number of elements in the queue.
142 pub fn size(&self) -> usize {
143 self.size
144 }
145
146 /// Returns the total capacity of the queue.
147 ///
148 /// This is the maximum number of elements the queue can hold,
149 /// determined at instantiation. It does not reflect the number of
150 /// currently available slots. To find the number of free slots,
151 /// subtract the current size from the capacity.
152 pub fn capacity(&self) -> usize {
153 self.capacity
154 }
155
156 /// Returns a reference to the front of the queue.
157 pub fn peek(&self) -> Option<&T> {
158 self.data[self.front].as_ref()
159 }
160
161 /// Adds an element to the back of the queue. This operation has no size or
162 /// capacity checks for efficiency and performance, may overwrite existing
163 /// slots. Use the `enqueue_with_check()` operation to prevent data overwrites
164 /// at the slight performance cost of an additional runtime check.
165 pub fn enqueue(&mut self, item: T) {
166 // Calculates the next available positionm, writes to it, and increases size
167 self.back = (self.front + self.size) % self.capacity;
168 self.data[self.back] = Some(item);
169 self.size += 1;
170 }
171
172 /// Adds an element to the back of the queue. This operation checks that
173 /// there is sufficient queue capacity to add an element and errors if you
174 /// attempt to add more elements than the queue's capacity. The
175 /// `enqueue()` operation is slightly more performanant but does not
176 /// automatically prevent overwrites.
177 pub fn enqueue_with_check(&mut self, item: T) -> Result<(), &str> {
178 // Ensures that the queue cannot take more elements than its capacity
179 if self.size == self.capacity {
180 return Err("Queue is full");
181 }
182 // Calculates the next available positionm, writes to it, and increases size
183 self.back = (self.front + self.size) % self.capacity;
184 self.data[self.back] = Some(item);
185 self.size += 1;
186 Ok(())
187 }
188
189 /// Removes and returns the front element of the queue, if an element exists.
190 pub fn dequeue(&mut self) -> Option<T> {
191 // Checks if queue is empty and returns proper None
192 if self.size == 0 {
193 return None;
194 }
195 // Otherwise take() the value from the front, leaving None in its place
196 let item = self.data[self.front].take();
197 // Properly advances the front index with wrapping
198 self.front = (self.front + 1) % self.capacity;
199 self.size -= 1;
200 item
201 }
202}
203
204/** Illustrates that the for loop is the most efficient way to initialize an
205array with None values:
206
207100x
208Default: 2.803µs
209None: 2.505µs
210For: 2.193µs
211
21210000x
213Default: 212.307µs
214None: 205.76µs
215For: 178.989µs
216
217100000000x
218Default: 2.285708255s
219None: 2.203727308s
220For: 1.947812524s
221
222*/
223//#[allow(clippy::type_complexity)]
224//fn _empirical_test() {
225// use std::time::{Duration, Instant};
226//
227// let allocations = [100, 10_000, 100_000_000];
228// let runs = 100;
229// // TODO: Clippy isn't happy about this
230// let methods: Vec<(&str, fn(usize) -> CircularQueue<char>)> = vec![(
231// "For",
232// CircularQueue::new as fn(usize) -> CircularQueue<char>,
233// )];
234//
235// for &n in &allocations {
236// println!("\n{}x", n);
237//
238// for &(method_name, method) in &methods {
239// let mut avg = Duration::new(0, 0);
240// for _ in 0..runs {
241// let start_time = Instant::now();
242// let _t: CircularQueue<char> = method(n);
243// let duration = start_time.elapsed();
244// avg += duration;
245// }
246// println!("{}: {:?}", method_name, avg / runs);
247// }
248// }
249//}
250// To avoid Clippy warning suppression:
251pub fn empirical_test() {
252 use std::time::{Duration, Instant};
253
254 let allocations = [100, 10_000, 100_000_000];
255 let runs = 100;
256
257 // Use closures to avoid function pointer type
258 let methods = [("For", |n| CircularQueue::<char>::new(n))];
259
260 for &n in &allocations {
261 println!("\n{n}x");
262
263 for &(name, constructor) in &methods {
264 let total_duration = (0..runs)
265 .map(|_| {
266 let start = Instant::now();
267 let _queue = constructor(n);
268 start.elapsed()
269 })
270 .sum::<Duration>();
271
272 let avg = total_duration / runs;
273 println!("{name}: {avg:?}");
274 }
275 }
276}
277
278pub fn example() {
279 //TODO
280}