什么是单链表
单链表是一种链式存取的数据结构,用一组地址任意的存储单元存放线性表中的数据元素。链表中的数据是以结点来表示的,每个结点的构成:元素(数据元素的映象) +指针(指示后继元素存储位置),元素就是存储数据的存储单元,指针就是连接每个结点的地址数据。
话不多说,看图
说白了就是一个结构体里边有一个变量,和一个本身结构体的指针;指针指向下一个结构体,就这样指着指着就成了一条链。这就是单链表
上图的结构体定义如下,数据域有两个变量,方便查看我直接用data代替这两个成员;
typedef struct _QUEUE_CB_
{
int data; //数据域
char name; //数据域
struct _QUEUE_CB_* next; //指针域 这里是定义了一个结构体指针
}Queue_cb; //这个只是上面的别名
创建一个单链表头
//创建一个链表表头
Queue_cb *Create()
{
Queue_cb *head; //声明一个表头
head = (Queue_cb *)malloc(sizeof(Queue_cb)); //申请一个动态内存,内存大小为这个Queue的长度,
//申请之后就要对他进行初始化
head -> next = NULL;
return head; //返回表头指针
}
理解起来也不难,声明一个Queue_cb 类型的指针,用来指向malloc申请的地址,地址的大小就是这个结构体的大小,申请出来后就初始化指针为空即可,数据可以先不初始化,形成差异化,方便后面更改数据类型的时候进行操作。
图示:
新建一个节点
//创建节点 节点是有参数传入的,传入的参数就是结构体的成员变量
Queue_cb *Node(char Name, int Age)
{
Queue_cb *node; //定义一个Queue结构体指针,作为新节点的指针
node = (Queue_cb*)malloc(sizeof(Queue_cb)); //这个指针指向Queue结构体的申请的内存
node -> name = Name; //初始化赋值
node -> age = Age; //初始化赋值
node -> next = NULL;
return node; //返回这个节点地址
}
相比上面的创建表头,没什么区别吧,只是多了数据域的传入而已,很简单吧,看图,数据域初始化了变量为函数传入的参数,指针照样指向空
链表的插入,拼接
上面介绍了表头的创建和节点的创建,那么接下来就是如何形成由点变成线的过程;怎么操作呢?很简单,把表头的指针指向新建立的节点地址就可以啦;那对于指向的节点,我想先建立的节点在表头的后边,后面的节点依次排队,又或者我想后面建立的节点放在表头后边;这就引入了一个节点的插入法,常见的插入法有头插法、尾插法、指定位置插法;
- 头插法:就是建立的节点一直往表头后边插,形象的比喻就是 排队领饭的时候,每个人都想先吃,后面来的直接插到第一个位置,先来到的只能往后站;还是看代码吧;
//链表的头插法插入
void NodeInsertHead(Queue_cb *head, char name, int age)
{
Queue_cb *InsertNode; //定义一个插入的节点结构体指针
InsertNode = Node(name,age); //创建这个结构体指针的内存,并初始化
InsertNode -> next = head -> next; //把当前的节点插入头结点后边
head -> next = InsertNode; //头结点的下一个就是当前的这个节点
}
可能看的不是很明白,我画个图讲解一下
Queue_cb *head 是你建立的头结点指针,传入你的头结点地址
先去掉中间的newNode,和与他相连的红色的箭头,当建立第一个节点的时候,表头的next指针就指向你建立的这个节点Node,也就是灰色的箭头,这就对应了代码 head -> next = InsertNode; //头结点的下一个就是当前的这个节点
InsertNode -> next = head -> next; //这里是第一个节点,头初始化的时候next是指向NULL的,所以这个节点的next指针代替头的next指针,指向空,这样就连接了头节点和第一个节点;
现在又来了一个新节点newNode,图示对应红色的箭头,把灰色的去掉,(不懂的最好自己画一下,我是偷懒直接把两个放在一起了:))
代码解释也跟上面一样:
head -> next = InsertNode 表头的next指针就指向你建立的这个新节点Node
InsertNode -> next = head -> next; 这个节点的next指针代替头的next指针,在上边第一个节点的时候
head -> next是指向第一个节点的,所以这里是直接代替他来指向第一个节点,这样头插法就成功了
看下实验现象把:
int main()
{
delay_init();
/*初始化USART 配置模式为 115200 8-N-1,中断接收*/
uart_init(115200);
LED_Init();
printf("Init successfully!\r\n");
Queue_cb *Nodelist = Create(); //创建链表表头
NodeInsertHead(Nodelist,'a',10); //头插法插入链表
NodeInsertHead(Nodelist,'d',11);
NodeInsertHead(Nodelist,'w',15);
NodeInsertHead(Nodelist,'G',20);
NodeInsertHead(Nodelist,'h',13);
NodeInsertHead(Nodelist,'j',44);
Printf_list(Nodelist); //打印链表
while(1)
{
}
}
现象:
看,是不是最后插进去的排在第一位;这就是头插法,理解了头插法,后面的就不难了。
-
尾插法: 就是依次排队下去,先遍历到最后一个,然后再插入。
`//链表的尾插法插入
void NodeInsertTail(Queue_cb *tail, char name , int age)
{
Queue_cb *Insertnode; //创建的节点指针while(tail->next != NULL) //遍历到当前的链表的表尾 { tail = tail->next; //因为最后一个的next是指向NULL的,所以会一直遍历到最后一个 } Insertnode = Node(name,age); //指针指向开辟的空间,并初始化 Insertnode -> next = NULL; //尾节点的下一个节点是空 tail->next = Insertnode; // 链表尾的下一个节点指向当前创建的节点
}
`
看现象
//在main函数中添加以下
NodeInsertTail(Nodelist,'O',50); //尾插法
NodeInsertTail(Nodelist,'P',70);
NodeInsertTail(Nodelist,'Q',90);
对比头插法,尾插法就是按顺序排列下去
- 指定位置插入: 思路是遍历链表的长度,然后如果插入的位置在长度之内,直接插入
int NodeInsert_Pos(Queue_cb *list, u8 pos, char name, int age)
{
u8 list_lenth,i = 1;
Queue_cb *Insertnode; //要插入的节点
list_lenth = Traves_lenth(list); //获取要插入的链表的长度
if(pos > list_lenth) //判断插入的位置是否非法
{
return -1;
}
if(pos == list_lenth) //如果插入的位置是这个链表的长度,那就是在尾巴插入
{
NodeInsertTail(list, name, age);
return 2;
}
while((list->next != NULL) && (i < (pos))) //遍历链表,遍历到指定位置的上一个节点
{
list = list->next;
++i;
}
Insertnode = (Queue_cb *)malloc(sizeof(Queue_cb)); //开辟一个地址,给你要插入的节点放数据
Insertnode -> next = list -> next; //把原来链表本来要指向的下一个节点,让插入的节点来代替它指向
list -> next = Insertnode; //把链表要指向的节点指向这个插入的节点
Insertnode -> age = age; //初始化数据
Insertnode -> name = name;
return 0;
}
//遍历链表长度代码
u8 Traves_lenth(Queue_cb *list)
{
u8 Lenth = 0;
while(list->next != NULL)
{
list = list->next; //把下一个节点的地址赋给当前的地址,让它遍历下去
Lenth++;
} //当list->next = NULL的时候证明已经是最后一个节点了,所以知道了这个链表的长度
return Lenth;
}
现象:
主函数添加
NodeInsert_Pos(Nodelist,5,‘X’,77); //增 在第5个位置插入X
不知道你们看不看得出我这个代码里边有个小bug,就是没有处理插入位置大于链表的长度的这种情况
链表的删除
有创建有会有删除,来看看删除操作是怎么样子的;
诶?这图怎么似曾相识?没错就是跟头插法一样的,只不过这个是反过来操作而已,第一步删除灰色的线,只留下红色的,这就是正常的链表,这个要删除的节点先叫A点,A点的上一个节点的next是指向A点的,A点的next是指向A点的下一个,所以删除操作就是把A点的上一个节点的next指向A点的下一个节点,直接跳过A,就行了,第二步就是 删除了之后就是去掉红色的箭头和中间的节点,保留灰色箭头的样子(原谅我懒)
还有一个就是怎么知道我要删除哪个节点呢?,通过遍历。遍历到你要删除节点的上一个,再执行上面的操作就行
一定要注意释放空间,一定要释放空间,一定要释放空间,一定要记得调用free()来释放你申请的空间至于为什么,后面讲到堆栈再说
看代码:
void Delect_node(Queue_cb *list, u8 index)
{
Queue_cb *p,*f;
u8 i = 1,lenth;
p = list;
if(p == NULL) //判断链表是否为空
{
printf("Error , list is empty\r\n");
}
lenth = Traves_lenth(list); //获取链表的长度
if(index > lenth) //判断要删除的节点是否在链表内
{
printf("Index is not in list\r\n");
}
while((p -> next != NULL) && (i < index)) //遍历到指定的位置的上一个
{
p = p -> next;
i++;
}
printf("已删除的节点内容 : %d name: %c age: %d \r\n",index,p->next->name, p->next->age);
//p是你删除节点的上一个节点,p->next就是你要删除的节点
f = p->next; //f指向的就是要删除的节点的地址,把f->next指向的地址代替f
p->next = p->next->next; // p的下一节点就指向 你要删除的节点(p->next)的下一个节点也就是 p->nex->next
free(f); //释放该空间
}
主函数添加 Delect_node(Nodelist,5); //删
现象
链表的查找
链表的查找也很简单,就是遍历到你要查找的位置就行了
void Search_list(Queue_cb *list, u8 Index)
{
Queue_cb *p;
u8 i, lenth;
p = list;
if(p == NULL)
{
printf("Error , list is empty\r\n");
}
lenth = Traves_lenth(list); //先获取链表的长度
if(Index > = lenth) //查看位置是否合法,此处不包括尾
{
printf("Index is not in list\r\n");
}
while((p -> next != NULL) && (i < Index)) //查找的依据是,你找的那个点在头和尾之间,index是你要找的位置。i是遍历了的节点,i不是你要找的节点就一直加
{
p = p -> next;
++i;
}
printf("查找的节点:%d name : %c age : %d \r\n",Index,p->name,p->age);
}```
现象:
主函数加入
int main()
{
delay_init();
/初始化USART 配置模式为 115200 8-N-1,中断接收/
uart_init(115200);
LED_Init();
printf(“Init successfully!\r\n”);
Queue_cb *Nodelist = Create(); //创建链表表头
NodeInsertHead(Nodelist,'a',10); //头插法插入链表
NodeInsertHead(Nodelist,'d',11);
NodeInsertHead(Nodelist,'w',15);
NodeInsertHead(Nodelist,'G',20);
NodeInsertHead(Nodelist,'h',13);
NodeInsertHead(Nodelist,'j',44);
NodeInsertTail(Nodelist,'O',50); //尾插法
NodeInsertTail(Nodelist,'P',70);
NodeInsertTail(Nodelist,'Q',90);
NodeInsert_Pos(Nodelist,5,‘X’,77); //增
Search_list(Nodelist,8); //找到第8个节点
Printf_list(Nodelist); //打印链表
while(1)
{
}
}
放不了图片了,我把现象发出来吧
打印的是:
[22:54:49.315]收←◆Init successfully!
查找的节点:8 name : \0 age : 0
j,44
h,13
G,20
w,15
X,77
d,11
a,10
O,50
P,70
Q,90
更改链表的节点
改节点那就简单啦,直接定位到你要改的节点,然后传入你要改的数据,OJBK
/*
* 更改链表中指定节点的内容
* 输入 需要更改的链表 要更改的位置,更改的内容
*/
void Change_Node(Queue_cb *list, u8 index, char name, int age)
{
Queue_cb *p;
u8 i = 1, lenth;
p = list;
if(p == NULL) //判断链表是否为空
{
printf("Error , list is empty\r\n");
}
lenth = Traves_lenth(list); //先获取链表的长度
if(index > lenth)
{
printf("Index is not in list\r\n");
}
while((p -> next != NULL) && (i < (index))) //遍历到指定位置的上一个
{
p = p -> next;
++i;
}
printf("更改前的节点:%d name : %c age : %d \r\n",index,p->next->name,p->next->age);
p->next->age = age;
p->next->name = name;
printf("更改后的节点:%d name : %c age : %d \r\n",index,p->next->name,p->next->age);
}
主函数 加入
Change_Node(Nodelist,5,‘L’,88); //改
现象:
[23:01:38.360]收←◆Init successfully!
更改前的节点:5 name : X age : 77
更改后的节点:5 name : L age : 88
j,44
h,13
G,20
w,15
L,88
d,11
a,10
O,50
P,70
Q,90
怎么样,简单吧
最后
虽然我知道我自己的C基础不是很扎实,比不上人家大佬,但我会一点一点的积累,慢慢的提升自己。
也希望有人能指出我的错误,让我及时改正。
下一篇:双向链表操作