數據結構(二)棧、隊列和數組
棧
棧的定義
棧是限制在表的一端進行插入和刪除的線性表。允許插入、刪除的這一端稱為棧頂,另 一個固定端稱為棧底。當表中沒有元素時稱為空棧。
棧是運算受限的線性表,線性表的存儲結構對棧也是適用的,只是操作不同而已。
利用順序存儲方式實現的棧稱為順序棧。
與線性表類似,棧的動態分配順序存儲結構如 下:
#define STACK_INIT_SIZE 100 //存儲空間的初始分配量 #define STACKINCREMENT 10 //存儲空間的分配增量 typedef struct{ SElemType *base; //在棧構造之前和銷毀之後,base 的值為 NULL SElemType *top; //棧頂指針 int stacksize; //當前已分配的存儲空間 }SqStack;
需要註意,在棧的動態分配順序存儲結構中,base 始終指向棧底元素,非空棧中的 top 始終在棧頂元素的下一個位置。
下面是順序棧上常用的基本操作的實現。
(1)入棧:若棧不滿,則將 e 插入棧頂。
int Push (SqStack &S, SElemType e) { if (S.top-S.base>=S.stacksize) {……} //棧滿,追加存儲空間 *S.top++ = e; //top始終在棧頂元素的下一個位置 return OK; }
int Pop (SqStack &S, SElemType &e) { if (S.top==S.base) return ERROR; e= *--S.top; return OK; }
出棧和讀棧頂元素操作,先判棧是否為空,為空時不能操作,否則產生錯誤。通常棧空常作為一種控制轉移的條件。
棧的應用舉例
由於棧的“先進先出”特點,在很多實際問題中都利用棧做一個輔助的數據結構來進行求解。
1、數值轉換
2、表達式求值
表達式求值是程序設計語言編譯中一個基本的問題,它的實現也是需要棧的加入。下 面的算法是由運算符優先法對表達式求值。在此僅限於討論只含二目運算符的算術表達式。
(1)中綴表達式求值
中綴表達式:每個二目運算符在兩個運算量的中間,假設所討論的算術運算符包括:+ 、 - 、、/、%、^(乘方)和括號()。
設運算規則為:
.運算符的優先級為:()——> ^ ——>*、/、%——> +、- ;
.有括號出現時先算括號內的,後算括號外的,多層括號,由內向外進行;
.乘方連續出現時先算右面的。
表達式作為一個滿足表達式語法規則的串存儲,如表達式“3*2^(4+2*2-1*3)-5”,它的求值過程為:自左向右掃描表達式,當掃描到 3*2 時不能馬上計算,因為後面可能還有更高的運算,正確的處理過程是:需要兩個棧:對象棧 s1 和運算符棧 s2。當自左至右掃描表達式的每一個字符時,若當前字符是運算對象,入對象棧,是運算符時,若這個運算符比棧頂運算符高則入棧,繼續向後處理,若這個運算符比棧頂運算符低則從對象棧出棧兩個運算量, 從運算符棧出棧一個運算符進行運算,並將其運算結果入對象棧,繼續處理當前字符,直到遇到結束符。
為了處理方便,編譯程序常把中綴表達式首先轉換成等價的後綴表達式,後綴表達式的運算符在運算對象之後。在後綴表達式中,不在引入括號,所有的計算按運算符出現的順序, 嚴格從左向右進行,而不用再考慮運算規則和級別。中綴表達式“3*2^(4+2*2-1*3)-5 ”的後 綴表達式為:“32422*+13-^*5-”
(2) 後綴表達式求值
計算一個後綴表達式,算法上比計算一個中綴表達式簡單的多。這是因為表達式中即無括號又無優先級的約束。具體做法:只使用一個對象棧,當從左向右掃描表達式時,每遇到一個操作數就送入棧中保存,每遇到一個運算符就從棧中取出兩個操作數進行當前的計算, 然後把結果再入棧,直到整個表達式結束,這時送入棧頂的值就是結果。
下面是後綴表達式求值的算法,在下面的算法中假設,每個表達式是合乎語法的,並且 假設後綴表達式已被存入一個足夠大的字符數組 A 中,且以‘#’為結束字符,為了簡化問題, 限定運算數的位數僅為一位且忽略了數字字符串與相對應的數據之間的轉換的問題。
typedef char SElemType ; double calcul_exp(char *A){ //本函數返回由後綴表達式 A 表示的表達式運算結果 SqStack s ; ch=*A++ ; InitStack(s) ; while ( ch != ’#’ ){ if (ch!=運算符) Push (s , ch) ; else { Pop (s , &a) ; Pop (s , &b) ; //取出兩個運算量 switch (ch).{ case ch= =’+’: c=a+b ; break ; case ch= =’-’: c=a-b ; break ; case ch= =’*’: c=a*b ; break ; case ch= =’/’: c=a/b ; break ; case ch= =’%’: c=a%b ; break ; } Push (s, c) ; } ch=*A++ ; } Pop ( s , result ) ; return result ; }
(3) 中綴表達式轉換成後綴表達式:
將中綴表達式轉化為後綴表達示和前述對中綴表達式求值的方法完全類似,但只需要運 算符棧,遇到運算對象時直接放後綴表達式的存儲區,假設中綴表達式本身合法且在字符數組 A 中,轉換後的後綴表達式存儲在字符數組 B 中。
具體做法:遇到運算對象順序向存儲後綴表達式的 B數組中存放,遇到運算符時類似於中綴表達式求值時對運算符的處理過程,但運算符出棧後不是進行相應的運算,而是將其送入B 中存放。
3、棧與遞歸
在高級語言編制的程序中,調用函數與被調用函數之間的鏈接和信息交換必須通過棧進行。當在一個函數的運行期間調用另一個函數時,在運行該被調用函數之前,需先完成三件事:
(1)將所有的實在參數、返回地址等信息傳遞給被調用函數保存;
(2)為被調用函數的局部變量分配存儲區;
(3)將控制轉移到被調用函數的入口。
從被調用函數返回調用函數之前,應該完成:
(1)保存被調函數的計算結果;
(2)釋放被調函數的數據區;
(3)依照被調函數保存的返回地址將控制轉移到調用函數。
多個函數嵌套調用的規則是:後調用先返回,此時的內存管理實行“棧式管理”。
遞歸函數的調用類似於多層函數的嵌套調用,只是調用單位和被調用單位是同一個函數而已。
將遞歸程序轉化為非遞歸程序時常使用棧來實現。
隊列
隊列的定義及基本運算
棧是一種後進先出的數據結構,在實際問題中還經常使用一種“先進先出”的數據結構: 即插入在表一端進行,而刪除在表的另一端進行,將這種數據結構稱為隊或隊列,把允許插入的一端叫隊尾(rear) ,把允許刪除的一端叫隊頭(front)。
隊列的存儲實現及運算實現
與線性表、棧類似,隊列也有順序存儲和鏈式存儲兩種存儲方法。
1.順序隊列
循環隊列的類型定義如下:
#define MAXQSIZE 100 //大隊列長度 typedef struct { QElemType *base; //動態分配存儲空間 int front; //頭指針,若隊列不空,指向隊列頭元素 int rear; //尾指針,若隊列不空,指向隊列尾元素的下一個位置 } SqQueue;
(1)入隊:
int EnQueue (SqQueue &Q, QElemType e) { if((Q.rear+1)%MAXQSIZE == Q.front) return ERROR; Q.base[Q.rear] = e; Q.rear = (Q.rear+1) % MAXQSIZE; return OK; }
(2)出隊:
int DeQueue (SqQueue &Q, QElemType &e) { if (Q.front = = Q.rear) return ERROR; e = Q.base[Q.front]; Q.front = (Q.front+1) % MAXQSIZE; return OK; }
(3)求循環隊列元素個數:
int QueueLength(SqQueue Q){ return (Q.rear-Q.front+MAXQSIZE) %MAXQSIZE; }
2.鏈隊列
鏈式存儲的隊稱為鏈隊列。和鏈棧類似,用單鏈表來實現鏈隊列,根據隊的先進先出原 則,為了操作上的方便,分別需要一個頭指針和尾指針。
鏈隊列的形式描述如下:
typedef struct QNode { // 結點類型 QElemType data; struct QNode *next; } QNode, *QueuePtr; typedef struct { //鏈隊列類型 QueuePtr front; //隊頭指針 QueuePtr rear; //隊尾指針 } LinkQueue;
定義一個指向鏈隊列的指針:LinkQueue Q;
下面是鏈隊列的基本運算的實現。
(1)入隊
int EnQueue (LinkQueue &Q, QElemType e) { QNode *p; p = (QNode *)malloc(sizeof(QNode)); p->data = e; p->next = NULL; Q.rear->next = p; Q .rear = p; return OK; }
(2)出隊
int DeQueue (LinkQueue &Q, QElemType &e) { if (Q.front == Q.rear) return ERROR; //隊空,出隊失敗 p = Q.front->next; e = p->data; //隊頭元素放 e 中 Q.front->next = p->next; if(Q.rear==p) Q.rear= Q.front; //只有一個元素時,此時還要修改隊尾指針 free (p); return OK; }
3.除了棧和隊列之外,還有一種限定性數據結構是雙端隊列。
(1)雙端隊列:可以在雙端進行插入和刪除操作的線性表。
(2)輸入受限的雙端隊列:線性表的兩端都可以輸出數據元素,但是只能在一端輸入數 據元素。
(3)輸出受限的雙端隊列:線性表的兩端都可以輸入數據元素,但是只能在一端輸出數 據元素。
特殊矩陣的壓縮存儲
數組
數組可以看作線性表的推廣。數組作為一種數據結構其特點是結構中的元素本身可以是具有某種結構的數據,但屬於同一數據類型,數組是一個具有固定格式和數量的數據有序集, 每一個數據元素有唯一的一組下標來標識,因此,在數組上不能做插入、刪除數據元素的操作。
通常在各種高級語言中數組一旦被定義,每一維的大小及上下界都不能改變。
通常,數組在內存被映象為向量,即用向量作 為數組的一種存儲結構,這是因為內存的地址空間是一維的,數組的行列固定後,通過一個 映象函數,則可根據數組元素的下標得到它的存儲地址。
對於一維數組按下標順序分配即可。對多維數組分配時,要把它的元素映象存儲在一維存儲器中,一般有兩種存儲方式:一是以行為主序(或先行後列)的順序存放,另一種是以列為主序(先列後行)的順序存放。
設有 m×n 二維數組 Amn,下面我們看按元素的下標求其地址的計算:
以“以行為主序”的分配為例:設數組的基址為 LOC(a11),每個數組元素占據 d 個地址單 元,那麽 aij 的物理地址可用一線性尋址函數計算:
LOC(aij) = LOC(a11) + ( (i-1)*n + j-1 ) * d
這是因為數組元素 aij的前面有 i-1 行,每一行的元素個數為 n,在第 i 行中它的前面還有 j-1 個數組元素。
在 C 語言中,數組中每一維的下界定義為 0,則: LOC(aij) = LOC(a00) + ( i*n + j ) * d 推廣到一般的二維數組:A[c1...d1][c2...d2],則 aij的物理地址計算函數為:
LOC(aij)=LOC(a c1 c2)+( (i- c1) *( d2 - c2 + 1)+ (j- c2) )d
數據結構(二)棧、隊列和數組