栈(stack)又名堆栈,它是一种运算受限的线性表。其限制是仅允许在表的一端进行插入和删除运算。这一端被称为栈顶,相对地,把另一端称为栈底。

一、实现一个栈类Stack

基于堆栈的特性,可以用数组做线性表进行存储。

初始化Stack类的结构如下:

function Stack(){
this.space = [];
} Stack.prototype = {
constructor: Stack,
/* 接口code */
};

接下来,就是在原型上,对入栈出栈清空栈读取栈顶读取整个栈数据这几个接口的实现。

Stack类默认以数组头部做栈底,尾部做栈顶。

1.1 入栈 push

入栈可以利用js数组的push方法,在数组尾部压入数据。

Stack.prototype = {
push: function(value){
return this.space.push(value);
}
}

1.2 出栈 pop

出栈同样是利用js数组的pop方法,在数组尾部推出数据。

Stack.prototype = {
pop: function(){
return this.space.pop();
}
}

1.3 清空栈 clear

清空栈相对简单,将存储数据的数组重置为空数组即可。

Stack.prototype = {
clear: function(){
this.space = [];
}
}

1.4 读取栈顶readTop

读取栈顶数据,采用数组下标的方式进行获取。带来的一个好处就是:下标超出数组有效范围时,返回值为undefined

Stack.prototype = {
readTop: function(){
return this.space[this.space.length - 1];
}
}

1.4 读取整个栈read

读取整个栈数据,直接返回当前数组即可。

Stack.prototype = {
read: function(){
return this.space;
}
}

1.5 聚合

最后,将所有功能聚合后,如下所示,一个堆栈的数据结构就搞定了。

function Stack(){
this.space = [];
} Stack.prototype = {
constructor: Stack,
push: function(value){
return this.space.push(value);
},
pop: function(){
return this.space.pop();
},
clear: function(){
this.space = [];
},
readTop: function(){
return this.space[this.space.length - 1];
},
read: function(){
return this.space;
}
};

二、实战

学数据结构和算法是为了更好、更高效率地解决工程问题。

这里学以致用,提供了几个真实的案例,来体会下数据结构和算法的魅力:)

2.1 数组reverse的实现

当前案例,将用堆栈来实现数组的反转功能。

function reverse(arr){
var ArrStack = new Stack(); for(var i = arr.length - 1; i >= 0; i--){
ArrStack.push(arr[i]);
} return ArrStack.read();
}

如代码所示,可分为以下几个步骤:

  • 实例化一个堆栈用于存储数据
  • 将传入的数组进行倒序遍历,并逐个压入堆栈
  • 最后使用read接口,输出数据

好像很简单,不用担心,复杂的在后面:)

2.2 十进制转换为二进制

数值转换进制的问题,是堆栈的小试牛刀。

讲解转换方法前,先来看一个小例子:

将十进制的13转换成二进制

    2 | 13      1
 ̄ ̄ ̄
2 | 6 0
 ̄ ̄ ̄
2 | 3 1
 ̄ ̄ ̄ ̄
1 1

如上所示:13的二进制码为1101

将手工换算,变成堆栈存储,只需将对2取余的结果依次压入堆栈保存,最后反转输出即可。

function binary(number){
var tmp = number;
var ArrStack = new Stack(); if(number === 0){
return 0;
} while(tmp){
ArrStack.push(tmp % 2);
tmp = parseInt(tmp / 2, 10);
} return reverse(ArrStack.read()).join('');
} binary(14); // 输出=> "1110"
binary(1024); // 输出=> "10000000000"

2.3 表达式求值

这个案例,其实可以理解为简化版的eval方法。

案例内容是对1+7*(4-2)的求值。

进入主题前,有必要先了解以下的数学理论:

  1. 中缀表示法(或中缀记法)是一个通用的算术或逻辑公式表示方法, 操作符是以中缀形式处于操作数的中间(例:3 + 4)。
  2. 逆波兰表示法(Reverse Polish notation,RPN,或逆波兰记法),是一种是由波兰数学家扬·武卡谢维奇1920年引入的数学表达式方式,在逆波兰记法中,所有操作符置于操作数的后面,因此也被称为后缀表示法。逆波兰记法不需要括号来标识操作符的优先级。

    常规中缀记法的“3 - 4 + 5”在逆波兰记法中写作“3 4 - 5 +”
  3. 调度场算法(Shunting Yard Algorithm)是一个用于将中缀表达式转换为后缀表达式的经典算法,由艾兹格·迪杰斯特拉引入,因其操作类似于火车编组场而得名。

提前说明,这只是简单版实现。所以规定有两个:

  1. 数字要求为整数
  2. 不允许表达式中出现多余的空格

实现代码如下:

