棧和隊列(順序棧、鏈棧、隊列、循環隊列、鏈隊列)

棧的定義

定義:棧是限定僅在表尾進行插入和刪除操作的線性表。我們把允許插入和刪除的一端稱爲棧頂,另一端稱爲棧底,不含任何數據元素的棧稱爲空棧。棧又稱後進先出(Last In First Out)的線性表,簡稱LIFO結構。

棧的插入操作,叫作進棧(Push),也稱壓棧、入棧。棧的刪除操作,叫作出棧(Pop),也有的叫作彈棧。示意圖如下:

進棧、出棧

棧的抽象數據類型

棧本身就是一個線性表,所以關於線性表的操作,對棧基本上也是符合的,只是在某些方面會有少許變化,以下是棧的抽象數據類型:

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

棧的順序存儲結構及實現

棧的順序存儲結構其實是線性表順序存儲結構的簡化,我們簡稱爲順序棧。在沒有指針的高級程序語言中,我們使用數組來實現線性表,棧也是同樣的道理,不過因爲棧後進先出的特點,我們定義下標爲0的一端作爲棧底,因爲首元素在棧底,變化最小。

棧的結構定義如下:

//SElemType類型根據實際情況而定,這裏假設爲int
typedef int SElemType;
typedef struct{
	SElemType data[MAXSIZE];
	//用於棧頂指針
	int top;
}SqStack;

壓棧和彈棧

  1. 壓棧(入棧)

    壓棧的示意圖如上,下面我們來看下push的代碼算法實現:

    //時間複雜度爲O(1)
    //插入元素e爲新的棧頂元素
    Status Push(SqStack *S,SElemType e){
      
      	//棧滿
      	if(S->top == MAXSIZE - 1){
            	return ERROR;
      	}
      	
      	//棧頂指針+1
      	S->top++;
      	//將新插入元素賦值給棧頂空間
      	S-data[S->top] = e;
      	return OK;
    }
    
  2. 彈棧(出棧)

    彈棧的示意圖如上,下面我們來看下pop的代碼算法實現:

 //時間複雜度爲O(1)
 //若棧不空,則刪除S的棧頂元素,用e返回其值,並返回OK;否則返回ERROR
 Status Push(SqStack *S,SElemType e){
   
   	//空棧
   	if(S->top == -1){
         	return ERROR;
   	}
   	
   	//將要刪除的棧頂元素賦值給e
   	*e = S-data[S->top];
   	//棧頂指針-1
   	S->top--;
   	return OK;
 }

兩棧共享空間

棧的順序存儲結構有一個很大的缺陷,就是必須事先確定數組存儲空間大小,萬一不夠用了,就需要編程手段來擴展數組的容量,非常麻煩。對於一個棧,我們也只能儘量考慮周全,設計出合適大小的數組來處理,但對於兩個相同類型的棧(前提),我們卻可以做到最大限度地利用其實現開闢的存儲空間來進行操作,這就是兩棧共享空間。

做法如下,數組有兩個端點,兩個棧有兩個棧底,讓一個棧的棧底爲數組的始端,即下標爲0處,另一個棧爲數組的末端,即下標爲數組長度n-1處。這樣,兩個棧如果增加元素,就是兩端點向中間延伸。

兩棧共享空間

兩棧共享空間的結構的代碼如下:

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

兩棧共享空間的push方法:

//插入元素e爲新的棧頂元素。除了插入元素值外,還需要有一個判斷是棧1還是棧2的棧號stackNumber
Status push(SqDoubleStack *S,SElemType e,int stackNumber){
  	//棧已滿,不能再push新元素了
  	if(S->top1+1 == S->top2){
    	return ERROR;	
  	}
  
  	//棧1有元素進棧
  	if(stackNumber == 1){
    	S->data[++S->top1] = e;
    }
  	//棧2有元素進棧
  	else if(stackNumber == 2){
      	S->data[--S->top2] =e;
    }
  
  	return OK;
}

兩棧共享空間的pop方法:

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

事實上,使用這樣的數據結構,通常都是當兩個棧的空間需求有相反關係時,也就是一個棧增長時另一個棧在縮短的情況。

棧的鏈式存儲結構及實現

棧的鏈式存儲結構,簡稱鏈棧。由於單鏈表有頭指針,而棧頂指針也是必須的,所以比較好的辦法是把棧頂放在單鏈表的頭部(如下圖)。因爲有了棧頂在頭部,單鏈表中比較常用的頭結點也就失去意義,通常對於鏈棧來說,是不需要頭結點的。

鏈棧

鏈棧基本上不存在棧滿的情況,除非內存已經沒有可以使用的空間。但對於空棧來說,鏈表原定義是頭指針指向空,那麼鏈棧的空其實就是top=NULL。

鏈棧的結構代碼如下:

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

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

進棧和出棧

鏈棧進出棧
1.進棧(上圖左側)
進棧即push操作,假設元素值爲e的新結點爲s,top爲棧頂指針,代碼實現如下:

//插入元素e爲新的棧頂元素
Status Push(LinkStack *S,SElemType e){
	LinkStackPtr s = (LinkStackPtr)malloc(sizeof(StackNode));
	s->data = e;
	//把當前的棧頂元素賦值給新結點的直接後繼
	s->next = S->top;
	//將新結點s賦值給棧頂指針
	S->top = s;
	S->count++;
	return OK;
}

2.出棧(上圖右側)
出棧即pop操作,假設變量p用來存儲要刪除的棧頂結點,將棧頂指針下移一位,最後釋放p,代碼實現如下:

