本文內容:
一、棧
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) 三個操作;棧操作集圖示:
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 0
,STACK_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 兩個操作;佇列操作集圖示:
3、佇列的 C 實現.
佇列其實更接近單連結串列了,具備頭和尾,當然遍歷方向也是從頭到尾,所以直接使用單連結串列來實現就可以了,不需要做太多的修改;
不過這裡的,陣列實現就要有點技巧了;
因為佇列是一個埠進,另一個埠出,也就是要有一個指向進入方向的下標 headIdx
,以及出方向的下標 tailIdx
;
這裡要注意的是, headIdx
與 tailIdx
的大小關係是不定的,這是由於陣列自初始化後,空間是固定的,而在頻繁的入隊與出隊操作後,會出現 headIdx > tailIdx
、headIdx < tailIdx
、headIdx = 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 + 1
;
Queue_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語言描述》
寫到這裡,本文結束!下一篇,《資料結構:集合》