这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战
栈
栈是一种特殊的线性表,仅能够在栈顶进行操作,有着先进后出、后进先出的特性
实现
class Stack {
#stackArr = [] // 私有属性
constructor() {
// 不建议直接在此定义stackArr, 因为stackArr只能通过暴露的方法进行操作, 不能让实例可以直接操作stackArr
// this.stackArr = []
}
// push 添加一个元素到栈顶
push(item) {
this.stackArr.push(item)
}
// pop 弹出栈顶元素
pop() {
return this.stackArr.pop()
}
// top 返回栈顶元素, 取值操作
top() {
return this.stackArr[this.stackArr.length - 1]
}
// isEmpty 判断栈是否为空
isEmpty() {
return this.stackArr.length === 0
}
// size 返回栈里面元素个数
size() {
return this.stackArr.length
}
// clear 清空栈 重置
clear() {
this.stackArr = []
}
}
栈的实现其实就是基于数组的封装,既然已经有了数组,为何还需要再封装一层实现栈呢?
- 数组提供了很多的操作方式,可以很方便的对于数据进行任意操作,其实是不可控的,基于数据封装一层栈结构,可以很好限制对数据的任意操作,只能遵从栈的操作特性(先进后出,后进先出),为实际业务开发中提供一种思考问题的方式
- 基于数组封装栈,是为了隐藏是实现细节,当我们遇到业务和开发问题时,站在栈的肩膀思考问题会来的更方便一些,而不是使用数组一把梭
应用
判断括号合法
let str1 = 'a(daj(fl(ks)aj)fl)kas' // 合法
let str2 = 'as(dj(flkajs)d(fsaj)fs)' // 合法
let str3 = 'as(dfl)ka(sf)j(skd)f)' // 不合法, 缺少
let str4 = 'asd(kl)alk)s(df' // 不合法 顺序不对
思路
循环字符串,针对不同类型的字符串做不同的操作
遇到左括号 - 压栈
遇到右括号 - 判断栈是否为空,若是为空则说明没有对应的左括号,不合法,栈不为空则弹栈,抵消掉一对括号
其他类型 - 跳过
当遍历完之后,若是栈为空, 则合法,否则就是不合法的
const STR1 = 'a(daj(fl(ks)aj)fl)kas' // 合法
const STR2 = 'as(dj(flkajs)d(fsaj)fs)' // 合法
const STR3 = 'as(dfl)ka(sf)j(skd)f)' // 不合法, 缺少
const STR4 = 'asd(kl)alk)s(df' // 不合法 顺序不对
/**
* 判断字符串中的括号是否合法
*/
const isLegalBracket = str => {
if (Object.prototype.toString.call(str) !== "[object String]") {
// throw new Error('str is must string');
return false;
}
const STACK = new Stack()
for (let i = 0; i < str.length; i++) {
const ELE = str[i]
if (ELE === '(') { // 压栈
STACK.push(ELE)
}else if(ELE === ')') {
if (STACK.isEmpty()) { // 栈为空则说明没有对应的左括号,不合法
return false
} else { // 弹栈
STACK.pop()
}
}
}
return STACK.isEmpty()
}
console.log(isLegalBracket(STR1)) // true
console.log(isLegalBracket(STR2)) // true
console.log(isLegalBracket(STR3)) // false
console.log(isLegalBracket(STR4)) // false
计算逆波兰表达式
用途
逆波兰表达式是一种十分有用的表达式,它将复杂表达式转换为可以依靠简单的操作得到计算结果的表达式。例如(a+b)(c+d)转换为ab+cd+
优势
它的优势在于只用两种简单操作,入栈和出栈就可以搞定任何普通表达式的运算。其运算方式如下:
如果当前字符为变量或者为数字,则压栈,如果是运算符,则将栈顶两个元素弹出作相应运算,结果再入栈,最后当表达式扫描完后,栈里的就是结果。
参照表
| 正常的表达式(中缀表达式) | 逆波兰表达式(后缀表达式) |
|---|---|
| a + b | [a, b, +] |
| a + (b - c) | [a, b, c, -, +] |
| a + (b - c) * d | [a, b, c, -, *, +] |
| a * (b + c) + d | [a, b, c, +, *, d, +] |
计算 ['1', '2', '3', '+', '*', '4', '+']等价于计算1 * (2 + 3) + 4
解题思路:
将数组依次遍历,当遇到非运算符时,将数据进行压栈处理,将当遇到预算符时,从栈中连续弹栈两次进行然后与运算符结合计算,并且将结果压栈,当循环结束之后,栈中只剩下一个数据,便是结果
const ARR = ['1', '2', '3', '+', '*', '4', '+']
const reversePolishNotation = arr => {
// 容错处理
if(!Array.isArray(arr)) {
throw new Error('arguments is must array');
}
const STACK = new Stack()
const OPERATORS = ['+', '-', '*', '/', '']
for (let i = 0; i < arr.length; i++) {
const ELE = arr[i]
if(OPERATORS.includes(ELE)){ // 操作符
// 1. 连续弹栈两次
const A = STACK.pop()
const B = STACK.pop()
// 2. 将数据与运算符拼接
const STR = B + ELE + A
// 3. 执行运算并将结果压栈
const RES = parseInt(eval(STR)).toString()
STACK.push(RES)
}else { // 非操作符 直接压栈
STACK.push(ELE)
}
}
return STACK.top()
}
console.log(reversePolishNotation(ARR)); // 9
队列
队列是一种特殊的线性表,它只允许再队列的头部删除元素,在队列的尾部添加新的元素,特性先进先出,后进后出
实现
class Queue {
constructor() {
this.queue = []
}
// 队列尾部添加元素
enqueue(queue) {
this.queue.push(queue)
}
// 队列头部删除元素
dequeue() {
return this.queue.shift()
}
// 查看队列头部元素 不操作队列 仅仅查看
header() {
return this.queue[0]
}
// 查看队列尾部元素 不操作队列 仅仅查看
tail() {
return this.queue[this.queue.length - 1]
}
// 返回队列大小
size() {
return this.queue.length
}
// 清楚队列
clear() {
this.queue.length = []
}
// 是否为空队列
isEmpty() {
return this.queue.length === 0
}
}
应用
约瑟夫环
有一个数组存放0-99的数字,要求每隔两个数删掉一个数,到末尾时循环至开头继续进行,求最后一个删除的数
若是 0-4
第一轮删除之后剩余 [0,1,3,4], 结束本轮,此时到末尾时数到2,下一轮开始为3
第二轮删除之后剩余 [1,3], 结束本轮,此时到末尾时数到3,刚好删除最后一位4, 下一轮开始为0
第三轮无删除操作,结束本轮,此时到末尾时数到2,下一轮开始为3, 但是数据本身不变
第四轮删除之后剩余[3], 此时只剩下最后一位数,便是最后一个需要删除的数
递归思想:
/**
* [约瑟夫环问题]
* @param {Number} count [累加器]
* @param {[type]} arr [数组]
* @return {[type]} [返回最后一个删除的数值]
*/
const josephus2 = (count = 0, arr) => {
if(arr.length === 1) { // 递归的结束条件, 当只剩余1个时,即为最后一个删除的数
return arr[0]
}
// 利用递归
for (var i = 0; i < arr.length; i++) {
count += 1
if(count % 3 === 0) {
arr.splice(i, 1)
i -= 1 // 每一次删除之后,必须要重置i的值,不然arr本身是被修改过的,会导致arr.length变化
}
}
return josephus2(count, arr)
}
console.log(josephus2(0, ARR))
队列思路:
/**
* 生成一个序列数组
* @param {[type]} start [开始, 包含开始]
* @param {[type]} end [结束, 不包含结束]
* @return {[type]} [生成的升序数组]
*/
const GENERATOR_NUMS = (start, end) => {
let RES = []
for (let i = start; i < end; i++) {
RES.push(i)
}
return RES
}
const ARR = GENERATOR_NUMS(0, 100)
/**
* 约瑟夫环问题
* @param {[type]} arr 数组
* @return {[type]} 返回最后一个删除的数值
*/
const josephus = arr => {
const QUEUE = new Queue()
// 1. 将数组arr的数据依次添加至队列中
arr.forEach(num => QUEUE.enqueue(num))
// 2. 定义一个变量,用于控制数据的删除
let index = 0
// 3. 使用while循环, 当 i % 3 === 0的时候时需要删除,否则就从新添加队列尾部
while(QUEUE.size() !== 1) {
const ITEM = QUEUE.dequeue()
index += 1
if(index % 3 != 0){ // 不满足 添加至队列尾部
QUEUE.enqueue(ITEM)
}
}
return QUEUE.header()
}
console.log(josephus(ARR))