hello,everybody. 我們又見面了,這次我們一起來學習數據結構中,非常有意思的兩種結構—Stack ,Queue.
首先來學習一下棧:
棧:限定只在表尾進行刪除插入操作的線性表。
顧名思義,棧是一種特殊的線性表。它特殊在什么地方呢?它只能在表尾進行插入或刪除操作,又就意味着,它只能是先進后出。給大家舉個現實中,利用棧的例子。我們都用瀏覽器瀏覽過網頁,我們對瀏覽器的前進后退按鈕一定都不陌生。當我們打開第一個網頁時,有一個圖片鏈接,於是我們又跳到了第二個網頁。此時,又有一個文字鏈接,我們又跳到了第三個網頁。此時,我點擊瀏覽器的后退按鈕,我們會回到第二個網頁。再點擊瀏覽器的后退按鈕,我們又跑到了第一個網頁。是不是最先打開的第一個網頁,是最后一個恢復的?是不是,先進后出?
看來,我們在學習第二章線性表時,付出的心血沒有白費。看看,我在學習棧與隊列時,感到很輕松,因為它講的一些概念,我都掌握了。所以,我們在學習知識時,一定要踏實,耐心。付出一定會有回報的,你用心付出,回報巨大,回報明顯。你不用心付出,回報微小,回報不明顯。你不付出,只是隨意看看,那么當別人說起這些知識時,你也可以裝下B。所以,付出是有回報的。只是為了,回報巨大,回報明顯,我們需要用心,態度要端正。
我們把允許刪除的一端稱為棧頂(Top),另一端稱為棧底(Bottom).不含任何數據元素的棧稱為空棧。棧又稱為后進先出(Last In First Out)的線性表。大家需要注意了,我們說,棧是限定了,只能在表尾進行刪除插入操作的線性表。這里所說的表尾,是指棧頂,而不是棧底。
棧的插入操作,叫作進棧,棧的刪除操叫做出棧。如下圖:
棧的抽象數據類型:
既然棧也是線性表,那么我們上章所學的線性表操作,也同樣適用於棧。那么,棧也同樣有順序存儲,與鏈式存儲的存儲方式。
棧的順序存儲結構:
我們在學習線性表的順序存儲時,我們是用數組來表示的。對於棧的順序存儲,我們同樣可以用數組來表示。那么大家思考一下,我們用哪頭來表示棧頂,哪頭來表示棧底呢?
因為,插入、刪除操作都是在棧頂進行。相對於棧頂,棧底是比較”安靜的”,比較穩定。那么,我們就用下標為0的位置,來定義我們的棧底。大家看下圖:
我們用Top來指向棧頂元素所在數組的下標,這個Top就相當於游標卡尺的游標。游標可以變大變小,意味着我們的Top可以變大表小,但是再怎么變,它也不能超過游標卡尺的長度。Top,不能超過我們的數組長度。當棧存在一個元素時,我們定義Top指向0.當棧為空時,我們定義Top指向-1.
棧的結構定義:
進棧操作:
Status Push(SqStack *S,SElemType e)
{
if(S->top==MAXSIZE-1) /*棧滿*/
return ERROR;
S->top++;
S->data[S->top]=e;/*將元素e添加棧頂空間*/
return ok;
}
出棧操作:
Status Pop(SqStack *S,SElemType *e)
{
if(S->top==-1)
return ERROR;
*e=S->data[S->top];/*將棧頂元素賦值給e*/
S->top—;
return ok;
}
兩者沒有涉及任何的循環操作,所以時間復雜度為O【1】.
其實,棧的順序存儲還是很方便的。因為它只允許在表尾進行刪除插入操作,不必移動其他元素。但是它有一個很大的不足,我們事前必須清楚數組的大小,給少了,溢出。給多了,浪費。
兩棧共享空間:
對於兩個相同類型的棧,我們可以把他們結合起來,用一個數組來存儲。充分利用數組的空間,提高效率。數組有兩個端點,棧也有兩個斷點。我們可以用數組下標為0的位置,存放棧的棧底。數組下標為n-1的位置,存放另一個棧的棧底。這樣兩個棧如果插入元素,就是從數組兩端向中間插入數據。如下圖:
這樣,我們就充分利用了數組的空間。大家可以發現,Top1是棧1的棧頂指針,Top2是棧2的棧頂指針。只要Top1與Top2不見面,兩棧就可以一直使用。那么當棧1為空棧時,Top1=-1.棧2為空棧時,Top2=n。那么什么時候棧滿呢?
想想極端情況,棧2為空棧。當Top1=n-1時,棧1為滿棧。棧1為空棧時,Top2=0時,棧2為滿棧。但是,更多的時候,是Top1與Top2見面時。也就是它們相差1.Top1+1=Top2.
下圖是兩棧共享空間結構
插入元素e為新棧頂元素:
Status Push(SqDoubleStack *S,SElemType e, int StackNumber)
{
if(S->top1+1==S->top2)/*棧滿了*/
return ERROR;
if(StackNumber==1)/*棧1有元素進棧*/
S->data[++S->top1]=e;/*Top1+1,把e賦值給棧頂元素*/
else if(StackNumber==2);/*棧2有元素進棧*/
S->data[--S->top];/*Top—,把e賦給棧2的棧頂元素*/
}
出棧操作:
Status Pop(SqDoubleStack *S,SElemType *e,int stackNumber)
{
if(stackNumber==1)
{
if(S->top1==-1)/*棧1為空棧*/
return ERROR;
*e=S->data[S->top1—];
}
else if(stackNumber==2)
{
if(S->Top2==Maxsize)/*棧2為空棧*/
return ERROR;
*e=S->data[S->top2++];
}
return ok;
}
棧的鏈式存儲結構及定義:
好了,我們來學習一下棧的鏈式存儲結構吧,簡稱鏈棧。
因為鏈表有頭指針,而棧又有棧頂指針,那么為何不讓它們合二為一呢?所以,我們通常將頭結點定義為棧頂。因為已經有了棧頂在頭部了,頭結點作用就不大了。所以,對於鏈棧,是不需要頭結點的。
對於空表來說,原來的鏈表是頭指針指向空,那么鏈棧的空其實就是Top=NULL的時候。
鏈棧的插入:
Status Push(LinkStack *S,SElemType e)
{
LinkStackPtr s=(LinkStackPtr)malloc(sizeof(StackNode));
s->data=e;
s->next=S->top;/*將原棧頂元素改為新元素的后繼元素*
S->top=s;/*將新元素S賦給棧頂元素*/
S->count++;
return OK;
}
/*鏈棧的刪除*/
Status Pop(LinkStack *S,SElemType *e)
{
LinkStackPtr p;
if(StackEmpty(S))
return ERROR;
*e=S->top->data;
p=S->top;
S->top=S->top->next;
free(p);
S->count—;
return OK;
}
大家可以發現,鏈棧的入棧與出棧的算法,都沒有循環語句,時間復雜度都是O【1】.我們可以發現,順序存儲的棧與鏈式存儲的棧,他們的時間性能都是一樣的。不同的是,空間性能上。線性棧,需要事前給出固定長度,這就可能造成內存空間浪費的可能。但是,線性棧,存取時定位很方便。而鏈式存儲雖然不用事前分配固定大小,但是每個元素必須開辟指針域,這也造成了空間的消耗。線性棧與鏈棧與線性表與鏈表是一樣的,當我們的元素變化比較大時,建議使用鏈棧,否則使用線性棧。
棧的作用:
大家可能會有疑問,既然棧也是一種線性表。那么我們已經有了線性表,為什么還要學習棧呢?李白說過:”天生我材必有用”。存在即合理,既然有這種數據結構,就一定有它的用武之地。線性表相當於我們的腳,理論上我們用腳可以走遍地球上 任何地方。但是,從北京到上海,我們是選擇用腳走,還是乘交通工具呢?棧,就相當於飛機、火車、汽車等交通工具。它的出現,是為了簡化程序的設計,使我們把精力全部放在問題本身上。是不是不好理解啊,呵呵,沒事兒的,我們一起來看看棧的實際用途吧。
棧的應用:
棧的一個重要應用,就是實現了程序設計語言的遞歸操作。什么是遞歸呢?通過學習一個經典的算法來了解遞歸的含義吧。
斐波那契(Fibonacci)數列實現:
1 1 2 3 5 8 13 21……..
前面相鄰的兩項的和,構成了后面的項。像這樣的數列,就叫做斐波那契數列。首先,我們設計一個算法實現這個斐波那契數列。
void Fibonacci()
{
int i;
int a[40];
a[0]=0;
a[1]=1;
printf(“%d ”,a[0]);
printf(“%d ”,a[1]);
for(i=2;i<40;i++)
{
a [i]=a[i-1]+a[i-2];
printf(“%d ”,a[i]);
}
}
運行結果:
我們設計的這個算法,是利用迭代功能實現的,我們將這個利用迭代實現斐波那契算法成為迭代斐波那契。
現在,我們再來看看利用遞歸實現斐波那契的算法。
int Fbi(int i)
{
if(i<2)
return i==0?0:1;
return Fbi(i-1)+Fbi(i-2);
}
運行結果:
兩種算法,結果是一樣的。大家可以觀察研究一下這兩個算法,通過觀察,我們發現利用遞歸斐波那契算法要比迭代斐波那契算法干凈,簡潔。所謂的遞歸,不過是一直在調用自己。那么我們就好了遞歸的含義.
遞歸:我們把一直調用自己或通過一系列語句間接地調用自己的函數,稱為遞歸。
設計遞歸算法時,最怕的就是陷入無限的調用中。所以,我們會給出一個條件,當遞歸函數滿足這個條件時,結束遞歸調用。這也是,這兩個算法的不同點。迭代算法,遞歸算法,說白了,前者是使用循環結構,而后者使用選擇結構。遞歸算法,使得代碼簡潔,易懂,減少了理解代碼的時間。但是,會創建多個函數副本,會耗費大量的時間和內存。迭代則不用反復調用函數和占用額外的內存。
因此,我們要根據實際情況來選擇不同的代碼實現方式。
那么,講這么多,遞歸跟棧到底有什么關系呢?大家可以發現,遞歸是一層層的在調用函數,我們把調用函數想象成棧的入棧,我們把參數、調用地址都壓入棧中。當滿足判斷條件時,我們在將位於棧頂的參數、調用地址彈出棧中。這樣,函數就會一層一層的返回,直至最先調用的函數。這樣大家是不是就明白了,棧是如何實現遞歸的吧。
棧還有一些其他應用:
棧的現實應用還有很多,我們來學習一下用的比較多的一個應用:四則運算表達式。
我們都用過高級計算器,它的高級之處是,可以進行帶括號的四則運算。那么計算器內部是如何實現的呢?就是利用棧這個數據結構實現的,一起來學習一下吧。
后綴表示法:
首先,我們要先了解一下后綴表示法。波蘭邏輯學家,想到了一種不需要括號的后綴表達法,我們稱為后綴表示法。稱為后綴表示法,是因為運算符全在要計算數字的后面。例如,9+(3-1)*3+10/2,對於這個四則運算表達式,轉換成后綴表達式就是9 3 1 – 3 *+10 2 + .大家看這個后綴表示法是不是很別扭,你不喜歡,可是計算機非常喜歡的。
中綴法表達式轉后綴法表達式:
我們把普通的四則運算表達式稱為中綴法,那么如何從中綴法轉后綴法呢?從左向右依次遍歷表達式,遇到數字就輸出,遇到符號,如果是右括號或是優先級高於棧頂符號的,就將棧中符號輸出,外面符號進棧。我們一起來做個練習吧,將中綴法9+(3-1)*3+10/2,轉為后綴法。
第一步,初始化一個空棧,用來對符號進出棧使用。如下圖左:
第一個字符是9,輸出9,后面是符號+,此時棧頂為空,沒有低於+的符號,所以+進棧,如上圖右.
第三個字符是(,因為是左括號,說明還未配對,故進棧。如下圖左所示:
第四個字符是3,輸出,此時輸出的字符為 9 3 .
第五個字符是-,進棧。如上圖右。
第六個字符是1,輸出。此時輸出的為 9 3 1
第七個字符是),要匹配),所以棧內符號依次出棧,直到匹配到).現在輸出為 9 3 1 -
此時棧內符號只有一個+。如下圖左:
第八字符是*,因為此時棧頂元素是+,比*級別低,所以不用出棧,*進棧。如下圖又:
第九個字符是3,輸出, 此時輸出為 9 3 1 – 3
第十個字符是+,因為此時棧頂元素是*,比+級別高,所以出棧,(棧里有* + 都不必+的運算級別低,所以全部輸出)。此時輸出* + 第十個字符+入棧。此時棧如下圖做所示:
此時輸出的為 9 3 1 – 3 * +
第十一個字符為10,輸出10 .9 3 1 – 3 * + 10
第十二個字符為/,因為棧頂元素為+,比/運算級別低,所以/進棧。如上圖右.
第十三個字符為2,輸出2.
沒有字符了,所以棧內符號依次 輸出。
最終輸出為9 3 1 – 3 * + 10 2 / +
以上就是中綴法轉后綴法。
我們有了后綴法的表達式,就可以讓計算機來計算了。那么計算機是如何計算的呢?
后綴表達式計算結果:
規則,從左到右依次遍歷后綴法表達式,遇到數字就進棧,遇到符號,就將棧內頭兩個數字出棧進行計算,計算結果再入棧,直到結算完為止。我們就拿9+(3-1)*3+10/2的后綴法9 3 1 – 3 * 10 2 / +來練習。
前三個字符分別為9 3 1,依次入棧。如下圖:
第四個字符為-,棧內頭兩個數字出棧,計算結構入棧。1、3 出棧。執行3-1 結果 2 入棧。
第六個字符*,棧內數字3、2出棧。執行2*3,結果6入棧。如下圖:
第7個字符+,6、9出棧,執行9+6,結果15入棧。如上圖右。
第八、九字符分別為10 ,2依次入棧。
第10字符為/,10 2 出棧,執行10/2 結果5入棧,如下圖:
第十一個字符為+,棧內數字5 15 出棧,執行15+5 結果20.入棧
因為是最后一個字符,所以20出棧,棧為空。20是最終的計算結果。如下圖:
計算機就是這樣執行四則運算的,到這里,你應該能明白棧、線性表同樣是線性表,為什么又要學習棧了。
好了,這就是棧全部內容,休息一下,我們再來學習隊列。





















