作者 | 小鹿
來源 | 小鹿動畫學編程
寫在前邊
對於棧的認識,相信每個學習數據結構的小夥伴多多少少有一定的認識和了解。很多剛剛學習的小夥伴說學習數據結構在實際中沒怎麼見到應用,那是因爲你沒有去仔細的觀察,而且像棧這常用到的數據結構通常會使用在實際開發中,比如:表達式的運算、花括號的匹配以及瀏覽器的前進後退等等很多。
這些實際開發的實現如果不去研究,你永遠不知道數據結構在實際中的應用,當你學習完今天的棧數據結構時,然後去研究下實際中已經使用到的應用,才能讓你在今後的實際開發中用到棧這種數據結構,從而使你的開發更加靈活、多變。
思維導圖
這篇文章將要學習到的內容。
1、基本常識
1.1 什麼是棧
我們用一種最簡單的生活常識描述一下,比如我們往櫃子裏放東西,先放的東西是需要放到櫃子最裏邊,後放的東西在櫃子的最外邊;如果我們要取東西,先要取櫃子最外邊的東西,才能取到櫃子最裏邊的東西。這種先進後出,後進先出的結構稱爲“棧”。
1.2 棧的特點
“先進後出,後進先出”。
1.3 棧的操作
棧的操作就兩種,分別爲出棧和入棧。那我們上邊的例子,我們往櫃子裏放東西的過程稱爲入棧;我們在櫃子裏拿東西的過程稱爲出棧。
PS:櫃子只有一個出口和入口,而且出口和入口是一樣的。如果兩端都有開口,就變成了隊列,後期的文章會講到。
2、棧的實現
我們前邊的文章也講過,所有的數據結構基本都是由數組和鏈表演化而來的,所以今天講的棧這種數據結構也不例外。
棧的實現主要有兩種,一種是數組的實現,叫做順序棧,另外一種是鏈表的實現,叫做鏈式棧。如下:
2.1 順序棧
2.2 鏈表棧
2.3 代碼實現
順序棧
/**
* 功能:基於數組的順序棧
* 公衆號:一個不甘平凡的碼農
* @author:小鹿
*
*/
public class ArrayStack {
private String[] items; // 數組
private int count; // 棧中元素個數
private int n; // 棧的大小
// 初始化數組,申請一個大小爲 n 的數組空間
public ArrayStack(int n) {
this.items = new String[n];
this.n = n;
this.count = 0;
}
/**
* 功能:入棧
* 說明:數組入棧的入口爲數組尾部
* @param item :入棧數據元素
* @return:是否入棧成功
*/
public boolean push(String item) {
// 數組空間不夠了,直接返回 false,入棧失敗。
if (count == n) return false;
// 將 item 放到下標爲 count 的位置
items[count] = item;
//數組長度+1
++count;
//入棧成功
return true;
}
/**
* 功能:出棧
*
* @return:返回出棧元素
*/
public String pop() {
// 棧爲空,則直接返回 null
if (count == 0) return null;
// 返回下標爲 count-1 的數組元素
String tmp = items[count-1];
//數組長度-1
--count;
//返回出棧數據元素
return tmp;
}
}
鏈式棧
/**
* 功能:基本鏈表的鏈式棧,入棧、出棧、輸出棧
* @author : 小鹿
* 公衆號:一個不甘平凡的碼農
*/
public class StackBasedLinkedList {
//定義棧頂指針
private Node top = null;
//定義棧結點
private static class Node {
//棧結點數據域
private int data;
//棧結點指針域
private Node next;
//構造函數
public Node(int data, Node next) {
this.data = data;
this.next = next;
}
//get 獲取數據域方法
public int getData() {
return data;
}
}
/**
* 功能:入棧
* @param value:要入棧的數據元素
*/
public void push(int value) {
//創建一個棧結點
Node newNode = new Node(value, null);
// 判斷棧是否爲空
if (top == null) {
//如果棧爲空,就將入棧的值作爲棧的第一個元素
top = newNode;
} else {
//否則插入到top棧結點前(所謂的就是單鏈表的頭插法)
newNode.next = top;
top = newNode;
}
}
/**
* 功能 : 出棧
* @return: -1 爲棧中沒有數據
*/
public int pop() {
// 如果棧的最頂層棧結點爲null,棧爲空
if (top == null) return -1;
//否則執行出棧操作,現將棧頂結點的數據元素賦值給 Value
int value = top.data;
//將 top 指針向下移動
top = top.next;
//返回出棧的值
return value;
}
/**
* 功能:輸出棧中所有元素
*/
public void printAll() {
//將棧頂指針賦值給p
Node p = top;
//循環遍歷棧(遍歷單鏈表)
while (p != null) {
System.out.print(p.data + " ");
//指向下一個結點
p = p.next;
}
System.out.println();
}
}
3、棧的性能
我們從上邊學到了棧的基本結構和特點,還有棧的基本操作。如果我們學習一種數據結構,主要分析它的性能如何。還記得怎麼分析數據結構性能嗎?主要從兩方面入手,第一,時間效率(時間複雜度);第二,空間上的消耗(空間複雜度)。我們就從以上兩個方面分析一下棧這種數據結構的性能。
3.1 時間複雜度
時間上的消耗主要分析棧的操作所消耗的時間,我們共兩種操作,入棧和出棧,其實在數組中中,我們操作尾部的數據就相當於入棧和出棧,直接根據下標取得相應的元素就好(JS 中數組的 pop 和 push 方法),所以時間複雜度是 O(1)。
3.2 空間複雜度
空間複雜度的判斷是所需要開闢的臨時空間,順序棧和鏈式棧只需要大小爲 n 的空間就可以,入棧和出棧需要一個臨時空間來存儲變量,空間複雜度爲 O(1)。
3.3 棧的動態擴容
大家有沒有想過這樣一種情況,如果棧滿的時候,再進行入棧操作,棧內就放不下了,我們需要動態擴容。主要是順序棧的動態擴容比較麻煩,和我麼你之前的數組的文章動態擴容一樣的,對於動態擴容的性能,可以自己嘗試一下。可以根據之前的文章來分析《佩奇學編程 | 複雜度分析原來這麼簡單》。
4、棧的實際應用
既然我們把棧的性能分析透了,理解透了,那麼我們看看棧在實際中有哪些應用吧。
4.1 應用一 :棧在函數中的應用
函數我們每個人再熟悉不過了,你是不是很納悶,棧怎麼會在函數中能夠應用的到呢,我學了這幾年函數,我咋不知道函數中還有棧的操作。
加入我們程序開始執行代碼,執行到我們聲明的函數時,計算機內部會發生什麼呢?首先,會爲該函數開闢一塊臨時的內存空間,這塊內存空被組織成“棧”這種數據結構,作用主要用來存儲函數內部聲明的臨時變量。
每執行一個函數,系統就將函數中的臨時變量組織成棧幀,執行入棧操作,當函數被調用完成的時候,臨時變量已經用不到了,所以要在內存中釋放,執行出棧操作。如以下函數:
function main(){
let i = 0;
let j = 1;
i++;
j++;
console.log(i+j)
}
main();
具體的動畫如下:
我們這時要想一個問題,那爲什麼函數會使用棧這種數據結構呢,爲什麼不用隊列、鏈表或者其他數據結構?全體注意,重點來了,以後分析其他的問題也用到一下的方法分析。
因爲函數調用的執行順序符合後進者先出,先進者後出的特點。
比如函數中的局部變量聲明的時間順序,早先定義的變量在內存中保存的時間長,後定義的變量在內存中保存的時間短,所有有一個先後的問題。我們再去腦海中把這種問題的特點抽象成數據結構,只有使用“棧”結構,才符合這種問題。
4.2 棧在表達式中應用
計算機中數字的運算也是使用棧這種數據結構的,我們舉個例子,我們要計算如下表達式:
1 + 2 × 4 - 6
如果比運算符棧頂元素的優先級高,就將當前運算符壓入棧;如果比運算符棧頂元素的優先級低或者相同,從運算符棧中取棧頂運算符,從操作數棧的棧頂取 2 個操作數,然後進行計算,再把計算完的結果壓入操作數棧,繼續比較。動畫如下:
4.3 其他應用
關於棧的應用,還有很多,比如花括號的匹配問題,有關練習去 LeedCode 實踐。這裏就不多舉例子。
❤️ 不要忘記留下你學習的腳印 [點贊 + 收藏 + 評論]
一切看文章不點贊都是“耍流氓”,嘿嘿ヾ(◍°∇°◍)ノ゙!開個玩笑,動一動你的小手,點贊就完事了,你每個人出一份力量(點贊 + 評論)就會讓更多的學習者加入進來!非常感謝! ̄ω ̄=
作者Info:
【作者】:小鹿
【原創公衆號】:小鹿動畫學編程。
【簡介】:和小鹿同學一起用動畫的方式從零基礎學編程,將 Web前端領域、數據結構與算法、網絡原理等通俗易懂的呈獻給小夥伴。先定個小目標,原創 1000 篇的動畫技術文章,和各位小夥伴共同努力一起學習!公衆號回覆 “資料” 送一從零自學資料大禮包!
【轉載說明】:轉載請說明出處,謝謝合作!~