大話數據結構筆記——第四章:棧與隊列


棧是限定僅在尾表進行插入和刪除操作的線性表。
隊列是隻允許在一端進行插入操作、而在另一端進行刪除操作的線性表。

棧的定義

定義:棧是限定僅在尾表進行插入和刪除操作的線性表。允許插入和刪除的一端稱爲棧頂(top),另一端稱爲棧底(bottom),不含任何數據元素的棧稱爲空棧。棧又稱爲後進先出(Last In First Out)的線性表,簡稱LIFO結構。
棧是一種特殊的線性表,只能在線性表的表尾進行插入和刪除操作,表尾指的就是棧頂。
棧的插入操作,叫做進棧,壓棧,入棧。棧的刪除操作,叫做出棧,彈棧。

進棧出棧的變化形式

最先進棧的元素不一定是隻能最後出棧的元素,棧對線性表中元素的進出時間沒有限制。三個元素有五種出棧次序。

棧的抽象數據類型

ADT 棧(stack)
Data
	同線性表。元素具有相同的類型,相鄰元素具有前驅和後繼關係
Operation.
	InitStack(*S):初始化,建立一個空棧
	DestroyStack(*S):棧空,銷燬
	ClearStack(*S):將棧清空
	StackEmpty(S):如果棧空,返回不同布爾值
	GetTop(S,*e):若棧存在且非空,用e返回S的棧頂元素
	Push(*S,e):若棧存在,插入新元素e到棧S中併成爲棧頂元素
	Pop(*S,*e):刪除棧S中棧頂元素,並用e返回其值
	StackLength(S):返回棧S的元素個數
endADT

棧的順序存儲及實現

結構定義:

/*棧的結構定義*/
typedef int SElemType;
typedef struct 
{
	SElemType data[MAXSIZE];
	int top; //用於棧頂指針,空棧是爲-1
}SqStack;

進棧操作:

/*插入元素e爲新的棧頂元素*/
Status Push(SqStack *S,SElemType e)
{
	if (S->top == MAXSIZE-1) //棧滿
	{
		return ERROR;
	}
	S->top++; //棧頂指針自增1
	S->data[S->top]=e; //新插元素賦值給棧頂空間
	return OK;
}

出棧操作:

/*插入元素e爲新的棧頂元素*/
Status Push(SqStack *S,SElemType e)
{
	if (S->top == MAXSIZE-1) //棧滿
	{
		return ERROR;
	}
	S->top++; //棧頂指針自增1
	S->data[S->top]=e; //新插元素賦值給棧頂空間
	return OK;
}

兩棧共享空間

棧頂指針在數組兩端,向中間靠攏。當棧1空時,也就是top1= -1,當棧2空時,即top2=n。當top1+1=top2時棧滿
兩站共享空間結構:

/*兩棧共享空間結構*/
typedef struct 
{
	SElemType data[MAXSIZE];
	int top1; //棧1棧頂指針
	int top2; //棧2棧頂指針
}SqDoubleStack;

/*插入元素e爲新的元素*/
Status Push(SqDoubleStack *S, SElemType e, int stackNumber)
{
	if (S->top1 == S->top2) //棧滿,不能再有新元素入棧
	{
		return ERROR;
	}
	if (stackNumber == 1) //棧1有元素進棧
	{
		S->data[++S->top1]=e; //先top1+1後給數組元素賦值
	}
	else if (stackNumber == 2) //棧2有元素進棧
	{
		S->data[--S->top2]=e; //先top2-1後給數組元素賦值
	}
	return OK;
}

/*若棧不空,則刪除S棧頂元素,用e返回其值,返回OK,否則返回ERROR*/
Status Pop(SqDoubleStack *S,SElemType *e,int stackNumber)
{
	if (stackNumber == 1)
	{
		if (S->top1 == -1)
		{
			return ERROR; //說明棧1已經是空棧,溢出
		}
		*e = S->data[S->top1--]; //將棧一的棧頂元素出棧,並使top1-1
	}
	else if (stackNumber == 2)
	{
		if (S->top2==MAXSIZE)
		{
			return ERROR; //說明棧2已經是空棧,溢出
		}
		*e = S->data[S->top2++]; //將棧二的棧頂元素出棧,並使top2+1
	}
	return OK;
}

