前言->本章重點:
1.介紹抽象資料型別(ADT的概念)
2.闡述如何對錶進行有效的操作
3.介紹棧ADT及其在實現遞迴方面的應用
4.介紹佇列ADT及其在作業系統和演算法設計中的應用
3.1 抽象資料型別
抽象資料型別(abstract data type,ADT)是一些操作的集合。抽象資料型別是數學的抽象:在ADT的定義中根本沒涉及如何實現操作的集合。這可以看作是模組化設計的擴充。
3.2 表ADT
我們將處理一般的形如A1,A2,A3...An的表。我們說,這個表的大小是N,我們稱大小為0的表為空表(empty list)。
現在我們討論 在表ADT上進行的操作的集合。PrintList和MakeEmpty是常用的操作,其功能顯而易見;Find返回關鍵字首次出現的位置;Insert和Delete一般是從表的某個位置插人和刪除某個關鍵字;而FindKth 則返回某個位置上(作為引數而被指定)的元素。
3.2.1 表的簡單陣列實現
侷限性:陣列的大小一旦確定就無法改變,通常需要估計的大一些,從而會浪費大量的空間,這是嚴重的侷限,特別是針對於許多未知大小的表。
執行時間:
PrintList和Find為O(n),FindKth為O(1),而Insert和Delete:最壞為O(n),花費是昂貴的,例如,在位置0的插人(這實際上是做一個新的第一元素)首先需要將整個陣列後移一個位置以空出空間來,而刪除第一個元素則需要將表中的所有元素前移一個位置,因此這兩種操作的最壞情況為O(N)。
分析:因為插入和刪除的執行時間是如此的慢以及表的大小還必須事先已知,所以簡單陣列一般不用來實現表這種結構。
3.2.2 連結串列
引入:為了避免插入和刪除的線性開銷,我們允許表可以不連續儲存。
定義:連結串列由一系列不必在記憶體中相連的結構組成。每一個結構均含有表元素和指向包含該元素後繼元的結構的指標。我們稱之為Next指標。最後一個單元的Next 指標指向 NULL;該值由C定義並且不能與其他指標混淆。ANSI C規定 NULL為零。
執行時間:
PrintList和Find為O(n),FindKth為O(n)。而Delete可以透過修改一個指標實現,Insert需要使用一次malloc呼叫從系統中得到一個新單元,並進行兩次指標調整。如下圖所示。
3.2.3 程式設計實現
head指標的引入:上面的描述存在一些問題,第一,並不存在從所給定義出發在表的前面插入元素的真正顯性的方法。第二,從表的前面實行刪除是一個特殊情況,因為它改變了表的起始端;程式設計中的疏忽將會造成表的丟失。第三個問題涉及一般的刪除。雖然上述指標的移動很簡單,但是刪除演算法要求我們記住被刪除元素前面的表元。
ADT的例程:
/*list.h*/
#ifndef _LIST_H
typedef int ElementType;
struct Node;
typedef struct Node * PtrToNode;
typedef PtrToNode List;
typedef PtrToNode Position;
List MakeEmpty(List L);//生成一個空連結串列
int IsEmpty(List L);//判斷是否為空連結串列
int IsLast(List L,Position P);//判斷是否是最後一個節點
Position Find(List L,ElementType X);//查詢含有元素X的結點
Position FindPrevious(List L,ElementType X); //查詢含有元素X的結點的前驅元
void Insert(List L,Position P,ElementType X);//在P位置之後,插入一個元素為X的結點
void Delete(List L,Position P);//刪除P結點
void DeleteList(List L);//刪除連結串列
Position Header(List L);//返回表頭結點
Position First(List L);//返回第一個結點
Position Advance(Position P); //返回P結點的後繼元
ElementType Retrieve(Position P);//返回P結點的元素
#endif
struct Node{
ElementType Element;
Position Next;
};
/*list.c*/
# include "list.h"
# include <stdlib.h>
# include <stdio.h>
//生成一個空連結串列
List MakeEmpty(List L){
if (L != NULL) {
DeleteList(L);
}else{
L = (List)malloc(sizeof(struct Node));
if(L==NULL){
printf("out of space!\n");
exit(1);
}
L->Next=NULL;
}
return L;
}
//判斷是否為空連結串列
int IsEmpty(List L){
return L->Next == NULL;
}
//判斷是否是最後一個節點
int IsLast(List L,Position P){
return P->Next == NULL;
}
//查詢含有元素X的結點(找不到P為NULL)
Position Find(List L,ElementType X){
Position P = L->Next;
while(P && P->Element!=X){
P = P->Next;
}
return P;
}
//查詢含有元素X的結點的前驅元 (找不到P為最後一個元素)
Position FindPrevious(List L,ElementType X){
Position P = L;
while(P->Next && P->Next->Element!=X){
P = P->Next;
}
return P;
}
//在P位置之後,插入一個元素為X的結點
void Insert(List L,Position P,ElementType X){
Position TempCell;
TempCell = (Position)malloc(sizeof(struct Node));
if (TempCell == NULL) {
printf("out of space!\n");
exit(1);
}
TempCell->Element = X;
TempCell->Next = P->Next;
P->Next = TempCell;
}
//刪除P結點
void Delete(List L,ElementType X){
Position PreviousCell,TmpCell;
PreviousCell = FindPrevious(L,X);
if(!IsLast(L,PreviousCell)){
TmpCell = PreviousCell->Next;
PreviousCell->Next=TmpCell->Next;
free(TmpCell);
}
}
//刪除連結串列(表頭還在)
void DeleteList(List L){
Position P = L->Next;
Position TempCell;
while(P!=NULL){
TempCell = P->Next;
free(P);
P=TempCell;
}
}
//返回表頭結點
Position Header(List L){
return L;
}
//返回第一個結點
Position First(List L){
return L->Next;
}
//返回P結點的後繼元
Position Advance(Position P){
return P->Next;
}
//返回P結點的元素
ElementType Retrieve(Position P){
return P->Element;
}
3.2.4 常見的錯誤
1."memory access violation"或"segmentation violation",這種資訊通常意味著有指標變數包含了偽地址。一種情況是未初始化變數P就直接使用,P未定義就不可能指向記憶體中的有效部分;一種情況是如果P是Null,那麼指向是非法的。
2.何時使用或何時不使用malloc來獲取一個新的單元。我們必須記住,宣告指向一個結構的指標並不建立該結構,而只是給出足夠的空間容納結構可能會使用的地址。建立尚未被宣告過的記錄的惟一方法是使用malloc庫函式。
警告:malloc(sizeof(PtrToNode))是合法的,但是它並不給結構體分配足夠的空間。它只給指標分配一個空間。
3.2.5 雙連結串列
引入:為了實現倒敘掃描連結串列,另外,簡化了刪除操作,我們不需要找到前驅元。
3.2.6 迴圈連結串列
讓最後的單元反過來直指第一個單元是一種流行的做法。它可以有表頭,也可以沒有表頭(若有表頭,則最後的單元就指向它),並且還可以是雙向連結串列(第一個單元的前驅元指標指向最後的單元)。圖3-17 顯示了一個無表頭的雙向迴圈連結串列。
3.2.7 三個使用連結串列的例子
1.多項式
我們可以用表來定義一種關於一元(具有非負次冪)多項式的抽象資料型別。令$F(X) = \sum_{i=0}NA_iX$。如果大部分系數非零,那麼我們可以用一個簡單陣列來儲存這些係數。然後可以編寫一些對多項式進行加、減、乘、微分及其他操作的例程。
但如果P(X)=10X1000+5X14+1,且P(X)=3X1990-2X1492+ 11X + 5,那麼執行時間就可能不可接受了。可以看出,大部分的時間都花在了乘以0和單步除錯兩個輸人多項式的大量不存在的部分上。這總是我們不願看到的。
陣列實現
//多項式ADT的陣列實現的型別宣告
typedef struct{
int CoeffArray[ MaxDegree+1];
int HighPower;//最高次冪
} * Polynomial;
//將多項式初始化為零
void ZeroPolynomial(Polynomial Poly){
int i;
for(i=0;i<=MaxDegree;i++){
Poly->CoeffArray[i]=0;
}
Poly->HighPower=0;
}
//兩個多項式相加
void AddPolynomial(const Polynomial Poly1,const Polynomial Poly2,Polynomial PolySum){
int i;
ZeroPolynomial(PolySum);
PolySum->HighPower=Max(Poly1->HighPower,Poly2->HighPower);
for(i=0;i<=PolySum->HighPower;i++){
PolySum->CoeffArray[i]=Poly1->CoeffArray[i]+Poly2->CoeffArray[i];
}
}
//兩個多項式相乘
void MultPolynomial(const Polynomial Poly1,const Polynomial Poly2,Polynomial PolyMult){
int i,j;
ZeroPolynomial(PolyMult);
PolyMult->HighPower=Poly1->HighPower+Poly2->HighPower;
for(i=0;i<=Poly1->HighPower;i++){
for(j=0;i<=Poly2->HighPower;i++){
PolyMult->CoeffArray[i+j]+=Poly1->CoeffArray[i]*Poly2->CoeffArray[j];
}
}
}
單連結串列實現(但這個時候,相乘或者相加要注意合併同類項)
//多項式ADT的連結串列表示
typedef struct Node *PtrToNode;
struct Node {
int Coefficient;//係數
int Exponent;//指數
PtrToNode Next;
};
typedef PtrToNode Polynomial;
2.基數排序
使用連結串列的第二個例子叫做基數排序(radix sort)。基數排序有時也稱為卡式排序(card sort),因為直到現代計算機出現之前,它一直用於對老式穿孔卡的排序。
如果我們有N個整數,範圍從1到M(或從0到M-1),我們可以利用這個資訊得到一種快速的排序,叫做桶式排序(bucket sort)。我們留置一個陣列,稱之為 Count,大小為M,並初始化為零。於是,Count有M個單元(或桶),開始時他們都是空的。當Ai被讀入時Count[Ai]增1。在所有的輸人被讀進以後,掃描陣列Gount,列印輸出排好序的表。該演算法花費O(M+N)。
基數排序是這種方法的推廣。瞭解方法含義的最容易的方式就是舉例說明。設我們有10個數,範圍在0到999之間,我們將其排序。一般說來,這是0到N^P-1間的N個數,P是某個常數。顯然,我們不能使用桶式排序,那樣桶就太多了。我們的策略是使用多趟桶式排序。
我們用最低(有效)“位”優先的方式進行桶式排序。有可能多於一個數落入相同的桶中,但有別於原始的桶式排序,這些數可能不同,因此我們把它們放到一個表中。注意,所有的數可能都有某位數字,因此如果使用簡單陣列表示表,那麼每個陣列必然大小為N,總的空間需求是 O(N^2)。下面例子說明10個數的桶式排序的具體做法。
為使演算法能夠得出正確的結果,要注意唯一出錯的可能是如果兩個數出自同一個桶但順序卻是錯誤的。不過,前面各趟排序保證了當幾個數進入一個桶的時候,它們是以排序的順序進入的。該排序的執行時間是O(P(N+B)),其中P是排序的趟數,N是要被排序的元素的個數,而B是桶數。本例中,B=N。
3.多重表
我們的最後一個例子闡述連結串列的更復雜的應用。一所有40000名學生和2500門課程的大學需要生成兩種型別的報告。第一個報告列出每個課程的註冊者,第二個報告列出每個學生註冊的課程。
常用的實現方法是使用二維陣列。這樣一個陣列將有1億項。平均大約一個學生註冊三門課程,因此實際上有意義的資料只有120000項,大約佔0.1%。現在需要的是列出每個課程及每個課程所包含的學生的表。我們也需要每個學生及其每個學生所註冊的課程的表。圖 3-27 顯示實現的方法。
3.2.8 連結串列的遊標實現
諸如BASIC和FORTRAN等許多語言都不支援指標。如果需要連結串列而又不能使用指標,那麼就必須使用另外的實現方法。我們將描述這種方法並稱為遊標(cursor)實現法。
在連結串列的指標實現中有兩個重要的特點:
1.資料儲存在一組結構體中。每一個結構體包含有資料以及指向下一個結構體的指標。
2.一個新的結構體可以透過呼叫malloc而從系統全域性記憶體(global memory)得到,並可透過呼叫 free 而被釋放。
遊標法必須能夠模仿實現這兩條特性。滿足條件1的邏輯方法是要有一個全域性的結構體陣列。對於該陣列中的任何單元,其陣列下標可以用來代表一個地址。下面給出連結串列遊標實現的宣告。
struct Node{
ElementType Element;
Position Next;
};
struct Node CursorSpace[SpaceSize];
現在我們必須模擬條件2,讓CursorSpace 陣列中的單元代行 malloc和 free的職能。為此,我們將保留一個表(即{reelist),這個表由不在任何表中的單元構成。該表將用單元0作為表頭。其初始配置如下圖中表示。對於 Next,0的值等價於NULL指標。CursorSpace的初始化是一個簡單的迴圈結構,為執行malloc 功能,將(在表頭後面的)第一個元素從freelist 中刪除。為了執行free功能,我們將該單元放在freelist的前端。注意,如果沒有可用空間,那麼我們的例程透過置P=0會正確的實現,他表示在沒有空間可以用。
申請和釋放空間程式碼如下:
//申請一塊空間
static Position CursorAlloc(void){
Position P ;
P = CursorSpace[0].Next;
CursorSpace[0].Next = CursorSpace[P].Next
return P;
}
//釋放一塊空間
static void CursorFree(Position P) {
CursorSpace[P].Next = CursorSpace[0].Next;
CursorSpace[0].Next = p;
}
//初始化遊標陣列
void InitializeCursorSpace(void){
int i;
for (i = 0; i < SpaceSize - 1; i++){
CursorSpace[i].Next = i + 1;
}
CursorSpace[0].Element=0;
CursorSpace[i].Next = 0;
}
下面是遊標實現連結串列的原始碼:
cursor.h
#ifndef _Cursor_H
typedef int ElementType;
typedef int PtrToNode;
typedef PtrToNode List;
typedef PtrToNode Position;
#define SpaceSize 50
void InitializeCursorSpace(void);
List MakeEmpty(List L);
int IsEmpty(const List L);
int IsLast(const List L,const Position P);
Position Find(const List L,ElementType X);
Position FindPrevious(List L,ElementType X);
void Insert(List L,Position P,ElementType X);
void Delete(List L,ElementType X);
void DeleteList(List L);
Position Header(const List L);
Position First(const List L);
Position Advance(const Position P);
ElementType Retrieve(const Position P);
#endif
struct Node{
ElementType Element;
Position Next;
};
struct Node CursorSpace[SpaceSize];
cursor.c
#include <stdio.h>
#include "cursor.h"
//初始化遊標陣列
void InitializeCursorSpace(void){
int i;
for (i = 0; i < SpaceSize - 1; i++){
CursorSpace[i].Next = i + 1;
}
CursorSpace[0].Element=0;
CursorSpace[i].Next = 0;
}
//申請一塊空間
static Position CursorAlloc(void){
Position P ;
P = CursorSpace[0].Next;
CursorSpace[0].Next = CursorSpace[P].Next
return P;
}
//釋放一塊空間
static void CursorFree(Position P) {
CursorSpace[P].Next = CursorSpace[0].Next;
CursorSpace[0].Next = p;
}
//生成一個空連結串列
List MakeEmpty(List L){
if (CursorSpace[L].Next != 0) {
DeleteList(L);
}
return L;
}
//判斷是否為空連結串列
int IsEmpty(const List L){
return CursorSpace[L].Next == 0;
}
//判斷是否是最後一個節點
int IsLast(const List L,const Position P){
return CursorSpace[P].Next == 0;
}
//查詢含有元素X的結點(找不到P為NULL)
Position Find(const List L,ElementType X){
Position Temp = CursorSpace[L].Next;
while(Temp!=0 && CursorSpace[Temp].Element!=X){
Temp = CursorSpace[Temp].Next;
}
}
//查詢含有元素X的結點的前驅元 (找不到P為最後一個元素)
Position FindPrevious(const List L,ElementType X){
Temp = CursorSpace[L].Next;
while(Temp!=0 && CursorSpace[Temp].Element!=X){
Temp = CursorSpace[Temp].Next;
}
}
//在P位置之後,插入一個元素為X的結點
void Insert(List L,Position P,ElementType X){
Position Temp = CursorAlloc();
if(Temp ==0 ){
printf("Out Of Space!!");
return;
}
CursorSpace[Temp].Element =X;
CursorSpace[Temp].Next = CursorSpace[P].Next;
CursorSpace[P].Next = Temp;
}
//刪除P結點
void Delete(List L,ElementType X){
Position P = FindPrevious(L,X);
Position Temp;
if(!IsLast(L,P)){
Temp = CursorSpace[P].Next;
CursorSpace[P].Next = CursorSpace[Temp].Next;
CursorFree(Temp);
}
}
//刪除連結串列(表頭還在)
void DeleteList(List L){
Position P = CursorSpace[L].Next;
Position TempCell;
while(P!=0){
TempCell = CursorSpace[P].Next;
CursorFree(P);
P=TempCell;
}
}
//返回表頭結點
Position Header(const List L){
return L;
}
//返回第一個結點
Position First(const List L){
return CursorSpace[L].Next;
}
//返回P結點的後繼元
Position Advance(const Position P){
return CursorSpace[P].Next;
}
//返回P結點的元素
ElementType Retrieve(const Position P){
return CursorSpace[P].Element;
}
3.3 棧
引入:freelist 從字面上看錶示一種有趣的資料結構。從freelist 刪除的單元是剛剛由free 放在那裡的單元。因此,最後被放在freelist的單元是被最先拿走的單元。有一種資料結構也具有這種性質,叫作棧(stack)。
3.3.2 棧模型
棧(stack)是限制插入和刪除只能在一個位置上進行的表,該位置是表的末端,叫做棧的頂(top)。對棧的基本操作有Push(進棧)和Pop(出棧),前者相當於插入,後者則是刪除最後插入的元素。最後插人的元素可以透過使用Top例程在執行Pop之前進行考查。對空棧進行的 Pop 或 Top 一般被認為是棧 ADT的錯誤。另一方面,當執行 Push 時空間用盡是一個實現錯誤,但不是 ADT錯誤。棧有時又叫做 LIFO(後進先出)表。
3.3.2 棧的實現
由於棧是一個表,因此任何實現表的方法都能實現棧。
1.連結串列實現
#ifndef _Stack_h
struct Node;
typedef struct Node *PtrToNode;
typedef PtrToNode Stack;
typedef int ElementType;
int IsEmpty(Stack S);
Stack CreateStack();
void DisposeStack(Stack S);
void MakeEmpty(Stack S);
void Push(Stack S,ElementType X);
ElementType Top(Stack S);
void Pop(Stack S);
#endif
struct Node{
ElementType Element;
PtrToNode Next;
};
#include "stack.h"
#include <stdio.h>
#include <stdlib.h>
//判斷棧是否為空
int IsEmpty(Stack S){
return S->Next==NULL;
}
//建立一個空棧
Stack CreateStack(){
Stack S;
S = (Stack)malloc(sizeof(struct Node));
if(S==NULL){
printf("Error! Out Of Space!");
}
S->Next=NULL;
return S;
}
//重置棧
void DisposeStack(Stack S){
PtrToNode P,TempCell;
P = S->Next;
S->Next=NULL;
while(P){
TempCell = P->Next;
free(P);
P = TempCell;
}
free(S);
}
//使棧為空
void MakeEmpty(Stack S){
if(S==NULL){
printf("Error! Must use CreateStack first!");
}
while(!IsEmpty(S)){
Pop(S);
}
}
//進棧
void Push(Stack S,ElementType X){
PtrToNode tempCell;
tempCell = (PtrToNode)malloc(sizeof(struct Node));
if(tempCell==NULL){
printf("Error! Out Of Space!");
}else{
tempCell->Element = X;
tempCell->Next = S->Next;
S->Next=tempCell;
}
}
//棧頂元素
ElementType Top(Stack S){
if(!IsEmpty(S)){
return S->Next->Element;
}else{
printf("Error! Empty stack!!");
}
}
//出棧
void Pop(Stack S){
if(!IsEmpty(S)){
PtrToNode FirstCell = S->Next;
S->Next=FirstCell->Next;
free(FirstCell);
}else{
printf("Error! Empty stack!!");
}
}
2.陣列實現
用一個陣列實現棧是很簡單的。每一個棧有一個TopOfStack,對於空棧它是-1(這就是空棧的初始化)。為了將某個元素X壓入到該棧中,我們將TopOfStack加1,然後置Stack[TopOfStack]= X.其中 Stack是代表具體棧的陣列。為了彈出棧元素,我們置返回值為 Stack[TopOfStack],然後 TopOfStack 減1。
注意,這些操作不僅以常數時間執行,而且是以非常快的常數時間執行。在某些機器上,若在帶有自增和自減定址功能的暫存器上操作,則(整數的)Push和Pop都可以寫成一條機器指令。最現代化的計算機將棧操作作為它的指令系統的一部分,這個事實強化了這樣一種觀念,即棧很可能是在電腦科學中在陣列之後最基本的資料結構。
一個影響棧的執行效率的問題是錯誤檢測。我們的連結串列實現中是仔細地檢查錯誤的。正如上面所描述的,對空棧的Pop或是對滿棧的Push都將超出陣列的界限並引起程式崩潰顯然,我們不願意出現這種情況。但是,如果把對這些條件的檢測放到陣列實現過程中,那就很可能要花費像實際棧操作那樣多的時間。由於這個原因,除非在錯誤處理極其重要的場合(如在作業系統中),一般在棧例程中省去錯誤檢測就成了普通的慣用手法。
#ifndef _Stack_h
struct StackRecord;
typedef struct StackRecord *Stack;
typedef int ElementType;
int IsEmpty(Stack S);
int IsFull(Stack S);
Stack CreateStack(int MaxElements);
void DisposeStack(Stack S);
void MakeEmpty(Stack S);
void Push(Stack S,Element X);
ElementType Top(Stack S);
void Pop(Stack S);
ElementType TopAndPop(Stack S);
#endif
#define EmptyTos (-1)
#define MinStackSize (5)
struct StackRecord{
int Capacity;//棧的容量
int TopOfStack;//棧頭元素索引
ElementType * Array;
};
#include "stack.h"
int IsEmpty(Stack S){
return S->TopOfStack==EmptyTos;
}
void MakeEmpty(Stack S){
S->TopOfStack==EmptyTos;
}
int IsFull(Stack S){
return S->TopOfStack=S->Capacity-1;
}
Stack CreateStack(int MaxElements){
if(MaxElements<MinStackSize){
printf("Error!Stack size is too small!");
}else{
Stack S;
S = (Stack)malloc(sizeof(struct StackRecord));
if(S==NULL){
printf("Error!Out Of Space!");
}else{
S->Capacity = MaxElements;
S->Array = (ElementType*)malloc(MaxElements*sizeof(ElementType));
if(S->Array ==NULL){
printf("Error!Out Of Space!");
}else{
MakeEmpty(S);
return S;
}
}
}
}
void DisposeStack(Stack S){
if(S!=NULL){
free(S->Array);
free(S);
}
}
void Push(Stack S,Element X){
if(!IsFull(S)){
S->Array[++(S->TopOfStack)] = X;
}else{
printf("Error!Full Stack!");
}
}
ElementType Top(Stack S){
if(!IsEmpty(S)){
return S->Array[S->TopOfStack];
}else{
printf("Error!Empty Stack!");
return 0;
}
}
void Pop(Stack S){
if(!IsEmpty(S)){
S->TopOfStack--;
}else{
printf("Error!Empty Stack!");
}
}
ElementType TopAndPop(Stack S){
if(!IsEmpty(S)){
return S->Array[S->TopOfStack--];
}else{
printf("Error!Empty Stack!");
}
return 0 ;
}
3.3.3 應用
毫不奇怪,如果我們把操作限制於一個表,那麼這些操作會執行得很快。然而,令人驚奇的是,這些少量的操作非常強大和重要。在棧的許多應用中,我們給出三個例子,第三個例項深刻說明程式是如何組織的。
1.平衡符號
編譯器檢查你的程式的語法錯誤,但是常常由於缺少一個符號(如遺漏一個花括號或是註釋起始符)引起編譯器列出上百行的診斷,而真正的錯誤並沒有找出。
在這種情況下一個有用的工具就是檢驗是否每件事情都能成對出現的一個程式。於是每一個右花括號、右方括號及右圓括號必然對應其相應的左括號。序列“[()]”是合法的,但“[( ])"是錯誤的。顯然,不值得為此編寫一個大型程式,事實上檢驗這些事情是很容易的。為簡單起見,我們僅就圓括號、方括號和花括號進行檢驗並忽略出現的任何其他字元。這個簡單的演算法用到一個棧,敘述如下:
做一個空棧。讀入字元直到檔案尾。
如果字元是一個開放符號,則將其推入棧中。
如果字元是一個封閉符號,則當棧空時報錯。否則,將棧元素彈出。
如果彈出的符號不是對應的開放符號,則報錯。
在檔案尾,如果棧非空則報錯。
你應該能夠確信這個演算法是會正確執行的。很清楚,它是線性的,事實上它只需對輸入進行一趟檢驗。因此,它是線上(on-line)的,是相當快的。當報錯時,決定如何處理需要做一些附加的工作--例如判斷可能的原因。
2.1 字尾表示式求值
假設我們有一個便攜計算器並想要計算一趟外出購物的花費。為此,我們將一列資料相加並將結果乘以1.06:它是所購物品的價格以及附加的地方稅。如果購物各項花銷為4.99.5.99,和6.99,那麼輸人這些資料的自然的方式將是4.99+5.99+6.99* 1.06=;隨著計算器的不同,這個結果或者是所要的答案19.05,或者是科學容案18.39。
最簡單的四功能計算器將給出第一個答案,但是許多先進的計算器是知道乘法的優先順序是高於加法的。
另一方面,有些項是需要上稅的而有些項則不是,因此,如果只有第一項和最後一項是要上稅的,那麼4.99* 1.06+5.99+6.99 * 1.06=將在科學計算器上給出正確的答案(18.69),而在簡單計算器上給出錯誤的答案(19.37) 。
科學計算器一般包含括號,因此我們總可以透過加括號的方法得到正確的答案,但是使用簡單計算器我們需要記住中間結果。該例的典型計算順序可以是將4.99和1.06相乘並存為A,然後將5.99和A 相加,再將結果存人A。我們再將6.99和1.06相乘並將答案存為A2,最後將A和相Az加並將最後結果放人A。我們可以將這種操作順序書寫如下:
4.99 1.06* 5.99+6.99 1.06 * +
這個記法叫做字尾 或 逆波蘭記法,其求值過程恰好就是我們上面所描述的過程。計算這個問題最容易的方法是使用一個棧。當見到一個數時就把它推入棧中;在遇到一個運算子時該算符就作用於從該棧彈出的兩個數(符號)上,將所得結果推入棧中,例如,字尾表示式
6 5 2 3 + 8 * + 3 + *
計算一個字尾表示式花費的時間是 (N),因為對輸入中的每個元素的處理都是由一些棧操作組成從而花費常數時間。該演算法的計算是非常簡單的。注意,當一個表示式以後級記號給出時,沒有必要知道任何優先規則。這是一個明顯的優點。
程式碼實現:
bool isNumber(char* token) {
return strlen(token) > 1 || ('0' <= token[0] && token[0] <= '9');
}
int evalRPN(char** tokens, int tokensSize) {
int n = tokensSize;
int stack[n], top = 0;
for (int i = 0; i < n; i++) {
char* token = tokens[i];
if (isNumber(token)) {
stack[top++] = atoi(token);
} else {
char sign = token[0];
int m = stack[--top];
int n = stack[--top];
switch (sign) {
case '+':
stack[top++] = m + n;
break;
case '-':
stack[top++] = n - m;
break;
case '*':
stack[top++] = m * n;
break;
case '/':
stack[top++] = n / m;
break;
}
}
}
return stack[top - 1];
}
2.2中綴轉字尾表示式
棧不僅可以用來計算字尾表示式的值,而且我們還可以用棧將一個標準形式的表示式(或叫做中綴式(infix))轉換成字尾式。設我們欲將中綴表示式
a + b * c + ( d * e + f ) * g
轉換成字尾表示式。正確的答案是
a b c * + d e * f + g * +
當讀到一個運算元的時候,立即把它放到輸出中。運算子不立即輸出,從而必須先存在某個地方。正確的做法是將已經見到過的運算子放進棧中而不是放到輸出中。當遇到左圓括號時我們也要將其推入棧中。我們從一個空棧開始計算。
如果見到一個右括號,那麼就將棧元素彈出,將彈出的符號寫出直到我們遇到一個(對
應的)左括號,但是這個左括號只被彈出,並不輸出。如果我們見到任何其他的符號(“+”,“*”,“(”),那麼我們從棧中彈出棧元素直到發現優先順序更低的元素為止。有一個例外:除非是在處理一個“)”的時候,否則我們絕不從棧中移走“(”。對於這種操作,“+”的優先順序最低,而“(”的優先順序最高。當從棧彈出元素的工作完成後,我們再將運算子壓人棧中。
最後,如果我們讀到輸人的末尾,我們將棧元素彈出直到該棧變成空棧,將符號寫到輸出中。
為了理解這種演算法的執行機制,我們將把上面的中綴表示式轉換成字尾形式。
程式碼實現:
#include <stdio.h>
#include <stdlib.h>
//#include "stack.h"
#include <string.h>
/* run this program using the console pauser or add your own getch, system("pause") or input loop */
int isNumber(char* C);
void infixToPostfix(char** tokens, int infixSize,char** postfix);
int precedence(char sign);
char* charToString(char c);
int main(int argc, char *argv[]) {
char* infix [] = {"12","+","3","*","67","+","(","78","*","78","+","5",")","*","6"};
int infixSize = sizeof(infix)/sizeof(char*);
printf("字首表示式:");
int i;
for(i = 0; i < infixSize; i++){
if(i == infixSize - 1){
printf("%s\n", infix[i]);
} else {
printf("%s ", infix[i]);
}
}
char** postfix = (char**)malloc(infixSize * sizeof(char*));
infixToPostfix(infix,infixSize,postfix);
printf("字尾表示式:");//這裡-2是減去兩個中括號
for(i = 0; i < infixSize - 2; i++){
if(i == infixSize - 1){
printf("%s\n", postfix[i]);
} else {
printf("%s ", postfix[i]);
}
}
}
//獲取優先順序
int precedence(char sign){
switch(sign){
case '+':
case '-':
return 1;
break;
case '*':
case '/':
return 2;
break;
case '(':
case ')':
return 0;
break;
}
}
//判斷C是否為陣列
int isNumber(char* C){
return strlen(C)>1 || C[0]>='0' && C[0]<='9';
}
//中綴轉字尾表示式
void infixToPostfix(char** tokens, int infixSize,char** postfix) {
//棧 用於存放符號
char stack[infixSize],top = 0;
//輸出陣列的最右邊
int postfixTop = 0;
int i;
for(i=0;i<infixSize;i++){
char* token = tokens[i];
if(isNumber(token)){
postfix[postfixTop++] = token;
}else{
char sign = token[0];
//如果棧為空,或者新加的符號優先順序比棧頂元素的優先順序高,那麼直接把符號放入
if(top==0 || sign == '(' || precedence(sign) > precedence(stack[top-1])){
stack[top++] = sign;
//如果新加的符號是)那麼棧一直彈出元素直到碰見(
}else if(sign == ')'){
while(stack[top-1]!='('){
char temp = stack[--top];
postfix[postfixTop++] = charToString(temp);
}
top--;
//如果新加的符號是優先順序沒有棧頂符號高,那麼棧一直彈出元素
}else{
while(top>=0 && precedence(sign) <= precedence(stack[top-1])){
char temp = stack[--top];
postfix[postfixTop++] = charToString(temp);
}
stack[top++] = sign;
}
}
}
//將剩餘在棧中的符號彈出
while(top>0){
char temp = stack[--top];
postfix[postfixTop++] = charToString(temp);
}
return postfix;
}
//字元轉字串
char* charToString(char c){
char* array = (char*)malloc(2 * sizeof(char));
array[0] = c;
array[1] = '\0';
return array;
}
3.函式呼叫
檢測平衡符號的演算法提供一種實現函式呼叫的方法。這裡的問題是,當呼叫一個新函式時,主調例程的所有區域性變數需要由系統儲存起來,否則被呼叫的新函式將會覆蓋呼叫例程的變數。不僅如此,該主調例程的當前位置必須要儲存,以便在新函式執行完後知道向哪裡轉移。這些變數一般由編譯器指派給機器的暫存器,但存在某些衝突(通常所有的過程都將某些變數指派給1#暫存器),特別是涉及遞迴的時候。該問題類似於平衡符號的原因在於函式呼叫和函式返回基本上類似於開括號和閉括號,二者想法是一樣的。
當存在函式呼叫的時候,需要儲存的所有重要資訊,諸如暫存器的值(對應變數的名字)和返回地址(它可從程式計數器得到,典型情況下計數器就是一個暫存器)等,都要以抽象的方式存在“一張紙上”並被置於一個堆(pile)的頂部。然後控制轉移到新函式,該函式自由地用它的一些值代替這些暫存器。如果它又進行其他的函式呼叫,那麼它也遵循相同的過程。當該函式要返回時,它査看堆(pile)頂部的那張“紙"並復原所有的暫存器。然後它進行返回
轉移。顯然,所有全部工作均可由一個棧來完成,而這正是在實現遞迴的每一種程式設計語言中實際發生的事實。 所儲存的資訊或稱為活動記錄(activationrecord),或叫做棧幀(stackframe)。在典型情況下,需要做些微調整:當前環境是由棧頂描述的。因此,一條返回語句就可給出前面的環境(不用複製)。在實際計算機中的棧常常是從記憶體分割槽的高階向下增長,而在許多的系統中是不檢測溢位的。由於有太多的同時在執行著的函式,用盡棧空間的情況總是可能發生的。顯而易見,用盡棧空間常是致命的錯誤。
3.4 佇列
像棧一樣,佇列(queue)也是表。然而,使用佇列時插人在一端進行而刪除則在另一端進行。
3.4.1 佇列模型
佇列的基本操作是 Enqueue(入隊),它是在表的末端(叫做隊尾(rear))插入一個元素,還有 Dequeue(出隊),它是刪除(或返回)在表的開頭(叫做隊頭(front))的元素。圖 3-56顯示一個佇列的抽象模型。
3.4.2 佇列的陣列實現
如同棧的情形一樣,對於佇列而言任何表的實現都是合法的。像棧一樣,對於每一種操作,連結串列實現和陣列實現都給出快速的(1)執行時間。下面演示陣列模擬環形佇列實現。
#ifndef _Queue_h
struct QueueRecord;
typedef struct QueueRecord *Queue;
typedef int ElementType;
int IsEmpty(Queue Q);
int IsFull(Queue Q);
Queue CreateQueue(int MaxElements);
void DisposeQueue(Queue Q);
void MakeEmpty(Queue Q);
void Enqueue(Queue Q,ElementType X);
ElementType Front(Queue Q);
void Dequeue(Queue Q);
ElementType FrontAndDequeue(Queue Q);
#endif
# define MiQueueSize (5)
struct QueueRecord{
int Capacity;
int Front; //指向第一個元素
int Rear; //指向最後一個元素的後一個位置
ElementType *Array;
};
#include <stdio.h>
#include <stdlib.h>
#include "queue.h"
int IsEmpty(Queue Q){
return Q->Front == Q->Rear;
}
//為了區分陣列為空或者佇列滿的兩種情況,我們認定當當有maxSize-1個元素的時候,佇列為滿。
int IsFull(Queue Q){
return (Q->Rear+1) % Q->Capacity == Q->Front;
}
Queue CreateQueue(int MaxElements){
Queue Q = (Queue)malloc(sizeof(struct QueueRecord));
if(Q==NULL){
printf("Out Of Space!");
exit(1);
}
Q->Front= 0;
Q->Rear = 0;
Q->Capacity = MaxElements;
Q->Array = (ElementType*)malloc(MaxElements * sizeof(ElementType));
if(Q->Array==NULL){
printf("Out Of Space!");
exit(1);
}
return Q;
}
void DisposeQueue(Queue Q){
free(Q);
Q = NULL;
}
void MakeEmpty(Queue Q){
Q->Front= 0;
Q->Rear = 0;
}
void Enqueue(Queue Q,ElementType X){
if(IsFull(Q)) {
printf("Full Queue!");
exit(1);
}else{
Q->Array[Q->Rear] = X;
Q->Rear= (Q->Rear+1)% Q->Capacity;
}
ElementType Front(Queue Q){
if(IsEmpty(Q)){
printf("Empty Queue!!");
exit(1);
}
return Q->Array[Q->Front];
}
void Dequeue(Queue Q){
if(IsEmpty(Q)) {
printf("Empty Queue!");
exit(1);
}else{
Q->Front = (Q->Front+1)%Q->Capacity;
}
}
ElementType FrontAndDequeue(Queue Q){
if(IsEmpty(Q)) {
printf("Empty Queue!");
exit(1);
}else{
ElementType X = Q->Array[Q->Front];
Q->Front= (Q->Front+1)%Q->Capacity;
return X;
}
}
3.4.3 佇列的應用
有幾種使用佇列給出提高執行效率的演算法。它們當中有些可以在圖論中找到,我們將在第9章討論它們。這裡,先給出某些應用佇列的例子。
- 當作業送交給一臺行式印表機,它們就按照到達的順序被排列起來。因此,被送往行式印表機的作業基本上是被放到一個佇列中。
實際生活中的每次排隊都(應該)是一個佇列。例如,在一些售票口排列的隊都是佇列。因為服務的順序是先來到的先買票。 - 另一個例子是關於計算機網路的。有許多種PC機的網路設定,其中磁碟是放在一臺叫做檔案伺服器的機器上的。使用其他計算機的使用者是按照先到先使用的原則訪問檔案的,因此其資料結構是一個佇列。
- 進一步的例子如下:
當所有的接線員忙得不可開交的時候,對大公司的傳呼一般都被放到一個佇列中在大規模的大學裡,如果所有的終端都被佔用,由於資源有限,學生們必須在一個等待表上簽字。在終端上呆得時間最長的學生將首先被強制離開,而等待時間最長的學生則將是下一個被允許使用終端的使用者。處理用機率的方法計算使用者排隊預計等待時間、等待服務的佇列能夠排多長,以及其他些諸如此類的問題將用到被稱為排隊論(queueingtheory)的整個數學分支。
問題的答案依賴於使用者加入佇列的頻率以及一旦使用者得到服務時處理服務花費的時間。這兩個引數作為機率分佈函式給出。在一些簡單的情況下,答案可以解析算出。一種簡單的例子是一條電話線有一個接線員。如果接線員忙,打來的電話就被放到一個等待佇列中(這還與某個容許的最大限度有關)。這個問題在商業上很重要,因為研究表明,人們會很快掛上電話。
如果我們有k個接線員,那麼這個問題解決起來要困難得多。解析求解困難的問題往往使用模擬的方法求解。此時,我們需要使用一個佇列來進行模擬。如果k很大,那麼我們還需要其他一些資料結構來使得模擬更有效地進行。在第6章將會看到模擬是如何進行的。那時我們將對k的若干值進行模擬並選擇能夠給出合理等待時間的最小的k。正如棧一樣,佇列還有其他豐富的用途,這樣一種簡單的資料結構竟然能夠如此重要實在令人驚奇。