一個使用場景
銀行辦理業務的排隊叫號
辦理業務的人先拿號,然後視窗叫號處理,沒有叫到的,則排隊等待。
基本介紹
佇列:是一個 有序列表,可以用 陣列 或 連結串列 實現。
特點:遵循 先入先出 原則。即:先存入的資料,先取出。
示意圖:
- front:隊首,佇列頭部
- rear:隊尾,佇列尾部
- 左 1 圖:佇列初始化的兩個變數值
- 中圖:存入資料後,的首位變化
- 右圖:取資料時,從隊首取,隊首的變數指向也在發生變化
陣列模擬佇列
佇列本身是 有序列表,使用陣列結構來儲存佇列的資料,則如前面基本介紹中的示意圖一樣。
宣告 4 個變數:
arr
:用來儲存資料的陣列maxSize
:該佇列的最大容量front
:隊首下標,隨著資料輸出而改變rear
:隊尾下標,隨著資料輸入而改變
佇列中常用操作分析,以 add,把資料存入佇列為例,思路分析:
- 將尾指標往後移:
rear + 1
,前提是當front == rear
時,佇列是空的 - 若尾指標
rear < maxSize -1
:- 則將資料存入 rear 所指的陣列元素中,
- 否則無法存入資料。
rear = maxSize -1
表示佇列滿了
以上思路是一個最基本的實現(不是完美的,看完程式碼就明白了)。程式碼實現如下
/**
* 陣列模擬佇列
*/
public class ArrayQueueDemo {
public static void main(String[] args) {
ArrayQueue queue = new ArrayQueue(3);
queue.add(1);
queue.add(2);
queue.add(3);
System.out.println("檢視佇列中的資料");
queue.show();
System.out.println("檢視佇列頭資料:" + queue.head());
System.out.println("檢視佇列尾資料:" + queue.tail());
// queue.add(4);
System.out.println("獲取佇列資料:" + queue.get());
System.out.println("檢視佇列中的資料");
queue.show();
}
}
class ArrayQueue {
private int maxSize; // 佇列最大容量
private int front; // 佇列頭,指向佇列頭的前一個位置
private int rear; // 佇列尾,指向佇列尾的資料(及最後一個資料)
private int arr[]; // 用於儲存資料,模擬佇列
public ArrayQueue(int arrMaxSize) {
maxSize = arrMaxSize;
arr = new int[maxSize];
front = -1;
rear = -1;
}
/**
* 取出佇列資料
*/
public int get() {
if (isEmpty()) {
throw new RuntimeException("佇列空");
}
return arr[++front];
}
/**
* 往佇列儲存資料
*/
public void add(int n) {
if (isFull()) {
System.out.println("佇列已滿");
return;
}
arr[++rear] = n;
}
/**
* 顯示佇列中的資料
*/
public void show() {
if (isEmpty()) {
System.out.println("佇列為空");
return;
}
for (int i = 0; i < arr.length; i++) {
System.out.printf("arr[%d] = %d \n", i, arr[i]);
}
}
/**
* 檢視佇列的頭部資料,注意:不是取出資料,只是檢視
*
* @return
*/
public int head() {
if (isEmpty()) {
throw new RuntimeException("佇列空");
}
return arr[front + 1]; // front 指向佇列頭前一個元素,取頭要 +1
}
/**
* 檢視隊尾資料
*
* @return
*/
public int tail() {
if (isEmpty()) {
throw new RuntimeException("佇列空");
}
return arr[rear];
}
// 佇列是否已滿
private boolean isFull() {
return rear == maxSize - 1;
}
// 佇列是否為空
private boolean isEmpty() {
return rear == front;
}
}
執行測試
檢視佇列中的資料
arr[0] = 1
arr[1] = 2
arr[2] = 3
檢視佇列頭資料:1
檢視佇列尾資料:3
獲取佇列資料:1
檢視佇列中的資料
arr[0] = 1
arr[1] = 2
arr[2] = 3
分析
目前實現了一個 一次性的佇列(不能複用),因為可以往佇列中新增資料,基本功能也是可以的,當佇列滿之後,再新增就加不進去了,獲取資料也不能清空原佇列中的資料。
優化方向:使用演算法將這個陣列改進成一個環形佇列。
陣列模擬環形佇列
思路分析
-
front:含義調整
表示:佇列的第一個元素,也就是說
arr[front]
就是佇列的第一個元素初始值:0
-
rear:含義調整
表示:佇列的最後一個元素的下一個位置
初始值(只是初始值):0
這個很重要,是一個小演算法,能更方便的實現我們的環形佇列。
-
佇列 滿 計算公式:
(rear + 1) % maxSize == front
-
佇列 空 計算公式:
rear == front
-
佇列中 有效元素個數 計算公式:
(rear + maxSize - front) % maxSize
為了能更清晰這個演算法,下面畫圖來演示佇列中元素個數,關鍵變數的值
該演算法取巧的地方在於 rear 的位置,注意看上圖,rear 所在的位置 永遠是空的,實現環形佇列的演算法也有多種,這裡空出來一個位置,是這裡演算法的核心。所以,迴圈佇列會浪費一個陣列的儲存空間。
程式碼實現
/**
* 陣列擬環形佇列
*/
public class CircleQueueDemo {
public static void main(String[] args) {
CircleQueue queue = new CircleQueue(3);
// 為了測試方便,寫一個控制檯輸入的小程式
Scanner scanner = new Scanner(System.in);
boolean loop = true;
char key = ' '; // 接受使用者輸入指令
System.out.println("s(show): 顯示佇列");
System.out.println("e(exit): 退出程式");
System.out.println("a(add): 新增資料到佇列");
System.out.println("g(get): 從佇列取出資料");
System.out.println("h(head): 檢視佇列頭的資料");
System.out.println("t(tail): 檢視佇列尾的資料");
System.out.println("p(isEmpty): 佇列是否為空");
while (loop) {
key = scanner.next().charAt(0);
switch (key) {
case 's':
queue.show();
break;
case 'e':
loop = false;
break;
case 'a':
System.out.println("請輸入要新增到佇列的整數:");
int value = scanner.nextInt();
queue.add(value);
break;
case 'g':
try {
int res = queue.get();
System.out.printf("取出的資料是:%d\n", res);
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case 'h':
try {
int res = queue.head();
System.out.printf("隊首資料:%d\n", res);
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case 't':
try {
int res = queue.tail();
System.out.printf("隊尾資料:%d\n", res);
} catch (Exception e) {
System.out.println(e.getMessage());
}
break;
case 'p':
System.out.printf("佇列是否為空:%s", queue.isEmpty());
break;
}
}
}
}
class CircleQueue {
private int maxSize; // 佇列最大容量
private int front; // 佇列頭,指向 隊頭 的元素
private int rear; // 佇列尾,指向 隊尾 的下一個元素
private int arr[]; // 用於儲存資料,模擬佇列
public CircleQueue(int arrMaxSize) {
maxSize = arrMaxSize + 1;
arr = new int[maxSize];
front = 0;
rear = 0;
}
/**
* 取出佇列資料
*/
public int get() {
if (isEmpty()) {
throw new RuntimeException("佇列空");
}
// front 指向的是隊首的位置
int value = arr[front];
// 需要向後移動,但是由於是環形,同樣需要使用取模的方式來計算
front = (front + 1) % maxSize;
return value;
}
/**
* 往佇列儲存資料
*/
public void add(int n) {
if (isFull()) {
System.out.println("佇列已滿");
return;
}
arr[rear] = n;
// rear 指向的是下一個位置
// 由於是環形佇列,需要使用取模的形式來喚醒他的下一個位置
rear = (rear + 1) % maxSize;
}
/**
* 顯示佇列中的資料
*/
public void show() {
if (isEmpty()) {
System.out.println("佇列為空");
return;
}
// 列印的時候,需要從隊首開始列印
// 列印的次數則是:有效的元素個數
// 獲取資料的下標:由於是環形的,需要使用取模的方式來獲取
for (int i = front; i < front + size(); i++) {
int index = i % maxSize;
System.out.printf("arr[%d] = %d \n", index, arr[index]);
}
}
/**
* 檢視佇列的頭部資料,注意:不是取出資料,只是檢視
*
* @return
*/
public int head() {
if (isEmpty()) {
throw new RuntimeException("佇列空");
}
return arr[front];
}
/**
* 檢視隊尾資料
*
* @return
*/
public int tail() {
if (isEmpty()) {
throw new RuntimeException("佇列空");
}
// rear - 1 是隊尾資料,但是如果是環形收尾相接的時候
// 那麼 0 -1 就是 -1 了,負數時,則是陣列的最後一個元素
return rear - 1 < 0 ? arr[maxSize - 1] : arr[rear - 1];
}
// 佇列是否已滿
private boolean isFull() {
return (rear + 1) % maxSize == front;
}
// 佇列是否為空
public boolean isEmpty() {
return rear == front;
}
// 有效個數
public int size() {
return (rear + maxSize - front) % maxSize;
}
}
執行測試功能輸出
s(show): 顯示佇列
e(exit): 退出程式
a(add): 新增資料到佇列
g(get): 從佇列取出資料
h(head): 檢視佇列頭的資料
t(tail): 檢視佇列尾的資料
p(isEmpty): 佇列是否為空
a
請輸入要新增到佇列的整數:
10
a
請輸入要新增到佇列的整數:
20
a
請輸入要新增到佇列的整數:
30
s
arr[0] = 10
arr[1] = 20
arr[2] = 30
a
請輸入要新增到佇列的整數:
40
佇列已滿
h
隊首資料:10
t
隊尾資料:30
g
取出的資料是:10
s
arr[1] = 20
arr[2] = 30
a
請輸入要新增到佇列的整數:
40
s
arr[1] = 20
arr[2] = 30
arr[3] = 40
h
隊首資料:20
t
隊尾資料:40
g
取出的資料是:20
s
arr[2] = 30
arr[3] = 40
a
請輸入要新增到佇列的整數:
50
s
arr[2] = 30
arr[3] = 40
arr[0] = 50
h
隊首資料:30
t
隊尾資料:50
可以看到上面的表現,和那個圖解分析是一致的, rear 所在的位置沒有元素,是動態的。