数据结构---单链表

news2024/9/23 3:30:44

专栏:数据结构
个人主页:HaiFan.
专栏简介:从零开始,数据结构!!

单链表

  • 前言
  • 顺序表的缺陷
  • 链表的概念以及结构
  • 链表接口实现
  • 打印链表中的元素SLTPrint
    • phead->next!=NULL和phead!=NULL的区别
  • 开辟空间SLTNewNode
  • 尾插SLTPushBack和尾删SLTPopBack
  • 头插SLTPushFront和头删SLTPopFront
  • 查找SLTFind
  • 在查找元素的后面插入SLTInsertAfter
  • 在查找元素的后面删除SLTEraseAfter
  • 销毁开辟的空间SLTDestory
  • 各个接口测试
  • 源代码
  • 链表和顺序表的区别

前言

在这里插入图片描述

顺序表的缺陷

顺序表每一次扩容,都是连续的空间,支持通过下标去访问数据。

但是顺序表的头插,头删,中间删元素,需要把后面的数据覆盖前面的元素,也就是挪动元素,这就导致效率非常的低

并且,顺序表在扩容之后,可能会有一部分空间没有用,这就导致了空间浪费。

链表的概念以及结构

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

链表的结点一般是在堆上申请出来的,从堆上申请的空间,是按照一定策略来分配的,两次申请的空间可能连续,也可能不连续。

链表是每用一块空间,便开辟一块空间,因此,不会造成空间浪费。

在这里插入图片描述

这个图什么意思呢?

把开辟的空间当作2个合在一起的小方块,一个小方块用来存元素的值,一个方块用来存放下一个空间的地址。

在这里插入图片描述

当这个1就是最后一个元素的时候,这个下一块空间的地址就是NULL,因为1后面没有元素了,不能让这个地址乱指向其他的空间。

链表接口实现

在这里插入图片描述

这里,在结构体中要用结构体指针,指针就是地址,这个地址就是用来存放下一个空间的地址的。

typedef int SLTDataType;

typedef struct SLTNode
{
	SLTDataType val;
	struct SLTNode* next;
}SLT;

void SLTPrint(SLT* phead);//打印单链表中的数据

SLT* SLTNewNode(SLTDataType x);//每要插入数据,就需要开一一块空间

void SLTPushBack(SLT** pphead, SLTDataType x);//尾插
void SLTPopBack(SLT** pphead);//尾删

void SLTPushFront(SLT** pphead, SLTDataType x);//头插
void SLTPopFront(SLT** pphead);//头删

SLT* SLTFind(SLT** pphead, SLTDataType x);//查找
//单链表在pos位置之后插入x
void SLTInsertAfter(SLT* pos, SLTDataType x);//插入
//单链表在pos位置之后删除元素
void SLTEraseAfter(SLT* pos);//删除

void SLTDestory(SLT** pphead);//销毁空间

打印链表中的元素SLTPrint

打印链表中的元素是最容易实现的一个接口。

void SLTPrint(SLT* phead)
{
	while (phead != NULL)
	{
		cout << phead->val << "->";
		phead = phead->next;
	}
	cout << "NULL" << endl;
}

这里用一级指针,因为打印链表不会更改链表中的一些东西。

每打印出一个元素,都让phead指向下一块空间,直到走到为NULL的时候为止

在这里插入图片描述

phead->next!=NULL和phead!=NULL的区别

在这里插入图片描述

假如现在phead是在3这块空间,那么phead->next就是指向下一块空间,因为下一块空间是NULL,所以,当你写出phead->next != NULL的时候,就会在3这里停止后面的打印。


phead !=NULL是当phead走到NULL的时候才会执行。

开辟空间SLTNewNode

链表是每用一块空间就开辟一块空间。这样不会造成空间浪费。

SLT* SLTNewNode(SLTDataType x)
{
	SLT* newnode = (SLT*)malloc(sizeof(SLT));

	newnode->next = NULL;
	newnode->val = x;
	return newnode;
}

