前面的10篇文章总体上过了一遍c语言的基础,这些基础是非常重要的基石。有助于我们理解下一个阶段的内容,我们常说程序=数据结构+算法
,本文将介绍一种常用的数据结构,链表。给出的示例代码具备复用性,如果你不关心内功的修炼,基本上可以拿来主义(白嫖);如果想提升内功,不妨仔细阅读代码。
什么是链表
链表(linked list)就是一串节点组成的集合,每一个节点就是一个独立的数据结构。可以将链表想象成一串珍珠,整串珍珠就相当于一个链表,每个珍珠就可看成是一个节点。
链表的类型有单链表和双链表,如果链表头尾相接那就是循环链表。链表的实现大体上可以分为两种,一种是基于数组,一种是基于动态节点,具体的实现细节也各不相同。每种类型的链表我会分享两种实现方式,第一种方式相对直观容易理解,第二种方式使用更多的技巧(下篇文章分享),使通用性更强一些。
单链表
重点在于吃透代码,如果理解上有困难,不妨拿纸笔画一画。或者评论留言。
方式一
先给出头文件link.h,介绍数据结构和几个API。结构体成员的作用以及API的作用和接口说明,已经非常详细的进行了注释。这种方式也是工程中常用的规范化的编码风格。
#ifndef _LINK_H
#define _LINK_H
#ifdef __cplusplus
extern "C"
{
#endif
#include <stdio.h>
#include <stdlib.h>
/**
* @brief 链表节点数据结构
*/
typedef struct _node
{
int data; // 节点的值
struct _node *next; // 指向下一个节点的指针
}node_t;
/**
* @brief head_append
* 向链表头中插入一个新的节点
*
* @param[in/out] header
* 链表头指针
* @param[in] val
* 新节点的值
*
* @return
* @val 0, 成功
* @val 1, 失败
*/
int head_append(node_t **header, int val);
/**
* @brief tail_append
* 向链表尾中插入一个新的节点
*
* @param[in/out] header
* 链表头指针
* @param[in] val
* 新节点的值
*
* @return
* @val 0, 成功
* @val 1, 失败
*/
int tail_append(node_t **header, int val);
/**
* @brief del_link
* 删除链表
*
* @param[in/out] header
*/
void del_link(node_t** header);
/**
* @brief find_mid
* 查找链表的中间节点
*
* @param[in] header
* 链表头指针
*
* @return 找到,则返回该中间节点的指针;否则,返回NULL
*/
node_t* find_mid(node_t** header);
/**
* @brief turnover_link
* 翻转链表
*
* @param[in/out] header
*/
void turnover_link(node_t** header);
/**
* @brief del_node
* 删除链表中值为val的所有节点
*
* @param[in/out] header
* @param[in] val
*/
void del_node(node_t** header, int val);
/**
* @brief print_link
* 打印链表
*
* @param[in] header
*/
void print_link(node_t *header);
#ifdef __cplusplus
}
#endif
#endif //_LINK_H
再来看看每个API在link.c中的具体实现。当然,每个函数的实现也进行了详细的注释。
#include "link.h"
// 头插,每个新节点都往链表头不插入
int head_append(node_t **header, int val)
{
// 创建新节点
node_t *new = malloc(sizeof(node_t));
if (NULL == new)
{
printf("malloc new node failed...\n");
return 1;
}
// 新节点的next指针指向链表的头节点
new->next = (*header);
new->data = val;
// 头指针指向新的节点
*header = new;
return 0;
}
// 尾插, 每个新节点都往链表尾部插入
int tail_append(node_t **header, int val)
{
node_t** tmp = header;
node_t* cur = NULL;
node_t *new = malloc(sizeof(node_t));
if (NULL == new)
{
printf("malloc new node failed...\n");
return 1;
}
new->data = val;
// 从前往后遍历找到最后一个节点
while(cur = *tmp)
tmp = &(cur->next);
new->next = *tmp;
*tmp = new;
return 0;
}
// 删除链表
// 从前往后遍历删除每一个节点
void del_link(node_t** header)
{
node_t *tmp = *header;
while(*header)
{
// 指向下一个节点
*header = (*header)->next;
// 释放头节点
free(tmp);
// 暂存下一个节点
tmp = *header;
}
}
// 查找中间节点
// 通过快慢指针查找中间节点
node_t* find_mid(node_t** header)
{
node_t* slow = *header;
node_t* fast = *header;
if (NULL == *header)
{
printf("empty link...\n");
return NULL;
}
while(fast->next)
{
slow = slow->next;
fast = fast->next;
if(fast->next != NULL)
fast = fast->next;
}
return slow;
}
// 翻转链表
// 通过两个指针保持旧链表的连接
// header总是指向新链表的头节点
void turnover_link(node_t** header)
{
node_t* tmp = *header; // tmp指向头节点
node_t* cur = NULL; // cur指针用来指向旧链表
*header = NULL; // header指向NULL
while(cur = tmp) // cur 指向待被转移的节点
{
tmp = tmp->next; // tmp指向待被转移的节点的下一个节点,保证不丢失旧链表
cur->next = *header; // 待被转移的节点指向新链表的头节点,待被转移节点与新链表建立联系
*header = cur; // 新链表的头指针指向待被转移的节点
}
}
// 删除链表中所有值为val的节点
void del_node(node_t** header, int val)
{
node_t* front = *header;
node_t* back = NULL; // 记录被删除节点的前一个节点
while(front)
{
if (front->data == val)
{
if (front == *header)
{
*header = front->next;
front->next = NULL;
free(front);
front = *header;
continue;
}
back->next = front->next;
front->next = NULL;
free(front);
front = back->next;
continue;
}
back = front;
front = front->next;
}
}
// 打印链表的内容
void print_link(node_t *header)
{
node_t* tmp = header;
if (NULL == header)
{
printf("empty link...\n");
return;
}
do
{
printf("%d\n",tmp->data);
}while(tmp = tmp->next);
}
以上只是完成了链表的实现,到底应该怎么调用,请继续往下看
#include "link.h"
// test_case1函数测试的功能:
// 简单的往链表中头插10个节点
// 打印链表节点
// 删除链表
int test_case1();
// test_case2函数测试的功能:
// 简单的往链表中尾插10个节点
// 打印链表节点,注意打印出的节点的值和test_case1中的order的不同
// 删除链表
int test_case2();
// test_case3函数测试的功能:
// 简单的往链表中尾插10个节点
// 打印链表节点
// 翻转链表
// 打印链表节点,注意比较翻转前后链表节点的次序
// 删除链表
int test_case3();
// test_case4函数测试的功能:
// 简单的往链表中尾插10个节点
// 打印链表节点
// 删除值为8的节点
// 打印链表节点,注意比较删除前后链表值的变化
// 删除链表
int test_case4();
// test_case4函数测试的功能:
// 简单的往链表中尾插9个节点
// 打印链表节点
// 查找链表的中间节点
// 删除链表
int test_case5();
int test_case1()
{
node_t* header = NULL;
int i = 0;
// 向链表中动态插入10个节点
for (i = 0; i < 10; i++)
head_append(&header, i);
print_link(header);
del_link(&header);
print_link(header);
return 0;
}
int test_case2()
{
node_t* header = NULL;
int i = 0;
// 向链表中动态插入10个节点
for (i = 0; i < 10; i++)
tail_append(&header, i);
print_link(header);
del_link(&header);
print_link(header);
return 0;
}
int test_case3()
{
node_t* header = NULL;
int i = 0;
// 向链表中动态插入10个节点
for (i = 0; i < 10; i++)
tail_append(&header, i);
printf("翻转前的链表:\n");
print_link(header);
turnover_link(&header);
printf("翻转后的链表:\n");
print_link(header);
del_link(&header);
print_link(header);
return 0;
}
int test_case4()
{
node_t* header = NULL;
int i = 0;
// 向链表中动态插入10个节点
for (i = 0; i < 10; i++)
tail_append(&header, i);
printf("节点删除前的链表:\n");
print_link(header);
// 删除一个val为8节点
printf("节点删除后的链表:\n");
del_node(&header, 8);
print_link(header);
del_link(&header);
print_link(header);
return 0;
}
int test_case5()
{
node_t* header = NULL;
node_t* mid = NULL;
int i = 0;
// 向链表中动态插入9个节点
for (i = 0; i < 9; i++)
tail_append(&header, i);
printf("链表节点信息:\n");
print_link(header);
mid = find_mid(&header);
printf("mid node data:%d\n", mid->data);
del_link(&header);
print_link(header);
return 0;
}
int main(int argc, char** argv)
{
int n = atoi(argv[1]);
switch (n)
{
case 1:
test_case1();
break;
case 2:
test_case2();
break;
case 3:
test_case3();
break;
case 4:
test_case4();
break;
case 5:
test_case5();
break;
default:
printf("not support operate!\n");
}
return 0;
}
下面就是每个test_case执行的结果。
$ ./slink1 1
9
8
7
6
5
4
3
2
1
0
empty link...
$ ./slink1 2
0
1
2
3
4
5
6
7
8
9
empty link...
$ ./slink1 3
翻转前的链表:
0
1
2
3
4
5
6
7
8
9
翻转后的链表:
9
8
7
6
5
4
3
2
1
0
empty link...
节点删除前的链表:
0
1
2
3
4
5
6
7
8
9
节点删除后的链表:
0
1
2
3
4
5
6
7
9
empty link...
$ ./slink1 5
链表节点信息:
0
1
2
3
4
5
6
7
8
mid node data:4
empty link...