棧的鏈式存儲及其實現

單鏈表有頭指針,棧頂指針也是必須的,所以可以讓它們倆合二爲一,所以好的方法是把棧頂放在單鏈表的頭部。因此也不需要頭結點了。
鏈棧的結構代碼:

typedef struct StackNode
{
	SElemType data;
	struct StackNode *next;
}StackNode,*LinkStackPtr;

typedef struct LinkStack
{
	LinkStackPtr top;
	int count;
}LinkStack;

進棧操作:

Status Push(LinkStack *S, SElemType e)
{
	LinkStackPtr s=(LinkStackPtr)malloc(sizeof(StackNode));
	s->data=e;
	s->next=S->top; //把當前的棧頂元素賦值,給新結點的的直接後繼
	S->top=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; //將棧頂結點賦值給P
	S->top=S->top->next; //使得棧頂指針下移一位,指向後一結點
	free(p);
	S->count--;
	return OK;
}

如果棧的使用過程中元素的變化不可預料,最好使用鏈棧,反而如果它的變化在可控範圍內,建議使用順序棧。時間複雜度都是O(1)。

棧的作用

棧的引入簡化了程序設計的問題,劃分了不同關注層次,使得思考範圍縮小,更加聚焦於我們要解決問題的核心。

遞歸

把一個直接調用自己或通過一系列的調用語句間接調用自己的函數,稱爲遞歸函數。

/*斐波那契的遞歸函數*/
int Fbi(int i)
{
	if (i<2)
	{
		return i == 0?0:1;
	}
	return Fbi(i-1)+Fbi(i-2); //這裏Fbi就是函數自己,它在調自己
}

int main()
{
	int i;
	for (int i = 0; i < 40; i++)
	{
		printf("%d", Fbi(i));
	}
	return 0;
}

四則運算表達式求值

逆波蘭表示法,不需要括號的後綴表達式法,所有的符號都是在要運算數字的後面出現,利用棧可以對後綴表達式進行處理。
規則:從左到右遍歷表達式的每個數字和符號,遇到數字就進棧,遇到符號,就將處於棧頂的兩個數字出棧,進行運算,運算結果進棧,一直到最終獲得結果。
中綴表達式:平時所用的標準四則運算表達式。
中綴表達式轉後綴表達式:從左到右遍歷中綴表達式的每個數字和符號,遇到數字就輸出,即成爲後綴表達式的一部分;若是符號,則判斷與棧頂符號的優先級,是右括號或優先級不高於棧頂符號則棧頂元素依次出棧並輸出,並將當前符號進棧,一直到最終輸出後綴表達式爲止。
計算機處理四則運算表達式

  1. 將中綴表達式轉化爲後綴表達式(棧用來進出運算的符號)。
  2. 將後綴表達式進行運算得出結果(棧用來進出運算的數字)。

隊列

隊列是隻允許在一端進行插入操作,而在另一端進行刪除操作的線性表。隊列是一種先進先出(First In First Out)的線性表,簡稱FIFO。允許插入的一端稱爲隊尾,允許刪除的一端稱爲隊頭。

隊列的抽象數據類型

ADT 隊列(queue)
Data 
	同線性表。元素具有相同的類型,相鄰元素具有前驅和後繼關係
Operation.
	InitQueue(*Q):初始化操作,建立一個空隊列Q
	DestroyQueue(*Q):隊列存在則銷燬它
	ClearQueue(*Q):將隊列Q清空
	QueueEmpty(Q):若隊列爲空,返回TRUE
	GetHead(Q,*e):若隊列Q存在且非空,用e返回隊列Q中的對頭元素
	EnQueue(*Q,e):若隊列存在,插入新元素e到隊列Q中併成爲隊尾元素
	DeQueue(*Q,*e):刪除隊列Q中對頭元素,並用e返回其值
	QueueLength(Q):返回隊列Q中元素個數
endADT

循環隊列

爲了避免當只有一個元素時,對頭和隊尾重合時處理變得麻煩,所以引入兩個指針,front指向隊頭元素,rear指向隊尾元素的下一個位置。把隊列的這種頭尾相接的順序存儲結構稱爲循環隊列
通用的計算隊列長度公式爲
(rear - front +QueueSize) %QueueSize

