目录
前言(这里全部用伪代码)
链表是可以用来优化管理零散存储区域的一种数据存储结构。一个链表是由若干个单位结构体变量组成的,单位结构变量里面包含了数据和以及结构体的指针。在底层开发,一般物理地址是连续固定的,因此可以将固定的数据写入连续的存储空间(例如数组等)。但是如果上升到系统应用层面,很显然这一种存储数据方式是很不合理的。例如以下的存储区域:虽然第一行数据A1\A2\A3\A4\A5是放在一起连续的数据,但是第二行存放的数据,数据之间的存放并不是连续的。若要存放连续的数据,则要开辟另外连续的存储空间,这样反而更加浪费系统资源。
数据A1 | 数据A2 | 数据A3 | 数据A4 | 数据A5 |
0xff | 数据B | 0xff | 数据C1 | 数据C2 |
而链表却能解决这一个问题。他可以把两个空的存储空间组成一个可以存放占用连续两个地址的数据。通过链表,把存放0xff的地址用某种方法连接起来,用于存放占用两个连续地址的数据。
数据A1 | 数据A2 | 数据A3 | 数据A4 | 数据A5 |
链表结构1 | 数据B | 链表结构2 | 数据C1 | 数据C2 |
1、建立一个链表
下面是一个结构体,里面有一个uint32_t类型的变量data,和一个node类型的结构体指针next。
struct node
{
uint32_t data;
struct node* next;
}
在定义好上面的结构体之后,在这里再定义几个结构体变量。然后把第一个结构体中的结构体指针node1.next取结构体node2的地址,把第二个结构体中的结构体指针node2.next取结构体node3的地址。
struct node node1 = {1,NULL};
struct node node2 = {2,NULL};
struct node node3 = {3,NULL};
node1 -> next = &node2;
node2 -> next = &node3;
这就成成为了一个有node1、node2、node3组成的静态链表。每个链表单位结构体组成如下:
uint32_t data | struct node* next |
然后上一个单位结构体的指针指向下一个单位结构体的地址。
node1 | -> | node2 | -> | node3 |
在就是链表(这种链表是静态的)的雏形,他有头部node1、中间成员node2以及尾部node3。每个单位结构体通常被称为:节点,一个节点通常包含着数据和指针信息。一般地,静态链表的作用有限,在实际应用中,都是多使用动态链表。
2、malloc和free
在C的库中,要知道两个函数:malloc和free,就可以控制系统的内存分配,这是后面建立动态链表的工具之一。
如下,是malloc以及free的使用方法:
uint32_t *p = (uint32_t*)malloc(sizeof(uint32_t));
\*定义一个指针p,然后用malloc开辟一个uint32_t类型的区域给指针p,并且把指针强制转换成uint32_t*类型*\
*p = 1;\\给p指针指向的区域写入一个数据:1
free(p);\\手动释放这个区域的空间
p = NULL;\\指针p用完了,指向NULL防止被其他程序误用
malloc是向内存(栈堆)申请一块可以存放指定类型数据的存储空间。然后free是把指定的空间释放出去。所以上面的操作,最后p指针指向的区域里面的数据本来是1,后面就释放掉了。可以是这样理解,比如地址0x80fe上面本来有一个变量为uint32_t a,然后用free释放了,系统就可以把这个空间分配给其他变量,例如分给了uint32_t b。所以free释放的只是空间,不是连指针也一起消失的。
关于栈和堆,栈上面是放的是指针,这些指针指向的却是堆上面的数据。当程序运行完之后相应的栈被销毁,但是,原来这个栈上指针指向的堆数据还在的!!!所以一般一个malloc就和一个free对应,不要数量不等,否则容易出问题。
3、建立一个动态链表
一般地,静态链表需要在程序运行完之后才会释放空间,这样使得系统的效率,存储空间利用率降低。为此,需要使用动态链表。有下面一段代码,建立了一个动态链表表头。
struct node
{
uint32_t data;
struct node* next;
}
struct node* creat_list(void)\\建立一个返回类型为struct node*的函数用来建立动态链表
{
struct node* head = (struct node*)malloc(sizeof(struct node));
\\建立一个struct node*类型的指针head,指向一个开辟的内存空间,可以存放struct node类型数据
head.data = 1;
head.next = NULL;
return head;
}
在这里,首先是建立一个nood的结构体,里面有一个uint32_t类型数据data,一个struct node*类型指针next。然后建立一个返回struct node*类型指针的函数creat_list,用于建立一个链表的表头,这个表头(头节点)里面数据是1,往后一个指针next指向NULL。整个指针函数返回的是表头head的指针。
4、建立节点
上一步是编写了一个返回表头的指针函数,这一步实现的是编写一个建立节点的函数。在上一步的基础上,再编写一个creat_node函数。
struct node
{
uint32_t data;
struct node* next;
}
struct node* creat_list(void)\\建立一个返回类型为struct node*的函数用来建立动态链表
{
struct node* head = (struct node*)malloc(sizeof(struct node));
\\建立一个struct node*类型的指针head,指向一个开辟的内存空间,可以存放struct node类型数据
head.data = 1;
head.next = NULL;
return head;
}
struct node* creat_node(uint32_t data1)\\建立一个返回类型为struct node*的函数用来建立节点
{
struct node* node_1 = (struct node*)malloc(sizeof(struct node));
\\建立一个struct node*类型的指针node_1,指向一个开辟的内存空间,可以存放struct node类型数据
head.data = data1;
head.next = NULL;
return node_1;
}
在这里建立的节点的函数和创建表头的函数结构差不多,都是一个结构体里面包含了一个数据和一个指针,然后返回该结构体地址。
5、插入节点(头插法)
在上一步的基础上,加入add_node函数。
struct node
{
uint32_t data;
struct node* next;
}
struct node* creat_list(void)\\建立一个返回类型为struct node*的函数用来建立动态链表
{
struct node* head = (struct node*)malloc(sizeof(struct node));
\\建立一个struct node*类型的指针head,指向一个开辟的内存空间,可以存放struct node类型数据
head.data = 1;
head.next = NULL;
return head;
}
struct node* creat_node(uint32_t data1)\\建立一个返回类型为struct node*的函数用来建立节点
{
struct node* node_new = (struct node*)malloc(sizeof(struct node));
\\建立一个struct node*类型的指针node_1,指向一个开辟的内存空间,可以存放struct node类型数据
head.data = data1;
head.next = NULL;
return node_new;
}
void add_node(struct node* head,uint32_t data2)\\插入节点函数,写入要插入的表头,数据
{
struct node* node_new = creat_node(data2);\\给新定义的struct node*类型结构体node_new开辟新的空间,然后存放数据data2
node_new.next = head.next;\\把结构体node_new的元素next赋值,内容为表头的next里面的值,其指向一个地址NULL
head.next = node_new;\\把表头的next赋值,内容为结构体node_new的地址
}
这一个函数,是把表头里面的指针元素next指向下一个插入节点的地址,然后插入节点的地址就会指向NULL,变成了尾部。如果再插入一个节点,表头里面的指针元素next指向最近插入节点的地址,然后再给最近插入节点里面的data赋值,如此类推。
struct node* list = creat_list();\\建立一个表头
add_node(list,2);\\建立一个节点,插入到list的后面
add_node(list,3);\\建立一个节点,插入到list的后面,上一个节点的排序往后移动
add_node(list,4);\\建立一个节点,插入到list的后面,上一个节点的排序往后移动
add_node(list,5);\\建立一个节点,插入到list的后面,上一个节点的排序往后移动
add_node(list,6);\\建立一个节点,插入到list的后面,上一个节点的排序往后移动
上述程序运行时的顺序如下表示:
list | -> | node1,2 |
list | -> | node2,3 | -> | node1,2 |
list | -> | node3,4 | -> | node2,3 | -> | node1,2 |
......
首先建立了一个表头,然后建立第二个节点。然后,建立第三个节点,其位置插入到表头和第二个节点之间。于是前面第二个节点位置往后推,变成了第三个节点,表头位置不变。这个就是头插法。
6、删除一个节点
假若,在上一步建立了一个表头list以及后面的5个节点。现在需要删除某一个节点,可以建立一个删除节点的函数del_node:
void del_node(struct node* head,uint32_t del_data)\\插入节点函数,写入要插入的表头,数据
{
struct node* node_del = head.nest;\\定义一个struct node*类型指针node_del,取值为head.nest,表头的下一个地址
struct node* node_delfront = head;\\定义一个struct node*类型指针node_delfront,内容为head的地址
if(node_del != NULL)\\判断一下node_del是不是只有表头
{
while(node_del.data != del_data)\\判断当前的节点数据是否与要删除的相同
{
node_delfront = node_del;
node_del = node_delfront.next;
if(node_del == NULL)\\如果要删除的数据,链表里面没有,结束程序
{
return 0;
}
}
node_delfront.next = node_del.next;\\把要删除节点的上一个节点的元素next赋值,为要删除节点的元素next的值
free(node_del);\\节点删除完毕后记得释放删除节点的空间
}
}
上述程序运行的过程如下:
list | -> | node2,3(将要删除) | -> | node1,2 |
list | -> | node1,2 |
过程就是从表头开始历遍整条链表,直到找到要删除的节点。然后把要删除节点的上一个节点里面的next指向的地址变成要删除节点的下一个节点地址,最后把要删除节点的空间释放,不然空间就会被一直占用,不利于空间的利用。下面是伪代码。
struct node* list = creat_list();\\建立一个表头
add_node(list,2);\\建立一个节点,插入到list的后面
add_node(list,3);\\建立一个节点,插入到list的后面,上一个节点的排序往后移动
add_node(list,4);\\建立一个节点,插入到list的后面,上一个节点的排序往后移动
add_node(list,5);\\建立一个节点,插入到list的后面,上一个节点的排序往后移动
add_node(list,6);\\建立一个节点,插入到list的后面,上一个节点的排序往后移动
\*当前结果为: 1 2 3 4 5 6*\
del_node(list,4);\\删除数据为4的节点
\*最后结果为: 1 2 3 5 6*\