//若棧不空,則刪除S的棧頂元素,用e返回其值,並返回OK;否則返回ERROR
Status Pop(LinkStack *S,SElemType *e){
	LinkStackPtr p;
	if(StackEmpty(*S)){
		return ERROR;
	}
	*e = S->top->data;
	//將棧頂結點賦值給p
	p = S->top;
	//使得棧頂指針下移一位,指向後一結點
	S->top = S->top->next;
	free(p);
	S->count--;
	return OK;
}

如果棧的使用過程中元素變化不可預料,有時很大,有時很小,那麼最好使用鏈棧;反之,如果它的變化在可控範圍之內,最好使用順序棧。

##隊列的定義
定義:隊列是隻允許一端進行插入操作,而在另一端進行刪除操作的線性表。允許插入的一端稱爲隊尾,允許刪除的一端稱爲隊頭。

隊列是**先進先出(First In First Out)**的線性表

假設隊列是q={a1,a2,…,an},那麼a1就是隊頭元素,而an是隊尾元素。刪除時總是從a1開始,而插入時,列在最後。
隊列

##隊列的抽象數據類型
隊列的操作基本和線性表差不多,不同的就是插入數據只能在隊尾,刪除數據只能在隊頭。

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

##隊列的順序存儲結構
假設一個隊列有n個元素,則順序存儲的隊列需建立一個大於n的數組,並把隊列的所有元素存儲在數組的前n個單元,數組下標爲0的一端即是隊頭。現在我們來研究一下關於入隊和出隊的流程:
出隊、入隊
從上面我們可以看出根據正常的隊列定義,我們入隊的時間複雜度爲O(1),但是出隊的時間複雜度確實O(n),原因是隊頭是下標爲0的位置,每一次在隊頭出隊一個元素,後面的元素就要全部向前移一格。這樣來看性能是很不好的,有什麼方式來優化呢?很簡單,就是我們不再限制把隊列中的所有元素存在數組的n個單元,具體如何實現呢,我們來看一下一個新的流程圖:
循環鏈表的出隊
在這裏我們引入了兩個指針,分別是front指向隊頭元素;rear指向隊尾的下一個位置。在出隊一個元素之後,隊頭元素後移一位,此時出隊的時間複雜度由O(n)優化爲了O(1),但是當前這個模式有一個問題,那就是假溢出。爲了解決假溢出問題,我們的循環隊列出來了。循環隊列的定義很簡單:我們把隊列的這種頭尾相接的順序存儲結構稱爲循環隊列。

什麼是假溢出呢?我們以上圖爲例,當前隊列的總容量爲5,我們先移除下標0、1中的元素,然後填充下標2、3、 4中的元素,此時隊頭指針指向下標2,那隊尾呢?好像已經越界了,這就是假溢出,因爲實際上當前數組中下標0、1還是可以填充隊列元素的。

根據循環隊列的定義,我們把上面出現假溢出的案例的流程圖補上:
循環隊列
這樣我們就圓滿的解決了假溢出的bug。但是不要高興太早,新bug來啦,那就是我們該如何判斷隊列什麼時候是空,什麼時候是滿?
爲了解決這個問題,我們把隊列空和隊列滿的條件做了一點修改:
(1)隊列空:front=rear
(2)隊列滿:若數組中海油一個空閒單元,我們就認爲隊列滿(見下圖)。
循環隊列滿
在隊列滿這裏,有一點需要注意,那就是rear既可能比front大,也可能比front小,所以儘管它們只相差一個位置時就是滿的,但也可能相差整整一圈,假設隊列的最大長度爲QueueSize,那麼隊列滿的判斷條件是:

(rear+1) % QueueSize == front

同時,在計算隊列的實際長度時也存在rear > front和rear < front,通用的計算隊列長度公式爲:

(rear - front + QueueSize) % QueueSize

下面我們開始介紹循環隊列的結構和一些常見操作。

##循環隊列的順序存儲結構

//QElemType類型根據實際情況而定,這裏假設爲int
typedef int QElemType;
//循環隊列的順序存儲結構
typedef struct{
	QElemType data[MAXSIZE];
	//頭指針
	int front;
	//尾指針,若隊列不爲空,指向隊尾元素的下一個位置
	int rear;
}

###循環隊列的常見操作
1.初始化

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

2.隊列長度

//返回隊列Q的元素個數
int QueueLength(SqQueue Q){
	return (Q.rear-Q.font+MAXSIZE)%MAXSIZE;
}

3.入隊

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

	return OK;
}

4.出隊

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

	return OK;
}

從上面的介紹我們可以發現,單是順序存儲,若不是循環隊列,算法的時間性能不高,但循環隊列又面臨着數組可能會溢出的問題,所以我們還需要研究一下隊列的鏈式存儲結構。

##隊列的鏈式存儲結構及實現
隊列的鏈式存儲結構,其實就是單鏈表,只不過它只能尾進頭出而已,我們把它稱爲鏈隊列。爲了操作上的方便,我們將隊頭指針front指向鏈隊列的頭結點,將隊尾指針rear指向終端結點:
鏈隊列

鏈隊列的結構:

//QElemType類型根據實際情況而定,這裏假設爲int
typedef int QElemType;

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

//隊列的鏈表結構
typedef struct{
	QueuePtr front,near;
}LinkQueue;

###鏈隊列的入隊、出隊
1.入隊
鏈隊列入隊

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

2.出隊
鏈隊列出隊

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

對於循環隊列和鏈隊列的比較,可以從兩方面考慮,時間上,它們的基本操作都是常數時間,即O(1),不過循環隊列是事先申請好空間,使用期間不釋放,而對於鏈隊列,每次申請和釋放結點存在一定的時間開銷;空間上,循環隊列必須有一個固定的長度,所以就有了空間浪費的問題,而鏈隊列不存在這個問題,儘管它需要一個指針域,會產生一些空間上的開銷,但也可以接受。

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