数据结构与算法的重温之旅(六)——栈

上一篇文章讲到了链表,接下来说的是数据结构里面最经典的一个结构:栈。算法

栈从定义上来说,是一个操做受限的线性表。栈只支持从一端存入数据和删除数据,即先进后出。先进后出是典型的栈的结构特色,不少实例都有应用到栈的特色来实现,好比浏览器的前进后退功能。栈对比前面所讲的数组和链表,功能上受限了不少,只支持在一段存入数据和删除数据,可是也比他们容易维护,因为只是在一端存入和删除数据,因此数据不会容易出错。数组

栈从实现上能够用数组和链表来实现,数组实现叫顺序栈,链表实现叫链式栈。下面的这组代码实现了这两种方式的栈声明:浏览器

class Node {
    constructor(element) {
        this.element = element
        this.next = null
    }
}
class linkedList {
    constructor() {
        this.head = null
    }
    pushData (item) {
        let curr = this.head
        let newNode = new Node(item)
        if (curr == null) {
            this.head = newNode
            curr = this.head
        }
        else {
            while (curr) {
                if (curr.next) {
                    curr = curr.next
                }
                else {
                    curr.next = newNode
                    break
                }
            }
        }
    }
    popData () {
        let curr = this.head
        if (curr == null) {
            return false
        }
        else if (this.head.next == null) {
            let temp = this.head
            this.head = null
            return temp
        }
        else {
            while (curr) {
                if (curr.next && curr.next.next) {
                    curr = curr.next
                }
                else if (curr.next && curr.next.next == null) {
                    let temp = curr.next
                    curr.next = null
                    return temp
                }
            }
        }
    }
}复制代码
class ArrayLisk {
    constructor () {
        this.stack = new Array()
    }
    pushData (item) {
        this.stack.push(item)
    }
    popData () {
        return this.stack.pop()
    }
}复制代码

能够看出用数组来实现栈的话比链表实现要简单不少,这里是用了JavaScript里的数组的一些语法糖,因此看起来比较简单实现。栈的时间复杂度和空间复杂度都是O(1)。由于在空间上只需用几个临时变量来存储值,在时间上只涉及插入和删除操做,不涉及遍历,因此时间和空间上的复杂度都是O(1)。bash

正常来讲,栈是有容量的,当栈的容量满的时候,须要给栈扩容。普通的作法就是给栈一个两倍容量的容器,将原来的值都存入到新的栈中。总体的时间复杂度是O(1)。数据结构

栈做为一个比较基础的数据结构,应用场景仍是蛮多的。其中,比较经典的一个应用场景就是函数调用栈。咱们知道,操做系统给每一个线程分配了一块独立的内存空间,这块内存被组织成“栈”这种结构, 用来存储函数调用时的临时变量。每进入一个函数,就会将临时变量做为一个栈帧入栈,当被调用函数执行完成,返回以后,将这个函数对应的栈帧出栈。为了让你更好地理解,咱们一块来看下这段代码的执行过程。函数

function first () {
    var b = 2
}
function second () {
    first()
    var a = 1
}
second()复制代码

在这里,second函数调用了first函数,因此在栈里先出的是var b = 2这条语句,而后到var a = 1这条语句。post

除了这个应用外,栈在表达式求值中也有实例能够应用。如1+2*3/4这条算术表达式中,先进行的确定是2*3,而后是除以4,最后才加1。那若是用代码的话他的设计思路是如何的呢?实际上,编译器就是经过两个栈来实现的。其中一个保存操做数的栈,另外一个是保存运算符的栈。咱们从左向右遍历表达式,当遇到数字,咱们就直接压入操做数栈;当遇到运算符,就与运算符栈的栈顶元素进行比较。若是比运算符栈顶元素的优先级高,就将当前运算符压入栈;若是比运算符栈顶元素的优先级低或者相同,从运算符栈中取栈顶运算符,从操做数栈的栈顶取 2 个操做数,而后进行计算,再把计算完的结果压入操做数栈,继续比较。下面用代码来实现一下:ui

