零基础数据结构与算法——第二章:基本数据结构-队列&总结

2.1 数组(Array)

2.2 链表(Linked List)

2.3 栈(Stack)

2.4 队列(Queue)

2.4.1 队列的定义与特点

想象一下排队买票的场景,先到的人先买票,后到的人排在队伍末尾,这就是队列的基本概念。

队列是一种遵循先进先出(FIFO, First-In-First-Out)原则的线性数据结构。

队列的主要特点包括:

  • 两端操作:在一端(队尾)添加元素,在另一端(队头)移除元素,就像人们在队伍末尾加入,从队伍前端离开。

  • 先进先出(FIFO):最先添加的元素最先被移除,就像排队买票,先到的人先服务。

  • 访问受限:只能访问队头和队尾元素,无法直接访问队列中间的元素,就像你只能看到队伍的第一个人和最后一个人。

生活中的例子:

  1. 超市结账队伍:顾客按照到达顺序排队,先到的先结账。

  2. 打印任务队列:文档按照提交顺序排队等待打印。

  3. 餐厅等位:客人按照到达顺序获得座位。

  4. 交通信号灯:车辆按照到达顺序通过十字路口。

2.4.2 队列的基本操作

让我们通过生活中的例子来理解队列的基本操作:

1. enqueue(入队)

操作:将元素添加到队尾。
时间复杂度:O(1),常数时间操作。

生活例子:新顾客加入排队队伍的末尾。

// 入队操作
public void enqueue(int item) {
    if (isFull()) {
        System.out.println("队列已满,无法添加元素");
        return;
    }
    rear = (rear + 1) % capacity;  // 循环队列实现
    array[rear] = item;
    size++;
    System.out.println(item + " 已入队");
}
2. dequeue(出队)

操作:移除并返回队头元素。
时间复杂度:O(1),常数时间操作。

生活例子:队伍最前面的人完成服务后离开。

// 出队操作
public int dequeue() {
    if (isEmpty()) {
        System.out.println("队列为空,无法移除元素");
        return -1;  // 表示错误
    }
    int item = array[front];
    front = (front + 1) % capacity;  // 循环队列实现
    size--;
    System.out.println(item + " 已出队");
    return item;
}
3. peek/front(查看队头)

操作:查看队头元素但不移除。
时间复杂度:O(1),常数时间操作。

生活例子:查看队伍最前面的人是谁,但不让他离开。

// 查看队头元素
public int peek() {
    if (isEmpty()) {
        System.out.println("队列为空,无法查看队头元素");
        return -1;  // 表示错误
    }
    return array[front];  // 只返回元素,不改变队列
}
4. isEmpty(检查队列是否为空)

操作:检查队列中是否有元素。
时间复杂度:O(1),常数时间操作。

生活例子:检查是否有人在排队。

// 检查队列是否为空
public boolean isEmpty() {
    return size == 0;  // 如果size为0,表示队列为空
}

2.4.3 队列的实现方式

队列可以通过两种主要方式实现:

1. 基于数组实现

特点

  • 固定大小,可能会溢出
  • 内存连续,访问效率高
  • 可以实现为循环队列,提高空间利用率

生活例子:电影院有固定数量的座位,一旦满了就不能再容纳更多观众。

2. 基于链表实现

特点

  • 动态大小,不会溢出
  • 内存不连续,需要额外的引用开销
  • 实现稍复杂

生活例子:根据需要可以无限延长的队伍,如网上购票系统的等待队列。

2.4.4 队列的实现示例

基于数组实现的队列
public class ArrayQueueExample {
    static class ArrayQueue {
        private int[] array;    // 用于存储队列元素的数组
        private int front;      // 队头指针
        private int rear;       // 队尾指针
        private int capacity;   // 队列的容量
        private int size;       // 队列中元素的数量
        
        // 构造函数,初始化队列
        public ArrayQueue(int capacity) {
            this.array = new int[capacity];
            this.front = 0;      // 初始化队头指针
            this.rear = -1;      // 初始化队尾指针
            this.capacity = capacity;
            this.size = 0;
        }
        
        // 添加元素到队尾
        public void enqueue(int item) {
            if (isFull()) {
                System.out.println("队列已满,无法添加元素");
                return;
            }
            rear = (rear + 1) % capacity;  // 循环队列实现
            array[rear] = item;
            size++;
            System.out.println(item + " 已入队");
        }
        