typedef int QElemType; //根據實際清空確定時int還是其他類型
/*循環隊列的順序存儲結構*/
typedef struct 
{
	QElemType data[MAXSIZE];
	int front; //頭指針
	int rear; //尾指針,若隊列不爲空,指向隊尾元素的下一個位置
}SqQueue;

/*初始化一個空隊列Q*/
Status InitQueue(SqQueue *Q)
{
	Q->front=0;
	Q->rear=0;
	return OK;
}

/*返回Q的元素個數,也就是隊列的當前長度*/
int QElemType(SqQueue)
{
	return (Q.rear-Q.front+MAXSIZE)%MAXSIZE;
}

/*若隊列未滿,則插入元素e爲Q的新的隊尾元素*/
Status EnQueue(SqQueue *Q,QElemType e)
{
	if ((Q->rear+1)%MAXSIZE == Q->front) //隊列滿的判斷
	{
		return ERROR;
	}
	Q->data[Q->rear]=e; //將元素e賦值給隊尾
	Q->rear=(Q->rear+1)%MAXSIZE; //rear指針向後移一位
	//若到最後則轉到數組的頭部
	return OK;
}

/*若隊列不空,則刪除Q中對頭元素,用e返回其值*/
Status DeQueue(SqQueue *Q,QElemType *e)
{
	if (Q->front == Q->rear) //隊列空的判斷
	{
		return ERROR;
	}
	*e = Q->data[Q->front]; //對頭元素賦值給e
	Q->front = (Q->front+1)%MAXSIZE; //front指針向後移一位置
	//若到最後則轉到數組的頭部
	return OK;
}

隊列的鏈式存儲結構

隊列的鏈式存儲結構,其實就是線性表的單鏈表,只不過它只能尾進頭出,簡稱爲鏈隊列。隊頭指針(front)指向鏈隊列的頭結點,隊尾指針(rear)指向終端結點。空對列時front和rear都指向頭結點。
鏈隊列的結構:

typedef int QElemType;
typedef struct QNode  //結點結構
{
	QElemType data;
	struct QNode *next
}QNode,*QueuePtr;

typedef struct  //隊列的鏈表結構
{
	QueuePtr front,rear; //對頭、隊尾指針
}LinkQueue;

入隊操作:

/*插入元素e爲Q的新的隊尾元素*/
Status EnQueue(LinkQueue *Q,QElemType e)
{
	QueuePtr s=(QueuePtr)malloc(sizeof(QNode));
	if (!s) //存儲分配失敗
	{
		exit(OVERFLOW);
	}
	s->data=e;
	s->next=NULL;
	Q->rear->next=s; //把擁有元素e的新結點s賦值給原隊尾元素的後繼
	Q->rear=s; //把當前的s設置爲隊尾結點,rear指向s
	return OK;
}

出隊操作:

/*若隊列不空,刪除Q的對頭元素,用e返回其值,並返回OK,否則返回ERROR*/
Status DeQueue(LinkQueue *Q,QElemType *e)
{
	QueuePtr p;
	if (Q->front == Q->rear)
	{
		return ERROR;
	}
	p=Q->front->next; //將欲刪除的對頭結點暫存給p
	*e=p->data; //將欲刪除的對頭結點的值賦值給e
	Q->front->next=p->next; //將原對頭結點的後繼p->next賦值給頭結點的後繼
	if (Q->rear==p) //若對頭是隊尾,則刪除後將rear指向頭結點。
	{
		Q->rear = Q->front;
	}
	free(p);
	return OK;
}

總結

主要講解了棧和隊列,它們均可以用線性表的順序存儲結構來實現,但都存在着順序存儲的一些弊端,因此它們各自使用技巧來解決這個問題。
對於棧來說,如果是兩個相同數據類型的棧,則可以用數組的兩端作棧底的方法來讓兩個棧共享數據,這就可以最大化地利用數組的空間。
對於隊列來說,爲了避免數組插入和刪除時需要移動數據,於是就引入循環隊列,使得對頭和隊尾可以在數組中循環變化。使得本來插入和刪除是O(n)的時間複雜度變成了O(1)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章