前言:
应部分粉丝要求,专门抽出一个专题来讨论下链表相关的数据结构,今天这篇文章来简单探讨下单向链表,后面会陆续出单向循环链表、双向链表、双向循环链表,还没关注的小伙伴抓紧时间关注起来啦。
简介:
链表也是一种数据结构,链表分为数据域和指针域
数据域:存放当前节点的数据
指针域:指向下一个节点,通过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
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);
list.c
/*
* 函数名称: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
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上就可以直接运行。
如果觉得本篇文章多少有点帮助的话大家不要忘了,点赞、关注、评论、转发哦,创作不易!你们的支持是小编创作的最大动力。