# 用Java學資料結構-佇列Queue ## 基本概念 - Queue 是一種線性的資料結構 - 一定要遵守 FIFO (First In First Out) 原則 - 與 Stack 相反,Stack 是 LIFO (Last In First Out) - 就好像是排隊的概念,先來的先服務 ### Queue定義與特性 - 元素只能在佇列的一端插入,稱為隊尾(rear/tail) -> 後面來的人只能排在後面 - 元素只能在佇列的另一端刪除,稱為隊首(front/head) -> 先來的人,可以先被服務 - 隊列的操作方式是先進先出(FIFO) ```mermaid graph LR A[入隊/Enqueue] --> B[4] --> C[3] --> D[2] --> E[1] --> F[出隊/Dequeue] style A fill:#796400 style F fill:#9F5000 ``` ### 應用場景實例 - 當我們的應用場景必須遵守先進先出的規則時,就可以使用 Queue 來實現。 - 銀行排隊系統 - 印表機列印任務 - 網路請求的處理 - 或是有需要做緩衝區的地方,也可以使用 Queue 來實現。 - 生產者消費者模式 - 影片串流緩衝區 ## Queue類型 ### 普通佇列(FIFO Queue) - 普通佇列是最基本的佇列,遵守先進先出的原則。 ```mermaid graph LR A[入隊/Enqueue] --> B[4] --> C[3] --> D[2] --> E[1] --> F[出隊/Dequeue] style A fill:#796400 style F fill:#9F5000 ``` ### 雙端佇列(Deque) - 雙端佇列是一種可以從兩端進行操作的佇列,可以從隊首或隊尾進行插入和刪除操作。 - 他結合了 Stack 和 Queue 的特性,可以實現 LIFO 和 FIFO 兩種操作。 ```mermaid graph LR A[隊首插入/InsertFront、隊首刪除/DeleteFront] --> B[1] --> C[2] --> D[3] --> E[隊尾插入/InsertLast、隊尾刪除/DeleteLast] E --> D D --> C C --> B B --> A style A fill:#796400 style E fill:#9F5000 ``` ### 優先佇列(Priority Queue) - 每個元素都有一個優先級 - 高優先級的元素先出隊 - 內部實現方式通常是堆(heap) - 預設會使用最小堆,也可以使用最大堆 - 例如在排工作任務時,就可以使用優先佇列,先處理優先級高的任務。 ```mermaid graph TD subgraph 優先佇列示意圖 A[10 優先級:1] --> B[8 優先級:2] A --> C[7 優先級:2] B --> D[5 優先級:3] B --> E[4 優先級:3] C --> F[3 優先級:3] C --> G[2 優先級:3] style A fill:#842B00,stroke:#333 style B fill:#796400,stroke:#333 style C fill:#796400,stroke:#333 end ``` ```mermaid sequenceDiagram participant 入佇列 participant PriorityQueue participant 出佇列 入佇列->>PriorityQueue: 加入(7, 優先級:2) 入佇列->>PriorityQueue: 加入(10, 優先級:1) 入佇列->>PriorityQueue: 加入(3, 優先級:3) Note over PriorityQueue: 根據優先級自動排序 出佇列->>PriorityQueue: poll() PriorityQueue-->>出佇列: 返回 10 (最高優先級) 出佇列->>PriorityQueue: poll() PriorityQueue-->>出佇列: 返回 7 (次高優先級) ``` ### 循環佇列(Circular Queue) - 是環狀結構、固定大小的佇列 - 頭尾相接,形成一個環 - 這樣可以充分利用空間,避免浪費 - 使用頭和尾來來判斷元素的位置 ```mermaid graph TD subgraph 循環佇列 A[0] --> B[1] B --> C[2] C --> D[3] D --> E[4] E --> A end style A fill:#f96 style E fill:#96f ``` ### 阻塞佇列(BlockingQueue) - 可以支援多執行緒的佇列 - 當佇列是空的時候,從佇列中取元素的操作會被阻塞 - 當佇列是滿的時候,往佇列中加入元素的操作會被阻塞 - 這樣可以避免多執行緒的競爭問題 ```mermaid sequenceDiagram participant Producer participant BlockingQueue participant Consumer Producer->>BlockingQueue: put(task) Note right of BlockingQueue: 如果佇列滿,則阻塞 BlockingQueue-->>Consumer: take() Note left of BlockingQueue: 如果佇列空,則阻塞 ``` ## 用 Java 實做基本操作與實作 - 先來建立基礎物件 ```java public class SimpleQueue { private int[] elements; private int front; // 隊首 private int rear; // 隊尾 private int size; // 元素個數 public SimpleQueue(int capacity) { elements = new int[capacity]; front = 0; rear = -1; // 隊尾指向-1 size = 0; } } ``` ### 新增元素(enqueue/offer) ```java // 新增元素 public void enqueue(int element) { if (size == elements.length) { throw new IllegalStateException("Queue is full"); } rear = (rear + 1) % elements.length; elements[rear] = element; size++; } ``` ### 移除元素(dequeue/poll) ```java // 移除元素 public int dequeue() { if (size == 0) { throw new IllegalStateException("Queue is empty"); } int element = elements[front]; front = (front + 1) % elements.length; size--; return element; } ``` ### 查看隊首(peek) ```java // 查看隊首元素 public int peek() { if (size == 0) { throw new IllegalStateException("Queue is empty"); } return elements[front]; } ``` ### 查看佇列大小(size) ```java // 查看佇列大小 public int size() { return size; } ``` ### 檢查是否為空(isEmpty) ```java // 檢查是否為空 public boolean isEmpty() { return size == 0; } ``` ### 測試程式 ```java public class SimpleQueueTest { public static void main(String[] args) { SimpleQueue queue = new SimpleQueue(5); queue.enqueue(1); queue.enqueue(2); queue.enqueue(3); queue.enqueue(4); queue.enqueue(5); // Queue 是FIFO,所以先進先出 System.out.println(queue.dequeue()); // 1 System.out.println(queue.dequeue()); // 2 // 已經移除了1,2,所以現在的隊首是3 System.out.println(queue.peek()); // 3 // 現在的佇列大小是3 System.out.println(queue.size()); // 3 System.out.println(queue.isEmpty()); // false } } ``` ### 操作時間複雜度 - 新增元素(enqueue/offer):O(1) - 移除元素(dequeue/poll):O(1) - 查看隊首(peek):O(1) - 查看佇列大小(size):O(1) - 檢查是否為空(isEmpty):O(1) ## 進階應用 (擴展知識) ### 生產者消費者模式 - 生產者消費者模式是一種經典的多執行緒設計模式 - 主要就是分為 生產者(Producer) 和 消費者(Consumer) 兩個角色 - 生產者 - 負責生產任務,加入佇列 - 消費者 - 負責消費任務,從佇列中取出任務 - 這樣可以有效的解耦生產者和消費者,提高系統的穩定性和可擴展性 - 簡單的生產者消費者模式範例 ```java public class ProducerConsumer { private BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10); public void producer() { while (true) { try { queue.put(1); } catch (InterruptedException e) { e.printStackTrace(); } } } public void consumer() { while (true) { try { queue.take(); } catch (InterruptedException e) { e.printStackTrace(); } } } } ``` ### BFS演算法實現 - BFS (Breadth First Search) 是一種廣度優先搜索演算法 - 是一種圖形與樹的搜索演算法,常用於圖形的最短路徑問題 - 核心概念是 **逐層擴展**,先擴展所有的鄰居節點,再擴展鄰居的鄰居節點 - 而 Queue 是 BFS 演算法的重要工具,用來存儲待擴展的節點 - BFS 的基本概念 - 廣度優先,從起點開始,先訪問當前節點的所有鄰居節點,再逐層深入,直到所有可達的節點都被訪問過 - FIFO 特性,BFS是先進先出的,也跟 Queue 的特性一樣 ```mermaid graph LR A[Alice] --- B[Bob] A --- C[Charlie] B --- D[David] B --- E[Eve] C --- F[Frank] E --- F style A fill:#5B5B00,stroke:#333 style B fill:#844200,stroke:#333 style C fill:#642100,stroke:#333 style D fill:#548C00,stroke:#333 style E fill:#548C00,stroke:#333 style F fill:#548C00,stroke:#333 %% 關係說明 classDef direct fill:#9cf; classDef secondDegree fill:#cfc; classDef root fill:#f96; ``` - 如果我想要寫一個推薦好友的功能,我們可以使用 BFS 演算法,這樣推薦的好友清單就會優先推薦與你關係最近的好友。 ```java public class SocialNetworkRecommendation { static class User { String id; String name; User(String id, String name) { this.id = id; this.name = name; } @Override public String toString() { return name; } } private Map<String, User> users; private Map<User, List<User>> network; public SocialNetworkRecommendation() { users = new HashMap<>(); network = new HashMap<>(); initializeUsers(); initializeNetwork(); } private void initializeUsers() { addUser("1", "Alice"); addUser("2", "Bob"); addUser("3", "Charlie"); addUser("4", "David"); addUser("5", "Eve"); addUser("6", "Frank"); } private void addUser(String id, String name) { User user = new User(id, name); users.put(name.toLowerCase(), user); } private void initializeNetwork() { User alice = users.get("alice"); User bob = users.get("bob"); User charlie = users.get("charlie"); User david = users.get("david"); User eve = users.get("eve"); User frank = users.get("frank"); network.put(alice, Arrays.asList(bob, charlie)); network.put(bob, Arrays.asList(alice, david, eve)); network.put(charlie, Arrays.asList(alice, frank)); network.put(david, Arrays.asList(bob)); network.put(eve, Arrays.asList(bob, frank)); network.put(frank, Arrays.asList(charlie, eve)); } public void recommendFriendsForUser(String userName) { User user = users.get(userName.toLowerCase()); if (user == null) { System.out.println("找不到用戶: " + userName); return; } // 建立一個佇列,用來存放待處理的用戶 Queue<User> queue = new LinkedList<>(); // 建立一個Map,用來存放每個用戶與起始用戶的距離 Map<User, Integer> distance = new HashMap<>(); // 建立一個Set,用來存放已經處理過的用戶 Set<User> visited = new HashSet<>(); // 建立一個List,用來存放推薦的用戶 List<User> recommendations = new ArrayList<>(); // 先將起始用戶加入佇列、設定距離為0,並標記為已處理 queue.offer(user); distance.put(user, 0); visited.add(user); System.out.println("\n為 " + user + " 尋找好友推薦:"); // 當佇列不為空時,持續處理 while (!queue.isEmpty()) { // 取出佇列中的用戶 User current = queue.poll(); // 取得該用戶與起始用戶的距離 int currentDistance = distance.get(current); // 遍歷 好友網路中的所有用戶 for (User friend : network.getOrDefault(current, Collections.emptyList())) { // 如果該用戶尚未處理過 if (!visited.contains(friend)) { // 將該用戶加入佇列,待會要遍歷這個用戶的好友 queue.offer(friend); // 把這個好友標注為已處理 visited.add(friend); // 設定這個好友與起始用戶的距離 distance.put(friend, currentDistance + 1); // 只推薦非直接好友的用戶(即距離大於1的用戶) if (currentDistance + 1 > 1) { recommendations.add(friend); } } } } if (recommendations.isEmpty()) { System.out.println("沒有可推薦的好友"); } else { System.out.println("好友推薦清單(不包含已是好友的用戶):"); for (int i = 0; i < recommendations.size(); i++) { User recommended = recommendations.get(i); System.out.printf("第%d優先順位 - %s (距離: %d度關係)%n", i + 1, recommended, distance.get(recommended)); } } } public static void main(String[] args) { SocialNetworkRecommendation system = new SocialNetworkRecommendation(); Scanner scanner = new Scanner(System.in); while (true) { System.out.print("\n請輸入要查詢的用戶名稱 (輸入exit退出): "); String input = scanner.nextLine().trim(); if (input.equalsIgnoreCase("exit")) { break; } system.recommendFriendsForUser(input); } scanner.close(); } } ``` - 稍微解釋一下這個程式 - 首先我們建立了一個用戶類 User,用來表示用戶的基本信息 - 然後我們建立了一個 SocialNetworkRecommendation 類,用來表示社交網路推薦系統 - 在這個類中,我們建立了用戶列表 users 和好友網路 network - 用戶列表 users 用來存放所有用戶的信息,好友網路 network 用來存放用戶之間的好友關係 - 在 initializeUsers 方法中,我們初始化了用戶列表 - 在 initializeNetwork 方法中,我們初始化了好友網路 - 在 recommendFriendsForUser 方法 - Queue 用來存放待處理的用戶,有 FIFO 的特性 - LinkedList 適合用來實現 Queue,頻繁的插入和刪除操作 - Map 用來存放每個用戶與起始用戶的距離 - Set 用來存放已經處理過的用戶,因為不需要重複的值,並且查找速度快 - List 用來存放推薦的用戶,且要保持順序 - 在 recommendFriendsForUser 方法中,我們使用 BFS 演算法來尋找好友推薦 - 首先將起始用戶加入佇列、設定距離為0,並標記為已處理 - 當佇列不為空時,持續處理 - 取出佇列中的用戶,取得該用戶與起始用戶的距離 - 遍歷好友網路中的所有用戶 - 如果該用戶尚未處理過,將該用戶加入佇列,待會要遍歷這個用戶的好友 - 把這個好友標注為已處理,設定這個好友與起始用戶的距離 - 只推薦非直接好友的用戶(即距離大於1的用戶) ### 任務排程系統 - 我們來做一個任務排程系統 - 排序的規則 1. 任務的優先級越高,越先執行 2. 如果優先級相同,則按照任務的到達時間先後執行 - 我們會使用 `PriorityQueue` 來實現這個系統 - `PriorityQueue` 的特性 - 是基於堆(heap)的一種優先佇列 - 預設是最小堆,也可以自定義排序規則 - 優先級高的元素先出隊 - 內部實現是一個完全二叉樹 ```java public class TaskSchedulingSystem { static class Task implements Comparable<Task> { private String name; private int priority; // 1-5, 1最高優先 private int executionTime; // seconds private long submissionTime; public Task(String name, int priority, int executionTime) { this.name = name; this.priority = priority; this.executionTime = executionTime; this.submissionTime = System.currentTimeMillis(); } // 我們會使用 PriorityQueue 來封裝這個 Task // 當我們使用了 PriorityQueue 的 offer() 方法時,會自動調用這個 compareTo() 方法 // 預攝氏最小堆,以數值小的或字串字典排序為優先 // 我們可以透過 @Override 來自定義排序規則 // 在這邊我們就優先排序優先級,再排序提交時間 @Override public int compareTo(Task other) { // 優先比較優先級,再比較提交時間 if (this.priority != other.priority) { return Integer.compare(this.priority, other.priority); } return Long.compare(this.submissionTime, other.submissionTime); } @Override public String toString() { return String.format("Task[%s, 優先級=%d, 執行時間=%ds]", name, priority, executionTime); } } static class TaskScheduler { private PriorityQueue<Task> taskQueue; public TaskScheduler() { taskQueue = new PriorityQueue<>(); } public void addTask(Task task) { taskQueue.offer(task); System.out.println("新增任務: " + task); } public void executeTasks() { System.out.println("\n開始執行任務..."); int totalTasks = taskQueue.size(); int completedTasks = 0; while (!taskQueue.isEmpty()) { Task currentTask = taskQueue.poll(); completedTasks++; System.out.printf("\n執行任務 (%d/%d): %s\n", completedTasks, totalTasks, currentTask); // 模擬任務執行 try { Thread.sleep(currentTask.executionTime * 1000); // 縮短等待時間 System.out.println("任務完成!"); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } System.out.println("\n所有任務已完成!"); } } public static void main(String[] args) { TaskScheduler scheduler = new TaskScheduler(); // 新增測試任務 scheduler.addTask(new Task("備份資料庫", 1, 5)); scheduler.addTask(new Task("更新快取", 2, 2)); scheduler.addTask(new Task("發送電子報", 3, 3)); scheduler.addTask(new Task("日誌清理", 2, 1)); scheduler.addTask(new Task("系統掃描", 1, 4)); // 執行任務佇列 scheduler.executeTasks(); } } ``` ### 銀行排隊系統 - 再來我們來做一個銀行排隊系統 - 銀行排隊系統的規則 - 可指定有多少個服務櫃檯 - 客戶到達時,會取號,VIP客戶優先 - 每個服務櫃檯只能服務一個客戶 - 模擬服務時間,每個客戶服務時間為1秒 - 我們會使用 `PriorityQueue` 來實現這個系統 ```java public class BankQueueSystem { // 客戶類別 static class Customer implements Comparable<Customer> { private String number; // 號碼牌 private boolean isVip; // VIP身份 private long arrivalTime; // 到達時間 public Customer(String number, boolean isVip) { this.number = number; this.isVip = isVip; this.arrivalTime = System.currentTimeMillis(); } @Override public int compareTo(Customer other) { // VIP優先,同級別按到達時間排序 if (this.isVip != other.isVip) { return this.isVip ? -1 : 1; } return Long.compare(this.arrivalTime, other.arrivalTime); } @Override public String toString() { return String.format("號碼:%s %s", number, isVip ? "(VIP)" : ""); } } static class BankCounter { private String name; private Customer currentCustomer; public BankCounter(String name) { this.name = name; } public void serveCustomer(Customer customer) { this.currentCustomer = customer; System.out.printf("%s開始服務 %s\n", name, customer); } public void finishService() { System.out.printf("%s完成服務 %s\n", name, currentCustomer); this.currentCustomer = null; } public boolean isAvailable() { return currentCustomer == null; } } private PriorityQueue<Customer> waitingQueue; private List<BankCounter> counters; private int normalNumber = 1; private int vipNumber = 1; public BankQueueSystem(int counterCount) { this.waitingQueue = new PriorityQueue<>(); this.counters = new ArrayList<>(); for (int i = 0; i < counterCount; i++) { counters.add(new BankCounter("櫃檯" + (i + 1))); } } public void addCustomer(boolean isVip) { String number; if (isVip) { number = String.format("V%03d", vipNumber++); } else { number = String.format("N%03d", normalNumber++); } Customer customer = new Customer(number, isVip); waitingQueue.offer(customer); System.out.printf("新客戶取號: %s\n", customer); } public void processQueue() { System.out.println("\n開始處理排隊..."); while (!waitingQueue.isEmpty()) { // 尋找可用櫃檯 for (BankCounter counter : counters) { if (counter.isAvailable() && !waitingQueue.isEmpty()) { Customer customer = waitingQueue.poll(); counter.serveCustomer(customer); // 模擬服務時間 try { Thread.sleep(1000); counter.finishService(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } } System.out.println("所有客戶已處理完畢!"); } public static void main(String[] args) { BankQueueSystem bank = new BankQueueSystem(3); // 3個服務櫃檯 // 模擬客戶到達 bank.addCustomer(false); // 一般客戶 bank.addCustomer(true); // VIP客戶 bank.addCustomer(false); // 一般客戶 bank.addCustomer(true); // VIP客戶 bank.addCustomer(false); // 一般客戶 // 處理排隊 bank.processQueue(); } } ```