        // 移除并返回队头元素
        public int dequeue() {
            if (isEmpty()) {
                System.out.println("队列为空,无法移除元素");
                return -1;  // 表示错误
            }
            int item = array[front];
            front = (front + 1) % capacity;  // 循环队列实现
            size--;
            System.out.println(item + " 已出队");
            return item;
        }
        
        // 查看队头元素但不移除
        public int peek() {
            if (isEmpty()) {
                System.out.println("队列为空,无法查看队头元素");
                return -1;  // 表示错误
            }
            return array[front];
        }
        
        // 检查队列是否为空
        public boolean isEmpty() {
            return size == 0;
        }
        
        // 检查队列是否已满
        public boolean isFull() {
            return size == capacity;
        }
        
        // 获取队列的大小
        public int size() {
            return size;
        }
        
        // 打印队列的内容
        public void printQueue() {
            if (isEmpty()) {
                System.out.println("队列为空");
                return;
            }
            
            System.out.print("队列内容(从队头到队尾): ");
            int count = 0;
            int index = front;
            while (count < size) {
                System.out.print(array[index]);
                if (count < size - 1) System.out.print(" -> ");
                index = (index + 1) % capacity;
                count++;
            }
            System.out.println();
        }
    }
    
    public static void main(String[] args) {
        ArrayQueue queue = new ArrayQueue(5);
        
        // 测试入队操作
        queue.enqueue(10);
        queue.enqueue(20);
        queue.enqueue(30);
        
        // 打印队列内容
        queue.printQueue();  // 输出:队列内容(从队头到队尾): 10 -> 20 -> 30
        
        // 测试查看队头元素
        System.out.println("队头元素: " + queue.peek());  // 输出:队头元素: 10
        
        // 测试出队操作
        queue.dequeue();  // 输出:10 已出队
        
        // 再次打印队列内容
        queue.printQueue();  // 输出:队列内容(从队头到队尾): 20 -> 30
        
        // 测试队列的大小
        System.out.println("队列的大小: " + queue.size());  // 输出:队列的大小: 2
    }
}
基于链表实现的队列
public class LinkedQueueExample {
    static class Node {
        int data;
        Node next;
        
        public Node(int data) {
            this.data = data;
            this.next = null;
        }
    }
    
    static class LinkedQueue {
        private Node front;  // 队头指针
        private Node rear;   // 队尾指针
        private int size;    // 队列中元素的数量
        
        public LinkedQueue() {
            this.front = null;
            this.rear = null;
            this.size = 0;
        }
        
        // 添加元素到队尾
        public void enqueue(int item) {
            Node newNode = new Node(item);
            
            // 如果队列为空,新节点既是队头也是队尾
            if (isEmpty()) {
                front = newNode;
                rear = newNode;
            } else {
                // 否则,将新节点添加到队尾
                rear.next = newNode;
                rear = newNode;
            }
            
            size++;
            System.out.println(item + " 已入队");
        }
        
        // 移除并返回队头元素
        public int dequeue() {
            if (isEmpty()) {
                System.out.println("队列为空,无法移除元素");
                return -1;  // 表示错误
            }
            
            int item = front.data;
            front = front.next;
            
            // 如果队列变空,更新rear为null
            if (front == null) {
                rear = null;
            }
            
            size--;
            System.out.println(item + " 已出队");
            return item;
        }
        
        // 查看队头元素但不移除
        public int peek() {
            if (isEmpty()) {
                System.out.println("队列为空,无法查看队头元素");
                return -1;  // 表示错误
            }
            return front.data;
        }
        
        // 检查队列是否为空
        public boolean isEmpty() {
            return front == null;
        }
        
        // 获取队列的大小
        public int size() {
            return size;
        }
        
        // 打印队列的内容
        public void printQueue() {
            if (isEmpty()) {
                System.out.println("队列为空");
                return;
            }
            
            System.out.print("队列内容(从队头到队尾): ");
            Node current = front;
            while (current != null) {
                System.out.print(current.data);
                if (current.next != null) System.out.print(" -> ");
                current = current.next;
            }
            System.out.println();
        }
    }
    
    public static void main(String[] args) {
        LinkedQueue queue = new LinkedQueue();
        
        // 测试入队操作
        queue.enqueue(10);
        queue.enqueue(20);
        queue.enqueue(30);
        
        // 打印队列内容
        queue.printQueue();  // 输出:队列内容(从队头到队尾): 10 -> 20 -> 30
        
        // 测试查看队头元素
        System.out.println("队头元素: " + queue.peek());  // 输出:队头元素: 10
        
        // 测试出队操作
        queue.dequeue();  // 输出:10 已出队
        
        // 再次打印队列内容
        queue.printQueue();  // 输出:队列内容(从队头到队尾): 20 -> 30
        
        // 测试队列的大小
        System.out.println("队列的大小: " + queue.size());  // 输出:队列的大小: 2
    }
}

