用C语言实现单链表操作

用C写一个链表

链表(Linked List)是一种非连续的线性数据结构,相对于数组,它允许数据在内存中非连续存储,但是不支持随机读取。

链表

链表由一个个节点(Node)组成,每个节点除了记录数据以外,还需要记录下一个节点的位置(如果是双向链表,还需要记录上一个节点的位置)

1
2
3
4
5
6
7
struct _Node;
typedef struct _Node Node;

struct _Node{
int data; //记录整型数据
Node *next;
};

对于第一个节点,我们有一个指针指向它的地址,对于最后一个节点,它需要指向NULL,表示链表结束了。

1
2
3
4
5
typedef struct _List {
Node *head; //记录头地址
Node *tail; //记录尾巴地址
int num;
} List;

有了链表的数据结构后,我们需要定义三个基本函数,用于创建链表,往链表中加入数据和删除链表

创建链表比较简单,就是为链表分配内存,并将其赋值给一个指针,然后返回

1
2
3
4
5
6
7
8
// 创建链表
List *CreateList()
{
List *list;
list = (List*)malloc( sizeof(List) );
list->num = 0;
return list;
}

加入数据时,我们需要先声明两个节点指针,第一个用于记录当前节点的位置,第二个是记录新节点的位置。如果链表中没有节点,也就是head指向为NULL,那么直接插入新节点即可。如果链表中已经有了节点,那么获取最后第一个节点的位置, 然后在它的后面加入节点,同时将tail指向新的节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
bool AddNode(List *list, int data)
{
Node *node;
Node *new_node;

new_node = (Node *)malloc( sizeof(Node) );
if ( new_node == NULL) return false;
new_node->data = data;
new_node->next = NULL;

//获取链表head
node = list->head ;
//如果head指向NULL, 则直接插入到下一个
if ( node == NULL){
list->head = new_node;
list->tail = new_node;
list->num = 1;
return true;
}
// 否则在尾部插入节点
node = list->tail ;
node->next = new_node;
list->tail = new_node;
list->num+=1;

return true;

}

删除列表分为两步,先删除节点内容,然后删除列表这个结构。如果节点存放的数据是其他结构,那么还需要先删除节点存放的其他数据。

1
2
3
4
5
6
7
8
9
10
11
12
void DestroyList(List *list)
{
Node *current;
Node *next;
current = list->head;
while (current->next != NULL){
next = current->next;
free(current);
current = next;
}
free(list);
}

我们还可以定一个输出函数,将链表里存放的数据依次输出

1
2
3
4
5
6
7
8
9
//打印整个链表
void dump(List *list){
Node *node;
node = list->head;
while (node != NULL){
printf("%08d\n", node->data);
node = node->next;
}
}

