資料結構:棧與佇列

半紙淵發表於2017-12-14

本文內容:
一、棧
1、什麼是棧?
2、棧的操作集.
3、棧的 C 實現.

二、佇列
1、什麼是佇列?
2、佇列的操作集.
3、佇列的 C 實現.

總表:《資料結構?》

工程程式碼 Github: Data_Structures_C_Implemention -- Stack & Queue


一、棧

1、什麼是棧?

棧 (Stack),是限制插入與刪除操作只能在末端 (Top) 進行的表,而這個末端也稱為棧頂;

它有時候也會被稱為,先進後出 (LIFO: Last In First Out) 表;

棧的抽象模型圖:

資料結構:棧與佇列

2、棧的操作集.

從上面的模型圖中就可以看出,棧的核心操作有: Push、Pop、Top (Peek) 三個操作;

棧操作集圖示:

棧 操作集.png

3、棧的 C 實現.

棧的實現方式,有兩類,

資料結構:棧與佇列

解析:
1、陣列的實現方式,我覺得不需要過多的解釋了,連結串列就是為了解決陣列的缺點而被設計出來的; 那麼為什麼要使用單連結串列來實現表,而不是其它連結串列形式呢?

首先,單連結串列是所有連結串列裡面最簡單的連結串列,空間佔用也是最小的,也就是開銷最小;只要證明單連結串列可以實現即可以了。

棧是一個表,而且是隻能在一個埠進行插入與刪除操作,遍歷方向是從棧底到棧頂; 而單連結串列也是一個表,而且它的操作可以在任意位置進行插入與刪除,遍歷方向是鏈頭與鏈尾;

從上面的兩個結論來看,棧可以看作是單連結串列的其中一種情況;

2、在這裡 tail 指標的指向改了一下,把它放在鏈頭 [一般習慣性左邊是指鏈頭] 而它指向的就是最後一個壓入的節點,也就是說左邊的第一個節點就是真正的鏈尾;這樣設計的目的就是為了讓連結串列可以從鏈尾直接進行遍歷操作,而且所有的插入與刪除操作在鏈尾就可以實現;【您如果覺得暈,就先保有疑惑,等到檢視下面的棧的入棧與出棧操作的具體程式碼實現的時候,我相信您就懂了!】

  • 節點[記憶體結構]與棧的定義:

節點[記憶體結構],

struct __StackNode {
	ElementTypePrt data;
#if _C_STACK_LINKEDLIST_IMP
	StackNode next;
#else
	int prevIdx;
#endif
};
複製程式碼

解析:
1、_C_STACK_LINKEDLIST_IMP 原型是 #define _C_STACK_LINKEDLIST_IMP 1 就是一個巨集開關,1 的時候是使用單連結串列的方式實現棧,0 就是使用陣列的方式實現棧;

2、ElementTypePrt 原型是 typedef void * ElementTypePrt;,使用 typedef 是方便後期進行修改;這裡使用指標的目的是為了讓連結串列支援更多的資料型別,使程式碼的可擴充套件性更強;【如: data 可以直接指向一個單純的 int 或者 一個 struct ,又或者是一個 list 等等】

3、在陣列實現的方式下,prevIdx 這個參量其實可以不要的,看君愛好吧,反正入棧、出棧與它無關;

棧,

typedef struct __StackNode * _StackNode;
typedef _StackNode StackNode;

typedef struct __Stack * _Stack;
typedef _Stack Stack;

struct __Stack {
	unsigned int size;
	MatchFunc matchFunc;
	DestroyFunc destroyFunc;
#if _C_STACK_LINKEDLIST_IMP
	StackNode tail;
#else 
	unsigned int capacity;
	int tailIdx;
	StackNode nodes;
#endif
};
複製程式碼

解析:
1、連結串列實現方式下,

#if _C_STACK_LINKEDLIST_IMP
    StackNode tail;
#else 
複製程式碼

是取消了 head 指標,只保留了單連結串列的 tail 指標;

2、陣列實現方式下,

#else 
    unsigned int capacity;
    unsigned int tailIdx;
    StackNode nodes;
#endif
複製程式碼