function calculate(exp){
var valueStack = new Stack(); // 数值栈
var operatorStack = new Stack(); // 操作符栈
var expArr = exp.split(''); // 切割字符串表达式
var FIRST_OPERATOR = ['+', '-']; // 加减运算符
var SECOND_OPERATOR = ['*', '/']; // 乘除运算符
var SPECIAL_OPERATOR = ['(', ')']; // 括号
var tmp; // 临时存储当前处理的字符
var tmpOperator; // 临时存储当前的运算符 // 遍历表达式
for(var i = 0, len = expArr.length; i < len; i++){
tmp = expArr[i];
switch(tmp){
case '(':
operatorStack.push(tmp);
break;
case ')':
// 遇到右括号,先出栈括号内数据
while( (tmpOperator = operatorStack.pop()) !== '(' &&
typeof tmpOperator !== 'undefined' ){
valueStack.push(calculator(tmpOperator, valueStack.pop(), valueStack.pop()));
}
break;
case '+':
case '-':
while( typeof operatorStack.readTop() !== 'undefined' &&
SPECIAL_OPERATOR.indexOf(operatorStack.readTop()) === -1 &&
(SECOND_OPERATOR.indexOf(operatorStack.readTop()) !== -1 || tmp != operatorStack.readTop()) ){
// 栈顶为乘除或相同优先级运算,先出栈
valueStack.push(calculator(operatorStack.pop(), valueStack.pop(), valueStack.pop()));
}
operatorStack.push(tmp);
break;
case '*':
case '/':
while( typeof operatorStack.readTop() != 'undefined' &&
FIRST_OPERATOR.indexOf(operatorStack.readTop()) === -1 &&
SPECIAL_OPERATOR.indexOf(operatorStack.readTop()) === -1 &&
tmp != operatorStack.readTop()){
// 栈顶为相同优先级运算,先出栈
valueStack.push(calculator(operatorStack.pop(), valueStack.pop(), valueStack.pop()));
}
operatorStack.push(tmp);
break;
default:
valueStack.push(tmp);
}
} // 处理栈内数据
while( typeof (tmpOperator = operatorStack.pop()) !== 'undefined' ){
valueStack.push(calculator(tmpOperator, valueStack.pop(), valueStack.pop()));
} return valueStack.pop(); // 将计算结果推出 /*
@param operator 操作符
@param initiativeNum 主动值
@param passivityNum 被动值
*/
function calculator(operator, passivityNum, initiativeNum){
var result = 0; initiativeNum = typeof initiativeNum === 'undefined' ? 0 : parseInt(initiativeNum, 10);
passivityNum = typeof passivityNum === 'undefined' ? 0 : parseInt(passivityNum, 10); switch(operator){
case '+':
result = initiativeNum + passivityNum;
console.log(`${initiativeNum} + ${passivityNum} = ${result}`);
break;
case '-':
result = initiativeNum - passivityNum;
console.log(`${initiativeNum} - ${passivityNum} = ${result}`);
break;
case '*':
result = initiativeNum * passivityNum;
console.log(`${initiativeNum} * ${passivityNum} = ${result}`);
break;
case '/':
result = initiativeNum / passivityNum;
console.log(`${initiativeNum} / ${passivityNum} = ${result}`);
break;
default:;
} return result;
}
}

实现思路:

  1. 采用调度场算法,对中缀表达式进行读取,对结果进行合理运算。
  2. 临界点采用operatorStack.readTop() !== 'undefined'进行判定。有些书采用#做结束标志,个人觉得有点累赘。
  3. 将字符串表达式用split进行拆分,然后进行遍历读取,压入堆栈。有提前要计算结果的,进行对应的出栈处理。
  4. 将计算部分结果的方法,封装为独立的方法calculator。由于乘除运算符前后的数字,在运算上有区别,所以不能随意调换位置。

2.4 中缀表达式转换为后缀表达式(逆波兰表示法)

逆波兰表示法,是一种对计算机友好的表示法,不需要使用括号。

下面案例,是对上一个案例的变通,也是用调度场算法,将中缀表达式转换为后缀表达式。

function rpn(exp){
var valueStack = new Stack(); // 数值栈
var operatorStack = new Stack(); // 操作符栈
var expArr = exp.split('');
var FIRST_OPERATOR = ['+', '-'];
var SECOND_OPERATOR = ['*', '/'];
var SPECIAL_OPERATOR = ['(', ')'];
var tmp;
var tmpOperator; for(var i = 0, len = expArr.length; i < len; i++){
tmp = expArr[i];
switch(tmp){
case '(':
operatorStack.push(tmp);
break;
case ')':
// 遇到右括号,先出栈括号内数据
while( (tmpOperator = operatorStack.pop()) !== '(' &&
typeof tmpOperator !== 'undefined' ){
valueStack.push(translate(tmpOperator, valueStack.pop(), valueStack.pop()));
}
break;
case '+':
case '-':
while( typeof operatorStack.readTop() !== 'undefined' &&
SPECIAL_OPERATOR.indexOf(operatorStack.readTop()) === -1 &&
(SECOND_OPERATOR.indexOf(operatorStack.readTop()) !== -1 || tmp != operatorStack.readTop()) ){
// 栈顶为乘除或相同优先级运算,先出栈
valueStack.push(translate(operatorStack.pop(), valueStack.pop(), valueStack.pop()));
}
operatorStack.push(tmp);
break;
case '*':
case '/':
while( typeof operatorStack.readTop() != 'undefined' &&
FIRST_OPERATOR.indexOf(operatorStack.readTop()) === -1 &&
SPECIAL_OPERATOR.indexOf(operatorStack.readTop()) === -1 &&
tmp != operatorStack.readTop()){
// 栈顶为相同优先级运算,先出栈
valueStack.push(translate(operatorStack.pop(), valueStack.pop(), valueStack.pop()));
}
operatorStack.push(tmp);
break;
default:
valueStack.push(tmp);
}
} while( typeof (tmpOperator = operatorStack.pop()) !== 'undefined' ){
valueStack.push(translate(tmpOperator, valueStack.pop(), valueStack.pop()));
} return valueStack.pop(); // 将计算结果推出 /*
@param operator 操作符
@param initiativeNum 主动值
@param passivityNum 被动值
*/
function translate(operator, passivityNum, initiativeNum){
var result = ''; switch(operator){
case '+':
result = `${initiativeNum} ${passivityNum} +`;
console.log(`${initiativeNum} + ${passivityNum} = ${result}`);
break;
case '-':
result = `${initiativeNum} ${passivityNum} -`;
console.log(`${initiativeNum} - ${passivityNum} = ${result}`);
break;
case '*':
result = `${initiativeNum} ${passivityNum} *`;
console.log(`${initiativeNum} * ${passivityNum} = ${result}`);
break;
case '/':
result = `${initiativeNum} ${passivityNum} /`;
console.log(`${initiativeNum} / ${passivityNum} = ${result}`);
break;
default:;
} return result;
}
} rpn('1+7*(4-2)'); // 输出=> "1 7 4 2 - * +"

2.5 汉诺塔

汉诺塔(港台:河内塔)是根据一个传说形成的数学问题:

有三根杆子A,B,C。A杆上有 N 个 (N>1) 穿孔圆盘,盘的尺寸由下到上依次变小。要求按下列规则将所有圆盘移至 C 杆:

  1. 每次只能移动一个圆盘;
  2. 大盘不能叠在小盘上面。

堆栈的经典算法应用,首推就是汉诺塔

理解该算法,要注意以下几点:

  1. 不要深究每次的移动,要抽象理解
  2. 第一步:所有不符合要求的盘,从A塔统一移到B塔缓存
  3. 第二步:将符合的盘移动到C塔
  4. 第三步:把B塔缓存的盘全部移动到C塔

以下是代码实现:

var ATower = new Stack(); // A塔
var BTower = new Stack(); // B塔
var CTower = new Stack(); // C塔 (目标塔)
var TIER = 4; // 层数 for(var i = TIER; i > 0; i--){
ATower.push(i);
} function Hanoi(n, from, to, buffer){
if(n > 0){
Hanoi(n - 1, from, buffer, to); // 所有不符合要求的盘(n-1),从A塔统一移到B塔缓存
to.push(from.pop()); // 将符合的盘(n)移动到C塔
Hanoi(n - 1, buffer, to, from); // 把B塔缓存的盘全部移动到C塔
}
} Hanoi(ATower.read().length, ATower, CTower, BTower);

汉诺塔的重点,还是靠递归去实现。把一个大问题,通过递归,不断分拆为更小的问题。然后,集中精力解决小问题即可。

三、小结

不知不觉,写得有点多ORZ。

后面章节的参考链接,还是推荐看看。也许配合本文,你会有更深的理解。

参考

[1] 中缀表示法

[2] 后缀表示法

[3] 调度场算法

[4] 汉诺塔

