1. 程式人生 > >資料結構-佇列

資料結構-佇列

佇列是一種列表, 不同的是佇列只能在隊尾插入元素, 在隊首刪除元素. 是一種 先進先出 的資料結構. 佇列被用在很多地方, 比如提交作業系統執行的一系列程序、列印任務池等, 一些模擬系統用佇列來模擬銀行或雜貨店排隊的顧客.

佇列的操作

主要有兩種操作:

  1. 向隊尾中插入新元素 入隊 ;
  2. 刪除隊頭的元素 _出隊_;

佇列的另一項重要操作是讀取隊頭的元素. 這個操作叫做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

修改上例, 使得候診室內的活動可以被控制

類似菜單系統, 讓使用者可以進行如下選擇:

  1. 患者進入候診室
  2. 患者就診
  3. 顯示等待就診患者名單
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