栈(Stack)是限定仅在表尾进行插入或删除操作的线性表。表尾为栈顶(top),表头为栈底(bottom),不含元素的空表为空栈。
栈又称为后进先出(last in first out)的线性表。
堆栈可以用链表和数组两种方式实现,一般为一个堆栈预先分配一个大小固定且较合适的空间并非难事,所以较流行的做法是 Stack 结构下含一个数组。如果空间实在紧张,也可用链表实现,且去掉表头。
1 | // 找的链式表示 function Stack() { this.top = null; this.size = 0; } module.exports = Stack; Stack.prototype = { constructor: Stack, push: function (data) { var node = { data: data, next: null }; node.next = this.top; this.top = node; this.size++; }, peek: function () { return this.top === null ? null : this.top.data; }, pop: function () { if (this.top === null) return null; var out = this.top; this.top = this.top.next; if (this.size > 0) this.size--; return out.data; }, clear: function () { this.top = null; this.size = 0; }, displayAll: function () { if (this.top === null) return null; var arr = []; var current = this.top; for (var i = 0, len = this.size; i < len; i++) { arr[i] = current.data; current = current.next; } return arr; } }; var stack = new Stack(); stack.push(1); stack.push('asd'); stack.pop(); stack.push({a: 1}); console.log(stack); |
堆栈的应用
回溯
递回
深度优先搜寻
示例1:数值进制转换
公式: N = (N / d) * d + N % d N:十进制数值, d:需要转换的进制数
1 | function numTransform(number, rad) { var s = new Stack(); while (number) { s.push(number % rad); number = parseInt(number / 8, 10); } var arr = []; while (s.top) { arr.push(s.pop()); } console.log(arr.join('')); } numTransform(1348, 8); numTransform(1348, 2); |
示例2:括号匹配检查
在算法中设置一个栈,每读入一个括号,若是右括号,则或者使置于栈顶的最急迫的期待得以消解,或者是不合法的情况;若是左括号,则作为一个新的更急迫的期待压入栈中,自然使得原有的在栈中的所有未消解的期待的急迫性都降一级。另外,在算法开始和结束时,栈都应该是空的。
1 | function bracketsMatch(str) { var stack = new Stack(); var text = ''; for (var i = 0, len = str.length; i < len; i++) { var c = str[i]; if (c === '[') { stack.push(c); } else if (c === ']') { if (!stack.top || stack.pop() !== '[') throw new Error('unexpected brackets:' + c); } else { text += c; } } console.log(text); } console.log(bracketsMatch('[asd]')); |
示例3:行编辑
当用户发现刚刚键入的一个字符是错的时,可补进一个退格符“#”,以表示前一个字符无效;如果发现当前键入的行内差错较多或难以补进,则可以键入一个退行符“@”,以表示当前行中的字符均无效。 为此,可设这个输入缓冲区为一个栈结构,每当从终端接收了一个字符之后先做如下判断:
如果它既不是”#”也不是”@”,则将字符压入栈;
如果是”#”,则从栈顶删去一个字符;
如果是”@”,则清空栈。
1 | function LineEditor(str) { this.stack = new Stack(); this.str = str || '' } LineEditor.prototype = { getResult: function () { var stack = this.stack; var str = this.str; for (var i = 0, len = str.length; i < len; i++) { var c = str[i]; switch (c) { case '#': stack.pop(); break; case '@': stack.clear(); break; default: stack.push(c); break; } } var result = ''; var current = stack.top; while (current) { result = current.data + result; current = current.next; } return result; } }; var le = new LineEditor('whli##ilr#e(s#*s)\ \noutcha@putchar(*s=#++)'); console.log(le.getResult()); |
示例4:表达式求值
表达式求值是程序设计语言编译中的一个最基本问题、它的实现是栈应用的又一个典型例子。这里介绍一种简单直观,广为使用的算法,通常称为“运算符优先法”。
// from: http://wuzhiwei.net/ds_app_stack/
1 | var prioty = { "+": 1, "-": 1, "%": 2, "*": 2, "/": 2, "^": 3, "(": 0, ")": 0, "`": -1 }; function doop(op, opn1, opn2) { switch (op) { case "+": return opn1 + opn2; case "-": return opn1 - opn2; case "*": return opn1 * opn2; case "/": return opn1 / opn2; case "%": return opn1 % opn2; case "^": return Math.pow(opn1, opn2); default: return 0; } } function opcomp(a, b) { return prioty[a] - prioty[b]; } function calInfixExpression(exp) { var cs = []; var ns = []; exp = exp.replace(/\s/g, ""); exp += '`'; if (exp[0] === '-') { exp = "0" + exp; } var c; var op; var opn1; var opn2; for (var i = 0; i < exp.length; ++i) { c = exp[i]; // 如果是操作符 if (c in prioty) { // 如果右边不是左括号且操作符栈的栈顶元素优先权比右边大 // 循环遍历进行连续运算 while (c != '(' && cs.length && opcomp(cs[cs.length - 1], c) >= 0) { // 出栈的操作符 op = cs.pop(); // 如果不是左括号或者右括号,说明是运算符 if (op != '(' && op != ')') { // 出栈保存数字的栈的两个元素 opn2 = ns.pop(); opn1 = ns.pop(); // 将与操作符运算后的结果保存到栈顶 ns.push(doop(op, opn1, opn2)); } } // 如果右边不是右括号,保存到操作符栈中 if (c != ')') cs.push(c); } else { // 多位数的数字的情况 while (!(exp[i] in prioty)) { i++; c += exp[i]; } ns.push(parseFloat(c)); i--; } } return ns.length ? ns[0] : NaN; } var exp1 = calInfixExpression('5+3*4/2-2^3+5%2'); console.log(exp1); |
实际上有效的递归调用函数(或过程)应包括两部分:递推规则(方法),终止条件。
为保证递归调用正确执行,系统设立一个“递归工作栈”,作为整个递归调用过程期间使用的数据存储区。
每一层递归包含的信息如:参数、局部变量、上一层的返回地址构成一个“工作记录” 。每进入一层递归,就产生一个新的工作记录压入栈顶;每退出一层递归,就从栈顶弹出一个工作记录。
从被调函数返回调用函数的一般步骤:
1若栈为空,则执行正常返回。
2从栈顶弹出一个工作记录。
3将“工作记录”中的参数值、局部变量值赋给相应的变量;读取返回地址。
4将函数值赋给相应的变量。
5转移到返回地址。
1 队列的基本概念
队列(Queue):也是运算受限的线性表。是一种先进先出(First In First Out ,简称FIFO)的线性表。只允许在表的一端进行插入,而在另一端进行删除。
队首(front) :允许进行删除的一端称为队首。
队尾(rear) :允许进行插入的一端称为队尾。
例如:排队购物。操作系统中的作业排队。先进入队列的成员总是先离开队列。
队列中没有元素时称为空队列。在空队列中依次加入元素a1, a2, …, an之后,a1是队首元素,an是队尾元素。显然退出队列的次序也只能是a1, a2, …, an ,即队列的修改是依先进先出的原则进行的
利用一组连续的存储单元(一维数组) 依次存放从队首到队尾的各个元素,称为顺序队列。
队列的链式存储结构简称为链队列,它是限制仅在表头进行删除操作和表尾进行插入操作的单链表。
1 | function Queue() { this.rear = this.front = null; this.size = 0; } exports.Queue = Queue; Queue.prototype = { clear: function () { this.rear = this.front = null; this.size = 0; }, getHead: function () { return this.front ? this.front.data : null; }, enQueue: function (elem) { if (this.front === null) { this.rear = this.front = {data: elem, next: null}; } else { var p = {data: elem, next: null}; this.rear.next = p; this.rear = p; } this.size++; }, deQueue: function () { if (this.front) { var elem = this.front.data; this.front = this.front.next; if (this.front === null) { this.rear = null; } this.size--; return elem; } else { return null; } }, queueTraverse: function (iterator) { var current = this.front; while (current) { if (iterator(current.data)) break; current = current.next; } }, peekAt: function (index) { index = index || 0; if (index < this.size) { var current = this.front; for (var i = 0; i < index; i++) { current = current.next; } return current.data; } return null; }, displayAll: function () { if (this.front === null) { return null; } var arr = []; var current = this.front; for (var i = 0, len = this.size; i < len; i++) { arr[i] = current.data; current = current.next; } return arr; } }; var queue = new Queue(); queue.enQueue(1); queue.deQueue(); queue.enQueue(2); queue.enQueue(3); console.log(queue.peekAt(0)); console.log(queue.peekAt(1)); console.log(queue.peekAt(2)); console.log(queue.peekAt(3)); console.log(queue.displayAll().join()); |
循环队列
顺序队列中存在“假溢出”现象。因为在入队和出队操作中,头、尾指针只增加不减小,致使被删除元素的空间永远无法重新利用。因此,尽管队列中实际元素个数可能远远小于数组大小,但可能由于尾指针巳超出向量空间的上界而不能做入队操作。该现象称为假溢出。
为充分利用向量空间,克服上述“假溢出”现象的方法是:将为队列分配的向量空间看成为一个首尾相接的圆环,并称这种队列为循环队列(Circular Queue)。
在循环队列中进行出队、入队操作时,队首、队尾指针仍要加1,朝前移动。只不过当队首、队尾指针指向向量上界(MAX_QUEUE_SIZE-1)时,其加1操作的结果是指向向量的下界0。
用模运算可简化为:i=(i+1)%MAX_QUEUE_SIZE ;
显然,为循环队列所分配的空间可以被充分利用,除非向量空间真的被队列元素全部占用,否则不会上溢。因此,真正实用的顺序队列是循环队列。
1 | // 循环队列 function CycleQueue() { this.base = {}; this.front = this.rear = 0; this.MAXQSIZE = 100; } exports.CycleQueue = CycleQueue; CycleQueue.prototype = { enQueue: function (data) { if ((this.rear + 1) % this.MAXQSIZE === 0) throw new Error('cycleQueue is already full!'); this.base[this.rear] = data; this.rear = (this.rear + 1) % this.MAXQSIZE; }, deQueue: function () { if (this.front === this.rear) throw new Error('cycleQueue is already empty'); var elem = this.base[this.front]; this.front = (this.front + 1) % this.MAXQSIZE; return elem; }, clear: function () { this.base = {}; this.front = this.rear = 0; }, size: function () { return (this.rear - this.front + this.MAXQSIZE) % this.MAXQSIZE; }, peekAt: function (index) { index = (index || 0 + this.MAXQSIZE) % this.MAXQSIZE; return this.base[index + this.front] || null; }, getHead: function () { var elem = this.base[this.front]; return elem ? elem : null; }, queueTraverse: function (iterator) { for (var i = this.front, len = this.rear = this.front; i < len; i++) { if (iterator(this.base[i], i)) break; } }, displayAll: function () { var base = [].slice.call(this.base); return base.slice(this.front, this.rear - this.front); } }; var queue = new CycleQueue(); queue.enQueue(1); queue.deQueue(); queue.enQueue(2); queue.enQueue(3); console.log(queue.peekAt(0)); console.log(queue.peekAt(1)); console.log(queue.peekAt(2)); |
1 | var List = require('../linkedList/complete-LinkedList'); var Queue = require('./Queue').Queue; // 事件类型,有序链表LinkList的数据元素类型 function Event(occurTime, eventType) { // 事件发生时刻 this.occurTime = occurTime || 0; // 事件类型,0表示到达事件,1至4表示四个窗口的离开事件 this.eventType = eventType || 0; } // 队列的数据元素类型 function QueueElemType(arrivalTime, duration) { // 到达时刻 this.arrivalTime = arrivalTime || 0; // 办理事务所需时间 this.duration = duration || 0; } function Bank() { // 事件表 this.eventList = null; this.event = null; // 4个客户队列 this.queues = new Array(4); this.totalTime = 0; this.customerNum = 0; this.closeTime = 0; } Bank.prototype = { cmp: function (event1, event2) { if (event1.occurTime < event2.occurTime) return -1; else if (event1.occurTime === event2.occurTime) return 0; else return 1; }, // 初始化操作 openForDay: function () { // 初始化累计时间和客户数为0 this.totalTime = 0; this.customerNum = 0; // 初始化事件链表 this.eventList = new List(); // 设定第一个用户到达事件 this.event = new Event(0, 0); // 插入到事件表 this.eventList.orderInsert(this.event, this.cmp); // 置空队列 for (var i = 0, len = this.queues.length; i < len; i++) this.queues[i] = new Queue(); }, // 处理客户到达事件 customerArrived: function (durtime, intertime) { ++this.customerNum; // 生成随机数 durtime = durtime || Math.floor(Math.random() * 20) + 1; // 办理业务所需时间 intertime = intertime || Math.floor(Math.random() * 5) + 1; // 两个相邻客户时间间隔 // 下一客户到达时刻 var t = this.event.occurTime + intertime; // 银行尚未关门,插入事件表,这里还包括客户的离开时间 if (t < this.closeTime && t + durtime < this.closeTime) { this.eventList.orderInsert(new Event(t, 0), this.cmp); } // 求长度最短队列 var minQueueIndex = 0; var allEqualed = false; for(var i = 0, len = this.queues.length; i < len && this.queues[i + 1]; i++){ if(this.queues[i].size === 0) { minQueueIndex = i; break; } if(this.queues[i].size < this.queues[i + 1].size){ minQueueIndex = i; allEqualed = false; } else if(this.queues[i].size < this.queues[i + 1].size){ minQueueIndex = i; allEqualed = true; } else { minQueueIndex = i + 1; allEqualed = false; } } // 如果所有队列长度都相等,取第一个 if(allEqualed) minQueueIndex = 0; this.queues[minQueueIndex] .enQueue(new QueueElemType(this.event.occurTime, durtime)); // 设定第i队列的一个离开事件并插入事件表 if (this.queues[minQueueIndex].size === 1) { this.eventList.orderInsert(new Event(this.event.occurTime + durtime, minQueueIndex + 1), this.cmp); } // 保存最新客户的到达时间 this.event.occurTime = t; }, // 处理客户离开事件 customerDeparture: function (type) { // 删除第i队列的排头客户 var i = type - 1 || 0; var customer = this.queues[i].deQueue(); // 累计客户逗留时间 this.totalTime += this.event.occurTime - customer.arrivalTime; // 设定第i队列的一个离开事件并插入事件表 if (this.queues[i].size) { customer = this.queues[i].getHead(); this.eventList.orderInsert(new Event(this.event.occurTime + customer.duration, i), this.cmp); } }, simulation: function (closeTime) { this.closeTime = closeTime || 0; this.openForDay(); while (this.eventList.head) { var elem = this.eventList.delFirst().data; if (elem.eventType === 0) this.customerArrived(); else this.customerDeparture(elem.eventType); } console.log('The average time is ' + this.totalTime / this.customerNum); } }; new Bank().simulation(20); |