开辟了空间之后,不要忘记把newnode->next置为NULL,不然就成为野指针了。最后把这块空间返回即可。

尾插SLTPushBack和尾删SLTPopBack

尾插的时候,先调用一下开辟空间的函数,再把开辟的空间返回,用一个结构体指针变量接收。

  1. 当链表为空的时候,newnode就可以当作链表中的第一块空间。
  2. 当链表不为空的时候,就需要先找到链表的尾部,然后把newnode链接在链表中即可
void SLTPushBack(SLT** pphead, SLTDataType x)
{
	SLT* newnode = SLTNewNode(x);

	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		SLT* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}

在这里插入图片描述

要用一个临时变量走向链表最后一个元素的位置,如果不用临时变量,直接用头元素走到最后一个位置,那么,在打印元素的时候,也是从最后一个元素的位置开始打印的,前面的元素不会打印,因为我们是用指针接收的头元素的位置,通过指针直接去访问内容,指针走到最后就会导致,原本应该指向头元素的指针,就指向了最后的元素。

所以在这里用 tail->next != NULL


尾删分两种情况,当链表中只有一个元素的时候和链表中有n个元素的时候。

第一种情况好说,直接把第一个元素的空间给free掉,在置为空即可。

第二种情况就是先找到尾,但是,在找到尾部之后,不能直接把尾部free掉,而是要先知道倒数第二个元素的位置。

void SLTPopBack(SLT** pphead)
{
	assert(*pphead);

	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLT* tail = *pphead;
		while (tail->next->next != NULL)
		{ 
			tail = tail->next;
		}
		free(tail->next);
		tail->next = NULL;
	}
}


/*SLTNode* prev = NULL;
		SLTNode* tail = phead;
		while (tail->next)
		{
		prev = tail;
		tail = tail->next;
		}

		free(tail);
		prev->next = NULL;*/

这两种方法都可以。

头插SLTPushFront和头删SLTPopFront

头插特别容易,先开辟一块空间,然后把这块空间指向头元素空间。在把头元素空间指向新开辟的空间即可。

在这里插入图片描述

void SLTPushFront(SLT** pphead, SLTDataType x)
{
	SLT* newnode = SLTNewNode(x);

	newnode->next = *pphead;
	*pphead = newnode;
}


头删之前,要先判断链表中有没有元素,没有元素直接断言。

有一个元素,直接free。

多个元素,先用一个临时变量记录一下头元素指向的下一个元素的地址,然后把头元素给free掉,再让头元素=临时变量。

void SLTPopFront(SLT** pphead)
{
	assert(*pphead);

	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLT* head = (*pphead)->next;
		free(*pphead);
		*pphead = NULL;
		*pphead = head;
	}
}

查找SLTFind

查找元素的时候,把该空间的地址给返回即可,遍历一遍链表即可实现。

SLT* SLTFind(SLT** pphead, SLTDataType x)
{

	SLT* tail = *pphead;

	while (tail != NULL)
	{
		if (tail->val == x)
		{
			return tail;
		}
		tail = tail->next;
	}

	return NULL;//没有找到
}

在查找元素的后面插入SLTInsertAfter

先通过SLTFind,找到要插入的位置,然后通过断言来判断这个位置是否合法。

在这里插入图片描述

比如要把3插入到1和2之间,把3->next = 1->next,再把1->next = 3就能完成插入。

void SLTInsertAfter(SLT* pos, SLTDataType x)
{
	assert(pos);

	SLT* newnode = SLTNewNode(x);

	newnode->next = pos->next;
	pos->next = newnode;

}

在查找元素的后面删除SLTEraseAfter

还是先断言,判断位置是否合法。

在这里插入图片描述
电脑上的画图实在是用不来,原谅我------。。。

