线性表之链表

news2024/11/25 21:52:15

1、链表概述

链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

顺序表的存储位置可以用一个简单直观的公式表示,它可以随机存取表中任意一个元素,但插入和删除需要移动大量元素。链式存储线性表时,不需要使用地址连续的存储单元,即不要求逻辑上相邻的元素在物理位置上也相邻,它通过“链”建立起元素之间的逻辑关系,因此插入和删除操作不需要移动元素,只需要修改指针,这也意味着链表失去了可随机存取的特点。

2、链表的分类

链表结构种类多样,可以按照是否带头、是否循环、单向或者双向大致分类。

(1)带头结点和不带头结点

 (2)单向链表和双向链表

 (3)循环链表和非循环链表

以上情况组合就有8中链表结构,但实际中应用最多的链表结构是无头单向非循环链表和带头双向循环链表。下面介绍两种链表的基本实现。

3、无头单向非循环链表的基本实现

为了建立数据元素之间的线性关系,链表结点除了存放数据,还需要存放一个指向其后继的指针。单链表可以解决顺序表需要大量连续存储单元的缺点。由于单链表的元素离散地分布在存储空间中,所以单链表时非随机存取的存储结构。

单链表的结点类型描述如下。

typedef int SLTDataType;
typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;
}SLTNode;

3.1单链表的打印

为了观感上更贴近单链表的定义,打印时先打印结点的值,“->”表示链表的指针,打印完所有元素后再打印“NULL”。

void SLTPrint(SLTNode* phead)
{
	SLTNode* cur = phead;
	while (cur != NULL)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}

3.2单链表的销毁

销毁时传入的是一级指针

void SLTDestroy(SLTNode* phead)
{
	SLTNode* cur = phead;
	while (phead)
	{
		cur = phead;
		phead = phead->next;
		free(cur);
		cur = NULL;
	}
	printf("success\n");
}

3.3头插法插入结点

使用头插法插入新结点时,不用考虑链表是否为空,因为不涉及空指针的引用,直接插入即可。

void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	SLTNode* newnode = BuySLTNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

3.4尾插法插入结点

使用尾插法插入新结点时需要判断链表是否为空,因为插入过程中涉及到了空指针的引用。

当链表为空时,新结点即链表第一个结点;当链表不为空时,先找到链表的尾结点,然后直接将新结点插到尾结点后面。

void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	SLTNode* newnode = BuySLTNode(x);
	if (*pphead == NULL)
		*pphead = newnode;
	else
	{
		SLTNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}

3.5头删法删除结点

删除之前需要判断链表是否为空,链表不为空时,删除第一个结点。

void SLTPopFront(SLTNode** pphead)
{
	assert(pphead);
	assert(*pphead);
	SLTNode* del = *pphead;
	*pphead = (*pphead)->next;
	free(del);
	del = NULL;
}

3.6尾删法删除结点

删除之前同样需要判断链表是否为空,此外还需要判断链表是否只有一个结点,因为删除过程中涉及到空指针的引用。如果链表只有一个结点,直接将该结点删除释放;如果链表有多个元素,找到倒数第二个结点,然后删除该结点的后继。

void SLTPopBack(SLTNode** pphead)
{
	assert(pphead);
	assert(*pphead);
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLTNode* tail = *pphead;
		while (tail->next->next != NULL)
		{
			tail = tail->next;
		}
		free(tail->next);
		tail->next = NULL;
	}
}

3.7按值查找

从头开始依次对比,如果找到值为x的结点则返回该结点的地址,如果没找到则返回空指针。

SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	assert(phead);
	SLTNode* cur = phead;
	while (cur)
	{
		if (cur->data == x)
			return cur;
		cur = cur->next;
	}
	return NULL;
}

3.8在指定位置之前插入结点

插入分为链表只有一个结点和有多个结点这两种情况。当链表只有一个元素或者指定位置为第一个结点时,相当于头插法;当链表有多个元素或者指定位置为其他结点时,找到指定位置的前驱,然后修改其前驱的后继以及新结点的后继。

