在討論鏈表(linked-list)之前,需要明確幾個概念:線性表(順序表, list, linear list), 數組(array),鏈表(linked-list)。
線性表:在中文裏,線性表也叫作順序表。在英文中,它稱爲list, linear list等。它是最基礎、最簡單、最常用的一種基本數據結構,線性表總存儲的每個數據稱爲一個元素,各個元素及其索引是一一對應的關係。線性表有兩種存儲方式:順序存儲方式和鏈式存儲方式。
數組(array):數組就是線性表的順序存儲方式。數組的內存是連續分配的,並且是靜態分配的,即在使用數組之前需要分配固定大小的空間。可以通過索引直接得到數組中的而元素,即獲取數組中元素的時間複雜度爲O(1)。
鏈表(linked-list):鏈表就是線性表的鏈式存儲方式。鏈表的內存是不連續的,前一個元素存儲地址的下一個地址中存儲的不一定是下一個元素。鏈表通過一個指向下一個元素地址的引用將鏈表中的元素串起來。
來源:http://i.stack.imgur.com/puPVJ.jpg
其實爲了簡便,通常我們也直接將list看做是鏈表。但是也不必太過糾結這種名稱定義,更重要的還是關注數據結構的實現。
The thing and the name of the thing are two different things - Richard Feynman
鏈表分類
鏈表分爲單向鏈表(Singly linked lis)、雙向鏈表(Doubly linked list)、循環鏈表(Circular Linked list)。
單向鏈表(Singly linked lis)
單向鏈表是最簡單的鏈表形式。我們將鏈表中最基本的數據稱爲節點(node),每一個節點包含了數據塊和指向下一個節點的指針。
來源:https://upload.wikimedia.org/wikipedia/commons/4/45/Link_zh.png
typedef struct node
{
int val;
struct node *next;
}Node;
頭結點
單向鏈表有時候也分爲有頭結點和無頭結點。有頭結點的鏈表實現比較方便(每次插入新元素的時候,不需要每次判斷第一個節點是否爲空),並且可以直接在頭結點的數據塊部分存儲鏈表的長度,而不用每次都遍歷整個鏈表。
// create a new node with a value
Node* CreateNode(int val)
{
Node *newNode = (Node*)malloc(sizeof(Node));
if (newNode == NULL)
{
printf("out of memory!\n");
return NULL;
} else
{
newNode->val = val;
newNode->next = NULL;
return newNode;
}
}
int main(){
Node *head = CreateNode(0);
//insert new value into list, end with END_INPUT(999)
int value;
while (scanf("%d", &value) && value != END_INPUT)
{
Insert(head, value);
}
return 0;
}
插入
在鏈表中插入一個新的元素有兩種方式:後插和前插。後插就是每次在鏈表的末尾插入新元素,前插就是在鏈表的頭插入新元素。
後插法比較符合平常的思維方式,並且保證插入數據的先後順序。但是由於只保存了頭結點,所以每次插入新元素必須重新遍歷到鏈表末尾。爲了解決這個問題,考慮增加一個尾指針,指向鏈表的最後一個節點。
來源:http://c.biancheng.net/cpp/uploads/allimg/140709/1-140F9153GJ93.jpg
void Insert(Node *head, Node *tail, int val)
{
Node *newNode = CreateNode(val);
tail->next = newNode;
tail = tail->next;
head->val ++;
}
由於前插法是在頭部插入新元素,那麼每次增加新元素可以直接通過頭指針索引,但是得到的元素順序與插入順序相反。
來源:http://c.biancheng.net/cpp/uploads/allimg/140709/1-140F9152T3201.jpg
void Insert(Node *head, int val)
{
Node *newNode = CreateNode(val);
newNode->next = head->next;
head->next = newNode;
head->val ++;
}
刪除
由於單向鏈表只存儲了頭指針,所以刪除單向鏈表中的元素時,需要找到目標節點的前驅節點。
void DeleteByVal(Node *head, int val)
{
if (head->next == NULL)
{
printf("empty list!\n");
return;
}
//find target node and its precursor
Node *cur = head->next, *pre = head;
while(cur)
{
if (cur->val == val)
break;
else {
cur = cur->next;
pre = pre->next;
}
}
//delete target node
pre->next = cur->next;
free(cur);
head->val--;
}
清空鏈表
由於鏈表裏面的內存是手動分配的,當不再使用這些內存時需要手動刪除。
void Free(Node *head)
{
for (Node *temp = head; temp != NULL; temp = head->next)
{
head = head->next;
free(temp);
}
}
鏈表反轉
Node* Reverse (Node* head) {
if (head == NULL || head->next == NULL)
return head;
else {
Node *cur = head->next,
*pre = NULL,
*next = NULL;
while (cur != NULL) {
next = cur->next;
cur->next = pre;
pre = cur;
cur = next;
}
head->next = pre;
return head;
}
}
雙向鏈表(Doubly linked list)
顧名思義,雙向鏈表就是有兩個方向的鏈表。同單向鏈表不同,在雙向鏈表中每一個節點不僅存儲指向下一個節點的指針,而且存儲指向前一個節點的指針。通過這種方式,能夠通過在O(1)時間內通過目的節點直接找到前驅節點,但是同時會增加大量的指針存儲空間。
typedef struct node
{
int val;
struct node *pre;
struct node *next;
}Node;
插入
在雙向鏈表中插入新元素的操作跟在單向鏈表中插入新元素的操作類似。
Node* CreateNode(int val)
{
Node *newNode = (Node*)malloc(sizeof(Node));
if (newNode == NULL)
{
printf("out of memory!\n");
return NULL;
} else
{
newNode->val = val;
newNode->next = NULL;
newNode->pre = NULL;
return newNode;
}
}
void Insert(Node *head, int val)
{
Node *newNode = CreateNode(val);
newNode->next = head->next;
head->next->pre = newNode;
head->next = newNode;
}
刪除
由於雙向鏈表中每個節點記錄了它的前驅結點,所以不需要像單向鏈表中一樣索引目的節點的前驅節點,而是可以通過目標節點直接獲得。
Node* FindByVal(Node *head, int val)
{
for(Node* temp = head; temp != NULL; temp = temp->next)
{
if (temp->val == val)
return temp;
}
return NULL;
}
void DeleteByVal(Node *head, int val)
{
Node *target = FindByVal(val);
if (target == NULL)
{
printf("not find target value!\n");
return;
}
target->pre->next = target->next;
target->next->pre = target->pre;
free(target);
}
其他
- 如何判斷當前節點是否爲第一個節點?
- 如何判斷當前節點是否爲最後一個節點?
在雙向鏈表中,第一個節點的前驅節點不是頭結點,而是指向一個空指針。同樣的,最後一個節點的後驅指向了一個空指針。
循環鏈表(Circular Linked list)
循環鏈表與雙向鏈表相似,不同的地方在於:在鏈表的尾部增加一個指向頭結點的指針,頭結點也增加一個指向尾節點的指針,以及第一個節點指向頭節點的指針,從而更方便索引鏈表元素。
來源:https://p-blog.csdn.net/images/p_blog_csdn_net/blacklord/%E5%9B%BE2.15.JPG
插入、刪除
循環鏈表的插入和刪除操作與雙向鏈表的實現方式一樣。
判斷空鏈表、鏈表頭和尾
從上圖(a)中可以明顯觀察到,一個空的雙向循環鏈表中只有一個頭節點,頭節點的前驅和後驅都指向本身。
從圖(b)中可以看到,不同於雙向鏈表,循環鏈表中第一個節點和尾節點不在指向空指針,而是指向了頭節點。