void SLTEraseAfter(SLT* pos)
{
	assert(pos);

	if (pos->next == NULL)
	{
		return;
	}

	SLT* newnode = pos->next;
	pos->next = newnode->next;
	free(newnode);
	newnode = NULL;
}

销毁开辟的空间SLTDestory

因为空间不一定是连续的,所以需要遍历链表,一个一个的释放

void SLTDestory(SLT** pphead)
{
	SLT* cur = *pphead;

	while (cur)
	{
		SLT* hh = cur->next;
		free(cur);
		cur = hh;
	}
}

各个接口测试

void TestSLTNode()
{
	SLT* plist = NULL;
	cout << "尾插尾删" << endl;
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);
	SLTPushBack(&plist, 4);


	SLTPopBack(&plist);
	SLTPopBack(&plist);
	SLTPopBack(&plist);
	SLTPopBack(&plist);


	SLTPrint(plist);
}

void TestSLTNode1()
{
	SLT* plist = NULL;
	cout << "头插头删" << endl;
	SLTPushFront(&plist, 1);
	SLTPushFront(&plist, 2);
	SLTPushFront(&plist, 3);
	SLTPushFront(&plist, 4);

	SLTPopFront(&plist);
	SLTPopFront(&plist);
	SLTPopFront(&plist);


	SLTPrint(plist);
}

void TestSLTNode2()
{
	SLT* plist = NULL;
	cout << "查找,删除,插入" << endl;
	SLTPushFront(&plist, -1);
	SLTPushBack(&plist, 1);
	SLTPushFront(&plist, -2);
	SLTPushBack(&plist, 2);
	SLTPushFront(&plist, -3);
	SLTPushBack(&plist, 3);
	SLTPushFront(&plist, -4);
	SLTPushBack(&plist, 4);
	SLT* ret = SLTFind(&plist, 3);
	if (ret)
	{
		cout << ret << endl;
	}
	SLTPrint(plist);

	SLTInsertAfter(ret, 100);
	SLTInsertAfter(ret, 100);
	SLTInsertAfter(ret, 100);
	SLTPrint(plist);
	SLTEraseAfter(ret);
	SLTEraseAfter(ret);
	SLTPrint(plist);

	SLTDestory(&plist);
}

int main()
{
	TestSLTNode();
	TestSLTNode1();
	TestSLTNode2();

	return 0;
}

在这里插入图片描述

源代码

.h文件

#pragma once


#include <iostream>
#include <stdlib.h>
#include <assert.h>

using namespace std;

typedef int SLTDataType;

typedef struct SLTNode
{
	SLTDataType val;
	struct SLTNode* next;
}SLT;

void SLTPrint(SLT* phead);//打印单链表中的数据

SLT* SLTNewNode(SLTDataType x);//每要插入数据,就需要开一一块空间

void SLTPushBack(SLT** pphead, SLTDataType x);//尾插
void SLTPopBack(SLT** pphead);//尾删

void SLTPushFront(SLT** pphead, SLTDataType x);//头插
void SLTPopFront(SLT** pphead);//头删

SLT* SLTFind(SLT** pphead, SLTDataType x);//查找
//单链表在pos位置之后插入x
void SLTInsertAfter(SLT* pos, SLTDataType x);//插入
//单链表在pos位置之后删除元素
void SLTEraseAfter(SLT* pos);//删除

void SLTDestory(SLT** pphead);//销毁空间

.cpp文件

#define _CRT_SECURE_NO_WARNINGS 1

#include "SList.h"

void SLTPrint(SLT* phead)
{
	while (phead)
	{
		cout << phead->val << "->";
		phead = phead->next;
	}
	cout << "NULL" << endl;
}

SLT* SLTNewNode(SLTDataType x)
{
	SLT* newnode = (SLT*)malloc(sizeof(SLT));

	newnode->next = NULL;
	newnode->val = x;
	return newnode;
}

void SLTPushBack(SLT** pphead, SLTDataType x)
{
	SLT* newnode = SLTNewNode(x);

	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		SLT* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}

