前言:
在這個系列的第一篇博客中,我主要強調了編程思維的重要性以及如何高效準確的編寫出能解決問題的代碼,這一篇博客開始就正式開始python數據結構相關的知識。
有一種數據結構,它的元素順序取決於添加的順序或者刪除的順序,一旦某個元素被添加進來,它與前後元素的相對位置也就保持不變了,這樣的數據集合就被稱爲線性數據結構。
常見的線性數據結構主要有:棧、隊列、雙端隊列、列表。接下來的幾篇博客會來詳細講講這幾種線性數據結構和它們的實現
今天的主角:棧
棧,它的添加和刪除操作總髮生在同一端,即頂端。它的特性呢就是“先進後出”(或者說“後進先出”,都是一個意思),也就是說最先進入棧的元素在棧中會呆最久,那麼頂端的元素總是最近才添加的,而底端的元素總是最開始添加的。
生活中有很多棧的例子,比如:我們洗好的盤子疊起來放,總是先洗好的放在最下面;一堆摞起來的書,我們要拿到其中某一本,就需要先移開上面的那些書;或者有沒有想過,我們瀏覽網頁之所以可以返回之前瀏覽的網頁,那是不是也是因爲我們瀏覽的網頁記錄被放到棧中了呢?
棧抽象數據類型的操作:
- stack() 初始化一個空棧,返回值一個空棧
- push(item) 入棧操作,將一個元素item放入棧中
- pop() 出棧操作,將棧頂元素刪除,並且返回棧頂元素的值
- peek() 查看棧頂元素,與pop不同,它不會刪除棧頂元素,只是將棧頂元素的值返回
- isEmpty() 檢查棧是否爲空
- size() 返回棧中的元素數量
- show() 顯示棧中所有元素
有了這些基本操作,那我們就嘗試着實現一個棧
1.棧的python實現
class Stack():
def __init__(self):
self.items=[]
def isEmpty(self):
return '棧爲空'if self.items==[] else '棧不爲空'
def push(self,item):
self.items.append(item)
def pop(self):
return self.items.pop()
def peek(self):
return self.items[len(self.items)-1]
def size(self):
return len(self.items)
def show(self):
return self.items
# 實例化測試一下
s=Stack()
# 初始應該爲空
print(s.isEmpty())
# 壓入幾個元素試試
s.push(4)
s.push('abc')
s.push(18)
s.push('DL')
# 再來看看是否爲空,以及棧的大小和棧頂元素
print(s.isEmpty())
print(s.size())
print(s.peek())
# 然後試試刪除棧頂元素
print(s.pop())
print(s.size())
# 顯示所有元素
print(s.show())
通過上面的代碼,我們可以很好的用代碼實現棧的各種操作。同時大家肯定也注意到了,我說過python寫數據結構比c、c++簡單,原因就在於python的列表是動態的,沒有固定的大小,有元素添加直接append就可以啦,相比於當時學習c的數據結構,一開始就要分配數組大小,然後添加元素還得判斷棧是否滿了,省去了大量的精力。不過,也許這樣確實簡單,但是我們還是需要去考慮python的動態列表是怎麼實現的呢?或者有興趣也可以去看看go語言切片的實現原理,瞭解一下長度和容量是如何變化的,爲什麼推薦go,因爲go更類似於C語言,但是會比C語言簡單,在這裏我就不拓展太多了,還是本着最低學習成本的思想來講
思考
如果我們用列表頭作爲棧頂,如何實現棧?與上面用列表尾實現效率上有什麼不同
簡單解釋:用列表頭作爲棧,入棧和出棧都是對items[0]進行操作,入棧用insert(0,item),出棧用pop(0);從效率上來說,insert(0),pop(0)時間複雜度是O(n),而append(),pop()時間複雜度是O(1),所以棧中元素越多,列表尾作爲棧頂的優勢越明顯。
2.棧的應用
今天先來說說常見的兩個應用:匹配符號,表達式轉換
-
我們知道(),[],{}總是成對出現,並且有一定的順序,所以我們可以利用棧實現一個檢測器,看輸入的符號是否符合規範。
-
對於表達式,我們有中序表達式也有前序,後序,它們之間的轉換也可以利用棧實現
思路:
-
我們遍歷輸入的表達式,將([{ 壓入棧,然後一旦遍歷到右邊的符號就去做一次判斷:首先把棧頂元素出棧,然後與它現在右邊的符號做一次匹配,看是否符合規範;依次這樣,如果最後棧爲空(全部左邊符號做過匹配),而且每一次匹配的結果(flag)都是正確的,那麼也就是符號匹配正確。
-
以中序轉後續爲例,我們先遍歷表達式,用棧保存運算符,並且每次遇到新的運算符都要先比較它與棧中元素的優先級,從而實現轉換
這裏呢,我們會使用pythonds這個庫裏面的數據結構,需要先pip install pythonds
from pythonds.basic import Stack
def check(string):
s=Stack()
flag=True
index=0
while(index<len(string) and flag):
symbol=string[index]
if symbol in "([{":
s.push(symbol)
else:
if s.isEmpty():
flag=False
else:
top=s.pop()
if not matches(top,symbol):
flag=False
index+=1
if flag and s.isEmpty():
return "符號匹配無錯誤"
else:
return "符號匹配有錯誤"
def matches(start,end):
starts='([{'
ends=')]}'
return starts.index(start)==ends.index(end)
print(check('(][){}'))
print(check('([{}])'))
print(check('[][][]'))
print(check('[{())}]'))
from pythonds.basic import Stack
import string
def infix_to_postfix(expr):
#先定義一下運算符的優先級
prec={}
prec['*']=3
prec['/']=3
prec['+']=2
prec['-']=2
prec['(']=1
op=Stack()
postfix_list=[]
# 以表達式中的空格分開,得到表達式中的每一個元素,返回列表
tokenlist=expr.split()
# 開始遍歷
for token in tokenlist:
# 如果匹配到大寫的英文字母直接添加到後續表達式
if token in string.ascii_uppercase:
postfix_list.append(token)
# 如果匹配到左括號,那就直接入棧
elif token=='(':
op.push(token)
# 匹配到右括號,就把棧中左括號之後添加的所有符號都拿出來放進後續表達式中
elif token==')':
topToken=op.pop()
while topToken!='(':
postfix_list.append(topToken)
topToken=op.pop()
# 棧不爲空,且當前符號優先級小於棧頂元素優先級,
# 那麼就把棧頂元素添加到後序表達式,同時把優先級小的運算符入棧
else:
while (not op.isEmpty()) and (prec[op.peek()]>=prec[token]):
postfix_list.append(op.pop())
op.push(token)
while not op.isEmpty():
postfix_list.append(op.pop())
return " ".join(postfix_list)
print(infix_to_postfix("( A + B ) * ( C + D )"))
print(infix_to_postfix("A + B * C"))
print(infix_to_postfix("A * B + C"))
總結
這篇博客,瞭解了棧的特點以及基本操作,然後還知道了可以利用棧去解決實現哪些問題,下一篇就講講隊列。這個系列的博客其實每篇內容都不多,也是爲了一點一點的能更好吸收。