引入一個 capacity 參量,我們知道,C 語言中陣列初始化是要指定長度的,而這個參量就是表明陣列的最大長度,也就是可以儲存多少個節點;

tailIdx 就是棧頂節點的下標; nodes 是一個結構體指標,相當於一個陣列的首指標,陣列中儲存的是 struct __StackNode

  • 棧的核心操作集:
/* Stack Create */
#if _C_STACK_LINKEDLIST_IMP
	Stack Stack_Create(DestroyFunc des);
#else
	Stack Stack_Create(unsigned int cap, DestroyFunc des);
#endif

void Stack_Init(Stack s, DestroyFunc des);
void Stack_Destroy(Stack s);

/* Stack Operations */
_BOOL Stack_Push(Stack s, ElementTypePrt x);
_BOOL Stack_Pop(Stack s, ElementTypePrtPrt data);
ElementTypePrt Stack_Peek(Stack s);
_BOOL Stack_PeekAndPop(Stack s, ElementTypePrtPrt data);
複製程式碼
  • 棧的建立與銷燬:

建立, 【鏈式實現】Stack Stack_Create(DestroyFunc des);

Stack Stack_Create(DestroyFunc des) {

	Stack s = (Stack)(malloc(sizeof(struct __Stack)));
	if (s == NULL) { printf("ERROR: Out Of Space !"); return NULL; }

	Stack_Init(s, des);

	return s;

}
複製程式碼

解析:
1、DestroyFunc 原型是 typedef void(*DestroyFunc) (void * data); 指向形如 void(*) (void * data); 的函式指標;data 如何進行釋放也由使用者決定;也是為了程式碼可擴充套件性;

2、malloc 原型是 void * malloc(size_t size)

3、Stack_Init(s, des);,請移步面下面的解釋 初始化

【陣列實現】Stack Stack_Create(unsigned int cap, DestroyFunc des);

Stack Stack_Create(unsigned int cap, DestroyFunc des) {

	if (cap == 0) { printf("ERROR: Bad Cap Parameter [cap > 0] !"); return NULL; }
	if (cap > STACK_MAXELEMENTS) { printf("ERROR: Bad Cap Parameter [cap < STACK_MAXELEMENTS(%d)] !", STACK_MAXELEMENTS); return NULL; }

	Stack s = (Stack)(malloc(sizeof(struct __Stack)));
	if (s == NULL) { printf("ERROR: Out Of Space !"); return NULL; }

	s->nodes = malloc(sizeof(StackNode) * cap);
	if (s->nodes == NULL) { printf("ERROR: Out Of Space !"); free(s); return NULL; }

	s->capacity = cap;
	Stack_Init(s, des);

	return s;

}

複製程式碼

解析:
1、形參 unsigned int cap 就是陣列初始化的最大記憶體空間;

2、STACK_MAXELEMENTS 原型是 #define STACK_MAXELEMENTS 100;

3、s->nodes = malloc(sizeof(StackNode) * cap); 這裡就是與鏈式實現最大的區別,提前申請要儲存節點的記憶體空間;

4、Stack_Init(s, des);,請移步面下面的解釋 初始化

初始化,

void Stack_Init(Stack s, DestroyFunc des) {

	if (s == NULL) { printf("ERROR: Please Using Stack_Create(...) First !"); return; }

	s->size = LINKEDLIST_EMPTY;
	s->matchFunc = NULL;
	s->destroyFunc = des;
#if _C_STACK_LINKEDLIST_IMP
	s->tail = NULL;
#else
	s->tailIdx = STACK_INVAILDINDEX;
#endif

}
複製程式碼

解析,
裡面的參量直接進行賦值為空就可以了,其中 LINKEDLIST_EMPTY 原型是 #define LINKEDLIST_EMPTY 0STACK_INVAILDINDEX 原型是 #define STACK_INVAILDINDEX -1;

銷燬,

void Stack_Destroy(Stack s) {
	
	if (s == NULL) { printf("ERROR: Please Using Stack_Create(...) First !"); return; }

	ElementTypePrt data;

	while (!Stack_IsEmpty(s)) {
		if ((Stack_Pop(s, (ElementTypePrtPrt)&data) == LINKEDLIST_TRUE) &&
			(s->destroyFunc != NULL)) {

			s->destroyFunc(data);
		}
	}

	memset(s, 0, sizeof(struct __Stack));

}
複製程式碼

解析,運作原理是不停地做出棧處理,直到棧空為止,就是清空棧的作用; 1、Stack_IsEmpty(s) 原型是 _BOOL Stack_IsEmpty(Stack s) { return (s->size == LINKEDLIST_EMPTY); }

2、Stack_Pop(s, (ElementTypePrtPrt)&data) == LINKEDLIST_TRUE) 請移步下面的 出棧操作

  • 入棧操作:
_BOOL Stack_Push(Stack s, ElementTypePrt x) {

	if (s == NULL) { printf("ERROR: Please Using Stack_Create(...) First !"); return LINKEDLIST_FALSE; }

	StackNode nNode;
	nNode = malloc(sizeof(struct __StackNode));
	if (nNode == NULL) { printf("ERROR: Out Of Space ! "); return LINKEDLIST_FALSE; }

	nNode->data = x;

#if _C_STACK_LINKEDLIST_IMP

	nNode->next = NULL;

	if (Stack_IsEmpty(s)) {
		s->tail = nNode;
	} else {

		/* Get Tail */
		StackNode tail = s->tail;

		/* Push Operations */
		nNode->next = tail;

		/* Set Tail */
		s->tail = nNode;

	}

	/* Size ++ */
	s->size++;

#else 

	/* Size ++ */
	s->size++;

	if (s->size > s->capacity) { 
		printf("ERROR: Out Of Space ! ");
		s->size--;
		return LINKEDLIST_FALSE;
	}

	nNode->prevIdx = s->tailIdx;

	s->tailIdx++;
	s->nodes[s->tailIdx] = *nNode;

#endif

	return LINKEDLIST_TRUE;

}
複製程式碼

解析,

入棧操作圖示, 【鏈式實現】

資料結構:棧與佇列

// 對應的核心程式碼
【鏈式實現】
nNode->next = tail;
s->tail = nNode;
複製程式碼

【陣列實現】

資料結構:棧與佇列

// 對應的核心程式碼
【陣列實現】
s->tailIdx++;
s->nodes[s->tailIdx] = *nNode;
複製程式碼
  • 出棧操作:
_BOOL Stack_Pop(Stack s, ElementTypePrtPrt data) {

	if (s == NULL) { printf("ERROR: Bad Stack !"); return LINKEDLIST_FALSE; }
	if (Stack_IsEmpty(s)) { printf("ERROR: Empty Stack !"); return LINKEDLIST_FALSE; }

#if _C_STACK_LINKEDLIST_IMP

	StackNode lDelete = s->tail;

	/* Get Data */
	*data = lDelete->data;

	/* Pop Operations */
	s->tail = s->tail->next;

	/* Free The Deleted Node */
	free(lDelete);

#else

	*data = s->nodes[s->tailIdx].data;
	s->tailIdx--;

#endif

	/* Size -- */
	s->size--;

	return LINKEDLIST_TRUE;

}
複製程式碼

解析,

出棧操作圖示,

【鏈式實現】

資料結構:棧與佇列

// 對應的核心程式碼
StackNode tail = s->tail;
s->tail = s->tail->next;

free(lDelete);
複製程式碼

【陣列實現】

資料結構:棧與佇列

// 對應的核心程式碼
s->tailIdx--;
複製程式碼

二、佇列

1、什麼是佇列?

佇列 (Queue),是限制插入操作在一端,而刪除操作要在另一端進行的表;

它有時候也會被稱為,先進先出 (FIFO: First In First Out) 表;

佇列的抽象模型圖:

資料結構:棧與佇列

2、佇列的操作集.

從上面的模型圖中就可以看出,佇列的核心操作集有: Enqueue、Dequeue 兩個操作;

佇列操作集圖示:

佇列 操作集.png

3、佇列的 C 實現.

佇列其實更接近單連結串列了,具備頭和尾,當然遍歷方向也是從頭到尾,所以直接使用單連結串列來實現就可以了,不需要做太多的修改;