void SLTPopBack(SLT** pphead)
{
	assert(*pphead);

	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLT* tail = *pphead;
		while (tail->next->next != NULL)
		{ 
			tail = tail->next;
		}
		free(tail->next);
		tail->next = NULL;
	}
}

void SLTPushFront(SLT** pphead, SLTDataType x)
{
	SLT* newnode = SLTNewNode(x);

	newnode->next = *pphead;
	*pphead = newnode;
}

void SLTPopFront(SLT** pphead)
{
	assert(*pphead);

	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLT* head = (*pphead)->next;
		free(*pphead);
		*pphead = NULL;
		*pphead = head;
	}
}

SLT* SLTFind(SLT** pphead, SLTDataType x)
{

	SLT* tail = *pphead;

	while (tail != NULL)
	{
		if (tail->val == x)
		{
			return tail;
		}
		tail = tail->next;
	}

	return NULL;//没有找到
}

void SLTInsertAfter(SLT* pos, SLTDataType x)
{
	assert(pos);

	SLT* newnode = SLTNewNode(x);

	newnode->next = pos->next;
	pos->next = newnode;

}

void SLTEraseAfter(SLT* pos)
{
	assert(pos);

	if (pos->next == NULL)
	{
		return;
	}

	SLT* newnode = pos->next;
	pos->next = newnode->next;
	free(newnode);
	newnode = NULL;
}

void SLTDestory(SLT** pphead)
{
	SLT* cur = *pphead;

	while (cur)
	{
		SLT* hh = cur->next;
		free(cur);
		cur = hh;
	}
}

test.cpp文件

#define _CRT_SECURE_NO_WARNINGS 1


#include "SList.h"

void TestSLTNode()
{
	SLT* plist = NULL;
	cout << "尾插尾删" << endl;
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);
	SLTPushBack(&plist, 4);


	SLTPopBack(&plist);
	SLTPopBack(&plist);
	SLTPopBack(&plist);
	SLTPopBack(&plist);


	SLTPrint(plist);
}

void TestSLTNode1()
{
	SLT* plist = NULL;
	cout << "头插头删" << endl;
	SLTPushFront(&plist, 1);
	SLTPushFront(&plist, 2);
	SLTPushFront(&plist, 3);
	SLTPushFront(&plist, 4);

	SLTPopFront(&plist);
	SLTPopFront(&plist);
	SLTPopFront(&plist);


	SLTPrint(plist);
}

void TestSLTNode2()
{
	SLT* plist = NULL;
	cout << "查找,删除,插入" << endl;
	SLTPushFront(&plist, -1);
	SLTPushBack(&plist, 1);
	SLTPushFront(&plist, -2);
	SLTPushBack(&plist, 2);
	SLTPushFront(&plist, -3);
	SLTPushBack(&plist, 3);
	SLTPushFront(&plist, -4);
	SLTPushBack(&plist, 4);
	SLT* ret = SLTFind(&plist, 3);
	if (ret)
	{
		cout << ret << endl;
	}
	SLTPrint(plist);

	SLTInsertAfter(ret, 100);
	SLTInsertAfter(ret, 100);
	SLTInsertAfter(ret, 100);
	SLTPrint(plist);
	SLTEraseAfter(ret);
	SLTEraseAfter(ret);
	SLTPrint(plist);

	SLTDestory(&plist);
}

int main()
{
	TestSLTNode();
	TestSLTNode1();
	TestSLTNode2();

	return 0;
}

链表和顺序表的区别

不同点顺序表链表
存储空间上物理上一定连续逻辑上连续,但物理上不一定 连续
随机访问支持O(1)不支持:O(N)
任意位置插入或者删除 元素可能需要搬移元素,效率低 O(N)只需修改指针指向
插入动态顺序表,空间不够时需要 扩容没有容量的概念
应用场景元素高效存储+频繁访问任意位置插入和删除频繁
缓存利用率

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

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

