嵌入式系统与单片机|技术阅读
登录|注册

您现在的位置是:嵌入式系统与单片机 > 技术阅读 > 链表篇---单向链表的C语言实现

链表篇---单向链表的C语言实现

前言:

    应部分粉丝要求,专门抽出一个专题来讨论下链表相关的数据结构,今天这篇文章来简单探讨下单向链表,后面会陆续出单向循环链表、双向链表、双向循环链表,还没关注的小伙伴抓紧时间关注起来啦。


简介:

    链表也是一种数据结构,链表分为数据域和指针域

    数据域:存放当前节点的数据

    指针域:指向下一个节点,通过next指针把每个节点串联起来,形成一个链式的数据结构,因此得名链表。

链表和数组的区别:

    说到这里很多人可能都在想,链式串联起来的数据结构不是和数组很像吗?确实,他们两之间有共同点,也有各自的优缺点,下面我们就来剖析下两者的区别:

    从内存分配上:

        ·    数组是静态分配的内存空间

        ·    链表是程序员在堆区动态分配的内存
    
从内存的存储方面上:

        ·    数组在内存中是连续存储的会造成一定的内存浪费,在定义数组的长度的时候必须预先规定其大小,如果长度过大会造成内存浪费。

        ·    链表在内存中是不连续的,通过指针将每个节点串接起来,其内存利用率高,能灵活的拓展我们要使用的内存空间。

    从查找元素方面上:

         ·    数组有下标查询定位,能快速的访问元素,查找的事件复杂度是O(1)

          ·    链表通过遍历所有节点查找元素,时间复杂度O(n)        

    从插入和删除元素上:

          ·    数组插入和删除元素需要移动其他元素,时间复杂度O(n)   

          ·    链表则不需要移动其他元素,只需要该表指针的指向即可,时间复杂度O(1),链表的插入和删除的效率高

删除节点图示:

插入节点图示

    

链表中节点的定义:

struct stList_node{ int data; /*! 数据域 */ struct stList_node *next; /*! 指针域 */};

    

    在使用单向链表的时候我们经常会定义一个头节点,该节点不存放数据,用于对整个链表的遍历查找,能方便的实现链表的遍历、头插、尾插等过程。


    下面小编整理了几个链表操作的常用接口,并针对这些接口实现了链表的代码。这些接口的作用通过函数名就能知道是什么作用了。下面来一个一个详细介绍下。

extern int CreatListHead(struct stList_node **head);extern eListType ifListempty(struct stList_node *head);extern void List_Print(struct stList_node *head);extern int ListInsertByHead(struct stList_node *head, int data);extern int ListInsertByTail(struct stList_node *head, int data);extern int ListDeleteByHead(struct stList_node *head);extern int ListDeleteByTail(struct stList_node *head);extern int ListDeleteByData(struct stList_node *head, int data, eListType type);

list.h

#ifndef __LIST_H__#define __LIST_H__
#include "stdio.h"#include "stdlib.h"
struct stList_node{ int data; /*! 数据域 */ struct stList_node *next; /*! 指针域 */};
typedef enum{ TYPE_NULL = 0, DELETE_ALL_SAME_DATA = 1, DELETE_ONE_DATA = 2, LIST_EMPTY = 3, LIST_NOT_EMPTY = 4,
}eListType;
extern int CreatListHead(struct stList_node **head);extern eListType ifListempty(struct stList_node *head);extern void List_Print(struct stList_node *head);extern int ListInsertByHead(struct stList_node *head, int data);extern int ListInsertByTail(struct stList_node *head, int data);extern int ListDeleteByHead(struct stList_node *head);extern int ListDeleteByTail(struct stList_node *head);extern int ListDeleteByData(struct stList_node *head, int data, eListType type);
#endif /* __LIST_H__ */

list.c

