In the world of computer science and programming, data structures are fundamental building blocks that help organize and manage data efficiently. One such essential data structure is the queue. A queue is a linear data structure that follows the First-In-First-Out (FIFO) principle, meaning that the first element added to the queue will be the first one to be removed.
Queues are widely used in various applications, including scheduling tasks, managing resources, and handling requests in operating systems. Understanding queues is crucial for developing efficient algorithms and solving real-world problems.
A queue can be visualized as a line of people waiting at a checkout counter. The person who arrives first gets served first, and the last person to arrive waits until everyone else has been served. This FIFO behavior is what makes queues unique and useful in many scenarios.
Queues can be implemented in various ways, but the most common methods are using arrays or linked lists. Here, we'll explore a simple implementation using an array.
Let's implement a basic queue using an array in JavaScript:
1class Queue {2constructor() {3this.items = [];4}56enqueue(element) {7this.items.push(element);8}910dequeue() {11if (this.isEmpty()) {12return "Underflow";13}14return this.items.shift();15}1617front() {18if (this.isEmpty()) {19return "No elements in Queue";20}21return this.items[0];22}2324isEmpty() {25return this.items.length === 0;26}2728printQueue() {29let str = "";30for (let i = 0; i < this.items.length; i++) {31str += this.items[i] + " ";32}33return str;34}35}3637// Example usage:38const queue = new Queue();39queue.enqueue(1);40queue.enqueue(2);41queue.enqueue(3);42console.log(queue.printQueue()); // Output: 1 2 343console.log(queue.dequeue()); // Output: 144console.log(queue.front()); // Output: 245console.log(queue.isEmpty()); // Output: false
Using a linked list can be more efficient in terms of memory management, especially when dealing with large queues. Here's how you might implement a queue using a singly linked list:
1class Node {2constructor(element) {3this.element = element;4this.next = null;5}6}78class LinkedListQueue {9constructor() {10this.head = null;11this.tail = null;12this.size = 0;13}1415enqueue(element) {16const node = new Node(element);17if (this.isEmpty()) {18this.head = node;19this.tail = node;20} else {21this.tail.next = node;22this.tail = node;23}24this.size++;25}2627dequeue() {28if (this.isEmpty()) {29return "Underflow";30}31const element = this.head.element;32this.head = this.head.next;33if (this.isEmpty()) {34this.tail = null;35}36this.size--;37return element;38}3940front() {41if (this.isEmpty()) {42return "No elements in Queue";43}44return this.head.element;45}4647isEmpty() {48return this.size === 0;49}5051printQueue() {52let str = "";53let current = this.head;54while (current) {55str += current.element + " ";56current = current.next;57}58return str;59}60}6162// Example usage:63const linkedListQueue = new LinkedListQueue();64linkedListQueue.enqueue(1);65linkedListQueue.enqueue(2);66linkedListQueue.enqueue(3);67console.log(linkedListQueue.printQueue()); // Output: 1 2 368console.log(linkedListQueue.dequeue()); // Output: 169console.log(linkedListQueue.front()); // Output: 270console.log(linkedListQueue.isEmpty()); // Output: false
Let's explore some practical examples to solidify our understanding of queues.
Imagine you have a printer that can only print one document at a time. You need to manage the documents in the order they are submitted.
1class PrinterQueue {2constructor() {3this.queue = new Queue();4}56addDocument(document) {7this.queue.enqueue(document);8console.log(`Added document: ${document}`);9}1011printNextDocument() {12if (this.queue.isEmpty()) {13console.log("No documents to print.");14} else {15const document = this.queue.dequeue();16console.log(`Printing document: ${document}`);17}18}19}2021// Example usage:22const printer = new PrinterQueue();23printer.addDocument("Document1");24printer.addDocument("Document2");25printer.printNextDocument(); // Output: Printing document: Document126printer.printNextDocument(); // Output: Printing document: Document2
A circular queue is a variation of the queue where the last element points back to the first, forming a circle. This can be useful in scenarios where you need to manage a fixed-size buffer.
1class CircularQueue {2constructor(size) {3this.items = new Array(size);4this.front = -1;5this.rear = -1;6this.size = size;7}89enqueue(element) {10if ((this.rear + 1) % this.size === this.front) {11console.log("Queue is full");12return false;13} else if (this.isEmpty()) {14this.front = 0;15this.rear = 0;16} else {17this.rear = (this.rear + 1) % this.size;18}19this.items[this.rear] = element;20console.log(`Enqueued: ${element}`);21return true;22}2324dequeue() {25if (this.isEmpty()) {26console.log("Queue is empty");27return false;28} else if (this.front === this.rear) {29const element = this.items[this.front];30this.front = -1;31this.rear = -1;32console.log(`Dequeued: ${element}`);33return true;34}35const element = this.items[this.front];36this.front = (this.front + 1) % this.size;37console.log(`Dequeued: ${element}`);38return true;39}4041isEmpty() {42return this.front === -1;43}44}4546// Example usage:47const circularQueue = new CircularQueue(3);48circularQueue.enqueue(1); // Enqueued: 149circularQueue.enqueue(2); // Enqueued: 250circularQueue.enqueue(3); // Enqueued: 351circularQueue.enqueue(4); // Queue is full52circularQueue.dequeue(); // Dequeued: 153circularQueue.enqueue(4); // Enqueued: 4
In the next section, we'll dive into another fundamental data structure called Hash Tables. Hash tables provide efficient ways to store and retrieve data using keys, making them essential for many applications.