相关文章

多分类、正则化问题

多分类问题 利用逻辑回归解决多分类问题&#xff0c;假如有一个训练集&#xff0c;有 3 个类别&#xff0c;分别为三角形 &#x1d466; 1&#xff0c;方框&#x1d466; 2&#xff0c;圆圈 &#x1d466; 3。我们下面要做的就是使用一个训练集&#xff0c;将其分成 3 个二…

计算机图形学:liang算法和Cyrus-Beck算法

其中Cyrus-Beck算法呢&#xff0c;是计算一根直线一个多边形的交线段&#xff1b;liang算法是Cyrus的一个特例&#xff0c;即多边形刚好是矩形&#xff1b;先看看Cyrus算法的思路【从别的博客找的图片】&#xff1a;这很容易理解&#xff0c;点积>0时就可能中内部嘛&#xf…

使用pynimate制作动态排序图

大家好&#xff0c;数据可视化动画使用Python包就可以完成&#xff0c;效果如下&#xff1a;想要使用Pynimate&#xff0c;直接import一下就行&#xff1a;import pynimate as nim输入数据后&#xff0c;Pynimate将使用函数Barplot&#xff08;&#xff09;来创建条形数据动画。…

Apollo(阿波罗)分布式配置安装详解

Apollo&#xff08;阿波罗&#xff09; Apollo&#xff08;阿波罗&#xff09;是携程框架部门研发的分布式配置中心&#xff0c;能够集中化管理应用不同环境、不同集群的配置&#xff0c;配置修改后能够实时推送到应用端&#xff0c;并且具备规范的权限、流程治理等特性&#…

【网络】套接字 -- UDP

&#x1f941;作者&#xff1a; 华丞臧. &#x1f4d5;​​​​专栏&#xff1a;【网络】 各位读者老爷如果觉得博主写的不错&#xff0c;请诸位多多支持(点赞收藏关注)。如果有错误的地方&#xff0c;欢迎在评论区指出。 推荐一款刷题网站 &#x1f449; LeetCode刷题网站 文章…

远程使用服务器上的Jupyter notebook

记录下如何远程使用服务器上的jupyter notebook。 主要是在服务器端执行以下操作&#xff1a; 激活需要使用的环境使用pip list 或conda list检查是否已经安装notebook。如果没有安装&#xff0c;则使用pip install jupyter notebook进行安装&#xff1b;反之忽略这一步&…

HDMI协议介绍(四)--Video

目录 视频格式 RGB444 YUV444 YUV422 YUV420 Color Depth Video控制信号 Pixel Repetition HDMI支持多种视频格式和分辨率。以hdmi1.4和2.0协议来说&#xff0c;视频格式支持RGB444、YUV444、YUV422和YUV420&#xff0c;其中RGB444和YUV444一般都是要求支持的。 视频格式…

【计算机网络】网络层IP协议

文章目录一、认识IP协议二、IP协议头部格式三、IP地址划分1. IP地址分类2. 子网划分四、IP地址数量危机1. IP地址的数量限制2. NAT技术五、私网IP和公网IP六、路由1. 认识路由2. 路由表生成算法一、认识IP协议 IP协议是Internet Protocol&#xff08;互联网协议&#xff09;的…

Centos7 安装jenkins java1.8版本

1. 首先安装好jdk1.8 2. 安装jenkins 命令&#xff1a;(可以在根目录&#xff0c;创建文件夹 mkdir home 然后在此文件夹下操作 cd /home) a 清华源&#xff0c;获取jenkins安装包 wget https://mirrors.tuna.tsinghua.edu.cn/jenkins/redhat/jenkins-2.346-1.1.noarch.rp…

2023软件测试金三银四常见的软件测试面试题-【测试理论篇】