void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead);
	assert(pos);
	if (pos == *pphead)
		SLTPushFront(pphead, x);
	else
	{
		SLTNode* posPrev = *pphead;
		SLTNode* newnode = BuySLTNode(x);
		while (posPrev->next != pos)
		{
			posPrev = posPrev->next;
		}
		posPrev->next = newnode;
		newnode->next = pos;
	}
}

3.9在指定位置之后插入结点

在指定位置之后插入不用考虑链表有几个结点,直接插入即可。注意,要先修改新结点的后继,再修改指定位置结点的后继。

void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = BuySLTNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

3.10删除指定位置的结点

当链表只有一个元素或者指定位置为第一个结点时,相当于链表的头删;当链表有多个元素或者指定位置为其他结点时,找到指定位置结点的前驱,并修改其后继,再删除并释放指定位置结点。

void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead);
	assert(pos);
	if (pos == *pphead)
		SLTPopFront(pphead);
	else
	{
		SLTNode* posPrev = *pphead;
		while (posPrev->next != pos)
		{
			posPrev = posPrev->next;
		}
		posPrev->next = pos->next;
		free(pos);
		pos = NULL;
	}
}

3.11删除指定位置之后的结点

在指定位置之后删除不用考虑链表有几个结点,直接删除即可。

void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);
	SLTNode* del = pos->next;
	pos->next = pos->next->next;
	free(del);
	del = NULL;
}

4、带头双向循环链表的基本实现

单链表结点中只有一个指向其后继的指针,使得单链表只能从头结点依次顺序地向后遍历。要访问某个结点的前驱结点时,只能从头开始遍历,访问后继结点的时间复杂度为O(1),访问前驱结点的时间复杂度为O(n)。为了克服单链表的这些缺点,引入双链表,双链表中有两个指针prev和next,分别指向前驱结点和后继结点。

双链表中结点类型的描述如下:

typedef int LTDataType;
typedef struct ListNode
{
	LTDataType data;
	struct ListNode* prev;
	struct ListNode* next;
}LNode;

双链表可以很方便找到前驱结点,因此,插入、删除操作的时间复杂度为O(1)。

此外,链表带头结点,无论链表是否为空,其头指针都是指向头结点的非空指针,因此在链表的插入和删除中空链表和非空链表的处理得到了统一。

4.1初始化双向链表

为了减少二级指针的使用,在初始化双链表时,使用一级指针定义并返回头结点。因此初始化时直接建立一个头结点并返回头结点的地址。

因为后续实现链表插入时还需要建立新结点,为了简化代码以及增强代码的可读性,定义一个建立新结点的函数,后续需要建立新结点时直接调用该函数即可。建立新结点函数如下。

LTNode* BuyLTNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		return NULL;
	}
	newnode->next = NULL;
	newnode->prev = NULL;
	newnode->data = x;
	return newnode;
}

初始化双链表函数如下。

LTNode* Init()
{
	LTNode* head = BuyLTNode(-1);
	head->next = head;
	head->prev = head;
	return head;
}

4.2双向链表的打印

需要注意的是,打印时从phead的next开始打印。为了观感上更贴近双链表的定义,打印时先打印“guard”表示头结点,“<==>”表示链表的双指针,打印完所有元素后再打印一次“guard”表示尾结点的链接到头结点。

void LTPrint(LTNode* phead)
{
	LTNode* cur = phead->next;
	printf("guard<==>");
	while (cur != phead)
	{
		printf("%d<==>", cur->data);
		cur = cur->next;
	}
	printf("guard\n");
}

4.3双向链表的销毁

双链表的销毁同单链表的销毁类似,这里使用的仍是一级指针,所以需要用户在调用完销毁函数后,手动置空头结点。

void LTDestory(LTNode* phead)
{
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		LTNode* next = cur->next;
		free(cur);
		cur = next;
	}
}

4.4头插法插入结点