#include "list.h"
/** 函数名称:CreatListHead* 输入参数:{struct stList_node **} head:头节点* 返 回 值:-1:创建失败,原因是内存申请失败 0:创建成功* 作 者:Barry* 功能描述:创建头链表的头指针并为其申请内存空间* 修改记录:None*/int CreatListHead(struct stList_node **head){ /* 创建头节点 */ *head = (struct stList_node *)malloc(sizeof(struct stList_node));
if(*head == NULL) return -1;
(*head)->data = -1; (*head)->next = NULL;
return 0;}
/** 函数名称:ifListempty* 输入参数:{struct stList_node **} head:头节点* 返 回 值:TRUE:空链表 FALSE:非空链表* 作 者:Barry* 功能描述:通过头指针判断链表是否为空* 修改记录:None*/eListType ifListempty(struct stList_node *head){ if(head->next == NULL) return LIST_EMPTY; else return LIST_NOT_EMPTY;}
/** 函数名称:List_Print* 输入参数:{struct stList_node **} head:头节点* 返 回 值:None* 作 者:Barry* 功能描述:通过头指针遍历链表节点并打印* 修改记录:None*/void List_Print(struct stList_node *head){ struct stList_node *p;
/* 空链表走这个分支 */ if(ifListempty(head) == LIST_EMPTY) return;
for(p = head->next; p != NULL; p = p->next) printf("%d \t", p->data); printf("\r\n");}
/** 函数名称:ListInsertByHead* 输入参数:{struct stList_node **} head:头节点 {int } data:待插入的数据* 返 回 值:-1:节点内存申请失败 0:删除成功* 作 者:Barry* 功能描述:链表头插法,在链表的头部插入数据* 修改记录:None*/int ListInsertByHead(struct stList_node *head, int data){ /* 定义待插入的节点 */ struct stList_node *node = NULL;
/* 插入的节点申请内存空间 */ node = (struct stList_node *)malloc(sizeof(struct stList_node)); if(node == NULL) return -1;
node->data = data;
node->next = head->next; head->next = node;
return 0;}
/** 函数名称:ListInsertByTail* 输入参数:{struct stList_node **} head:头节点 {int } data:待插入的数据* 返 回 值:-1:节点内存申请失败 0:删除成功* 作 者:Barry* 功能描述:链表尾插法,在链表的尾部插入数据* 修改记录:None*/int ListInsertByTail(struct stList_node *head, int data){ /* 定义待插入的节点 */ struct stList_node *node = NULL; /* 遍历链表 */ struct stList_node *p = head;
/* 插入的节点申请内存空间 */ node = (struct stList_node *)malloc(sizeof(struct stList_node)); if(node == NULL) return -1;
node->data = data; node->next = NULL;
while(p->next != NULL) { p = p->next; }
p->next = node;
return 0;}
/** 函数名称:ListDeleteByHead* 输入参数:{struct stList_node **} head:头节点* 返 回 值:-1:删除失败,原因:链表为空 0:删除成功* 作 者:Barry* 功能描述:链表头删法,在链表的头部删除数据* 修改记录:None*/int ListDeleteByHead(struct stList_node *head){ struct stList_node *p = head->next;
/* 空链表走这个分支 */ if(ifListempty(head) == LIST_EMPTY) return -1;
head->next = p->next;
free(p);
return 0;}
/** 函数名称:ListDeleteByTail* 输入参数:{struct stList_node **} head:头节点* 返 回 值:-1:删除失败,原因:链表为空 0:删除成功* 作 者:Barry* 功能描述:链表尾删法,在链表的尾部删除数据* 修改记录:None*/int ListDeleteByTail(struct stList_node *head){ /* 当前节点 */ struct stList_node *cur = NULL; /* 上一个节点 */ struct stList_node *prev = NULL;
/* 空链表走这个分支 */ if(ifListempty(head) == LIST_EMPTY) return -1;
cur = head;
while(cur->next != NULL) { prev = cur; cur = cur->next; } free(cur); /* 将上一节点的next指针指向空,否则free cur节点后prev->next没有任何指向,导致程序异常 */ prev->next = NULL;
return 0;}
/** 函数名称:ListDeleteByTail* 输入参数:{struct stList_node **} head:头节点 {int } data:待删除的数据 {eListType } type:删除方式 DELETE_ALL_SAME_DATA:删除链表中所有的data数据 DELETE_ONE_DATA :删除链表中遍历到的第一个data数据(从头部开始遍历)* 返 回 值:-1:删除失败,原因:链表为空 0:删除成功* 作 者:Barry* 功能描述:删除指定数据的链表,删除单个数据或链表中所有等于data的节点* 修改记录:None*/int ListDeleteByData(struct stList_node *head, int data, eListType type){ struct stList_node *cur = NULL; struct stList_node *prev = NULL; struct stList_node *ptemp = NULL;
/* 空链表走这个分支 */ if(ifListempty(head) == LIST_EMPTY) return -1;
cur = head; prev = head; ptemp = cur; while(cur != NULL) { if(cur->data == data) { prev->next = cur->next; ptemp = cur; free(ptemp); /* 释放ptemp后cur没有指向任何内容,将其指向下一个要判断数据的节点 */ cur = prev->next;
if(type == DELETE_ONE_DATA) break; } else { prev = cur; /* 向后移动一个节点 */ cur = cur->next; } }
return 0;}


    有兴趣的想要深入学习的可以研究下上面的代码。如有问题也请评论区指出,小编修改更正。


来简单的测试下上面的几个接口:

main.c

#include "list.h"
struct stList_node *ListHead = NULL;
int main(void){ CreatListHead(&ListHead);
ListInsertByTail(ListHead, 4); ListInsertByTail(ListHead, 4); ListInsertByTail(ListHead, 1); ListInsertByTail(ListHead, 2); ListInsertByTail(ListHead, 3); ListInsertByTail(ListHead, 4); ListInsertByTail(ListHead, 3); ListInsertByTail(ListHead, 3); ListInsertByTail(ListHead, 4); List_Print(ListHead);
ListDeleteByData(ListHead, 4, DELETE_ALL_SAME_DATA); List_Print(ListHead);
return 0;}

从上面的代码知道我们利用尾插法插入一些元素,并打印出来,再调用删除节点的接口删除指定数据,下面来看下运行结果吧!

由于我们上面删除的接口是删除链表中所有数据是4的节点,因此删除前后的打印是上图这样的。


我们修改一下传入的参数,修改成只删除一个数据即

ListDeleteByData(ListHead, 4, DELETE_ONE_DATA);

再看下效果吧!

这次就只删除了第一个数据为4的节点。


我们再来测试下其他的接口:

使用头插法插入数据,再通过尾删法删除一个数据

ListInsertByHead(ListHead, 4); ListInsertByHead(ListHead, 3); ListInsertByHead(ListHead, 2); ListInsertByHead(ListHead, 1); List_Print(ListHead);
ListDeleteByTail(ListHead); List_Print(ListHead);

运行结果如下:

每次插入数据都在头部插入数据,因此链表中的数据是1 2 3 4,数据4在插入完成后变成尾部,通过尾删法删除后的打印是1 2 3,结果也是符合预期的。


其他的接口小编都测试过了,在这里就不一一测试啦,有兴趣的小伙伴可以测试下。


上述的三个文件放到VS Code上就可以直接运行。


如果觉得本篇文章多少有点帮助的话大家不要忘了,点赞、关注、评论、转发哦,创作不易!你们的支持是小编创作的最大动力。