三、测试理论 3.1 你们原来项目的测试流程是怎么样的? 我们的测试流程主要有三个阶段&#xff1a;需求了解分析、测试准备、测试执行。 1、需求了解分析阶段 我们的SE会把需求文档给我们自己先去了解一到两天这样&#xff0c;之后我们会有一个需求澄清会议&#xff0c; 我…

GO进阶(4) 深入Go的内存管理

Go语言成为高生产力语言的原因之一自己管理内存&#xff1a;Go抛弃了C/C中的开发者管理内存的方式&#xff0c;实现了主动申请与主动释放管理&#xff0c;增加了逃逸分析和GC&#xff0c;将开发者从内存管理中释放出来&#xff0c;让开发者有更多的精力去关注软件设计&#xff…

1633_xv6 book PC硬件与BootLoader

全部学习汇总&#xff1a; GreyZhang/g_unix: some basic learning about unix operating system. (github.com) 按照课程的建议&#xff0c;先去读了一下xv6 book的附录&#xff0c;感觉还是有一些收获的。这中间去扫盲增补各种概念的过程就已经收获不少。 1. 这里介绍了一下计…

C#:Krypton控件使用方法详解(第九讲) ——kryptonRadioButton

今天介绍的Krypton控件中的kryptonRadioButton&#xff0c;这是一个单选按钮控件。下面开始介绍这个控件的属性&#xff1a;首先介绍的是外观属性&#xff0c;如下图所示&#xff1a;Cheacked属性&#xff1a;表示设置kryptonRadioButton控件的初始选中状态是什么样的&#xff…

Buuctf [ACTF新生赛2020]Universe_final_answer 题解

1.程序逻辑 程序逻辑并不复杂: 首先输入字符串,然后对字符串进行一个判断是否满足条件的操作 如果满足则对字符串进行处理并输出,输出的就是flag 2.judge_860函数 显然根据这十个条件可以通过矩阵解线性方程组,这里对变量的命名做了一些调整,让Vi对应flag[i]方便读 ​​​​…

2018年蓝桥杯省赛试题-5道(Python)

文章目录一、日志统计思考二、递增三元组思考三、螺旋折线思考四、乘积最大思考五、全球变暖思考尾声提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 一、日志统计 题目描述 小明维护着一个程序员论坛。 现在他收集了一份"点赞"日志&#xf…

mysql数据库表的多条件查询

mysql数据库表的多条件查询 一、select语句基本查询 SELECT 字段1,字段2....FROM 表名[WHERE 条件] [LIMIT N][ OFFSET M]select可以返回多条数据也可以返回一条数据如果要查询所有的字段可以用 *****代替where后面跟的是筛选条件&#xff08;可选&#xff09;N 是返回的数据…

1632_x86中几种地址概念的理解

全部学习汇总&#xff1a; GreyZhang/g_unix: some basic learning about unix operating system. (github.com) 在看xv6的资料的时候发现有几个概念没弄清楚&#xff0c;结果让我理解资料的时候感觉比较模糊。这几个概念名词也倒是简单&#xff1a;逻辑地址、线性地址、物理地…

vmware创建虚拟机centor7

右键 选择下好的centos7 设置密码 登录

Spring是怎么解决循环依赖的

1.什么是循环依赖&#xff1a; 这里给大家举个简单的例子&#xff0c;相信看了上一篇文章大家都知道了解了spring的生命周期创建流程。那么在Spring在生命周期的哪一步会出现循环依赖呢&#xff1f; 第一阶段&#xff1a;实例化阶段 Instantiation 第二阶段&#xff1a;属性赋…

代码随想录 NO52 | 动态规划_leetcode 647. 回文子串 516.最长回文子序列

动态规划_leetcode 647. 回文子串 516.最长回文子序列今天是动态规划最后一天的题了&#xff0c;整个过程已经接近尾声了&#xff01; 647. 回文子串 确定dp数组&#xff08;dp table&#xff09;以及下标的含义 本题如果我们定义&#xff0c;dp[i] 为 下标i结尾的字符串有 dp…