在使用头插法插入新结点时,要注意指针修改顺序,指针顺序虽然不是唯一的,但也不是任意的。新结点前驱和后继的修改必须在头结点后继的修改之前进行,否则头结点的后继结点的指针就会丢掉,导致插入失败。如图所示,1和2必须在4之前进行。

void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = BuyLTNode(x);
	
	newnode->next = phead->next;
	phead->next->prev = newnode;

	newnode->prev = phead;
    phead->next = newnode;
}

还有一种方法,可以不用考虑指针修改的先后顺序,就是重新定义一个结点存放头结点的后继结点,这样就不用担心头结点的后继指针丢失的问题了。

void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = BuyLTNode(x);
	LTNode* first = phead->next;  
    newnode->prev = phead;  
    phead->next = newnode;
    
    newnode->next = first;
    first->prev = newnode;
}

4.5尾插法插入结点

使用尾插法时,不用像单链表那样遍历找尾,头结点的前驱结点就是尾结点。插入时同样需要注意指针的修改顺序,1和2必须要在3之前进行。

和头插法类似,重新定义一个结点保存头结点的前驱结点,就不用考虑指针修改的顺序了。

void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = BuyLTNode(x);
	LTNode* tail = phead->prev;
	tail->next = newnode;
	newnode->prev = tail;

	phead->prev = newnode;
	newnode->next = phead;
}

4.6头删法删除结点

在删除之前需要将头结点的后继结点保存起来,否则无论在修改指针之前free后继结点还是在修改指针之后free后继结点都会出现错误。

 注意,当链表为空时(只有头结点)时不能继续删除,所以在删除之前需要判断链表是否为空。

bool LTEmpty(LTNode* phead)
{
	assert(phead);
	return phead->next == phead;
}
void LTPopFront(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));
	LTNode* first = phead->next;
	phead->next = first->next;
	first->next->prev = phead;
	free(first);
}

4.7尾删法删除结点

在删除之前需要将尾结点保存起来,为了方便,将尾结点的前驱结点也保存一下。与头删法同样,在删除前需要判断双链表是否为空,如果为空则不能继续删除。

void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));
	LTNode* tail = phead->prev;
	LTNode* tailPrev = tail->prev;
	tailPrev->next = phead;
	phead->prev = tailPrev;
	free(tail);
}

4.8按值查找

需要注意的是,从头结点的后继结点开始查找,并且停止查找的条件是当前结点不等于头结点。

LTNode* LTFind(LTNode* phead, LTDataType x)
{
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
			return cur;
		cur = cur->next;
	}
	return NULL;
}

4.9在指定位置之前插入结点

在插入之前先保存pos的前驱结点,这样在插入时就不用考虑指针的修改顺序了。

void LTInsert(LTNode* pos, LTDataType x)
{
	LTNode* newnode = BuyLTNode(x);
	LTNode* posPrev = pos->prev;
	posPrev->next = newnode;
	newnode->prev = posPrev;

	newnode->next = pos;
	pos->prev = newnode;
}

 头插法和尾插法插入结点可以通过该函数的复用实现。

//头插法
void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTInsert(phead->next, x);
}

//尾插法
void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTInsert(phead, x);
}

注意,头插法传入的pos为头结点的后继结点;尾插法传入的pos为头结点,因为头结点的前驱结点就是尾结点。

4.10删除指定位置的结点

在删除之前先保存pos的前驱结点和后继结点。

void LTErase(LTNode* pos)
{
	LTNode* posPrev = pos->prev;
	LTNode* posNext = pos->next;
	free(pos);
	posPrev->next = posNext;
	posNext->prev = posPrev;
}

同样地,头删法和尾删法删除结点可以通过该函数的复用实现。

//头删法
void LTPopFront(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));
	LTErase(phead->next);
}

//尾删法
void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));
	LTErase(phead->prev);
}

注意,头删法传入的pos是头结点的后继结点;尾删法传入的pos是头结点的前驱结点。

5、顺序表与链表的比较

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

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

