本文接着上文,上文我们认识了线性表的概念,并实现了静态、动态顺序表。接下来我们认识一个新概念链表。并实现单向链表的各种操作。顺序表还有不明白的看这一篇文章
(13条消息) 【数据结构和算法】实现线性表中的静态、动态顺序表_小王学代码的博客-CSDN博客
目录
一、链表是什么?
1.链表的概念和结构
2.链表的分类
1.单向或者双向
2.带头或者不带头
3.循环或者非循环
二、链表的实现
1.无头单向非循环链表
2.函数功能的实现
1.初始化和打印链表
2.头插和尾插
3.头删和尾删
4.单链表的查找
5.在pos结点位置之前或之后插入数据
6.在pos结点位置删除数据(删除pos结点)
7.删除pos位置之后一个结点
8.摧毁链表
三、完整代码
1.LinkList.h
2.LinkList.c
3.test.c
前言
我们知道了数据结构中线性表的概念,我们应该会感觉比较好理解,因为顺序表的建立主要涉及到结构体和动态内存管理函数,是类似于数组的一种形式。
我们要思考这样一个问题
1.增容需要申请新空间,拷贝数据,释放旧空间,会有不小的消耗。
2.增容一般都是2倍扩容,有时候也会浪费一定的空间
于是,为了解决上面这样的问题,我们引入了线性表中的链表,这一概念。
一、链表是什么?
1.链表的概念和结构
概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。
图示如下:
由上图可知,链表的特征为:
1.链式结构在逻辑上是连续的,但是在物理上是不一定连续的。 (每一个结点的地址是不一定的)
2.现实中的结点一般都是从堆中申请出来的。
3.从堆上申请的空间,是按照一定策略来分配,根据编辑器的不同而不同,再次申请的空间可能连续,也可能不连续。
2.链表的分类
实际上链表的结构有很多中,以下组合起来有8种主要的链表结构的情况
1.单向或者双向
2.带头或者不带头
头节点使用的话,就不需要对其数据域赋值,只起到一个成为建立链表的基点的作用,不使用的话,第一个结点存储数值就可以,创建一个新节点给这个第一个结点phead即可,这样头节点链表,就变成了非头结点的链表
主要就是看第一个结点是否用到了其数值域
用第一结点数据域 非头节点
没用 头节点
3.循环或者非循环
以上这些类型情况,我们常用的有两种,无头单向非循环链表,带头双向循环链表
如图所示
1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
2. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。
二、链表的实现
1.无头单向非循环链表
结构体为:
typedef int SLDataType;
//单向链表的实现、
typedef struct ListNode {
SLDataType data;//数据域
struct ListNode* next;//指针域
}List;
要实现的函数为:
//打印单链表
void ListPrint(List* ps);
//单链表的尾插
void ListPushBack(List** ps, SLDataType data);
//单链表的头插
void ListPushFront(List** ps, SLDataType data);
//单链表的尾删
void ListPopBack(List** ps);
//单链表的头删
void ListPopFront(List** ps);
//单链表的查找
List* ListFind(List* ps);
//在pos位置上插入数据
void ListInsertBefore(List** ps, SLDataType x, List* pos);
//在pos位置之后插入数据
void ListInsertAfter(List** ps, SLDataType x, List* pos);
//在pos位子删除数据
void ListErase(List** ps, List* pos);
//在pos位置之后一位删除数据
void ListEraseAfter(List* pos);
//单链表的摧毁
void ListDestory(List** ps);
2.函数功能的实现
1.初始化和打印链表
//初始化链表
void InitList(List* ps) {
ps->data = 0;
ps->next = NULL;
}
//打印单链表
void ListPrint(List* ps) {
List* cur = ps;
while ((cur) != NULL) {
printf("%d -> ", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
2.头插和尾插
尾部插入图示如下:
代码如下:
//创建一个新节点
List* CreateNode(SLDataType x) {
List* newNode = (List*)malloc(sizeof(List));
if (newNode == NULL) {
perror("malloc fail\n");
exit(-1);
}
else {
newNode->data = x;
newNode->next = NULL;
}
return newNode;
}
//单链表的尾插
void ListPushBack(List** ps, SLDataType data) {
//创建新的节点
assert(ps);//断言
List* newNode = CreateNode(data);
if (*ps == NULL) {
//说明是空链表
*ps = newNode;
}
else {
List* tail = *ps;
while (tail->next != NULL) {
tail = tail->next;
}
tail->next = newNode;
}
}
头部插入如图所示:
代码如下:
//单链表的头插
void ListPushFront(List** ps, SLDataType data) {
//先断言是否为空
assert(ps);
//将新地址指向头结点下一个next结点的地址,然后在用头结点指向新节点
List* newNode = CreateNode(data);
newNode->next = (*ps); //new指向ps当前的位置,然后new是第一个位置了,将new赋值给ps,这样new就作为头部连接链表了
(*ps) = newNode;//原本ps位置的数值不变,这样的话就成 new->next=ps,new数值在前,ps的数值在后
}
3.头删和尾删
尾部删除如图所示:
代码演示:
//单链表的尾删
void ListPopBack(List** ps) {
assert(ps);//断言
//三种情况
//1.空链表
//2.一个节点
//3.多个节点
if (*ps == NULL) {
return;
}
//只有一个节点的情况为
else if ((*ps)->next == NULL) {
free(*ps); //如果只有一个头节点的话
*ps = NULL;
}
else {
//多个节点的情况下、
List* tail = *ps;
while (tail->next->next!= NULL) {
tail = tail->next;
}
free(tail->next);
tail->next= NULL;
}
}
头部删除如图所示:
代码如下:
//单链表的头删
void ListPopFront(List** ps) {
assert(ps);
//1.空
//2.非空
if (*ps == NULL) {
//为空
return;
}
else {
List* tail = (*ps)->next;//创建临时变量tail,将头节点之后的地址给tail
free(*ps);//滞空头节点
*ps = NULL;//可有可不有,接下来也要用
*ps = tail;//将tail也就是ps的下一个List节点给ps
}
}
4.单链表的查找
代码如下:
//单链表的查找
List* ListFind(List* ps,SLDataType data) {
//进行查找就是进行判断是否为空链表,为空直接返回
if (ps == NULL) {
printf("链表为空、无法查找\n");
return;
}
List* tail = ps;
while (tail != NULL) {//从头节点开始,进行循环,
if (tail->data == data) {
return tail;
}
tail = tail->next;
}
return tail;//最后还找不到data,tail就为NULL了
}
5.在pos结点位置之前或之后插入数据
在pos结点位置之前插入数据,如图所示:
代码如下:
//在pos位置上插入数据
void ListInsertBefore(List** ps, SLDataType x, List* pos) {
//先判断是否为空
assert(ps);
assert(pos);
//空链表排除
//1.pos是第一个节点
//2.pos不是第一个节点
if (*ps == pos) {
//是第一个节点,那就直接头插
ListPushFront(ps, x);
}
else {
List* prev = *ps;
while (prev->next != pos) {
prev = prev->next;
}
List* newnode = CreateNode(x);
prev->next = newnode;
newnode->next = pos;
}
}
在pos结点位置之后插入结点,如图所示:
代码如下:
//在pos位置之后插入数据
void ListInsertAfter(List** ps, SLDataType x, List* pos) {
assert(ps);
//assert(pos);//断言
List* newnode = CreateNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
6.在pos结点位置删除数据(删除pos结点)
如图所示:
代码如下:
//在pos位子删除数据
void ListErase(List** ps, List* pos) {
assert(ps);
assert(pos);
if (*ps == pos) {
ListPopFront(ps);
}
else {
List* next = *ps;
while (next->next != pos) {
next = next->next;
}
//这个时候next->next == pos
next->next = next->next->next;
/*free(next->next);*/
free(pos);
pos = NULL;
}
}
7.删除pos位置之后一个结点
如图所示:
代码如下:
//在pos位置之后一位删除数据
void ListEraseAfter(List* pos) {
assert(pos);
List* next = pos->next;//将pos 的下一个结点赋值给next
if (next != NULL) {
pos->next = pos->next->next;//表示pos的下一个的下一个结点的地址赋值给pos的指针域 实质上是将pos的下一个结点给跳过
free(next); //将pos的下一个结点给free释放
next = NULL; //next指向为NULL 防止野指针
}
}
8.摧毁链表
代码如下:
//链表的摧毁 直接将头指针指针域指向NULL
void ListDestory(List** ps) {
//assert(ps); //防止空链表
一个结点一个结点释放
//List* next = *ps;
//while (next) {
// List* cur = next->next;
// free(next);
// next = cur;
//}
*ps = NULL;
}
因为是二级指针,所以直接 *ps=NULL 即可,或者一个一个free
三、完整代码
1.LinkList.h
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<malloc.h>
#include<assert.h>
typedef int SLDataType;
//单向链表的实现、
typedef struct ListNode {
SLDataType data;//数据域
struct ListNode* next;//指针域
}List;
//打印单链表
void ListPrint(List* ps);
//单链表的尾插
void ListPushBack(List** ps, SLDataType data);
//单链表的头插
void ListPushFront(List** ps, SLDataType data);
//单链表的尾删
void ListPopBack(List** ps);
//单链表的头删
void ListPopFront(List** ps);
//单链表的查找
List* ListFind(List* ps);
//在pos位置上插入数据
void ListInsertBefore(List** ps, SLDataType x, List* pos);
//在pos位置之后插入数据
void ListInsertAfter(List** ps, SLDataType x, List* pos);
//在pos位子删除数据
void ListErase(List** ps, List* pos);
//在pos位置之后一位删除数据
void ListEraseAfter(List* pos);
//单链表的摧毁
void ListDestory(List** ps);
2.LinkList.c
#define _CRT_SECURE_NO_WARNINGS
#include"单向链表.h"
//链表的使用,在插入上面
// 如果是尾部插入,如果是空链表直接将新节点给ps 是先找到链表尾部,然后创建新节点,连接即可
// 如果是头部插入,先进行断言判空,之后创建新节点,将新节点的数据
// new->next=ps 这个是找到对应的位置,连接起来
// ps=new; 将新节点的信息传递给ps,这样ps还是头节点
//
//
//
//
//
//进行单链表的实现
//初始化链表
void InitList(List* ps) {
ps->data = 0;
ps->next = NULL;
}
//打印单链表
void ListPrint(List* ps) {
List* cur = ps;
while ((cur) != NULL) {
printf("%d -> ", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
//创建一个新节点
List* CreateNode(SLDataType x) {
List* newNode = (List*)malloc(sizeof(List));
if (newNode == NULL) {
perror("malloc fail\n");
exit(-1);
}
else {
newNode->data = x;
newNode->next = NULL;
}
return newNode;
}
//单链表的尾插
void ListPushBack(List** ps, SLDataType data) {
//创建新的节点
assert(ps);//断言
List* newNode = CreateNode(data);
if (*ps == NULL) {
//说明是空链表
*ps = newNode;
}
else {
List* tail = *ps;
while (tail->next != NULL) {
tail = tail->next;
}
tail->next = newNode;
}
}
//单链表的头插
void ListPushFront(List** ps, SLDataType data) {
//先断言是否为空
assert(ps);
//将新地址指向头结点下一个next结点的地址,然后在用头结点指向新节点
List* newNode = CreateNode(data);
newNode->next = (*ps); //new指向ps当前的位置,然后new是第一个位置了,将new赋值给ps,这样new就作为头部连接链表了
(*ps) = newNode;//原本ps位置的数值不变,这样的话就成 new->next=ps,new数值在前,ps的数值在后
}
//单链表的尾删
void ListPopBack(List** ps) {
assert(ps);//断言
//三种情况
//1.空链表
//2.一个节点
//3.多个节点
if (*ps == NULL) {
return;
}
//只有一个节点的情况为
else if ((*ps)->next == NULL) {
free(*ps); //如果只有一个头节点的话
*ps = NULL;
}
else {
//多个节点的情况下、
List* tail = *ps;
while (tail->next->next!= NULL) {
tail = tail->next;
}
free(tail->next);
tail->next= NULL;
}
}
//单链表的头删
void ListPopFront(List** ps) {
assert(ps);
//1.空
//2.非空
if (*ps == NULL) {
//为空
return;
}
else {
List* tail = (*ps)->next;//创建临时变量tail,将头节点之后的地址给tail
free(*ps);//滞空头节点
*ps = NULL;//可有可不有,接下来也要用
*ps = tail;//将tail也就是ps的下一个List节点给ps
}
}
//单链表的查找
List* ListFind(List* ps,SLDataType data) {
//进行查找就是进行判断是否为空链表,为空直接返回
if (ps == NULL) {
printf("链表为空、无法查找\n");
return;
}
List* tail = ps;
while (tail != NULL) {//从头节点开始,进行循环,
if (tail->data == data) {
return tail;
}
tail = tail->next;
}
return tail;
}
//在pos位置上插入数据
void ListInsertBefore(List** ps, SLDataType x, List* pos) {
//先判断是否为空
assert(ps);
assert(pos);
//空链表排除
//1.pos是第一个节点
//2.pos不是第一个节点
if (*ps == pos) {
//是第一个节点,那就直接头插
ListPushFront(ps, x);
}
else {
List* prev = *ps;
while (prev->next != pos) {
prev = prev->next;
}
List* newnode = CreateNode(x);
prev->next = newnode;
newnode->next = pos;
}
}
//在pos位置之后插入数据
void ListInsertAfter(List** ps, SLDataType x, List* pos) {
assert(ps);
//assert(pos);//断言
List* newnode = CreateNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
//在pos位子删除数据
void ListErase(List** ps, List* pos) {
assert(ps);
assert(pos);
if (*ps == pos) {
ListPopFront(ps);
}
else {
List* next = *ps;
while (next->next != pos) {
next = next->next;
}
//这个时候next->next == pos
next->next = next->next->next;
/*free(next->next);*/
free(pos);
pos = NULL;
}
}
//在pos位置之后一位删除数据
void ListEraseAfter(List* pos) {
assert(pos);
List* next = pos->next;//将pos 的下一个结点赋值给next
if (next != NULL) {
pos->next = pos->next->next;//表示pos的下一个的下一个结点的地址赋值给pos的指针域 实质上是将pos的下一个结点给跳过
free(next); //将pos的下一个结点给free释放
next = NULL; //next指向为NULL 防止野指针
}
}
//链表的摧毁 直接将头指针指针域指向NULL
void ListDestory(List** ps) {
//assert(ps); //防止空链表
一个结点一个结点释放
//List* next = *ps;
//while (next) {
// List* cur = next->next;
// free(next);
// next = cur;
//}
*ps = NULL;
}
3.test.c
#define _CRT_SECURE_NO_WARNINGS
#include"单向链表.h"
void test()
{
List* phead=NULL;//作为头节点
//单链表的尾插
ListPushBack(&phead, 1);
ListPushBack(&phead, 2);
ListPushBack(&phead, 3);
ListPushBack(&phead, 4);
ListPushBack(&phead, 5);
ListPrint(phead);
ListPushFront(&phead, 1);
ListPrint(phead);
ListPopBack(&phead);
ListPrint(phead);
ListPopFront(&phead);
ListPrint(phead);
ListErase(&phead, phead->next);
ListInsertAfter(&phead, 10, phead->next);
ListEraseAfter(phead->next);
ListPrint(phead);
ListDestory(&phead);
}
int main()
{
test();
return 0;
}
总结
本文主要讲解了链表的分类是什么,两种常用的类型,无头单向非循环链表、有头双向循环链表,我们实现了无头单向非循环链表,这是比较简单的一种链表的实现,我们使用的是二级指针传参,当然使用一级指针传参也可以,主要实现函数为头尾插入,头尾删除,pos指定结点位置前后添加或者删除元素。
接下来,下文我们将跟大家介绍一下最常用链表的另一种形式,带头双向循环链表。