1. 程式人生 > 實用技巧 >3.0 棧和佇列

3.0 棧和佇列


title: 資料結構 | 棧和佇列
date: 2019-12-6 22:34:58
tags: 資料結構


順序棧、鏈棧、迴圈佇列、鏈佇列

  • 學習目標
    掌握棧和佇列這兩種抽象資料型別的特點,並能在相應的應用問題中正確選用它們。
    熟練掌握棧型別的兩種實現方法。
    熟練掌握迴圈佇列和鏈佇列的基本操作實現演算法。
    理解遞迴演算法執行過程中棧的狀態變化過程。
  • 知識點
    順序棧、鏈棧、迴圈佇列、鏈佇列

只允許在一端插入和刪除的順序表
允許插入和刪除的一端稱為棧頂 (top) —表尾
另一端稱為棧底(bottom) —表頭
不含元素的空表稱空棧
特點: 先進後出(FILO)或後進先出(LIFO)

ADT

ADT Stack
{
資料物件:D={ai|ai∈ElemSet,i=1,2,…,n,n≥0}
資料關係:R={<ai-1,ai>|ai,ai-1∈D,i=2,…,n}
		          約定:an為棧頂,a1為棧底
基本操作:
	InitStack(&S)
	操作結果:構造一個空的棧S。 
	DestroyStack(&S)
	初始條件: 棧S已經存在。
	操作結果: 銷燬棧S。
	ClearStack(&S)
	初始條件: 棧S已經存在。
	操作結果: 將棧S重置為空棧。	
	StackEmpty(S)
	初始條件: 棧S已經存在。
	操作結果: 若棧S為空棧,則返回TURE;否則返回FALSE
		-判定棧是否為空棧是棧在應用程式中經常使用的操作,通常以它作為迴圈結束的條件。 
	StackLength(S)
	初始條件: 棧S已經存在。
	操作結果: 返回棧S中的資料元素個數。
	GetTop(S,&e)
	初始條件: 棧S已經存在且非空。
	操作結果: 用e返回棧S中棧頂元素的值。
		-這是取棧頂元素的操作,只以 e 返回棧頂元素,並不將它從棧中刪除。
	Push(&S,e)
	初始條件: 棧S已經存在。
	操作結果: 插入元素e為新的棧頂元素。
		-這是入棧操作,在當前的棧頂元素之後插入新的棧頂元素。 
	Pop(&S,&e)
	初始條件: 棧S已經存在且非空。
	操作結果: 刪除S的棧頂元素並用e返回其值。
		-這是出棧操作,不僅以 e 返回棧頂元素,並將它從棧中刪除。 
	StackTraverse(S,visit ())
	初始條件: 棧S已經存在且非空。
	操作結果: 從棧底到棧頂依次對S的每個元素呼叫函式visit ()。一旦visit ()失敗,則操作失敗。
		-這是對棧進行從棧底到棧頂的"遍歷"操作,應用較多的場合是,輸出棧中所有資料元素。
}

棧的表示和實現

兩種

  • 順序儲存結構__順序棧
  • 鏈式儲存結構__鏈棧

順序棧的定義

利用一組地址連續的儲存單元依次自棧底到棧頂存放棧的資料元素。

  • 棧頂指標top
    指向實際棧頂後的空位置,初值為0
  • top=0,棧空, 此時出棧,則下溢(underflow)
    top=M,棧滿,此時入棧,則上溢(overflow)

順序棧的資料型別

#define STACK_INIT_SIZE   100;//儲存空間初始分配量
#define STACKINCREMENT   10;//儲存空間分配增量

typedef struct
{
     SElemType   *base; //在棧構造之前和銷燬之後,值為null
     SElemType   *top;   //棧頂指標
     int   StackSize;  //當前已分配的儲存空間,以元素為單位
} SqStack;

順序棧的操作

InitStack

Status InitStack( SqStack &S )
{
	S.Base=(SElemType *)malloc(STACK_INIT_SIZE*sizeof(SElemType));
	if(!S.Base)
	{
		return OVERFLOW;
	}
	S.Top = S.Base;
	S.StackSize = STACK_INIT_SIZE;
	return OK;
}// InitStack

