在前面幾篇博文中曾經提到鏈表(list)、隊列(queue)和(stack),為了更加系統化,這里統一介紹着三種數據結構及相應實現。
1)鏈表
首先回想一下基本的數據類型,當需要存儲多個相同類型的數據時,優先使用數組。數組可以通過下標直接訪問(即隨機訪問),正是由於這個優點,數組無法動態添加或刪除其中的元素,而鏈表彌補了這種缺陷。首先看一下C風格的單鏈表節點聲明:
// single list node define
typedef struct __ListNode
{
int val;
struct __ListNode *next;
}ListNode;
所謂單鏈表,即只有一個指針,該指針指向下一個元素的地址。通常只要知道鏈表首地址,則可以遍歷整個鏈表。由於鏈表節點是在堆區動態申請的,其地址並不是連續的,因此無法進行隨機訪問,只有通過前一節點的next指針才能定位下一節點的地址。
單鏈表只能向后遍歷,無法逆序遍歷,因此誕生了使用更廣泛的雙鏈表,即節點內部增加一個字段*prev,用以存儲該節點的前一個節點地址。雙鏈表可以雙向遍歷,但仍然只能順序訪問,無法像數組那樣隨機訪問。以下均以單鏈表為例介紹其構造、插入和刪除。
構造一個鏈表:
// Init single list without head node
ListNode *list_init(int arr[], int n)
{
ListNode *h;
for(h=NULL; n--; list_append(&h,arr[n]));
return h;
}
注意,帶頭節點的鏈表可以簡化大量操作,因此有些鏈表操作需要頭結點(頭結點不存儲鏈表內容)。此處返回一個不帶頭結點的鏈表,當需要頭結點時,可以新建一個節點,並將其next指向該鏈表首地址,但這並不意味着鏈表頭節點存儲在堆區。換言之,鏈表頭節點可以存儲在棧區(注意這里的堆區和棧區指內存中的特定區域,非數據結構中的堆和棧)。一種典型的使用是:
// Using Virtual Head which stored in stack memory not in heap memory ... ListNode *listhead=list_init(arr,n); ListNode H; // stored in stack memory not in heap memory H.next = listhead; list_dosome(&H);// do some operator by using &H which contains head node ...
當上面執行完畢,會自動將棧區的數據進行釋放,則H節點會自動釋放,其next地址作為真正的鏈表首地址。這樣的好處是,代碼結構與有頭結點的鏈表操作一樣簡單,並且頭結點不會永久占用內存空間,達到隨時使用,隨時“申請”的效果。如不加特殊說明,下面均以不帶頭結點的鏈表進行操作。
/* Single list append and erase node sketch * ListNode *h=0x0010; * List: 15->20->10->15->NULL * addr: 0x0010 +-> 0x2010 +-> 0x1014 +-> 0x0200 * val: 15 | 20 | 10 | 15 * next: 0x2010 -+ 0x1014 -+ 0x0200 -+ 0x0000 -->NULL * * List: 15->20->10->15->NULL * append:7 * addr: 0x3014 +-> 0x0010 +-> 0x2010 +-> 0x1014 +-> 0x0200 * val: 7 | 15 | 20 | 10 | 15 * next: 0x0010 -+ 0x2010 -+ 0x1014 -+ 0x020- -+ 0x0000 -->NULL * ListNode *h=0x3014; h->next = 0x0010; * List: 7->15->20->10->15->NULL * * List: 15->20->10->15->NULL * erase: 15 * addr: 0x2010 +-> 0x1014 * val: 20 | 10 * next: 0x1014 -+ 0x0000 -->NULL * ListNode *h=0x2010; * List: 20->10->NULL * */
如圖,插入節點使用頭插入法,因此插入7時,需要將鏈表首地址更改為7的地址,並將其next指向原來的鏈表首地址;鏈表刪除,需要注意鏈表的重復元素,以及當刪除的節點為首地址時的情況。
下面給出一種單鏈表的插入節點方法:
// list append, using head insertion
void list_append(ListNode **head, int val)
{
ListNode *ln = (ListNode*)malloc(sizeof(ListNode));
ln->val = val;
ln->next = *head;
*head = ln;
}
單鏈表的插入方法有頭插入和尾插入,前者將新節點插入到鏈表開始位置,后者將新節點插入到尾部。通常只給出鏈表首地址,所以上面提供了頭插入方法。注意,此處鏈表插入的參數為二級指針,為什么這樣操作?因為,每次插入時,鏈表首地址將發生改變(假如一個鏈表帶頭結點,則不需這種處理)。也可通過返回值回傳新的鏈表首地址,然而每次插入一個節點,都要將新鏈表地址重新寫回(請回想二叉樹的插入方法)。
單鏈表的刪除操作略有不同,鏈表節點可能存在重復,因此需要刪除所有為給定值的節點,如下代碼,其返回值為刪除的節點個數(0表示沒有找到該節點):
// list erase, may erase first node
int list_erase(ListNode **head, int val)
{
int c = 0;
ListNode *t, *h, H;
for(H.next=*head, h=&H, t=h->next; t; t=h->next){
if (t->val == val){
h->next = t->next;
free(t);
++c; // may have several node value equal to val
}
else h=t;
}
*head = H.next;
return c; // return erase count
}
如果一個鏈表有兩個節點,其值均為10,而此時需要刪除10,那么就要處理鏈表首地址為待刪除節點的情況。上面代碼同樣需要傳入二級鏈表首地址。注意5~14行,這里就是在棧區添加一個頭節點,方便了大量操作。
最后,當一個鏈表確定不再需要時,請不要忘記將其釋放掉,並將鏈表首地址指向NULL。
2)隊列(queue)
隊列即按照數據到達的順序進行排隊,每次新插入一個節點,將其插到隊尾;每次只有對頭才能出隊列。簡言之,對於數據元素的到達順序,做到“先進先出”。由於隊列通常頻繁的插入與刪除,為了高效,一般使用固定長度的數組進行實現,並且可循環使用數組空間,所以要經常處理當前隊列是否為滿或為空。如需要動態長度,可以用鏈表實現,只需要同時記住鏈表首地址(隊列的頭)和尾地址(隊列的尾)。下面使用定長數組實現一個循環隊列:
// recyle queue struct to storage information
typedef struct __QueueInfo{
int *date;
unsigned int front, rear;
unsigned int capacity;
}QueueInfo;
// recyle queue initializatio
QueueInfo *queue_init(unsigned int size)
{
if (size < 1) return NULL;
QueueInfo *q = (QueueInfo*)malloc(sizeof(QueueInfo));
q->data = (int*)malloc(sizeof(int)*size);
q->capacity = size;
q->front = q->rear = 0;
return q;
}
其中使用QueueInfo存儲當前隊列的一些信息,data為動態申請的連續的隊列空間,front指向隊列頭,rear為隊列尾部,capacity為隊列可容納的大小。初始化時,將front與rear都置為0。由於是循環使用隊列空間,當逐漸入隊capacity個元素時,此時front超過了隊列容量,需要將其重置到0位置,這樣將無法判斷當前隊列是滿還是空。一種解決辦法是,僅使用capacity-1個空間進行存儲,始終保持front與rear之間存在不小於1個可用空間,此方法與鏈表的頭節點有異曲同工之妙。
/* Recyle Queue Operator Push and Pop Sketch * Queue Size: 4 * Capacity : 7 * front rear front rear rear front * | | | | | | * 1 2 3 4[ ][ ][ ] [ ]1 2 3 4[ ][ ] 4[ ][ ][ ]1 2 3 * (1) (2) (3) * * PUSH : 5 * front rear front rear rear front * | | | | | | * 1 2 3 4 5[ ][ ] [ ]1 2 3 4 5[ ] 4 5[ ][ ]1 2 3 * (1+) (2+) (3+) * * POP : * front rear front rear rear front * | | | | | | * [ ]2 3 4[ ][ ][ ] [ ][ ]2 3 4[ ][ ] 4[ ][ ][ ][ ]2 3 * (1-) (2-) (3-) * */
對於同樣容量為7,大小為4的循環隊列,有以上三種情況。所以當判斷隊列是否為空、或者是否有可用空間時,切勿直接判斷front與rear的大小。因此,當進行入隊和出隊時,也要針對不同情況進行處理。每次入隊時,將元素覆蓋在rear處,並將rear后移一位,注意判斷隊列為空還是滿,並且保證其不大於capacity。出隊則從隊頭刪除,只需將front向后移動即可。
下面是隊列的插入、刪除:
// recyle queue push
int queue_push(QueueInfo *q, int val)
{
if (q==NULL) return -1; // need queue
if ((q->rear+1)%q->capacity == q->front) return 0;
q->data[q->rear] = val;
q->rear = (q->rear+1)%q->capacity;
return 1; // return push count
}
// recyle queue pop
int queue_pop(QueueInfo *q)
{
if (q==NULL || q->front==q->rear) return 0;
q->front = (q->front+1)%q->capacity;
return 1; // return pop count
}
通常為了便於調用使用,一般提供訪問當前隊列的隊頭,和獲取隊列大小、容量信息,如下:
// get queue front
int queue_front(QueueInfo *q)
{
if (q==NULL || q->front==q->rear) return 0;
return q->data[q->front];
}
// get queue size
unsigned int queue_size(QueueInfo *q)
{
if (q==NULL) return 0;
if (q->front <= q->rear) return q->rear - q->front;
else return q->capacity - q->front + q->rear - 1;
}
// get queue capacity
unsigned int queue_capacity(QueueInfo *q)
{
if (q==NULL) return 0;
return q->capacity;
}
3)棧(stack)
棧的特點與隊列正好相反,按照數據入棧順序逆序出棧,即“后進先出”。每次入棧將元素放在棧頂,出棧時從棧頂開始出棧。通常會對棧進行頻繁入棧和出棧,與隊列類似,一般使用定長數組存儲棧元素,而不是動態申請節點空間。同樣給出棧的定義和初始化代碼:
// easy stack struct to storage stack information
typedef struct __StackInfo
{
int *data;
unsigned int size;
unsigned int capacity;
}StackInfo;
// stack init, size=0
StackInfo *stack_init(unsigned int capacity)
{
if (capacity < 1) return NULL;
StackInfo *s = (StackInfo*)malloc(sizeof(StackInfo));
s->data = (int*)malloc(sizeof(int)*capacity);
s->capacity = capacity;
s->size = 0;
return s;
}
與隊列類似,使用一個結構體存儲當前棧的大小和容量。由於入棧和出棧都在棧頂,所以只需要一個size字段存儲當前棧的大小。每次入棧時,將size向后移動;出棧時將size向前移動,注意不要超過容量,初始化size為0。
/* easy stack push and pop sketch * initialize push:4 pop * Capacity: 7 7 7 * Size : 3 4 2 * Top<---- [ ] [ ] [ ] * [ ] [ ] [ ] * [ ] size<--- [ ] [ ] * size<--- [ ] 4 [ ] * 3 3 size<--- [ ] * 2 2 2 * Bottom<-- 1 1 1 * */
下面給出入棧出棧的一種實現:
// stack push
int stack_push(StackInfo *s, int val)
{
if (s==NULL) return -1; // need stack
if (s->size >= s->capacity) return 0;
s->data[s->size++] = val;
return 1; // return push count
}
// stack pop
int stack_pop(StackInfo *s)
{
if (s==NULL || s->size<1) return 0;
s->size--;
return 1; // return pop count
}
棧的操作比較簡單,只有一個指針size,並且不需要循環操作。通常也需要獲取當前棧的大小等信息,如下:
// get stack top
int stack_top(StackInfo *s)
{
if (s==NULL || s->size<1) return 0;
return s->data[s->size-1];
}
// get stack size
unsigned int stack_size(StackInfo *s)
{
if (s==NULL) return 0;
return s->size;
}
// get stack capacity
unsigned int stack_capacity(StackInfo *s)
{
if (s==NULL) return 0;
return s->capaccity;
}
鏈表、隊列和棧的概念介紹完畢,雖然很簡單,但是就像數組那樣簡單而又廣泛使用。以上均為C風格代碼,對於C++風格並沒介紹。因為STL中已經包含了這三種數據結構,並使用模板類進行書寫。其中隊列和棧為動態增長的,不必要初始其容量。當需要使用這三種數據結構時,優先使用STL提供的代碼,而不是自己動手實現。
注:本文涉及的源碼:single list : https://git.oschina.net/eudiwffe/codingstudy/blob/master/src/list/singlelist.c
recyle queue : https://git.oschina.net/eudiwffe/codingstudy/blob/master/src/queue/recylequeue.c
esay stack : https://git.oschina.net/eudiwffe/codingstudy/blob/master/src/stack/easystack.c
