程序猿打野基本功之数据结构:栈

371 阅读6分钟

温故知新,程序猿打野基本功之数据结构~

开局一张图,其余全靠编,此篇的核心话题即是:栈(Stack)。image-20220227122643950.png

基础

定义image-20220227124746769.png

栈(Stack)又称为堆栈,一种线性表,只允许在一端执行操作,操作特征是后进先出(LIFO,Last In First Out),参考下方的弹匣。

注意是弹匣,不是弹夹,更要胡思乱想考虑左轮。

image-20220227123608706.png 栈顶:线性表允许操作的一端。

栈底:线性表的另一端,不允许操作的、固定的一端。

空栈:不含任何元素。

注意不要与堆混淆。

一般而言,定义一个栈需要有一个初始的大小,这就是栈的初始容量。当需要放入的元素大于这个容量时,就需要进行扩容。

特征

后进先出,只能一端操作。

基本操作方法

  • push:添加一个元素到栈顶(向弹匣压入子弹)
  • pop:弹出栈顶元素(弹匣弹出一个子弹)
  • top:返回栈顶元素(瞅一眼弹匣是否还有子弹。注意:是瞅一眼,不是弹出)
  • size:返回栈内元素个数(数数还有多少子弹)
  • clear:清空栈(梭哈,清空)
  • isEmpty:栈是否为空(确认一下,是否清空弹匣)

实现

基于存储空间的连续性分类,其实现的方式分为:基于数组、基于链表。其各自的特点如头图所示。

image-20220227125705072.png 顺序存储结构(基于数组):利用一组地址连续的存储单元存放自栈底至栈顶的元素,同时设有一个指针用于指示当前栈顶元素的位置。可分为顺序栈、共享栈。

顺序栈:顺序存储的栈称为顺序栈,是利用一组地址连续的存储单元存放自栈底到栈顶的数据元素,同时附设一个指针(top)指示当前栈顶的位置。

共享栈:利用栈底位置相对不变的特性,可以让两个顺序栈共享一个一维数据空间,将两个栈的栈底分别设置在共享空间的两端,两个栈顶向共享空间的中间延伸。

  • 基于数组自定义基础栈
//Stack.js
//基于数组
 class MyStack {
    constructor() {
        this.stack = [];
    }
    // 压栈/入栈
    push(ele) {
        this.stack.push(ele);
        // return this.stack.length;//不需要有返回值。
    }
    // 退栈/出栈
    pop() {
        return this.stack.pop();
    }
    // 返回栈的长度
    size() {
        return this.stack.length;
    }
    // 判断栈是否为空
    isEmpty() {
        return this.stack.length === 0;
    }
    // 清空栈
    clear() {
        this.stack = [];
    }
    // 返回栈顶元素
    top() {
        return this.stack[this.stack.length - 1];
    }
}
​
exports.Stack = MyStack;

image-20220227125800475.png链式存储结构(基于链表):采用链式存储的栈称为链式栈,优点为多栈共享存储空间和提高效率,且不存在栈满上溢问题。通常采用单链表形式,并规定操作都是在单链表的表头进行。

基于链表的实现待后续链表部分实现

性能

栈的主要操作是入栈push和出栈pop,因此主要总结出入栈的性能

基于数组的栈是在数组尾部进行入栈和出栈操作,因此复杂度为O(1);

基于链表的栈是在链表头部进行入栈和出栈操作,因此复杂度也为O(1);

当操作次数较少时,两种方式实现的栈的性能差别不大。当数据量庞大时,因基于链表的栈需要进行new操作,故相较于基于数组(有初始化容量的栈)的实现是存在一定的性能劣势的。

场景案例

实现成对符号的匹配

//判断字符串中的括号是否匹配合法。
//利用栈,但是这种方法的实现并没有严格的沿用栈的格式,仅供参考思路。
const Stack = require('./Stack.js')
function is_leagal_breaks(string) {
    let i = 0, stacks = new Stack.Stack();
    while (i < string.length) {
        let ele = string[i];
        // 遇到左括号,入栈
        if (ele === '(') {
            stacks.push(ele);
        } else if (ele === ')') {//遇到右括号判断stacks是否为空
            if (!stacks.isEmpty()) {
                stacks.pop();
            } else {
                return false;//说明右括号多
            }
        }
        i++;
    }
    //左右括号匹配或者左括号多
    return stacks.isEmpty();
}
​
//test
let { log } = console;
let str1 = '(3(1)4)f(5)(6)(7)d)d';
let str2 = 'q(w)r(r)t(flak(jhs((diuhivb)zk)xlhuidsf)kjb)';
let str3 = 'e(e)t'
log(is_leagal_breaks(str1))
log(is_leagal_breaks(str2))
log(is_leagal_breaks(str3))
//更多实战--->文档编辑的撤销与恢复。

基于逆波兰表达式实现简单的计算器(仅支持整数型计算)。

通常情况下,计算器计算普通表达式时要采用递归的方式进行优先级的判断,对于复杂表达式处理则会效率低下甚至崩溃,而逆波兰表达式则只需要简单的出入栈操作就可以完成任意普通表达式的计算。

整体思路:中缀表达式转化为后缀表达式,基于后缀表达式进行计算。 重点在于中缀表达式转化为后缀表达式。

