上一篇文章講到了鏈表,接下來說的是數據結構裏面最經典的一個結構:棧。算法
棧從定義上來說,是一個操做受限的線性表。棧只支持從一端存入數據和刪除數據,即先進後出。先進後出是典型的棧的結構特色,不少實例都有應用到棧的特色來實現,好比瀏覽器的前進後退功能。棧對比前面所講的數組和鏈表,功能上受限了不少,只支持在一段存入數據和刪除數據,可是也比他們容易維護,因爲只是在一端存入和刪除數據,因此數據不會容易出錯。數組
棧從實現上能夠用數組和鏈表來實現,數組實現叫順序棧,鏈表實現叫鏈式棧。下面的這組代碼實現了這兩種方式的棧聲明:瀏覽器
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一個對象的引用或地址存儲在棧區,指向該對象存儲在堆區中的真實數據。
上一篇文章:數據結構與算法的重溫之旅(五)——如何運用鏈表
下一篇文章:數據結構與算法的重溫之旅(七)——隊列