GetTop

// 用e返回棧S的棧頂元素,若棧空,函式返回ERROR
Status GetTop( SqStack S, SElemType &e)  
{
	if( S.Top != S.Base )		// 棧空嗎?
	{
		e = *( S.Top – 1 );
		return OK;
	}
	else
	{
		return ERROR;
	}
}// GetTop

Push

//把元素e入棧
Status Push(SqStack &S, SElemType e )
{
	// 若棧滿,追加儲存空間
	if( S.Top >= S.Base + S.StackSize )
	{
		S.Base= (SElemType *)realloc(S.Base,(S.StackSize + STACKINCREMENT) *sizeof(SElemType));
		if( !S.Base )
			return OVERFLOW; //儲存分配失敗
		S.Top = S.Base + S.StackSize;
		S.StackSize += STACKINCREMENT;
	} 
	*S.Top = e;
	S.Top++;
	return OK;
}// Push

Pop

// 出棧
Status Pop( SqStack &S, SElemType &e )
{
	if( S.Top == S.Base )	// 空嗎?
	{
		return ERROR;
	}
	S.Top --;
	e = *S.Top;
	return OK;
}// Pop

鏈棧

定義

typedef struct{    
	SLink top;  // 棧頂指標    
	int length; // 棧中元素個數  
}Stack; 
typedef struct node{   
	int data;
    struct node *next;
}*SLink;

棧的應用舉例

把10進位制數159轉換成8進位制數


因此,需要先儲存在計算過程中得到的八進位制數的各位,然後逆序輸出,因為它是按"後進先出"的規律進行的,所以用棧最合適。

void conversion () 
{// 對於輸入的任意一個非負十進位制整數,列印輸出與其等值的八進位制數
	InitStack(S); // 構造空棧
	scanf ("%d",N);
	while (N) {
		Push(S, N % 8);
	    N = N/8;
	}
	while (!StackEmpty(S)) {
	    Pop(S,e);
	    printf ( "%d", e );
	}
} // conversion

括弧匹配檢驗

現在的問題是,要求檢驗一個給定表示式中的括弧是否正確匹配?
檢驗括號是否匹配的方法可用“期待的急迫程度”這個概念來描述。

status matching(string& exp) {
// 檢驗表示式中所含括弧是否正確巢狀,若是,則返回
// OK,否則返回ERROR
	int state = 1;
	while (i<=length(exp) && state) {
		swith of exp[i] {
	    	case "(": {Push(S,exp[i]); i++; break;}
	        case ")": {   
	            if (NOT StackEmpty(S) && GetTop(S) = "(")
	                { Pop(S,e); i++; }   
	            else{ state = 0 }
	                break;
	        }
	 …  }
	}
	if ( state && StackEmpty(S) ) 
		return OK
	else return ERROR;
}

行編輯程式問題

  • 一個簡單的行編輯程式的功能是:接受使用者從終端輸入的程式或資料,並存入使用者的資料區。每接受一個字元即存入使用者資料區。
  • 較好的做法是,設立一個輸入緩衝區,用以接受使用者輸入的一行字元,然後逐行存入使用者資料區。允許使用者輸入出差錯,並在發現有誤時可以及時更正。
    例如,可用一個退格符“#”表示前一個字元無效;可用一個退行符“@”,表示當前行中的字元均無效。
    例如,假設從終端接受了這樣兩行字元:

whli##ilr#e(s#s)
outcha@putchar(
s=#++);

則實際有效的是下列兩行:

while (s)
putchar(
s++);

void LineEdit() {
// 利用字元棧S,從終端接收一行並傳送至呼叫過程
// 的資料區。
	InitStack(S); //構造空棧S
	ch = getchar(); //從終端接收第一個字元
	while (ch != EOF) { //EOF為全文結束符
		while (ch != EOF && ch != '\n') {
			switch (ch) {
				case '#' : Pop(S, c); break; // 僅當棧非空時退棧
				case '@': ClearStack(S); break; // 重置S為空棧
				default : Push(S, ch); break; // 有效字元進棧,未考慮棧滿情形
				}
			ch = getchar(); // 從終端接收下一個字元
			}
			將從棧底到棧頂的字元傳送至呼叫過程的資料區
			ClearStack(S); // 重置S為空棧
			if (ch != EOF) ch = getchar();
		}
	DestroyStack(S);
}

表示式求值

  • 規則:
    先乘除,後加減;
    從左到右;
    先括號內,後括號外;
  • 把運算子和界限符統稱為算符
  • “算符優先法”
    4 - 10 / 5 + 2 * ( 3 + 8 )

4
4 -
4 - 10
4 - 10 /
4 - 10 / 5
4 - 10 / 5 + => 4 – 2 +
4 - 2 + => 2 +
2 + 2
2 + 2 *
2 + 2 * (
2 + 2 * ( 3
2 + 2 * ( 3 +
2 + 2 * ( 3 + 8
2 + 2 * ( 3 + 8 ) => 2 + 2 * 11 => 2 + 22 => 26

算符優先法
根據這個運算優先關係的規定來實現對錶達式的編譯或解釋執行。

算符間的優先關係

演算法描述

//表示式求值
OpendType EvaluateExpression( ){
	InitStack( OPTR );
	Push( OPTR, ‘#’ );
	InitStack( OPND );
	c = getchar( );
	while(!(c == ‘#’ && GetTop( OPTR ) == ‘#’) ){
		if(!In(c,OP)) // c是運算子?,OP是運算子集合
		{
			Push( OPND, c);
			c = getchar( );
		}
		else
		{
		switch( Precede ( GetTop( OPTR), c ))
		{
			case ‘<’ :     //棧頂元素優先權低
				Push( OPTR, c );
				c = getchar( );
				break;
			case ‘=‘ :	// c為’)’
				Pop( OPTR, x );
				c = getchar( );
				break;
			case ‘>’:  //退棧並將運算結果入棧
				Pop( OPTR, t );
				Pop( OPND, b );
				Pop( OPND, a );
				Push( OPND, Operate( a, t, b ));
				break;
		}// switch
	}// while
	return  GetTop( OPND );
}// EvaluateExpression

過程的巢狀呼叫

……

漢諾塔問題

……

佇列

定義

一種先進先出的線形表。只允許在表一端插入,在另一端刪除。

  • 概念
    隊尾rear:插入端,線性表的表尾。
    隊頭front:刪除端,線性表的表頭。
    FIFO(First In First Out)

ADT

ADT Queue
{
資料物件:D={ai|ai∈ElemSet,i=1,2,…,n,n≥0}
資料關係:R={<ai-1,ai>|ai,ai-1∈D,i=2,…,n}
		          約定:a1為佇列頭,an為佇列尾
基本操作:
	InitQueue( &Q );	// 初始化空佇列
	DestroyQueue( &Q );	// 銷燬佇列
	ClearQueue( &Q );	// 清空佇列
	QueueEmpty( Q );	// 佇列空?
	QueueLength( Q );	// 佇列長度
	GetHead( Q, &e );	// 取對頭元素
	EnQueue( &Q, e );	// 入佇列
	DeQueue( &Q, &e );	// 出佇列
	QueueTraverse( Q, visit( ));	// 遍歷
}//ADT Queue;

雙端佇列簡介

可以從兩端進行插入或者刪除操作的佇列。

佇列的表示和實現

兩種

  • 鏈式儲存結構__鏈佇列;
  • 順序儲存結構__迴圈佇列;

鏈佇列

型別說明

typedef struct Qnode   {
	QElemType data;
	struct QNode *next;
}QNode,*QueuePtr;		// 結點
typedef struct{
	QueuePtr fornt;	// 隊頭指標
	QueuePtr rear;	// 隊尾指標
}LinkQueue;

結點定義

typedef struct Qnode{
	QElemType data;
	struct QNode *Next;
}QNode


鏈佇列的基本操作

初始化
//  初始化一個空佇列
Status InitQueue( LinkQueue &Q )
{
	Q.Front=Q.Rear=(QueuePtr)malloc(sizeof(QNode));
	if(!Q.front)
	{
		return OVERFLOW;
	}
	Q.front->Next=NULL;
}//InitQueue
銷燬
// 銷燬佇列
Status DestroyQueue(LinkQueue &Q){
	while(Q.Front){  
        QRear = Q.Front ->next;
        free( Q.Front );   
        Q.Front= QRear 
    }
	return OK; 
}// DestroyQueue
入隊
// 入佇列
Stauts EnQueue( LinkQueue &Q, QEmemType e )
{
    p=(QueuePtr)malloc(sizeof(QNode));
	if( !p){return OVERFLOW;  }//儲存分配失敗
	p->data = e;  p->next = null ;
	Q.Rear->next = p;
	Q.Rear=p;
	return OK;
}
出隊
// 出佇列 3-3-12-1.swf
Status DeQueue( LinkQueue &Q, QElemType &e )
{
	if(Q.Front==Q.rear)	
		return ERROR;
	p = Q.Front->Next ;//p指向隊頭
	e = p->data;//取隊頭元素值(可以直接用e完成對隊頭取值)
    Q.Front->next = p->next;//頭結點指向原隊頭的下一個節點
    if(Q.rear==p) //尾頭同指向,佇列就空了,釋放佇列
    	Q.rear=Q.front; 
	free( p );
	return OK;
}// Dequeue

迴圈佇列

基本思想:把佇列設想成環形,讓sq[0]接在sq[M-1]之後,若rear+1==M,則令rear=0;
》實現:利用“模”運算
》入隊: rear=(rear+1)%M; sq[rear]=x;
》出隊: front=(front+1)%M; x=sq[front];
》隊滿、隊空判定條件

  • 會存在判斷隊空和隊滿時,條件均為front==rear的情況
  • 解決方案:少用一個元素空間
    隊空:front= =rear
    隊滿:(rear+1)%M= =front
    約定以“佇列頭指標在佇列尾指標的下一位置上”作為佇列呈“滿”狀態的標誌,在同一位置則是空。

迴圈佇列的基本操作

基本模組說明

#define MAXQSIZE 100	//佇列最大長度
typedef struct
{
	QElemType *Base; // pBase指向陣列名(通常靜態佇列都使用迴圈佇列)
	int Front;		// 頭指標,陣列下標,此處規定從零開始
	int Rear;		// 尾指標
}SqQueue;
初始化
Status InitQueue( SqQueue &Q ){
	Q.Base = (QElemTYpe *)malloc( MAXQSIZE * sizeof( QElemType ));
	if( !Q.Base ){
		return OVERFLOW;}
	Q.Front = Q.Rear = 0;
	return OK;
}// InitQueue
佇列長度
int QueueLength( SqQueue Q )
{
     return Q.Rear – Q.Front  + MAXQSIZE) % MAXQSIZE;
}
3 入佇列
Status 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;
}// EnQueue
出佇列
Status 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;
}// DeQueue;

佇列應用舉例

由事件驅動的程式

銀行業務模擬系統

  • 顧客到達銀行
    選擇一個人數最少的櫃檯排隊,如果該櫃檯沒有人,則直接開始辦理業務,並準備該顧客的業務完成事件。
    如果未到下班時間,則準備下一個顧客到達銀行的事件。
  • 顧客業務完成
    顧客完成業務離開,下一個顧客開始辦理業務,並準備該佇列中下一個顧客的業務完成事件。

初始化事件佇列;
起始事件(第一個顧客到達事件)入隊;
迴圈,直至事件佇列空
事件出隊;
若為到達事件,則
選擇人數最少的櫃檯排隊;
若該櫃檯無人排隊,則業務完成事件入隊;
若未到下班時間,則下一個顧客到達事件入隊;
若為業務完成事件,則
顧客離開;
若該櫃檯還有人排隊,則準備下一個人的業務完成事件;