有了上面的基本函数时候,我们就能够读取存放数字的文本,将其加入到链表中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int main(int argc, char const *argv[])
{
/* code */
if (argc == 1) exit(EXIT_FAILURE);

FILE *fp;
fp = fopen(argv[1], "r");
if (fp == NULL){
perror(argv[1]);
exit(EXIT_FAILURE);
}
int data;
List *list;
list = CreateList();
while (fscanf(fp, "%d", &data) != EOF){
AddNode(list, data);
}
dump(list)
return 0;

我们的链表还应该支持插入操作和删除操作。对于插入操作,我们要分为是插入到给定位置前,还是给定位置后。对于删除而言,也就是都是删除当前节点,而为了删除当前节点,我们需要前一个节点的位置。

无论是插入还是删除,我们都需要知道插入的位置和删除的位置,因此我们还需要一个搜索函数,用于搜索等于给定值的节点位置或者是上一个位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 查找元素
// situ=true时, 返回当前位置, false, 则返回上一个位置
Node *Search(List *list, int data, bool situ)
{
Node *node;
node = list->head;
if ( situ ){
while ( node->next != NULL ){
if ( node->data == data)
return node;
node = node->next;
}
} else {
while ( node->next->next != NULL) {
if (node->next->data == data)
return node;
node = node->next;
}
}
return NULL;
}

我们先写一个删除操作, 用于删除等于给定的节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//删除节点
bool DeleteNode( List* list, int data){

Node *node;
Node *tmp;
node = list->head;
// 判断这个节点是否是首节点
if ( node->data == data ){
free(list->head);
list->head = NULL;
list->tail = list->head;
list->num = 0;
return true;
}
// 查找给定节点的前一个节点
node = Search(list, data, false);
// 找不到节点
if ( node == NULL){
return false;
}
//删除
tmp = node->next->next;
free(node->next);
node->next = tmp;
return true;
}

然后将元素加入函数分为两种,一种是插入(当前位置前),一种是追加(当前位置后)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
//在给定元素前加节点
bool InsertNode( List* list, int query, int data){

Node *node;
Node *new_node;

// 为新节点分配内存
new_node = (Node *)malloc( sizeof(Node) );
if ( new_node == NULL) return false;
new_node->data = data;

node = list->head;
// 判断这个节点是否是首节点
if ( node->data == query ){
new_node->next = node->next ;
node->next = new_node;
return true;
}

// 查找给定节点的前一个节点
node = Search(list, query, false);
// 找不到节点
if ( node == NULL){
return false;
}
new_node->next = node->next ;
node->next = new_node;

return true;

}

//在给定元素后加
bool AppendNode( List* list, int query, int data){

Node *node;
Node *new_node;

// 为新节点分配内存
new_node = (Node *)malloc( sizeof(Node) );
if ( new_node == NULL) return false;
new_node->data = data;

// 查找给定节点的位置
node = Search(list, query, true);
// 找不到节点
if ( node == NULL){
return false;
}
new_node->next = node->next;
node->next = new_node;

return true;

}

进阶操作

上面都是链表的基础操作,创建、摧毁,增加,删除。下面几个则是考验对链表对深刻理解,

  • 单链表反转
  • 链表中环的检测
  • 两个有序链表的合并
  • 删除链表倒数第N个结点
  • 求链表的中间结点

单链表反转

如果要将单链表进行反转,每次移动的时候需要三个位置,前一个位置,当前位置和head。每次将head向后移动,记录了当前位置的下一个节点,然后将当前位置指向前一个位置。最后将前一个位置和当前位置向后移动。图解如下, 首先head指向链表第一个节点

head赋值

然后将cur设置到当前的head

赋值cur
接着将head往后移动一个位置, 保存了原本在cur后面的位置

head后移

然后将cur指向到res,也就是前面的位置

cur指向res

上面的操作后,就将res和cur的顺序反转了。接着就是将res和cur往后移动

移动cur和res

代码为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
List* reverseList(List* list){

Node *curr, *res;
res = NULL;
curr = list->head;
//尾巴是之前的开头
list->tail = list->head;
while ( curr ){
//移动head
list->head = list->head->next;
//将当前位置指向前一个位置
curr->next = res;
//依次向后移动res和curr
res = curr;
curr = list->head;
}
list->head = res;
return list;
}

中间节点

为了寻找中间节点,我们可以定义两个指针,快指针和慢指针。慢指针一次一步,快指针一次两步. 如果是偶数,那么快指针最后是NULL,如果是奇数,那么快指针的下一个是NULL。

1
2
3
4
5
6
7
8
9
10
11
12
Node *FindMidlle(List *list)
{
if (list->num == 0) return NULL;
Node *fast = list->head;
Node *slow = list->head;
while ( fast != NULL && fast->next != NULL){
slow = slow->next;
fast = fast->next->next;
}
return slow;

}

删除倒数第N个指针

同上,也是快慢两个指针,快指针先走N步,然后两个指针再一起走。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
bool RemoveLastN(List *list, int n)
{
//删除第一个
if ( list->num == n){
list->head = list->head->next;
return true;
}
Node *fast = list->head;
Node *slow = list->head;
Node *tmp;
while (n-- > 0){
fast = fast->next;
}
while (fast->next != NULL){
fast = fast->next;
slow = slow->next;
}
tmp = slow->next;
slow->next = slow->next->next;
free(tmp);
return true;
}

有序链表合并

假设两个有序链表分别为1->3->5->7->82->3->4->5->8, 那么合并之后应该是1->2->3->3->4->5->7->8.

我们需要创建一个新的链表用于存放两个链表排序的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
//合并两个链表
List *MergeSortedList(List *list_a, List *list_b)
{
List *list_c;
list_c = CreateList();
Node *node_a, *node_b, *node_c;
node_a = list_a->head;
node_b = list_b->head;

//确定新列表的head
if ( node_a->data < node_b->data ){
list_c->head = node_a;
node_a = node_a->next;
} else {
list_c->head = node_b;
node_b = node_b->next;

}
node_c = list_c->head;
while( true ){
if (node_a->data < node_b->data){
node_c->next = node_a;
node_a = node_a->next;
if (node_a == NULL) break;
} else{
node_c->next = node_b;
node_b = node_b->next;
if (node_b == NULL) break;
}
node_c = node_c->next;
}
while ( node_a != NULL){
node_c->next = node_a;
node_a = node_a->next;
node_c = node_c->next;

}
while ( node_b != NULL){
node_c->next = node_b;
node_b = node_b->next;
node_c = node_c->next;
}
return list_c;
}

为了测试这个代码正确性,我写了一个测试函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
int MergeTest( const char *file1, const char *file2){
FILE *f1;
FILE *f2;

int data;
f1 = fopen(file1, "r");

List *list1;
list1 = CreateList();
//读取数据
while (fscanf(f1, "%d", &data) != EOF){
AddNode(list1, data);
}
dump(list1);
fclose(f1);

f2 = fopen(file2, "r");
if (f2 == NULL){
perror(file2);
exit(EXIT_FAILURE);
}
List *list2;
list2 = CreateList();

//读取数据
while (fscanf(f2, "%d", &data) != EOF){
AddNode(list2, data);
}
dump(list2);
fclose(f2);

List *res;
res = MergeSortedList(list1, list2);
dump(res);
return 0;

}

最终的代码在GitHub上https://github.com/xuzhougeng/learn-algo/blob/master/link_list.c

LeetCode和链表有关的几个题目