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}