不過這裡的,陣列實現就要有點技巧了;

資料結構:棧與佇列

因為佇列是一個埠進,另一個埠出,也就是要有一個指向進入方向的下標 headIdx ,以及出方向的下標 tailIdx

這裡要注意的是, headIdxtailIdx 的大小關係是不定的,這是由於陣列自初始化後,空間是固定的,而在頻繁的入隊與出隊操作後,會出現 headIdx > tailIdxheadIdx < tailIdxheadIdx = tailIdx '這三種情況,而且它們會不停地進行切換;【當然這裡也是要在程式碼實現的時候要特別細心處理的地方】

  • 節點[記憶體結構]與佇列的定義:

節點,【與棧節點定義一致】

typedef struct __QueueNode * _QueueNode;
typedef _QueueNode QueueNode;

struct __QueueNode {
	ElementTypePrt data;
#if _C_QUEUE_LINKEDLIST_IMP
	QueueNode next;
#else
	int prevIdx;
#endif
};

複製程式碼

佇列,

typedef struct __Queue * _Queue;
typedef _Queue Queue;

struct __Queue {
	unsigned int size;
	MatchFunc matchFunc;
	DestroyFunc destroyFunc;
#if _C_QUEUE_LINKEDLIST_IMP
	QueueNode head;
	QueueNode tail;
#else 
	unsigned int capacity;
	int headIdx;
	int tailIdx;
	QueueNode nodes;
#endif
};
複製程式碼

解析,
與棧對比,鏈式實現下,重新引入 QueueNode head; 指標,用於進行出隊的操作;陣列實現下,引入 int headIdx; 方便進行出隊操作;

  • 佇列核心操作集:
/* Queue Create */
#if _C_QUEUE_LINKEDLIST_IMP
  Queue Queue_Create(DestroyFunc des);
#else
  Queue Queue_Create(unsigned int cap, DestroyFunc des);
#endif

void Queue_Init(Queue q, DestroyFunc des);
void Queue_Destroy(Queue q);

/* Queue Operations */
_BOOL Queue_Enqueue(Queue q, ElementTypePrt x);
_BOOL Queue_Dequeue(Queue q, ElementTypePrtPrt data);
ElementTypePrt Queue_Peek(Queue q);
_BOOL Queue_PeekAndDequeue(Queue q, ElementTypePrtPrt data);
複製程式碼

解析,_C_QUEUE_LINKEDLIST_IMP 原型是 #define _C_QUEUE_LINKEDLIST_IMP 1 鏈式實現或陣列實現的開關;

  • 佇列的建立與銷燬:

建立,這裡的程式碼實現與棧的實現一致;

初始化,

void Queue_Init(Queue q, DestroyFunc des) {
	
	if (q == NULL) { printf("ERROR: Please Using Queue_Create(...) First !"); return; }

	q->size = LINKEDLIST_EMPTY;
	q->matchFunc = NULL;
	q->destroyFunc = des;
#if _C_QUEUE_LINKEDLIST_IMP
	q->tail = NULL;
#else
	q->headIdx = 0;
	q->tailIdx = QUEUE_INVAILDINDEX;
#endif

}
複製程式碼

解析,這裡要注意的是,在 陣列實現方式下,

#else
    q->headIdx = 0;
    q->tailIdx = QUEUE_INVAILDINDEX; // -1
#endif
複製程式碼

headIdx = 0 這裡選擇 0 而不是 QUEUE_INVAILDINDEX,因為這樣做在入隊操作的時候就可以使用headIdx 而不用增加一個判斷【您可以先留有疑惑,結合下面的入隊操作,我相信您就懂了】;

銷燬, 與棧的出棧操作原理上是一樣,只不過這裡使用的是佇列的出隊操作罷了;

void Queue_Destroy(Queue q) {
	
	if (q == NULL) { printf("ERROR: Please Using Queue_Create(...) First !"); return; }

	ElementTypePrt data;

	while (!Queue_IsEmpty(q)) {
		if ((Queue_Dequeue(q, (ElementTypePrtPrt)&data) == LINKEDLIST_TRUE) &&
			(q->destroyFunc != NULL)) {

			q->destroyFunc(data);
		}
	}

	memset(q, 0, sizeof(struct __Queue));

}
複製程式碼
  • 入隊操作:
_BOOL Queue_Enqueue(Queue q, ElementTypePrt x) {
	
	if (q == NULL) { printf("ERROR: Please Using Queue_Create(...) First !"); return LINKEDLIST_FALSE; }

	QueueNode nNode;
	nNode = malloc(sizeof(struct __QueueNode));
	if (nNode == NULL) { printf("ERROR: Out Of Space ! "); return LINKEDLIST_FALSE; }

	nNode->data = x;

#if _C_QUEUE_LINKEDLIST_IMP

	nNode->next = NULL;

	if (Queue_IsEmpty(q)) {
		q->head = q->tail = nNode;
	} else {

		/* Get Tail */
		QueueNode tail = q->tail;

		/* Push Operations */
		nNode->next = tail->next;
		tail->next = nNode;

		/* Set Tail */
		q->tail = nNode;

	}

	/* Size ++ */
	q->size++;

#else 

	/* Size ++ */
	q->size++;

	if (q->size > q->capacity) {
		printf("ERROR: Out Of Space ! ");
		q->size--;
		return LINKEDLIST_FALSE;
	}

	q->tailIdx = Queue_VaildIdx(q->tailIdx, q);
	nNode->prevIdx = q->tailIdx;

	q->nodes[q->tailIdx] = *nNode;

#endif

	return LINKEDLIST_TRUE;

}
複製程式碼

解析,入隊操作,在鏈式實現下就直接在鏈尾進行,而陣列實現下直接在陣列的最後一個下標節點進行;

入隊操作圖示,

【鏈式實現】

資料結構:棧與佇列

// 對應的核心程式碼
nNode->next = tail->next;
tail->next = nNode;

q->tail = nNode;
複製程式碼

【陣列實現】

資料結構:棧與佇列

// 對應的核心程式碼
q->tailIdx = Queue_VaildIdx(q->tailIdx, q);
nNode->prevIdx = q->tailIdx;

q->nodes[q->tailIdx] = *nNode;
複製程式碼

解析,這裡要注意的是,tailIdx 的取值範圍是 [0 ~ (size - 1) ~ 0] 它是一個迴圈,而不是簡單地 taildIdx + 1Queue_VaildIdx 原型是

int Queue_VaildIdx(int idx, Queue q) {
	if (++idx == q->capacity) { return 0; }
	return idx;
}
複製程式碼

提示,++idx == q->capacity 它的運算過程是 idx = idx + 1, idx == q->capacity

  • 出隊操作:
_BOOL Queue_Dequeue(Queue q, ElementTypePrtPrt data) {
	
	if (q == NULL) { printf("ERROR: Bad Queue !"); return LINKEDLIST_FALSE; }
	if (Queue_IsEmpty(q)) { printf("ERROR: Empty Queue !"); return LINKEDLIST_FALSE; }

#if _C_QUEUE_LINKEDLIST_IMP

	QueueNode lDelete = q->head;

	/* Get Data */
	*data = lDelete->data;

	/* Pop Operations */
	q->head = q->head->next;

	/* Free The Deleted Node */
	free(lDelete);

#else

	*data = q->nodes[q->headIdx].data;
	q->headIdx = Queue_VaildIdx(q->headIdx, q);

#endif

	/* Size -- */
	q->size--;

	return LINKEDLIST_TRUE;

}
複製程式碼

解析,出隊操作,在鏈式實現下就直接在鏈頭進行,而陣列實現下直接在陣列的第一個下標節點進行;

出隊操作圖示,

【鏈式實現】

資料結構:棧與佇列

// 對應的核心程式碼
QueueNode lDelete = q->head;
q->head = q->head->next;

free(lDelete);
複製程式碼

【陣列實現】

資料結構:棧與佇列

// 對應的核心程式碼
q->headIdx = Queue_VaildIdx(q->headIdx, q);
複製程式碼

參考書籍:
1、《演算法精解_C語言描述(中文版)》
2、《資料結構與演算法分析—C語言描述》

寫到這裡,本文結束!下一篇,《資料結構:集合》

相關文章