2.4.5 队列的类型

队列有几种常见的变体,每种都有其特定的用途:

1. 普通队列

标准的FIFO队列,如上面实现的示例。

生活例子:超市结账队伍。

2. 循环队列

为了有效利用空间,将队列的头尾相连形成一个环。当队尾指针到达数组末尾时,它会绕回到数组开头。

生活例子:旋转餐厅的传送带,食物不断循环经过每个座位。

3. 双端队列(Deque)

可以在两端(队头和队尾)进行插入和删除操作的队列。

生活例子:双向通行的走廊,人们可以从任一端进入或离开。

4. 优先队列

元素按优先级排序,而不是按照添加顺序。优先级最高的元素最先出队。

生活例子:医院急诊室的分诊系统,病情严重的患者优先就诊。

2.4.6 基于数组实现的循环队列示例

循环队列是队列的一种高效实现,它通过将队列的头尾相连,解决了普通队列在数组实现中可能出现的"假溢出"问题。

public class CircularQueueExample {
    static class CircularQueue {
        private int[] array;    // 用于存储队列元素的数组
        private int front;      // 队头指针
        private int rear;       // 队尾指针
        private int capacity;   // 队列的容量
        
        // 构造函数,初始化循环队列
        public CircularQueue(int capacity) {
            this.array = new int[capacity];
            this.front = -1;     // 初始化为-1,表示队列为空
            this.rear = -1;      // 初始化为-1,表示队列为空
            this.capacity = capacity;
        }
        
        // 添加元素到队尾
        public void enqueue(int item) {
            if (isFull()) {
                System.out.println("队列已满,无法添加元素");
                return;
            }
            
            // 如果队列为空,初始化front
            if (isEmpty()) {
                front = 0;
            }
            
            // 循环移动rear指针
            rear = (rear + 1) % capacity;
            array[rear] = item;
            System.out.println(item + " 已入队");
        }
        
        // 移除并返回队头元素
        public int dequeue() {
            if (isEmpty()) {
                System.out.println("队列为空,无法移除元素");
                return -1;  // 表示错误
            }
            
            int item = array[front];
            
            // 如果队列中只有一个元素
            if (front == rear) {
                front = -1;
                rear = -1;
            } else {
                // 循环移动front指针
                front = (front + 1) % capacity;
            }
            
            System.out.println(item + " 已出队");
            return item;
        }
        
        // 查看队头元素但不移除
        public int peek() {
            if (isEmpty()) {
                System.out.println("队列为空,无法查看队头元素");
                return -1;  // 表示错误
            }
            return array[front];
        }
        
        // 检查队列是否为空
        public boolean isEmpty() {
            return front == -1;
        }
        
        // 检查队列是否已满
        public boolean isFull() {
            return (front == 0 && rear == capacity - 1) || (rear == (front - 1) % (capacity - 1));
        }
        
        // 获取队列的大小
        public int size() {
            if (isEmpty()) {
                return 0;
            }
            if (rear >= front) {
                return rear - front + 1;
            }
            return capacity - front + rear + 1;
        }
        
        // 打印队列的内容
        public void printQueue() {
            if (isEmpty()) {
                System.out.println("队列为空");
                return;
            }
            
            System.out.print("队列内容(从队头到队尾): ");
            int i = front;
            do {
                System.out.print(array[i]);
                if (i != rear) System.out.print(" -> ");
                i = (i + 1) % capacity;
            } while (i != (rear + 1) % capacity);
            System.out.println();
        }
    }
    
    public static void main(String[] args) {
        CircularQueue queue = new CircularQueue(5);
        
        // 测试入队操作
        queue.enqueue(10);
        queue.enqueue(20);
        queue.enqueue(30);
        queue.enqueue(40);
        
        // 打印队列内容
        queue.printQueue();  // 输出:队列内容(从队头到队尾): 10 -> 20 -> 30 -> 40
        
        // 测试出队操作
        queue.dequeue();  // 输出:10 已出队
        queue.dequeue();  // 输出:20 已出队
        
        // 再次打印队列内容
        queue.printQueue();  // 输出:队列内容(从队头到队尾): 30 -> 40
        
        // 测试循环特性
        queue.enqueue(50);
        queue.enqueue(60);
        queue.printQueue();  // 输出:队列内容(从队头到队尾): 30 -> 40 -> 50 -> 60
        
        // 测试队列已满
        queue.enqueue(70);  // 输出:队列已满,无法添加元素
    }
}