相关文章

【多线程】Synchronize关键字之对象锁和类锁

目录 类锁和对象锁的区别 类锁 对象锁 测试用例 1.m1()和m2()方法都加锁【都为非静态&#xff0c;都加锁互斥执行】 2.m1()加锁&#xff0c;m2()不加锁【都为非静态&#xff0c;互不影响】 3.m3()和m4()都加锁【都为静态&#xff0c;互斥】 4.m3()加锁&#xff0c;m4()不…

WPF快速开发(2):图标库知识点

文章目录 前言知识点windows资源Style:样式Setter:属性继承关系 Trigger:触发器 WPF层级划分数据绑定声明数据上下文绑定数据模板 前言 图标资源下载 iconfont 知识点 windows资源 Window.Resources&#xff1a;资源位置声明X:Key&#xff1a;资源Id&#xff0c;用于前端的…

ARM DAY3 点亮三盏灯

1.汇编代码 .text .global _start _start: //RCC初始化 RCC_INIT://设置GPIOE组使能ldr r0,0x50000A28ldr r1,[r0]orr r1,r1,#(0x1<<4)str r1,[r0]//设置GPIOF组使能 ldr r0,0x50000A28ldr r1,[r0]orr r1,r1,#(0x1<<5)str r1,[r0]//LED1灯初始化 LED1_INIT://设置…

WebLLM项目:在浏览器中运行LLM聊天机器人

大家好&#xff0c;基于LLM的聊天机器人可以通过前端访问&#xff0c;而且它们涉及到大量且昂贵的服务器端API调用。但如果可以让LLM完全在浏览器中运行——利用底层系统的计算能力呢&#xff1f;这样&#xff0c;LLM的全部功能都将在客户端可用——无需担心服务器的可用性、基…

算法竞赛入门【码蹄集新手村600题】(MT1020-1040)

