javascript实现数据结构与算法系列:队列 -- 链队列和循环队列实现及示例
1 队列的基本概念
队列(Queue):也是运算受限的线性表。是一种先进先出(First In First Out ,简称FIFO)的线性表。只允许在表的一端进行插入,而在另一端进行删除。
队首(front) :允许进行删除的一端称为队首。
队尾(rear) :允许进行插入的一端称为队尾。
例如:排队购物。操作系统中的作业排队。先进入队列的成员总是先离开队列。
队列中没有元素时称为空队列。在空队列中依次加入元素a1, a2, …, an之后,a1是队首元素,an是队尾元素。显然退出队列的次序也只能是a1, a2, …, an ,即队列的修改是依先进先出的原则进行的
利用一组连续的存储单元(一维数组) 依次存放从队首到队尾的各个元素,称为顺序队列。
队列的链式存储结构简称为链队列,它是限制仅在表头进行删除操作和表尾进行插入操作的单链表。
链队列结构图:
链队列的表示:
1 function Queue() { 2 this.rear = this.front = null; 3 this.size = 0; 4 } 5 exports.Queue = Queue; 6 Queue.prototype = { 7 clear: function () { 8 this.rear = this.front = null; 9 this.size = 0; 10 }, 11 getHead: function () { 12 return this.front ? this.front.data : null; 13 }, 14 enQueue: function (elem) { 15 if (this.front === null) { 16 this.rear = this.front = {data: elem, next: null}; 17 } else { 18 var p = {data: elem, next: null}; 19 this.rear.next = p; 20 this.rear = p; 21 } 22 this.size++; 23 }, 24 deQueue: function () { 25 if (this.front) { 26 var elem = this.front.data; 27 this.front = this.front.next; 28 if (this.front === null) { 29 this.rear = null; 30 } 31 this.size--; 32 return elem; 33 } else { 34 return null; 35 } 36 }, 37 queueTraverse: function (iterator) { 38 var current = this.front; 39 while (current) { 40 if (iterator(current.data)) break; 41 current = current.next; 42 } 43 }, 44 peekAt: function (index) { 45 index = index || 0; 46 47 if (index < this.size) { 48 var current = this.front; 49 for (var i = 0; i < index; i++) { 50 current = current.next; 51 } 52 return current.data; 53 } 54 55 return null; 56 }, 57 displayAll: function () { 58 if (this.front === null) { 59 return null; 60 } 61 62 var arr = []; 63 var current = this.front; 64 65 for (var i = 0, len = this.size; i < len; i++) { 66 arr[i] = current.data; 67 current = current.next; 68 } 69 70 return arr; 71 } 72 }; 73 74 var queue = new Queue(); 75 queue.enQueue(1); 76 queue.deQueue(); 77 queue.enQueue(2); 78 queue.enQueue(3); 79 console.log(queue.peekAt(0)); 80 console.log(queue.peekAt(1)); 81 console.log(queue.peekAt(2)); 82 console.log(queue.peekAt(3)); 83 console.log(queue.displayAll().join());
循环队列
顺序队列中存在“假溢出”现象。因为在入队和出队操作中,头、尾指针只增加不减小,致使被删除元素的空间永远无法重新利用。因此,尽管队列中实际元素个数可能远远小于数组大小,但可能由于尾指针巳超出向量空间的上界而不能做入队操作。该现象称为假溢出。
为充分利用向量空间,克服上述“假溢出”现象的方法是:将为队列分配的向量空间看成为一个首尾相接的圆环,并称这种队列为循环队列(Circular Queue)。
在循环队列中进行出队、入队操作时,队首、队尾指针仍要加1,朝前移动。只不过当队首、队尾指针指向向量上界(MAX_QUEUE_SIZE-1)时,其加1操作的结果是指向向量的下界0。
用模运算可简化为:i=(i+1)%MAX_QUEUE_SIZE ;
显然,为循环队列所分配的空间可以被充分利用,除非向量空间真的被队列元素全部占用,否则不会上溢。因此,真正实用的顺序队列是循环队列。
例:设有循环队列QU[0,5],其初始状态是front=rear=0,各种操作后队列的头、尾指针的状态变化情况如下图:
入队时尾指针向前追赶头指针,出队时头指针向前追赶尾指针,故队空和队满时头尾指针均相等。因此,无法通过front=rear来判断队列“空”还是“满”。解决此问题的方法是:约定入队前,测试尾指针在循环意义下加1后是否等于头指针,若相等则认为队满。即:
◆ rear所指的单元始终为空。
◆ 循环队列为空:front=rear 。
◆ 循环队列满:(rear+1)%MAX_QUEUE_SIZE =front。
循环队列结构图:
循环队列的表示:
1 // 循环队列 2 function CycleQueue() { 3 this.base = {}; 4 this.front = this.rear = 0; 5 this.MAXQSIZE = 100; 6 } 7 exports.CycleQueue = CycleQueue; 8 CycleQueue.prototype = { 9 enQueue: function (data) { 10 if ((this.rear + 1) % this.MAXQSIZE === 0) throw new Error('cycleQueue is already full!'); 11 12 this.base[this.rear] = data; 13 this.rear = (this.rear + 1) % this.MAXQSIZE; 14 }, 15 deQueue: function () { 16 if (this.front === this.rear) throw new Error('cycleQueue is already empty'); 17 18 var elem = this.base[this.front]; 19 this.front = (this.front + 1) % this.MAXQSIZE; 20 21 return elem; 22 }, 23 clear: function () { 24 this.base = {}; 25 this.front = this.rear = 0; 26 }, 27 size: function () { 28 return (this.rear - this.front + this.MAXQSIZE) % this.MAXQSIZE; 29 }, 30 peekAt: function (index) { 31 index = (index || 0 + this.MAXQSIZE) % this.MAXQSIZE; 32 33 return this.base[index + this.front] || null; 34 }, 35 getHead: function () { 36 var elem = this.base[this.front]; 37 return elem ? elem : null; 38 }, 39 queueTraverse: function (iterator) { 40 for (var i = this.front, len = this.rear = this.front; i < len; i++) { 41 if (iterator(this.base[i], i)) break; 42 } 43 }, 44 displayAll: function () { 45 var base = [].slice.call(this.base); 46 47 return base.slice(this.front, this.rear - this.front); 48 } 49 }; 50 51 var queue = new CycleQueue(); 52 queue.enQueue(1); 53 queue.deQueue(); 54 queue.enQueue(2); 55 queue.enQueue(3); 56 console.log(queue.peekAt(0)); 57 console.log(queue.peekAt(1)); 58 console.log(queue.peekAt(2));
单元测试代码:
1 //var Queue = require('./Queue').Queue; 2 //var CycleQueue = require('./Queue').CycleQueue; 3 4 describe('Queue tests', function(){ 5 var queue = new Queue(); 6 7 it('should enqueue 1', function(){ 8 queue.enQueue(1); 9 expect(queue.getHead()).toBe(1); 10 expect(queue.size).toBe(1); 11 expect(queue.rear).toBe(queue.front); 12 }); 13 14 it('should be an empty queue', function(){ 15 queue.deQueue(); 16 expect(queue.getHead()).toBe(null); 17 expect(queue.size).toBe(0); 18 expect(queue.rear).toBe(null); 19 expect(queue.front).toBe(null); 20 }); 21 22 it('should enqueue 2 elems', function(){ 23 queue.enQueue(2); 24 expect(queue.getHead()).toBe(2); 25 expect(queue.size).toBe(1); 26 queue.enQueue(3); 27 expect(queue.front.next.data).toBe(3); 28 expect(queue.size).toBe(2); 29 30 expect(queue.rear.data).toBe(3); 31 expect(queue.front.data).toBe(2); 32 }); 33 34 35 var queue2 = new CycleQueue(); 36 37 38 it('should insert an element into cycleQueue, and then delete it, lastly it must be an empty queue', function(){ 39 queue2.enQueue(1); 40 expect(queue2.front).toBe(0); 41 expect(queue2.rear).toBe(1); 42 expect(queue2.base[queue2.front]).toBe(1); 43 expect(queue2.base[queue2.rear]).toBe(undefined); 44 expect(queue2.getHead()).toBe(1); 45 expect(queue2.size()).toBe(1); 46 47 queue2.deQueue(); 48 expect(queue2.front).toBe(1); 49 expect(queue2.rear).toBe(queue2.front); 50 expect(queue2[this.front]).toBe(undefined); 51 expect(queue2.size()).toBe(0); 52 }); 53 54 it('should insert into two elements into cycleQueue, and peek them in order', function(){ 55 queue2.enQueue(2); 56 expect(queue2.front).toBe(1); 57 expect(queue2.rear).toBe(2); 58 expect(queue2.base[queue2.front]).toBe(2); 59 expect(queue2.base[queue2.rear]).toBe(undefined); 60 61 queue2.enQueue(3); 62 expect(queue2.rear).toBe(3); 63 expect(queue2.base[queue2.front]).toBe(2); 64 expect(queue2.base[queue2.front + 1]).toBe(3); 65 expect(queue2.base[this.rear]).toBe(undefined); 66 expect(queue2.peekAt(0)).toBe(2); 67 expect(queue2.peekAt(1)).toBe(3); 68 expect(queue2.peekAt(2)).toBe(null); 69 }); 70 71 });
示例1:离散事件模拟,利用链表和队列模拟银行业务程序
假设某银行有4个窗口对外接待客户,从早晨银行开门起不断有客户进入银行。由于每个窗口在某个时刻只能接待一个客户,因此在客户人数众多时需在每个窗口前顺次排队们对于刚进入银行的客户,如果某个窗口的业务员正空闲,则可上前办理业务。反之,若4个窗口均有客户所占,他便会排在人数最少的队伍后面。现在需要编制一个程序以模拟银行的这种业务活动并计算一天中客户的银行逗留的平均时间。
1 var List = require('../linkedList/complete-LinkedList'); 2 var Queue = require('./Queue').Queue; 3 4 5 // 事件类型,有序链表LinkList的数据元素类型 6 function Event(occurTime, eventType) { 7 // 事件发生时刻 8 this.occurTime = occurTime || 0; 9 // 事件类型,0表示到达事件,1至4表示四个窗口的离开事件 10 this.eventType = eventType || 0; 11 } 12 13 // 队列的数据元素类型 14 function QueueElemType(arrivalTime, duration) { 15 // 到达时刻 16 this.arrivalTime = arrivalTime || 0; 17 // 办理事务所需时间 18 this.duration = duration || 0; 19 } 20 21 function Bank() { 22 // 事件表 23 this.eventList = null; 24 this.event = null; 25 // 4个客户队列 26 this.queues = new Array(4); 27 this.totalTime = 0; 28 this.customerNum = 0; 29 this.closeTime = 0; 30 } 31 Bank.prototype = { 32 cmp: function (event1, event2) { 33 if (event1.occurTime < event2.occurTime) 34 return -1; 35 else if (event1.occurTime === event2.occurTime) 36 return 0; 37 else 38 return 1; 39 }, 40 // 初始化操作 41 openForDay: function () { 42 // 初始化累计时间和客户数为0 43 this.totalTime = 0; 44 this.customerNum = 0; 45 // 初始化事件链表 46 this.eventList = new List(); 47 // 设定第一个用户到达事件 48 this.event = new Event(0, 0); 49 // 插入到事件表 50 this.eventList.orderInsert(this.event, this.cmp); 51 52 // 置空队列 53 for (var i = 0, len = this.queues.length; i < len; i++) 54 this.queues[i] = new Queue(); 55 }, 56 // 处理客户到达事件 57 customerArrived: function (durtime, intertime) { 58 ++this.customerNum; 59 60 // 生成随机数 61 durtime = durtime || Math.floor(Math.random() * 20) + 1; // 办理业务所需时间 62 intertime = intertime || Math.floor(Math.random() * 5) + 1; // 两个相邻客户时间间隔 63 // 下一客户到达时刻 64 var t = this.event.occurTime + intertime; 65 // 银行尚未关门,插入事件表,这里还包括客户的离开时间 66 if (t < this.closeTime && t + durtime < this.closeTime) { 67 this.eventList.orderInsert(new Event(t, 0), this.cmp); 68 } 69 70 // 求长度最短队列 71 var minQueueIndex = 0; 72 var allEqualed = false; 73 for(var i = 0, len = this.queues.length; i < len && this.queues[i + 1]; i++){ 74 if(this.queues[i].size === 0) { 75 minQueueIndex = i; 76 break; 77 } 78 if(this.queues[i].size < this.queues[i + 1].size){ 79 minQueueIndex = i; 80 allEqualed = false; 81 } else if(this.queues[i].size < this.queues[i + 1].size){ 82 minQueueIndex = i; 83 allEqualed = true; 84 } else { 85 minQueueIndex = i + 1; 86 allEqualed = false; 87 } 88 } 89 // 如果所有队列长度都相等,取第一个 90 if(allEqualed) minQueueIndex = 0; 91 92 this.queues[minQueueIndex] 93 .enQueue(new QueueElemType(this.event.occurTime, durtime)); 94 95 // 设定第i队列的一个离开事件并插入事件表 96 if (this.queues[minQueueIndex].size === 1) { 97 this.eventList.orderInsert(new Event(this.event.occurTime + durtime, minQueueIndex + 1), this.cmp); 98 } 99 // 保存最新客户的到达时间 100 this.event.occurTime = t; 101 }, 102 // 处理客户离开事件 103 customerDeparture: function (type) { 104 // 删除第i队列的排头客户 105 var i = type - 1 || 0; 106 var customer = this.queues[i].deQueue(); 107 // 累计客户逗留时间 108 this.totalTime += this.event.occurTime - customer.arrivalTime; 109 110 // 设定第i队列的一个离开事件并插入事件表 111 if (this.queues[i].size) { 112 customer = this.queues[i].getHead(); 113 this.eventList.orderInsert(new Event(this.event.occurTime + customer.duration, i), this.cmp); 114 } 115 }, 116 simulation: function (closeTime) { 117 this.closeTime = closeTime || 0; 118 this.openForDay(); 119 while (this.eventList.head) { 120 var elem = this.eventList.delFirst().data; 121 if (elem.eventType === 0) 122 this.customerArrived(); 123 else 124 this.customerDeparture(elem.eventType); 125 } 126 console.log('The average time is ' + this.totalTime / this.customerNum); 127 } 128 }; 129 130 new Bank().simulation(20);
假设每个客户办理业务时间不超过20分钟;两个相邻到达银行的客户的时间间隔不超过5分钟。模拟程序从第一个客户到达时间为0开始运行。
删除事件表上地一个结点,得到event.occurTime = 0,因为event.eventType = 0,则随即得到两个随机数(23, 4),生成下一个客户到达银行的事件Event(occurTime = 4, eventType = 0)插入事件表;刚到的第一位客户排在第一个窗口的队列中QueueElemType(arrivalTime = 0, duration = 23),
由于他是排头,故生成一个客户将离开的事件Event(occurTime = 23, eventType = 1)插入事件表。
删除时间表上第一个结点,仍是新客户到达事件(因为event.eventType = 0),event.occurTime = 4,得到随机数为(3, 1),则下一个客户到达银行的时间为occurTime = 4 + 1 = 5,由于此时第二个窗口是空的,刚到的第二位客户为第二个队列的队头QueueElemType(arrivalTime = 4, duration = 3),因而生成一个客户将离开的事件Event(occurTime = 7, eventType = 2)插入事件表。
其他以此类推,直到银行关门时间结束程序。