2.4.7 队列的应用场景

队列在计算机科学和日常生活中有许多应用:

  1. 任务调度

    • 操作系统使用队列来管理进程和线程的执行顺序。
    • 生活例子:医院挂号系统,按照挂号顺序依次就诊。
  2. 缓冲区管理

    • 打印机队列存储等待打印的文档。
    • 生活例子:餐厅厨房的订单队列,按照接收顺序处理。
  3. 广度优先搜索(BFS)

    • 在图的遍历算法中,BFS使用队列来记录访问顺序。
    • 生活例子:社交网络中查找"六度人脉",按层次扩展搜索范围。
  4. 消息队列

    • 在分布式系统中,消息队列用于在不同组件之间传递消息。
    • 生活例子:邮政系统,信件按照收到的顺序处理和递送。
  5. 流媒体缓冲

    • 视频或音频流使用队列来缓冲数据,确保平滑播放。
    • 生活例子:自助餐厅的食物补充,先放入的食物先被取用。
  6. 等待系统

    • 客服中心的来电排队系统。
    • 生活例子:主题公园的排队系统,按照到达顺序进入游乐设施。

2.4.8 队列的优缺点

优点

  • 实现简单,操作高效(O(1)时间复杂度)
  • 保持元素的顺序
  • 适合需要按照到达顺序处理的场景

缺点

  • 访问受限,只能访问队头和队尾元素
  • 不支持随机访问
  • 基于数组实现的普通队列可能会浪费空间

2.4.9 队列与其他数据结构的比较

特性 队列 数组
访问模式 FIFO(先进先出) LIFO(后进先出) 随机访问
插入/删除位置 一端插入,另一端删除 同一端插入和删除 任意位置
随机访问 不支持 不支持 支持
主要应用 任务调度、缓冲区 函数调用、表达式求值 存储和访问数据

何时使用队列?

  • 当你需要按照元素到达的顺序处理它们时
  • 当你需要实现一个缓冲区时
  • 当你需要实现广度优先搜索算法时
  • 当问题具有"先来先服务"的特性时

2.5 本章小结

在本章中,我们学习了四种基本的数据结构:数组、链表、栈和队列。这些数据结构是构建更复杂数据结构和算法的基础。

  • 数组:提供随机访问能力,但大小固定且插入删除操作可能需要移动元素。就像一排固定的座位,你可以直接找到任何一个座位,但要在中间加入新座位就需要移动其他座位。

  • 链表:动态大小,高效的插入删除操作,但不支持随机访问。就像一条手拉手的人链,可以随时加入或离开,但要找到特定的人需要从头开始数。

  • :后进先出(LIFO)的数据结构,适用于需要逆序处理的场景。就像一堆盘子,只能从顶部添加或移除。

  • 队列:先进先出(FIFO)的数据结构,适用于按顺序处理的场景。就像排队买票,先到的人先服务。

理解这些基本数据结构的特性和适用场景,对于选择合适的数据结构解决问题至关重要。每种数据结构都有其优缺点,适合不同的应用场景。在实际编程中,我们需要根据问题的特点选择最合适的数据结构,以提高程序的效率和可维护性。

2.6 推荐阅读

  • 《数据结构与算法分析》by Mark Allen Weiss - 深入讲解各种数据结构的实现和分析

  • 《算法导论》第10章:基本数据结构 - 提供了数据结构的理论基础和数学分析

  • 《Java编程思想》第16章:数组和集合 - 详细介绍了Java中的数组和集合框架

  • 在线资源:

    • GeeksforGeeks上的数据结构教程 - 提供了大量的示例和练习
    • LeetCode上的数据结构相关题目 - 可以通过实际编程练习加深理解
    • Visualgo.net - 提供了数据结构和算法的可视化展示
  • Java集合框架文档:

    • ArrayList - Java中的动态数组实现
    • LinkedList - Java中的双向链表实现
    • Stack - Java中的栈实现
    • Queue和Deque接口 - Java中的队列和双端队列接口
    • PriorityQueue - Java中的优先队列实现

你可能感兴趣的:(零基础数据结构与算法——第二章:基本数据结构-队列&总结)