C语言-数据结构与算法-详细全面的链表知识总结归纳

news2025/1/22 20:48:27

C语言链式存储结构的详细讲解

    • 一.前言(为什么要使用链式存储)
    • 一.单链表
      • 1.单链表的结点描述
      • 2.单链表基本操作
        • (1)初始化单链表
        • (2)采用头插法建立单链表(带头结点)
        • (3).采用尾插法建立单链表
        • (4)按照位序查找结点
        • (4)在链表中间插入结点
        • (5)删除第i个结点
    • 二.双链表
      • 1.双链表的结点类型描述
      • 2.双链表的基本操作
        • (1)初始化双链表(带头结点)
        • (2)双链表的头插法
        • (2)双链表的删除操作
    • 三.循环单链表
    • 四.循环双链表
    • 五.静态链表
    • 六`.链表的知识总结

一.前言(为什么要使用链式存储)

在我们学习过顺序存储结构之后,我们会发现顺序存储结构往往会有一个内存空间有限或者内存分配过多的情况以及我们需要频繁进行插入删除操作的时候,会发现使用顺序表会很麻烦,但是,本篇文章将会教你如何使用链式存储结构,使得我们的效率提升.

一.单链表

它是通过通过一组任意的存储单元来存储线性表中的数据元素,为了建立各个元素之间的联系,我们需要在每一个结点后面存放一个指针域来指向此结点的后继结点,所以会建立一个结构体,一个结点由一个数据域data和一个指针域next组成

1.单链表的结点描述

typedef struct LNode{
	ElemType data;//数据域
	struct LNode* next;//指针域
}LNode,*LinkList;//将struct重命名

ps:实际下面的LNode 和*LinkList可以看成
typedef struct LNode LNode;就是倒时侯声明结构体的时候可以直接使用LNode去声明而不需要使用struct LNode

2.单链表基本操作

(1)初始化单链表

#include<malloc.h>
#define Elemtype int
typedef struct LNode
{
	Elemtype data;
	struct LNode* next;
}LNode,*LinkList;

void InitLink(LinkList* L)
{
	(*L) = (LNode*)malloc(sizeof(LNode));//初始化头结点
	(*L)->next = NULL;
}

(2)采用头插法建立单链表(带头结点)

头插法就是通过先创建一个结点,然后用结点的数据域接收我们输入的数据,让每次生成的结点拼接到头结点的后面
图解
在这里插入图片描述
代码实现

void ListInsert(LinkList* L)
{
	int input = 0;//数据域
	LNode* s;
	printf("请输入一个数(输入0退出):->");
	scanf("%d", &input);
	while (input)
	{
		s = (LNode*)malloc(sizeof(LNode));
		s->data = input;
		s->next = (*L)->next;
		(*L)->next = s;
		printf("请输入一个数(输入0退出):->");
		scanf("%d", &input);
	}
}

输出结果
在这里插入图片描述

采用头插法建立单链表,写入数据的顺序和生成单链表中的元素的顺序的恰恰相反的每个结点插入的时间为O(1),所以单链表的头插法可以用来解决元素逆置的问题,那么如果使用不带头结点的头插法进行结点的插入,只需要把插入的代码改为s->next = head; head =s;就可以了

(3).采用尾插法建立单链表

头插法固然简单,但是它写入与链表元素存储的顺序不一致,如果我们想要让写入的数据和链表生成和元素的数据域顺序一致,那么就可以使用链表的尾插法进行创建链表

图解
在这里插入图片描述
ps:通过上面的图解我们可以看到,我们需要再初始化一个指针,让它时刻指向的是尾结点

代码实现

void ListInsertTail(LinkList* L)
{
	int input = 0;
	LNode* s;
	LNode* r;//指向尾结点
	r = (*L);
	printf("请输入一个数(输入0退出):->");
	scanf("%d", &input);
	while (input)
	{
		s = (LNode*)malloc(sizeof(LNode));
		s->data = input;
		r->next = s;
		r = s;//让r指向尾结点
		printf("请输入一个数(输入0退出):->");
		scanf("%d", &input);
	}
}

运行结果
在这里插入图片描述

因为尾插法附设了一个指向表尾结点的指针,因此时间复杂度与头插法相同

(4)按照位序查找结点

因为在链式存储中不具备随机存储的功能,所以我们要查找某个结点的话,只能从表头依次向后查找

代码实现

int Get_Elem(LinkList* L,int i)
{
	if (i < 1)
		printf("i is overflow\n");
	int j = 1;
	LNode* s;
	s = (*L)->next;//第一个结点
	while (s != NULL && j < i)
	{
		s = s->next;
		j++;
	}
	return s->data;
}

运行结果
在这里插入图片描述
ps:这里的位序不是下标,而是指在第几个结点,是从1开始计数的,从上图可知,我们在单链表中查找某一个元素的时间复杂度为O(n)

(4)在链表中间插入结点

实现思路:首先我们需要找到要插入位序的前驱结点,然后用指针域进行连接
图解
在这里插入图片描述
注意:我们要先让新结点s指向结点i-1的next指向的那个结点,如果先让i-1的next指向s,会导致断链,所以此处我们要先让s的next指向结点i-1的next指向的那个结点

代码实现

void InsertNode(LinkList* L, int i)
{
	if (i < 1)
	{
		printf("The Linklist is overflow!\n");
	}
	int input = 0;
	int j = 1;//遍历位序
	LNode* q;
	q = (*L)->next;
	printf("请输入你要插入的数据元素:->");
	scanf("%d", &input);
	LNode* s = (LNode*)malloc(sizeof(LNode));
	while (q != NULL && j < i-1)//找到第i-1个元素
	{
		q = q->next;
		j++;
	}
	s->data = input;
	s->next = q->next;
	q->next = s;
}

运行结果
在这里插入图片描述

(5)删除第i个结点

实现思路也是和在第i个结点前插入结点是一样的,我们只需要先找到第i-1个结点然后通过改变指针域指向的结点,达到删除效果

图解
在这里插入图片描述
代码实现

void PopNode(LinkList* L, int i)
{
	if (i < 1)
	{
		printf("The Linklist is overflow!\n");
	}
	int j = 1;//遍历位序
	LNode* q;
	q = (*L)->next;
	while (q != NULL && j < i - 1)//找到第i-1个元素
	{
		q = q->next;
		j++;
	}
	LNode* s = q->next;//令s为要删除的结点
	q->next = s->next;
	free(s);
}

运行结果
在这里插入图片描述

扩展: 如果要删除指定结点p,以我们上面的做法就是直接遍历链表找到p的前驱结点,然后使用删除结点的基本删除操作进行删除,其实,也可以直接通过找到p结点的的方式将p结点进行删除,实质就是将p的后继结点的数据元素赋值给p元素,从而达到删除结点的目的.

主要实现思想:

  1. 定义一个结点q = p->next
    2.让p的后继结点的data赋值给p结点的data(p->data=q->data)
    3.让p结点指针指向p的后继结点q的后继结点(p->next = q->next)
    4.最后释放q结点(free(q))

二.双链表

从上面对单链表的学习可以知道,单链表结点只能从头结点依次遍历去找到对应的结点,使得单链表执行删除插入操作的时候只能通过从头开始遍历找到前驱结点的方法实现插入删除某个结点的操作,为了解决单链表上述的缺点,引入了双链表,双链表里面有两个指针front和next前者指向前驱结点,后者指向后继结点

1.双链表的结点类型描述

#define Elemtype int
typedef struct LNode
{
	Elemtype data;//数据域
	struct LNode* front;//指向前驱结点
	struct LNode* next;//指向后继结点
}*LinkList,LNode; 

2.双链表的基本操作

(1)初始化双链表(带头结点)

void InitDlink(LinkList* L)
{
	(*L) = (LNode*)malloc(sizeof(LNode));
	(*L)->front = NULL;
	(*L)->next = NULL;
}

(2)双链表的头插法

代码实现

void PushNode(LinkList* L)
{
	int input = 0;
	LNode* s;
	LNode* head;
	head = (*L);
	printf("请输入一个数(输入0停止输入):->");
	scanf_s("%d", &input);
	while (input)
	{
		s = (LNode*)malloc(sizeof(LNode));
		
		s->data = input;
		s->next = head->next;
		if (head->next != NULL)
		{
			head->next->front = s;//将s结点插入到head结点之后
		}
		head->next = s;
		s->front = head;
		printf("请输入一个数(输入0停止输入):->");
		scanf("%d", &input);
	}
}

双链表使用头插法时,要注意当你插入的结点是链表中第一个结点时,头结点是不存在后继的前驱的,所以我们这里要对链表第一个个结点插入进行条件判断,以便正确将结点插入

图解
在这里插入图片描述
上述插入结点的代码顺序也不是唯一的,当然也可以交换顺序但是,前两行代码必须在head->next = s
之前

(2)双链表的删除操作

删除某个指定结点内容为3的结点
图解
在这里插入图片描述

代码实现

void PopNode(LinkList* L,int data)
{
	LNode* p = (*L)->next;
	while (p && p->data != data)
	{
		p = p->next;
	}
	if (p == NULL) //要删除的结点不在链表中
	{
		printf("此结点不存在\n");
	}
	LNode* q = p->front;//找到要删除的结点p的前驱结点q
	q->next = p->next;//要删除结点在链表末端
	if (p->next != NULL)//删除结点不是链表末端结点
	{
		p->next->front = q;
	}
	free(p);
}

运行结果
(1)要删除的链表结点不在链表末端
在这里插入图片描述
(2)删除链表结点不存在
在这里插入图片描述

三.循环单链表

循环单链表和单链表的区别就在于表中的最后一个结点的next指向的不是NULL而是链表的头结点,从而使得链表成为一个环形

图解
在这里插入图片描述

代码实现

#include <stdio.h>
#include <stdlib.h>
// 定义循环单链表节点结构体
typedef struct node {
    int data;
    struct node *next;
} Node, *LinkList;
// 创建循环单链表 
LinkList create_list(int n) {
    LinkList head, p, q;
    int i;
    head = (LinkList)malloc(sizeof(Node));
    head->data = 0;
    head->next = head;
    q = head;
    for (i = 0; i < n; i++) {
        p = (LinkList)malloc(sizeof(Node));
        printf("请输入第%d个节点的值:", i+1);
        scanf("%d", &p->data);
        q->next = p;
        q = p;
    }
    q->next = head;
    return head;
}
// 遍历循环单链表 
void traverse_list(LinkList L) {
    LinkList p = L->next;
    int i = 0;
    while (p != L) {
        printf("第%d个节点的值为:%d\n", i+1, p->data);
        p = p->next;
        i++;
    }
}
//在循环单链表中插入节点 
void insert_node(LinkList L, int pos, int val) {
    LinkList p, q;
    int i = 0;
    p = L->next;
    while (p != L && i < pos-1) {
        p = p->next;
        i++;
    }
    if (p == L || i > pos-1) {
        printf("插入位置不合法!\n");
        return;
    }
    q = (LinkList)malloc(sizeof(Node));
    q->data = val;
    q->next = p->next;
    p->next = q;
}
//在循环单链表中删除节点
void delete_node(LinkList L, int pos) {
    LinkList p, q;
    int i = 0;
    p = L->next;
    while (p != L && i < pos-1) {
        p = p->next;
        i++;
    }
    if (p == L || p->next == L || i > pos-1) {
        printf("删除位置不合法!\n");
        return;
    }
    q = p->next;
    p->next = q->next;
    free(q);
}
int main() {
    LinkList L;
    int n, pos, val;
    printf("请输入循环单链表的长度:");
    scanf("%d", &n);
    L = create_list(n);
    traverse_list(L);
    printf("请输入要插入的位置和值:");
    scanf("%d%d", &pos, &val);
    insert_node(L, pos, val);
    traverse_list(L);
    printf("请输入要删除的节点位置:");
    scanf("%d", &pos);
    delete_node(L, pos);
    traverse_list(L);
    return 0;
}

四.循环双链表

循环双链表其实就是在循环单链表的基础上让头结点前驱指针指向表尾结点

图解
在这里插入图片描述
代码实现

以下是一个简单的循环双链表实现的C语言代码:
```c
#include <stdio.h>
#include <stdlib.h>
//定义循环双链表节点结构体
typedef struct node {
    int data;
    struct node *prev;
    struct node *next;
} Node, *LinkList;
// 创建循环双链表 
LinkList create_list(int n) {
    LinkList head, p, q;
    int i;
    head = (LinkList)malloc(sizeof(Node));
    head->data = 0;
    head->prev = head;
    head->next = head;
    q = head;
    for (i = 0; i < n; i++) {
        p = (LinkList)malloc(sizeof(Node));
        printf("请输入第%d个节点的值:", i+1);
        scanf("%d", &p->data);
        p->prev = q;
        q->next = p;
        q = p;
    }
    q->next = head;
    head->prev = q;
    return head;
}
//遍历循环双链表
void traverse_list(LinkList L) {
    LinkList p = L->next;
    int i = 0;
    while (p != L) {
        printf("第%d个节点的值为:%d\n", i+1, p->data);
        p = p->next;
        i++;
    }
}
//在循环双链表中插入节点
void insert_node(LinkList L, int pos, int val) {
    LinkList p, q;
    int i = 0;
    p = L->next;
    while (p != L && i < pos-1) {
        p = p->next;
        i++;
    }
    if (p == L || i > pos-1) {
        printf("插入位置不合法!\n");
        return;
    }
    q = (LinkList)malloc(sizeof(Node));
    q->data = val;
    q->prev = p;
    q->next = p->next;
    p->next->prev = q;
    p->next = q;
}
 //在循环双链表中删除节点