堆栈的应用——用JavaScript描述数据结构的更多相关文章

  1. 《数据结构与算法JavaScript描述》

    <数据结构与算法JavaScript描述> 基本信息 作者: (美)Michael McMillan 译者: 王群锋 杜欢 丛书名: 图灵程序设计丛书 出版社:人民邮电出版社 ISBN:9 ...

  2. 翻阅《数据结构与算法javascript描述》--数组篇

    导读: 这篇文章比较长,介绍了数组常见的操作方法以及一些注意事项,最后还有几道经典的练习题(面试题). 数组的定义: JavaScript 中的数组是一种特殊的对象,用来表示偏移量的索引是该对象的属性 ...

  3. 数据结构与算法javascript描述

    <数据结构与算法javascript描述>--数组篇 导读: 这篇文章比较长,介绍了数组常见的操作方法以及一些注意事项,最后还有几道经典的练习题(面试题). 数组的定义: JavaScri ...

  4. 列表的实现-----数据结构与算法JavaScript描述 第三章

    实现一个列表 script var booklist = new List(); booklist.append('jsbook'); booklist.append('cssbook'); book ...

  5. 《数据结构与算法JavaScript描述》中的一处错误

    最近在看<数据结构与算法JavaScript描述>这本书,看到选择排序这部分时,发现一个比较大的错误. 原书的选择排序算法是这样的: function selectionSort() { ...

  6. 数据结构与算法 Javascript描述

    数据结构与算法系列主要记录<数据结构与算法 Javascript描述>学习心得

  7. 十大经典排序算法总结(JavaScript描述)

    前言 读者自行尝试可以想看源码戳这,博主在github建了个库,读者可以Clone下来本地尝试.此博文配合源码体验更棒哦~~~ 个人博客:Damonare的个人博客 原文地址:十大经典算法总结 这世界 ...

  8. javascript实现数据结构与算法系列:栈 -- 顺序存储表示和链式表示及示例

    栈(Stack)是限定仅在表尾进行插入或删除操作的线性表.表尾为栈顶(top),表头为栈底(bottom),不含元素的空表为空栈. 栈又称为后进先出(last in first out)的线性表. 堆 ...

  9. javascript实现数据结构:广义表

    原文:javascript实现数据结构:广义表  广义表是线性表的推广.广泛用于人工智能的表处理语言Lisp,把广义表作为基本的数据结构. 广义表一般记作: LS = (a1, a2, ..., an ...

随机推荐

  1. git无法提交问题

    今天在git提交时遇到一个问题,如下图 根据它的提示手动删除index.lock文件就可以了. 那为什么会有这个index.lock文件呢?我就觉得是莫名其妙出现的,别人的解释是这样的: 在你进行某些 ...

  2. 在 Windows 上可以用 Docker 吗?

    作者:陈计节 个人博客:https://blog.jijiechen.com/post/docker-on-windows/ Docker,或者准确一点说,容器技术,在近几年里几乎成为了应用分发和集群 ...

  3. MFC原理第一讲.MFC的本质.以及手工编写MFC的程序

    MFC原理第一讲.MFC的本质.以及手工编写MFC的程序 PS: 这个博客属于复习知识.从头开始讲解. 在写这篇博客之前.已经写了3篇MFC的本质了.不过掌握知识点太多.所以从简重新开始. 一丶MFC ...

  4. 【杂谈】Remember-Me的实现

    前言 此篇随笔记录了Remember-Me实现过程中出现的问题和解决方案,以及相关的思考. 正文 1. RememberMe是什么? RememberMe意为记住我,对应登录界面的那个勾选项.另一种说 ...

  5. JS 事件循环机制 - 任务队列、web API、JS主线程的相互协同

    一.JS单线程.异步.同步概念 从上一篇说明vue nextTick的文章中,多次出现“事件循环”这个名词,简单说明了事件循环的步骤,以便理解nextTick的运行时机,这篇文章将更为详细的分析下事件 ...

  6. Java反射,注解,以及动态代理

    Java反射,注解,以及动态代理 基础  最近在准备实习面试,被学长问到了Java反射,注解和动态代理的内容,发现有点自己有点懵,这几天查了很多资料,就来说下自己的理解吧[如有错误,望指正] Java ...

  7. vue_drf之实现短信验证码

    一.需求 1,需求 我们在做网站开发时,登录页面很多情况下是可以用手机号接收短信验证码,然后实现登录的,那我们今天就来做一做这一功能. 伪代码: 进入登录页面,点击短信登录 输入手机号码,点击获取验证 ...

  8. 好好耕耘 redis和memcached的区别

    观点一: 1.Redis和Memcache都是将数据存放在内存中,都是内存数据库.不过memcache还可用于缓存其他东西,例如图片.视频等等: 2.Redis不仅仅支持简单的k/v类型的数据,同时还 ...

  9. 再谈 C# 对象二进制序列化,序列化并进行 AES 加密

    对象的二进制序列化非常有用,也非常方便. 我们可以把对象序列化为字节数组,也可以把对象序列化到文件,还可以把对象序列化到文件并进行加密. 先引用这些命名空间: using System.IO;usin ...

  10. Java高并发 -- 线程池

    Java高并发 -- 线程池 主要是学习慕课网实战视频<Java并发编程入门与高并发面试>的笔记 在使用线程池后,创建线程变成了从线程池里获得空闲线程,关闭线程变成了将线程归坏给线程池. ...