算法竞赛入门【码蹄集新手村600题】(MT1020-1040&#xff09; 目录MT1021 %f格式符MT1022 小数、指数MT1023 进制乱炖MT1024 进制形式MT1025 八、十六进制MT1026 合并MT1027 整数逆序MT1028 四位数逆序MT1029 位数MT1030 最大公约数MT1031 最简分数MT1032 最小公倍数MT1033 多项…

LeetCode[327]区间和的个数

难度&#xff1a;Hard 题目&#xff1a; 给你一个整数数组 nums 以及两个整数 lower 和 upper 。求数组中&#xff0c;值位于范围 [lower, upper] &#xff08;包含 lower 和 upper&#xff09;之内的 区间和的个数 。 区间和 S(i, j) 表示在 nums 中&#xff0c;位置从 i 到 …

一文了解UML

目录 1 什么是UML? 2 UML视图&#xff08;UML View&#xff09; 2.1 用户视图&#xff08;Users View&#xff09; 2.2 结构视图&#xff08;Structural Views&#xff09; 2.3 行为视图&#xff08;Behavioral Views&#xff09; 2.4 环境视图&#xff08;Environmenta…

Vc - Qt - 自定义ComboBox

示例代码创建了一个名为ComboBoxWidget的自定义QWidget类&#xff0c;并在initUI方法中创建了一个垂直布局。然后将一个只读的QLineEdit和一个QPushButton添加到布局中。当按钮被点击时&#xff0c;会调用showMenu方法&#xff0c;该方法创建一个QMenu并添加选项。每个选项连接…

vite打包性能优化以及填坑

目录 前言 项目优化前 分析 优化 拆分包 去除debugger CDN 加速 按需导入 文件压缩 图片压缩 viteImagemin报错 填坑 坑1 坑2 总结 配置 前言 最近在使用 Vite4.0 构建一个中型前端项目的过程中&#xff0c;遇到了一些坑&#xff0c;也做了一些项目在构建生产环…

RepViT:从ViT的角度重新审视mobile CNN

文章目录 RepViT: Revisiting Mobile CNN From ViT Perspective摘要本文方法代码实验结果 RepViT: Revisiting Mobile CNN From ViT Perspective 摘要 近年来&#xff0c;与轻量级卷积神经网络(cnn)相比&#xff0c;轻量级视觉变压器(ViTs)在资源受限的移动设备上表现出了更高…

JVM系统优化实践(21):GC生产环境案例(四)

您好&#xff0c;这里是「码农镖局」CSDN博客&#xff0c;欢迎您来&#xff0c;欢迎您再来&#xff5e; 前面说了一般应用的OOM情况&#xff0c;但是OOM不知发生在应用层&#xff0c;有时候专门负责运行Java的Tomcat也会偶尔罢工一下&#xff0c;抛出OOM异常。因为Tomcat本身也…

git在工作区和本地库的操作命令

本文介绍一些开发时&#xff0c;常用的在工作区和本地库之间的操作命令 一、提交修改内容到 本地库 工作树的修改内容要提交到本地库&#xff0c;首先需要先添加到缓存区stage&#xff0c;在commit到本地库。 # filename就是你修改后需要提交的文件 git add <filename>…

unity进阶--json的使用学习笔记

文章目录 unity自带的json使用方法第三方--LitJson的使用第一种使用方式第二种--使用jsonData unity自带的json使用方法 创建数据类 转化成json 解析json 第三方–LitJson的使用 第一种使用方式 数据类 创建和解析 第二种–使用jsonData 创建 解析

奇舞周刊第500期:TQL,巧用 CSS 实现动态线条 Loading 动画

记得点击文章末尾的“ 阅读原文 ”查看哟~ 下面先一起看下本期周刊 摘要 吧~ 奇舞推荐 ■ ■ ■ TQL&#xff0c;巧用 CSS 实现动态线条 Loading 动画 最近&#xff0c;群里有个很有意思的问题&#xff0c;使用 CSS 如何实现如下 Loading 效果&#xff1a; leaferjs&#xff0c…

docker安装redis启动异常问题

问题描述 启动redis容器报错如下 se > /sys/kernel/mm/transparent_hugepage/enabled as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled (set to madvise or never). 1:M 21 Ju…

Python - Opencv应用实例之树叶自动分割、标签及统计分析系统

Python - Opencv应用实例之树叶自动分割、标签及统计分析系统 本文通过Python+opencv 实现这样的需求:输出位置和角度(x, y, r),并标记出轮廓基于传统图像处理算法实现,算法原理:输入图像 -> 灰度化 -> 二值化 -> 形态学处理 -> 轮廓提取 -> 树叶中心定位 -…

docker系列5:docker安装nginx

传送门 前面介绍了docker的安装&#xff1a;docker系列1&#xff1a;docker安装 还有docker镜像加速器&#xff1a;docker系列2&#xff1a;阿里云镜像加速器 以及docker的基本操作&#xff1a; docker系列3&#xff1a;docker镜像基本命令 以及容器的基本命令&#xff1a;…

DRL(自用)

RL学习算法 基于策略的算法&#xff1a;这是最通用的优化类型。策略将状态映射到操作。学习策略的 RL 代理可以创建从当前状态到目标状态的动作轨迹 REINFORCE 是一种基于策略的算法。基于策略的算法&#xff0c;优势在于它们可以应用于各种强化学习问题&#xff1b;但是其样…

1 js嵌入html使用

1.1 直接在html内部使用js代码 使用script标签&#xff0c;在前后标签内部写的代码即为js代码。 <body><p id"p1">初始段落</p> <!--id是为了定位需要更改内容的标签--><button type"button" onclick"showNum()">…

App测试中ios和Android的区别

1、Android长按home键呼出应用列表和切换应用&#xff0c;然后右滑则终止应用&#xff1b; 2、多分辨率测试&#xff0c;Android端20多种&#xff0c;ios较少&#xff1b; 3、手机操作系统&#xff0c;Android较多&#xff0c;ios较少且不能降级&#xff0c;只能单向升级&…