中缀表达式与后缀表达式其简单对比形式如下表所示:

中缀表达式后缀表达式(逆波兰表达式)
a+bab+
a+(b-c)abc-+
a+d*(b-c)adbc-*+

中缀表达式转换为后缀表达式

思路:

  • 建立两个栈:运算符栈S1,数据中转栈S2

  • 自左至右顺序扫描表达式,

    • 遇到操作数时,压入栈S2

    • 遇到操作符

      • 如果栈S1为空,或者栈顶元素为左括号‘(’,则直接将操作符压入栈S1
      • 否则,若操作符的优先级比栈S1的栈顶元素高,也将操作符压入栈S1
      • 否则,将栈S1栈顶元素弹出并压入到栈S2中,再次将操作符与栈S1新的栈顶元素进行比较直至压入
    • 遇到括号

      • 若是左括号‘(’,直接将左括号压入到栈S1
      • 若是右括号‘)’,则依次弹出栈S1栈顶的操作符并压入到栈S2中,直到遇到左括号‘(’为止,此时将这对括号丢弃。
  • 扫描结束,将栈S1中的操作符依次弹出并压入到栈S2中

  • 依次弹出栈S2中的元素,所得结果的逆序即为该中缀表达式对应的后缀表达式。

代码实现:

const Stack = require('./Stack.js')
​
const priority_map = {
    "+": 1,
    "-": 1,
    "*": 2,
    "/": 2};
​
function infix_exp_2_postfix_exp(exp){
    let stack = new Stack.Stack();
​
    let postfix_lst = [];
    for(let i = 0;i<exp.length;i++){
        const item = exp[i];
        // 如果是数字,直接放入到postfix_lst中
        if(!isNaN(item)){
            postfix_lst.push(item);
        }else if (item == "("){
            // 遇到左括号入栈
            stack.push(item);
        }else if (item == ")"){
            // 遇到右括号,把栈顶元素弹出,直到遇到左括号
            while(stack.top() != "("){
                postfix_lst.push(stack.pop());
            }
            stack.pop();   // 左括号出栈
        }else{
            // 遇到运算符,把栈顶的运算符弹出,直到栈顶的运算符优先级小于当前运算符
            while(!stack.isEmpty() && ["+", "-", "*", "/"].indexOf(stack.top()) >= 0
            && priority_map[stack.top()] >= priority_map[item]){
                // 把弹出的运算符加入到postfix_lst
                postfix_lst.push(stack.pop());
            }
            // 当前的运算符入栈
            stack.push(item);
        }
​
    }
​
    // for循环结束后, 栈里可能还有元素,都弹出放入到postfix_lst中
    while(!stack.isEmpty()) {
        postfix_lst.push(stack.pop())
    }
​
    return postfix_lst
};
​
// 12+3
console.log(infix_exp_2_postfix_exp(["12","+", "3"]))
// 2-3+2
console.log(infix_exp_2_postfix_exp(["2","-", "3", "+", "2"]))
// (1+(4+5+3)-3)+(9+8)
var exp = ["(","1","+","(","4","+","5","+","3",")","-","3",")","+","(","9","+","8",")"];
console.log(infix_exp_2_postfix_exp(exp))
​
// (1+(4+5+3)/4-3)+(6+8)*3
var exp = ['(', '1', '+', '(', '4', '+', '5', '+', '3', ')', '/', '4', '-', '3', ')', '+', '(', '6', '+', '8', ')', '*', '3']
console.log(infix_exp_2_postfix_exp(exp))
​
console.log(infix_exp_2_postfix_exp(["12","+", "3","*", "5"]))
console.log(infix_exp_2_postfix_exp(["12","*", "3","+", "5"]))

基于后缀表达式计算

思路:

  • 自左至右顺序读取,值为数值时,执行入栈操作;
  • 遇到运算符,则弹出栈顶两个元素并进行使用运算符进行计算,再将结果入栈
  • 直至整个表达式被计算完毕。

代码实现:

​
​
function calc_exp(exp){
    let stack = new Stack.Stack();
    for(let i = 0; i < exp.length;i++){
        const item = exp[i];
​
        if(["+", "-", "*", "/"].indexOf(item) >= 0){
            // 从栈顶弹出两个元素
            const value_1 = stack.pop();
            const value_2 = stack.pop();
            // 拼成表达式
            const exp_str = value_2 + item + value_1;
            // 计算并取整
            const res = parseInt(eval(exp_str));
            // 将计算结果压如栈
            stack.push(res.toString());
        }else{
            stack.push(item);
        }
    }
    // 表达式如果是正确的,最终,栈里还有一个元素,且正是表达式的计算结果
    return stack.pop();
};
​
​
var exp_1 = ["4", "13", "5", "/", "+"];
var exp_2 = ["10", "6", "9", "3", "+", "-11", "*", "/", "*", "17", "+", "5", "+"];
var exp_3 = [ '1', '4', '5', '+', '3', '+', '+', '3', '-', '9', '8', '+', '+' ];
console.log(calc_exp(exp_1));
console.log(calc_exp(exp_2));
console.log(calc_exp(exp_3));

完整的计算器需要进一步处理

支持 + - * / ( )

多位数,支持小数,

兼容处理, 过滤任何空白字符,包括空格、制表符、换页符

\