[數據結構]——鏈表(list)、隊列(queue)和棧(stack)


在前面幾篇博文中曾經提到鏈表(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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM