資料結構-佇列
佇列是一種列表, 不同的是佇列只能在隊尾插入元素, 在隊首刪除元素. 是一種 先進先出 的資料結構. 佇列被用在很多地方, 比如提交作業系統執行的一系列程序、列印任務池等, 一些模擬系統用佇列來模擬銀行或雜貨店排隊的顧客.
佇列的操作
主要有兩種操作:
- 向隊尾中插入新元素 入隊 ;
- 刪除隊頭的元素 _出隊_;
佇列的另一項重要操作是讀取隊頭的元素. 這個操作叫做peek()
. 該操作返回隊頭元素, 但不把它從佇列中刪除. 除了讀取隊頭元素, 我們還想知道佇列中儲存了多少元素, 可以使用length
屬性滿足該需求; 想要清空佇列中所有元素, 可以使用clear()
方法.
用陣列實現的佇列
使用陣列來實現佇列看起來順理成章. js中的陣列具有其他程式語言中沒有的優點, 陣列的push()
shift()
方法則可刪除陣列的第一個元素.
push()
方法將它的引數插入陣列中第一個開放的位置, 該位置總是在資料的末尾, 即使是一個空陣列也是如此. eg:
class Queue { constructor() { this._dataStore = []; } enqueue(element) { this._dataStore.push(element); } dequeue(element) { return this._dataStore.shift(); } front() { return this._dataStore[0]; } back() { return this._dataStore[this._dataStore.length - 1]; } toString() { return this._dataStore.join(' '); } empty() { if(this._dataStore.length) { return false; }; return true; } count() { return this._dataStore.length; } }; // 測試 const q = new Queue(); q.enqueue('a'); q.enqueue('b'); q.enqueue('c'); console.log(q.toString()); q.dequeue(); console.log(q.toString()); console.log(`隊首: ${q.front()}`); console.log(`隊尾: ${q.back()}`); /* 輸出結果: a b c b c 隊首: b 隊尾: c */
例項
方塊舞的舞伴分配問題
當男男女女來到舞池, 他們按照自己的性別排成兩列. 當舞池中有地方空出來時, 選兩個佇列中的第一個人組成舞伴. 他們身後的人各自向前移動一位, 變成新的隊首. 當一對舞伴邁入舞池時, 主持人會大聲喊出他們的名字. 當一對舞伴走出舞池, 且兩排隊伍中有任意一隊沒人時, 主持人也會把這種情況告訴大家.我們把跳舞的男男女女存在persons
物件中.
function getDancers(maleDancers, femaleDancers) { let dancers = []; for(let i = 0; i < 8; i++) { dancers.push({ name: `男${i + 1}`, sex: 'M' }); if(i < 5) { dancers.push({ name: `女${i + 1}`, sex: 'F' }); } }; dancers.forEach(i => { if(i.sex === 'F') { femaleDancers.enqueue(i); } else { maleDancers.enqueue(i) } }) }; function dance(maleDancers, femaleDancers) { while(!maleDancers.empty() && !femaleDancers.empty()) { const female = femaleDancers.dequeue(); const male = maleDancers.dequeue(); console.log(`將要入場的舞者是: ${female.name}和${male.name}`); } } const maleDancers = new Queue(); const femaleDancers = new Queue(); getDancers(maleDancers, femaleDancers); dance(maleDancers, femaleDancers);
程式輸出為:
將要入場的舞者是: 女1和男1
將要入場的舞者是: 女2和男2
將要入場的舞者是: 女3和男3
將要入場的舞者是: 女4和男4
將要入場的舞者是: 女5和男5
可能還想對該程式作如下修改: 想顯示排隊等候跳舞的男性和女性的數量. 佇列中目前尚沒有顯示元素個數的方法, 加入count()
在Queue
類中.
function getDancers(maleDancers, femaleDancers) {
let dancers = [];
for(let i = 0; i < 8; i++) {
dancers.push({
name: `男${i + 1}`,
sex: 'M'
});
if(i < 5) {
dancers.push({
name: `女${i + 1}`,
sex: 'F'
});
}
};
dancers.forEach(i => {
if(i.sex === 'F') {
femaleDancers.enqueue(i);
} else {
maleDancers.enqueue(i)
}
})
};
function dance(maleDancers, femaleDancers) {
while(!maleDancers.empty() && !femaleDancers.empty()) {
const female = femaleDancers.dequeue();
const male = maleDancers.dequeue();
console.log(`將要入場的舞者是: ${female.name}和${male.name}`);
}
};
const maleDancers = new Queue();
const femaleDancers = new Queue();
getDancers(maleDancers, femaleDancers);
dance(maleDancers, femaleDancers);
if(maleDancers.count() > 0) {
console.log(`還有${maleDancers.count()}名男舞者等候`)
};
if(femaleDancers.count() > 0) {
console.log(`還有${maleDancers.count()}名女舞者等候`)
};
程式輸出為:
將要入場的舞者是: 女1和男1
將要入場的舞者是: 女2和男2
將要入場的舞者是: 女3和男3
將要入場的舞者是: 女4和男4
將要入場的舞者是: 女5和男5
還有3名男舞者等候
資料排序
佇列不僅用於執行顯示生活中與排隊有關的操作, 還可以用於對於資料進行排序. 計算機剛出現時, 程式是通過穿孔卡輸入主機的, 每張卡包含一條程式語句. 這些穿孔卡裝在個盒子裡, 經一個機械裝置進行排序. 這種排序叫做 基礎排序 . 雖然它不是最快的排序演算法, 但是它展現了一些有趣的佇列使用方法.
對於0~99的數字, 基數排序將資料集掃描兩次. 第一次按個位上的數字進行排序, 第二次按照十位上的數字進行排序. 每個數字根據對應位上的數值被分在不同盒子裡. 假設有如下數字:
91, 46, 85, 15, 92, 35, 31, 22
經過基數排序第一次掃描之後, 數字被分配到如下盒子中:
Bin 0:
Bin 1: 91, 31
Bin 2: 92, 22
Bin 3:
Bin 4:
Bin 5: 85, 15, 35
Bin 6: 46
Bin 7:
Bin 8:
Bin 9:
根據盒子的順序, 對數字進行一次排序的結果如下:
91, 31, 92, 22, 85, 15, 35, 46
然後根據十位上的數值再將上次排序的結果分配到不同的盒子中:
Bin 0:
Bin 1: 15
Bin 2: 22
Bin 3: 31, 35
Bin 4: 46
Bin 5:
Bin 6:
Bin 7:
Bin 8: 85
Bin 9: 91, 92
最後將盒子中的數字取出, 組成一個新的列表, 該列表即為排好序的數字:
15, 22, 31, 35, 45, 85, 91, 92
演算法如下:
function distribute(nums, queues, n, digit) {
for(let i = 0; i < n; i++) {
if(digit === 1) {
queues[nums[i] % 10].enqueue(nums[i]);
} else {
queues[Math.floor(nums[i] / 10)].enqueue(nums[i]);
}
}
};
function collect(queues, nums) {
let i = 0;
for(let digit = 0; digit < 10; digit++) {
while (!queues[digit].empty()) {
nums[i++] = queues[digit].dequeue();
}
}
};
function dispArray(arr) {
let str = '';
arr.forEach(i => {
str += ' ' + i
});
console.log(str)
};
const queues = [];
for(let i = 0; i <= 10; i++) {
queues[i] = new Queue()
};
const nums = [];
for(let i = 0; i < 10; i++) {
nums[i] = Math.floor(Math.floor(Math.random() * 100))
};
dispArray(nums);
distribute(nums, queues, 10, 1);
collect(queues, nums);
distribute(nums, queues, 10, 10);
collect(queues, nums);
dispArray(nums);
下面是執行幾次的結果:
32 35 21 63 59 83 72 22 16 10
10 16 21 22 32 35 59 63 72 83
80 22 14 57 95 11 38 99 6 11
6 11 11 14 22 38 57 80 95 99
優先佇列
一般情況下, 從佇列中刪除元素, 一定是率先入隊的元素. 但是也有一些使用佇列的應用, 在刪除刪除元素時不必遵守先進先出的約定. 這種應用, 需要使用一個叫做 優先佇列 的資料結構來進行模擬.
從優先佇列中刪除元素時, 需要考慮優先權的限制. 比如醫院急症科的候診室, 就是一個採用 優先佇列 的例子. 當病人進入候診室時, 分診護士會評估患者病情的嚴重程度, 然後給一個優先順序程式碼. 高優先順序的患者先於低優先順序的患者就醫, 同樣優先順序的患者按照先來先服務的順序就醫.
首先定義儲存佇列元素的物件, 然後再構建我們的 優先佇列 系統:
function Patient(name, code) {
this.name = name;
this.code = code;
};
變數code
是一個整數, 表示患者的優先順序或病情嚴重程度.
現在需要重新定義dequeue()
方法, 使其刪除佇列中擁有最高階優先順序的元素. 我們規定: 優先碼的值最小的元素優先順序最高. 新的dequeue()
方法遍歷佇列的底層儲存陣列, 從中找出優先碼最低的元素, 然後使用陣列的splice()
方法刪除優先順序最高的元素. 新的dequeue()
方法如下:
dequeue(element) {
let entry = 0;
this._dataStore.forEach((i, index) => {
if(i.code < this._dataStore[entry].code) {
entry = index;
}
});
return this._dataStore.splice(entry, 1);
}
dequeue()
方法使用簡單的順序查詢方法尋找優先順序最高的元素(程式碼越小優先順序越高, eg: 1比5的優先順序高). 該方法返回包含一個元素的陣列 一一 從佇列中刪除的元素.
最後需要定義toString()
方法來顯示Patient
物件:
toString() {
var str = '';
this._dataStore.forEach(i => {
str += i.name + ' code:' + i.code + '\n';
});
return str;
}
實現:
const ed = new Queue();
ed.enqueue(new Patient('病患1', 5))
ed.enqueue(new Patient('病患2', 4))
ed.enqueue(new Patient('病患3', 6))
ed.enqueue(new Patient('病患4', 1))
ed.enqueue(new Patient('病患5', 1))
console.log(ed.toString())
const seen = ed.dequeue();
console.log(seen[0].name)
console.log(ed.toString())
const seen1 = ed.dequeue();
console.log(seen1[0].name)
console.log(ed.toString())
const seen2 = ed.dequeue();
console.log(seen2[0].name)
console.log(ed.toString())
輸出如下:
病患1 code:5
病患2 code:4
病患3 code:6
病患4 code:1
病患5 code:1
病患4
病患1 code:5
病患2 code:4
病患3 code:6
病患5 code:1
病患5
病患1 code:5
病患2 code:4
病患3 code:6
病患2
病患1 code:5
病患3 code:6
練習
雙向佇列
修改一個Queue
類, 形成一個Deque
類. 這是一個和佇列類似的資料結構, 允許從佇列兩端新增和刪除元素, 因此也叫 雙向佇列 . 寫一段測試程式測試該類:
class Queue {
constructor() {
this._dataStore = [];
}
// 隊尾入隊
enqueueBack(element) {
this._dataStore.push(element);
}
// 隊首入隊
enqueueFront(element) {
this._dataStore.unshift(element);
}
// 隊尾出隊
dequeueBack() {
return this._dataStore.pop();
}
// 隊首出隊
dequeueFront() {
return this._dataStore.shift();
}
// 第一個
front() {
return this._dataStore[0];
}
// 最後一個
back() {
return this._dataStore[this._dataStore.length - 1];
}
// 輸出
toString() {
return this._dataStore.join(' ');
}
// 是否為空
empty() {
if(this._dataStore.length) {
return false;
};
return true;
}
// 個數
count() {
return this._dataStore.length;
}
};
const d = new Queue();
d.enqueueBack(3)
d.enqueueBack(4)
d.enqueueBack(5)
console.log(d.toString())
d.enqueueFront(2)
d.enqueueFront(1)
console.log(d.toString())
d.dequeueBack()
d.dequeueBack()
console.log(d.toString())
d.dequeueFront()
console.log(d.toString())
輸入結果:
3 4 5
1 2 3 4 5
1 2 3
2 3
使用Deque
類來判斷一個給定單詞是否為迴文
首先讓單詞每個字母入隊, 然後兩端同時出隊, 判斷隊首隊尾字母是否相等.
const str = '121'
function isPalindrome(word) {
const d = new Queue();
for(let w of word) {
d.enqueueBack(w)
};
while(!d.empty()) {
if(d.front() !== d.back()) {
return false;
} else {
d.dequeueBack();
d.dequeueFront();
}
};
return true;
};
console.log(isPalindrome(str)); // true
修改文章中 優先佇列 使得優先順序高的元素優先碼也大.
這裡只需要在出隊的時候按照優先碼最大的先出隊.
...
dequeue(element) {
let entry = 0;
this._dataStore.forEach((i, index) => {
if(i.code > this._dataStore[entry].code) {
entry = index;
}
});
return this._dataStore.splice(entry, 1);
}
...
驗證:
function Patient(name, code) {
this.name = name;
this.code = code;
};
const ed = new Queue();
ed.enqueue(new Patient('病患1', 5))
ed.enqueue(new Patient('病患1-1', 5))
ed.enqueue(new Patient('病患3', 6))
ed.enqueue(new Patient('病患4', 1))
ed.enqueue(new Patient('病患5', 1))
console.log(ed.toString())
const seen = ed.dequeue();
console.log('出隊病患 =>', seen[0].name)
console.log(ed.toString())
const seen1 = ed.dequeue();
console.log('出隊病患 =>', seen1[0].name)
console.log(ed.toString())
const seen2 = ed.dequeue();
console.log('出隊病患 =>', seen2[0].name)
console.log(ed.toString())
輸入結果:
病患1 code:5
病患1-1 code:5
病患3 code:6
病患4 code:1
病患5 code:1
出隊病患 => 病患3
病患1 code:5
病患1-1 code:5
病患4 code:1
病患5 code:1
出隊病患 => 病患1
病患1-1 code:5
病患4 code:1
病患5 code:1
出隊病患 => 病患1-1
病患4 code:1
病患5 code:1
修改上例, 使得候診室內的活動可以被控制
類似菜單系統, 讓使用者可以進行如下選擇:
- 患者進入候診室
- 患者就診
- 顯示等待就診患者名單
class Queue {
constructor() {
this._dataStore = [];
}
enqueue(element) {
this._dataStore.push(element);
}
dequeue(element) {
let entry = 0;
this._dataStore.forEach((i, index) => {
if(i.code > this._dataStore[entry].code) {
entry = index;
}
});
return this._dataStore.splice(entry, 1);
}
front() {
return this._dataStore[0];
}
back() {
return this._dataStore[this._dataStore.length - 1];
}
toString() {
var str = '';
this._dataStore.forEach(i => {
str += i.name + ' code:' + i.code + '\n';
});
return str;
}
empty() {
if(this._dataStore.length) {
return false;
};
return true;
}
count() {
return this._dataStore.length;
}
};
class Room {
constructor() {
this._q = new Queue();
}
// 患者進入候診室 code值越大 優先順序越高
enter(name, code) {
this._q.enqueue({
name, code
});
}
// 患者就診 也是就是從候診室佇列中出隊
seeDoctor() {
return this._q.dequeue()[0].name;
}
// 顯示等待就診患者名單
showRoom() {
return this._q.toString();
}
};
const r = new Room();
r.enter('患者1', 2);
r.enter('患者2', 3);
r.enter('患者3', 1);
r.enter('患者4', 4);
r.enter('患者5', 4);
r.enter('患者6', 5);
console.log(r.showRoom())
console.log(r.seeDoctor(), ' 就診')
console.log(r.seeDoctor(), ' 就診')
console.log(r.showRoom())
console.log(r.seeDoctor(), ' 就診')
console.log(r.showRoom())
輸出結果:
患者1 code:2
患者2 code:3
患者3 code:1
患者4 code:4
患者5 code:4
患者6 code:5
患者6 就診
患者4 就診
患者1 code:2
患者2 code:3
患者3 code:1
患者5 code:4
患者5 就診
患者1 code:2
患者2 code:3
患者3 code:1