class ArrayLisk {
    constructor () {
        this.stack = new Array()
    }
    pushData (item) {
        this.stack.push(item)
    }
    popData () {
        return this.stack.pop()
    }
}
function numberOperation(operation, num1, num2) {
    num1 = Number(num1)
    num2 = Number(num2)
    if (operation === '*') return num1 * num2
    else if (operation === '/') return num1 / num2
    else if (operation === '+') return num1 + num2
    else if (operation === '-') return num1 - num2
}
let stringPattern = '1+5*4/2-2'
let numberStack = new ArrayLisk()
let operationStack = new ArrayLisk()
for (let a = 0; a < stringPattern.length; a++) {
    let operaNum1 = 0
    let operaNum2 = 0
    if (!isNaN(+stringPattern[a])) {
        numberStack.pushData(stringPattern[a])
    }
    else {
        if (stringPattern[a] === '*' || stringPattern[a] === '/') {
            operaNum1 = numberStack.popData()
            operaNum2 = stringPattern[a+1]
            numberStack.pushData(numberOperation(stringPattern[a], operaNum1, operaNum2))
            ++a
        }
        else {
            let temp = operationStack.popData()
            if (temp) {
                operaNum1 = numberStack.popData()
                operaNum2 = stringPattern[a+1]
                let detail = numberOperation(stringPattern[a], operaNum1, operaNum2)
                numberStack.pushData(detail)
                operationStack.pushData(temp)
                ++a
            }
            else {
                operationStack.pushData(stringPattern[a])
            }
        }
    }
    if (a === stringPattern.length - 1) {
        operaNum1 = numberStack.popData()
        operaNum2 = numberStack.popData()
        console.log(numberOperation(operationStack.popData(), operaNum2, operaNum1))
    }
}复制代码

目前这个只能进行个位数的加减乘除,超过个位数因为判断的问题会出现判断不许的状况。其实若是想要全数字能够的话则要对当前字符串下标后面的值进行判断,看是不是数字,若是是的话则继续拼接字符串,直到后面不是数字为止。this

除了这个应用外,在作acm或者leetCode的过程当中,也有一道十分经典的题,就是括号匹配问题。判断字符串是不是合法的括号,如{[()]}这个字符串确定是合法的,而}[()]{确定是非法的。那如何解决这种问题了,咱们能够经过栈来解决。先按从左往右的顺序把字符压入栈中,若是恰好栈顶的字符可以跟它下一个字符相匹配则出栈,而后当前栈顶的继续匹配看是否连续,当当前栈顶与当前遍历到的字符串不匹配的时候则说明当前字符串的括号不彻底匹配,反之则彻底匹配。代码以下:url

class ArrayLisk {
    constructor () {
        this.stack = new Array()
    }
    pushData (item) {
        this.stack.push(item)
    }
    popData () {
        return this.stack.pop()
    }
}
let pattern = '{[()]}{{{}}}[[[]]]((()))'
let myStack = new ArrayLisk()
let tempLeft = ['{', '[', '(']
let state = true
for (let a = 0; a < pattern.length; a++) {
    if (tempLeft.includes(pattern[a])) {
        myStack.pushData(pattern[a])
    }
    else {
        let temp = myStack.popData()
        if (temp === '{' && pattern[a] === '}') {}
        else if (temp === '[' && pattern[a] === ']') {}
        else if (temp === '(' && pattern[a] === ')') {}
        else {
            state = false
            break
        }
    }
}
if (myStack.stack.length) {
    state = false
}复制代码

最后一种与栈相关的应用就是浏览器页面的后退和前进功能。当用户点击一个新的页面的时候浏览器都会将页面的url压入栈中,当点击后退的时候则栈顶出栈,而且将出栈的元素压入另外一个栈中存储起来。当点击前进的时候将另外一个栈的栈顶出栈,压回以前的栈中。就这样就能够实现一个浏览器的前进后退功能。

最后说一下内存中的堆栈跟数据结构中的堆栈有什么区别。内存中的堆栈和数据结构堆栈不是一个概念,能够说内存中的堆栈是真实存在的物理区,数据结构中的堆栈是抽象的数据存储结构。内存空间在逻辑上分为三部分:代码区、静态数据区和动态数据区,动态数据区又分为栈区和堆区。

代码区:存储方法体的二进制代码。高级调度(做业调度)、中级调度(内存调度)、低级调度(进程调度)控制代码区执行代码的切换。

静态数据区:存储全局变量、静态变量、常量,常量包括final修饰的常量和String常量。系统自动分配和回收。

栈区:存储运行方法的形参、局部变量、返回值。由系统自动分配和回收。

堆区:new一个对象的引用或地址存储在栈区,指向该对象存储在堆区中的真实数据。


上一篇文章:数据结构与算法的重温之旅(五)——如何运用链表​​​​​​​

下一篇文章:数据结构与算法的重温之旅(七)——队列​​​​​​​

相关文章
相关标签/搜索