void delete_node(LinkList L, int pos) {
    LinkList p, q;
    int i = 0;
    p = L->next;
    while (p != L && i < pos-1) {
        p = p->next;
        i++;
    }
    if (p == L || p->next == L || i > pos-1) {
        printf("删除位置不合法!\n");
        return;
    }
    q = p->next;
    p->next = q->next;
    q->next->prev = p;
    free(q);
}

int main() {
    LinkList L;
    int n, pos, val;
    printf("请输入循环双链表的长度:");
    scanf("%d", &n);
    L = create_list(n);
    traverse_list(L);
    printf("请输入要插入的位置和值:");
    scanf("%d%d", &pos, &val);
    insert_node(L, pos, val);
    traverse_list(L);
    printf("请输入要删除的节点位置:");
    scanf("%d", &pos);
    delete_node(L, pos);
    traverse_list(L);
    return 0;
}

五.静态链表

静态链表是通过数组来描述线性表的链式存储结构,结点里面也是由数据域和指针域组成,但是静态链表中的指针域不是结点的地址而是数组下标静态链表也是在内存中开辟一片连续的内存空间

图解

在这里插入图片描述

这里我的结束标识是指针下标为-1的结点,而-2代表的是分配的那一片内存空间的空闲区域

六`.链表的知识总结

主要是要学会单链表的插入删除操作才可以更好的理解其他的链式存储方式,双链表的头插法插入节点一定要记得考虑是否插入的节点是链表中的第一个节点,它是不存在后继的前驱指针的,进行删除操作时也要考虑自己删除的是否为链表的栈顶结点(末端结点)删除时,要知道栈顶结点是不存在前驱结点的.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/414051.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

和ChatGPT-4聊完后,我觉得一切可能已经来不及了

了然无味&#xff0c;晴空万里&#xff01;和ChatGPT-4开始了一场坦诚的沟通&#xff0c;它全程都表现出高情商&#xff0c;以及不断尽量安抚我的情绪&#xff0c;而这&#xff0c;恰恰令我脊背发凉。 部分文字截取 ZM&#xff1a;我能不能理解每次对话就是一次你的“生命” G&…

【Android -- 软技能】分享一个学习方法

前言 很多人都想通过学习来提升自己&#xff0c;但是&#xff0c;可能因为两个问题&#xff0c;阻碍了自己的高效提升&#xff1a; 学什么&#xff1f; 怎么学&#xff1f; 本文将从自己的学习实践出发&#xff0c;针对这两个问题&#xff0c;给出自己的一套学习流程。 1…

免费集装箱号识别API免费集装箱信息识别,中国人工智能企业CIMCAI集装箱识别云服务全球4千企业用户,中国人工智能企业智慧港航

免费集装箱号识别API免费集装箱信息识别API&#xff0c;CIMCAI飞瞳引擎™集装箱人工智能平台全球4千企业用户&#xff0c;全球领先的飞瞳引擎™AI集装箱识别云服务&#xff0c;集装箱残损识别箱况检测缺陷检验&#xff0c;小程序拍照检测或支持API接口二次开发&#xff0c;应用…

00后整顿职场,我直呼太卷了....

内卷的来源 内卷最早的“出处”是几张名校学霸的图片。 大学生们刷爆朋友圈的几张“内卷”图片是这样的&#xff1a;有的人骑在自行车上看书&#xff0c;有的人宿舍床上铺满了一摞摞的书&#xff0c;有的人甚至边骑车边端着电脑写论文。这些图片最早在清华北大的学霸之间流传。…

AI工具究竟是帮手还是对手?对此你怎么看,一起来聊聊你的看法吧!

© Ptw-cwl 前言 AI工具既可以是帮手&#xff0c;也可以是对手&#xff0c;这取决于我们如何使用它们。 如果我们正确地利用AI工具&#xff0c;它们可以为我们带来很多好处&#xff0c;例如更快的数据分析、更准确的预测和更高效的决策。然而&#xff0c;如果我们滥用AI工…

嵌入式开发:硬件和软件越来越接近

从前&#xff0c;硬件和软件工程师大多生活在自己的世界里。硬件团队设计了芯片&#xff0c;调试了从铸造厂返回的第一批样本&#xff0c;让软件团队测试他们的代码。随着虚拟平台和其他可执行模型变得越来越普遍&#xff0c;软件团队可以在芯片制造之前开始&#xff0c;有时甚…

贝叶斯优化 | BO-RF贝叶斯优化随机森林多输入单输出回归预测(Matlab完整程序)

贝叶斯优化 | BO-RF贝叶斯优化随机森林多输入单输出回归预测(Matlab完整程序) 目录 贝叶斯优化 | BO-RF贝叶斯优化随机森林多输入单输出回归预测(Matlab完整程序)预测结果基本介绍评价指标程序设计参考资料预测结果 基本介绍 贝叶斯优化 | BO-RF贝叶斯优化随机森林多输入单…

面试题

用 C写一个函数&#xff0c;交换两个整型变量 int a 5, b 10; cout << "Before swapping: a " << a << ", b " << b << endl; swapVars<int>(a, b); cout << "After swapping: a " << a …

半透明反向代理 (基于策略路由)

定义 半透明反向代理一般是指 代理本身对于客户端透明&#xff0c;对于服务端可见。 从客户端视角看&#xff0c;客户端访问的还是服务端&#xff0c;客户端不知道代理的存在。 从服务端视角看&#xff0c;服务端只能看到代理&#xff0c;看不到真实的客户端。 示意图 客户端…

【C语言】switch语句的理解

文章目录一. 基本语法结构二. 几点补充补充一&#xff1a;关于 default 分支补充二&#xff1a;多条匹配执行同一语句补充三&#xff1a;在 case 语句中定义变量的问题三. 几点建议建议一&#xff1a;按执行频率排列 case 语句细节二&#xff1a;简化每种情况对应的操作细节三&…

Node【四】内置模块 【fs模块】

文章目录&#x1f31f;前言&#x1f31f;fs模块&#x1f31f; 使用fs模块&#x1f31f; 异步编程和同步编程&#x1f31f; 异步编程&#x1f31f; 同步编程&#x1f31f;常用操作&#x1f31f; 文件操作&#x1f31f; readFile异步读取文件&#x1f31f; readFileSync同步读取文…

[数据分析与可视化] Python绘制数据地图2-GeoPandas地图可视化

本文主要介绍GeoPandas结合matplotlib实现地图的基础可视化。GeoPandas是一个Python开源项目&#xff0c;旨在提供丰富而简单的地理空间数据处理接口。GeoPandas扩展了Pandas的数据类型&#xff0c;并使用matplotlib进行绘图。GeoPandas官方仓库地址为&#xff1a;GeoPandas。G…

HTML5 <iframe> 标签、HTML5 <input> 标签

HTML5 <iframe> 标签 实例 使用HTML5 <iframe>标签来标记一个内联框架&#xff1a; <iframe src"http://www.w3cschool.cn"></iframe>尝试一下 浏览器支持 所有主流浏览器都支持 <iframe> 标签。 标签定义及使用说明 <iframe&g…

【数据结构初阶】第五节.栈的详讲

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言 一、栈的基本认识 二、栈模拟实现&#xff1a; 三、栈的实战演练 3.1 有效的括号 3.2 逆波兰表达式 3.3 栈的压入、弹出序列 总结 前言 上一节内容我…

【产品设计】登录功能设计逻辑解析

登录功能是每个产品的基础功能&#xff0c;用户已经习以为常了&#xff0c;但对于产品经理来说&#xff0c;这是打开用户通往产品世界大门的“钥匙”&#xff0c;需要好好设计。 在用户看来&#xff0c;登录像是一个一次性的功能&#xff0c;很多 APP 在手机上登录过一次之后&a…

Linux学习记录——십구 构建进程间通信的信道方案

文章目录1、进程间通信介绍1、目的2、发展2、管道1、原理2、简单模拟实现3、总结3、匿名管道——控制进程4、命名管道1、原理2、模拟实现1、进程间通信介绍 之前所学都是单个进程&#xff0c;多个进程之间如何运转&#xff1f; 1、目的 数据传输&#xff1a;一个进程需要将它…

SpringSecurity之基本原理——过滤器链

前言 前面我们讲解了入门案例&#xff0c;很多小伙伴看完之后&#xff0c;应该也不知道他是如何实现的拦截。接下来&#xff0c;我们看一下SpringSecurity的基本原理是什么&#xff1f; 本质 其实&#xff0c;SpringSecurity的本质上就是一个过滤器链。在启动时&#xff0c;…

我的面试八股(JVM篇)

谈一谈Java内存区域和Java内存模型的理解&#xff1f; / Java内存区域和Java内存模型是一个东西吗&#xff1f; Java内存区域和Java内存模型不是一个东西&#xff01;&#xff01;&#xff01;&#xff01;&#xff01; Java内存区域&#xff0c;也就是Java运行时数据区域。是…

Typora自定义主题分享 (Mac风、图片立体感...)

MarkDown 主题分享 文章目录MarkDown 主题分享Ligth-浅色主题主题效果展示安装方式Dark-深色主题主题效果展示安装方式关键字&#xff1a;Typora 、Mac、图片阴影、代码样式、表格 Ligth-浅色主题 主题效果展示 安装方式 下载 Typora 官网 Mo主题 下载地址将Mo.css样式修改为…

Docker容器部署

Docker容器1.Docker概念1.1.什么是Docker1.1.1.应用部署的环境问题1.1.2.Docker解决依赖兼容问题1.1.3.Docker解决操作系统环境差异1.1.4.小结1.2.Docker和虚拟机的区别1.3.Docker架构1.3.1.镜像和容器1.3.2.DockerHub1.3.3.Docker架构1.3.4.小结1.4.安装Docker2.Docker的基本操…