数据结构与算法【02】—线性表

CSDN系列专栏:数据结构与算法专栏
针对以前写的数据结构与算法系列重写(针对文字描述、图片、错误修复),改动会比较大,一直到更新完为止

前言

通过前面数据结构与算法基础知识我们知道了数据结构的一些概念和重要性,那么本章总结下线性表相关的内容。当然,我用自己的理解分享给大家。

其实说实话,可能很多人依然分不清线性表顺序表,和链表之间的区别和联系!

  • 线性表:逻辑结构, 就是对外暴露数据之间的关系,不关心底层如何实现,数据结构的逻辑结构大分类就是线性结构和非线性结构而顺序表、链表都是一种线性表。
  • 顺序表、链表:物理结构,他是实现一个结构实际物理内存上的结构。比如顺序表就是用数组实现。而链表主要用指针完成工作。不同的结构在不同的场景有不同的区别。

在Java中,大家都知道List接口,这就是逻辑结构,它封装了一个线性关系的一系列方法,用于表示和维护线性关系。而具体的实现其实就是跟物理结构相关的内容。比如顺序表的内容存储使用数组的,然后一个get,set,add方法都要基于数组来完成,而链表是基于指针的,基于不同的物理结构要根据结构的特性维护数据的存储和线性关系。

下面用一个图来浅析物理结构中顺利表和链表之间的区别。

image-20210104160901005

线性表基本架构

对于一个线性表来说,不管它的具体实现如何,它们的方法函数名和实现效果应该一致(即使用方法相同、达成逻辑上的效果相同,差别的是实现方式可能针对不同的场景效率不同)。线性表的概念与Java的接口/抽象类有一些相似之处。最著名的例子就是List接口的ArrayList和LinkedList,List是一种逻辑上的结构,表示这种结构为线性表,而ArrayList和LinkedList更多的是一种物理结构(数组和链表)。

所以基于面向对象的编程思维,我们可以将线性表写成一个接口,而具体实现的顺序表和链表的类可以实现这个线性表的方法,以提高程序的可读性。还有一点非常重要,初学数据结构与算法时实现的线性表都是固定类型(例如int),随着知识的进步,我们应当采用泛型来实现更合理的方式。至于接口的具体设计如下:

public interface ListInterface<T> {void init(int initialSize); // 初始化表int length();boolean isEmpty(); // 是否为空int elemIndex(T t); // 找到编号T getElem(int index); // 根据index获取数据void add(int index, T t) ; // 根据index插入数据void delete(int index) ;void add(T t) ; // 尾部插入void set(int index, T t) ;String toString(); // 转成String输出
}

顺序表

顺序表是基于数组实现的,所有实现需要基于数组特性。对于顺序表的结构应该有一个存储数据的数组data和有效使用长度size.

这里为了简单就不实现扩容、异常处理相关的操作。

下面着重讲解一些初学者容易混淆的概念和方法实现。这里把顺序表比作一队坐在板凳上的人。

插入操作

add(int index,T value)

其中index为插入的编号位置,value为插入的数据,插入的流程为:

(1)从后(最后一个有数据位)向前到index依次后移一位,腾出index位置的空间

(2)将待插入数据赋值到index位置上,完成插入操作

image-20231029221307141

顺序表很长,在靠前的地方如果插入效率比较低(插入时间复杂度为O(n)),如果频繁的插入那么复杂度挺高的。

删除操作

同理,删除原理和插入类似,删除index位置的操作就是从index开始(index+1)数据赋值到index位置,一直到size-1位置,具体可以看这张图:

image-20231029221644046

代码实现

这里实现一个简单的顺序表:

public class SeqList<T> implements ListInterface<T> {private T[] array;private int size;public SeqList() {// 默认构造函数init(10);}@Overridepublic void init(int initialSize) {array = (T[]) new Object[initialSize];size = 0;}@Overridepublic int length() {return size;}@Overridepublic boolean isEmpty() {return size == 0;}@Overridepublic int elemIndex(T value) {for (int i = 0; i < size; i++) {if (array[i].equals(value)) {return i;}}return -1;}@Overridepublic T getElem(int index) {if (index < 0 || index >= size) {throw new IndexOutOfBoundsException("Index is out of bounds.");}return array[index];}@Overridepublic void add(int index, T value) {if (index < 0 || index > size) {throw new IndexOutOfBoundsException("Index is out of bounds.");}if (size == array.length) {// 如果数组已满,扩展数组resizeArray();}// 将index之后的元素后移一位for (int i = size; i > index; i--) {array[i] = array[i - 1];}array[index] = value;size++;}@Overridepublic void delete(int index) {if (index < 0 || index >= size) {throw new IndexOutOfBoundsException("Index is out of bounds.");}// 将index之后的元素前移一位for (int i = index; i < size - 1; i++) {array[i] = array[i + 1];}size--;}@Overridepublic void add(T value) {if (size == array.length) {// 如果数组已满,扩展数组resizeArray();}array[size] = value;size++;}@Overridepublic void set(int index, T value) {if (index < 0 || index >= size) {throw new IndexOutOfBoundsException("Index is out of bounds.");}array[index] = value;}@Overridepublic String toString() {StringBuilder sb = new StringBuilder("[");for (int i = 0; i < size; i++) {sb.append(array[i]);if (i < size - 1) {sb.append(", ");}}sb.append("]");return sb.toString();}private void resizeArray() {int newCapacity = (int) (array.length * 1.5);T[] newArray = (T[]) new Object[newCapacity];for (int i = 0; i < size; i++) {newArray[i] = array[i];}array = newArray;}
}

链表

在学习C/C++时,链表往往让许多人感到复杂,其中一个主要原因可能是因为涉及到指针。尽管在Java中不直接使用指针,但我们仍然需要理解指针的原理和应用。链表与顺序表(数组)不同,它的结构就像一条链一样,将节点链接成一个线性结构。在链表中,每个节点都存在于不同的内存地址中,指针指向(链表存储)了相邻节点的地址,节点能够通过这些指针找到下一个的节点形成一条链。

就物理存储结构而言,地址之间的联系是无法更改的,相邻地址就是相邻。但在链式存储中,下一个地址是由上一个节点"主动记录的",因此可以进行更改。这就好比亲兄弟从出生就是同姓兄弟,而在我们的成长过程中,最好的朋友可能会因为阶段性的变化而有所不同!

举个例子,就像西天取经的唐僧、悟空、八戒、沙和尚。他们本来没有直接的联系,但通过结拜为师徒兄弟,他们建立了联系。如果你问悟空他的师父是谁,他会立刻想到唐僧,因为他们之间有五指山下的约定。

image-20231029223556929

基本结构

对于线性表,我们只需要一个data数组和size就能表示基本信息。而对于链表,我们需要一个Node类节点(head头节点),和size分别表示存储的节点数据和链表长度,这个节点有数据域指针域。数据域就是存放真实的数据,而指针域就是存放下一个Node类节点的指针,其具体结构为:

 private static class Node<T> {T data;Node<T> next;Node(T data) {this.data = data;this.next = null;}}

带头结点链表VS不带头结点链表

有许多人可能会对带头结点和不带头结点链表的区别感到困惑,甚至不清楚什么是带头结点和不带头结点。我来为大家阐述一下:

带头结点:在带头结点的链表中,head指针始终指向一个节点,这个节点不存储有效值,仅仅起到一个标识作用(有点像班主任带着学生)。

不带头结点:在不带头结点的链表中,head指针始终指向第一个有效节点,这个节点存储有效数值。

那么带头结点和不带头结点的链表有什么区别呢?

查找方面:在查找操作上,它们没有太大区别,带头结点需要多进行一次查找。

插入方面:对于非第0个位置的插入操作,区别不大,但不带头结点的链表在插入第0号位置之后需要重新改变head头指针的指向。

image-20231029224835482

删除方面:对于非第0个位置的删除操作,区别不大,不带头结点的链表在删除第0号位置之后需要重新改变head头指针的指向。

  • 头部删除(带头结点):在带头结点的链表中,头部删除操作和普通删除操作一样。只需执行 head.next = head.next.next,这样head的next直接指向第二个元素,从而删除了第一个元素。
  • 头部删除(不带头结点):不带头结点的链表的第一个节点(head)存储有效数据。在不带头结点的链表中,删除也很简单,只需将head指向链表中的第二个节点即可,即:head = head.next

image-20231029225238384

总而言之:带头结点通过一个固定的头可以使链表中任意一个节点都同等的插入、删除。而不带头结点的链表在插入、删除第0号位置时候需要特殊处理,最后还要改变head指向。两者区别就是插入删除首位(尤其插入),个人建议以后在使用链表时候尽量用带头结点的链表避免不必要的麻烦。

带头指针VS带尾指针

基本上是个链表都是要有头指针的,那么头尾指针是个啥呢?

头指针: 其实头指针就是链表中head节点,表示链表的头,称为为头指针。

**尾指针: **尾指针就是多一个tail节点的链表,尾指针的好处就是进行尾插入的时候可以直接插在尾指针的后面。

image-20231029225930693

但是带尾指针的单链表如果删除尾的话效率不高,需要枚举整个链表找到tail前面的那个节点进行删除。

插入操作

add(int index,T value)
其中index为插入的编号位置,value为插入的数据,在带头结点的链表中插入那么操作流程为

  1. 找到对应index-1号节点成为pre。
  2. node.next=pre.next,将插入节点后面先与链表对应部分联系起来。此时node.next和pre.next一致。
  3. pre.next=node 将node节点插入到链表中。

image-20231029231042857

当然,很多时候链表需要插入在尾部,如果频繁的插入在尾部每次枚举到尾部的话效率可能比较低,可能会借助一个尾指针去实现尾部插入。

删除操作

按照index移除(主要掌握):delete(int index)

带头结点链表的通用方法(删除尾也一样),找到该index的前一个节点pre,pre.next=pre.next.next

image-20231029231902762

代码实现

在这里我也实现一个单链表给大家作为参考使用:

public class LinkedList<T> implements ListInterface<T> {private Node<T> head;private int size;public LinkedList() {head = new Node<>(null); // 头结点不存储数据size = 0;}@Overridepublic void init(int initialSize) {head.next = null;size = 0;}@Overridepublic int length() {return size;}@Overridepublic boolean isEmpty() {return size == 0;}@Overridepublic int elemIndex(T value) {Node<T> current = head.next;int index = 0;while (current != null) {if (current.data.equals(value)) {return index;}current = current.next;index++;}return -1;}@Overridepublic T getElem(int index) {if (index < 0 || index >= size) {throw new IndexOutOfBoundsException("Index is out of bounds.");}Node<T> current = head.next;for (int i = 0; i < index; i++) {current = current.next;}return current.data;}@Overridepublic void add(int index, T value) {if (index < 0 || index > size) {throw new IndexOutOfBoundsException("Index is out of bounds.");}Node<T> newNode = new Node<>(value);Node<T> pre = head;for (int i = 0; i < index; i++) {pre = pre.next;}newNode.next = pre.next;pre.next = newNode;size++;}@Overridepublic void delete(int index) {if (index < 0 || index >= size) {throw new IndexOutOfBoundsException("Index is out of bounds.");}Node<T> pre = head;for (int i = 0; i < index; i++) {pre = pre.next;}pre.next = pre.next.next;size--;}@Overridepublic void add(T value) {add(size, value); // 在末尾添加元素}@Overridepublic void set(int index, T value) {if (index < 0 || index >= size) {throw new IndexOutOfBoundsException("Index is out of bounds.");}Node<T> current = head.next;for (int i = 0; i < index; i++) {current = current.next;}current.data = value;}@Overridepublic String toString() {StringBuilder sb = new StringBuilder("[");Node<T> current = head.next;while (current != null) {sb.append(current.data);if (current.next != null) {sb.append(", ");}current = current.next;}sb.append("]");return sb.toString();}private static class Node<T> {T data;Node<T> next;Node(T data) {this.data = data;this.next = null;}}public static void main(String[] args) {LinkedList<Integer> list = new LinkedList<>();list.init(10); // 初始化表list.add(1);list.add(2);list.add(3);list.add(1, 4); // 在索引1处插入值4list.delete(2); // 删除索引2处的值System.out.println(list.toString()); // 打印表的内容}
}

总结

这里的只是简单实现,实现基本方法。链表也只是单链表。完善程度还可以优化。

单链表查询速度较慢,因为他需要从头遍历,如果在尾部插入,可以考虑设计带尾指针的链表。而顺序表查询速度虽然快但是插入很费时,实际应用根据需求选择

Java中的Arraylist和LinkedList就是两种方式的代表,不过LinkedList使用双向链表优化,并且JDK也做了大量优化。所以大家不用造轮子,可以直接用,但是手写顺序表、单链表还是很有学习价值的。

CSDN专栏:数据结构与算法专栏
开源仓库:bigsai-algorithm仓库 ,欢迎支持
如果觉得不错 还请三连支持一下!

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

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

相关文章

【UE 材质】简单的闪闪发光材质

效果 节点 参考视频&#xff1a; https://www.bilibili.com/video/BV1uK411y737/?vd_source36a3e35639c44bb339f59760641390a8

MySQL(8):聚合函数

聚合函数介绍 聚合函数&#xff1a; 对一组数据进行汇总的函数&#xff0c;输入的是一组数据的集合&#xff0c;输出的是单个值。 聚合函数类型&#xff1a;AVG(),SUM(),MAX(),MIN(),COUNT() AVG / SUM 只适用于数值类型的字段&#xff08;或变量&#xff09; SELECT AVG(…

【LeetCode】每日一题 2023_11_4 数组中两个数的最大异或值

文章目录 刷题前唠嗑题目&#xff1a;数组中两个数的最大异或值题目描述代码与解题思路 结语 刷题前唠嗑 LeetCode? 启动&#xff01;&#xff01;&#xff01; 题目&#xff1a;数组中两个数的最大异或值 题目链接&#xff1a;421. 数组中两个数的最大异或值 题目描述 代…

前端埋点方式

前言&#xff1a; 想要了解用户在系统中所做的操作&#xff0c;从而得出用户在本系统中最常用的模块、在系统中停留的时间。对于了解用户的行为、分析用户的需求有很大的帮助&#xff0c;想实现这种需求可以通过前端埋点的方式。 埋点方式&#xff1a; 1.什么是埋点&#xff1f…

基于Jenkins实现接口自动化持续集成,学完涨薪5k

一、JOB项目配置 1、添加描述 可选选项可填可不填 2、限制项目的运行节点 节点中要有运行环境所需的配置 节点配置教程&#xff1a;https://blog.csdn.net/YZL40514131/article/details/131504280 3、源码管理 需要将脚本推送到远程仓库中 4、构建触发器 可以选择定时构建…

【python】路径管理+路径拼接问题

路径管理 问题相对路径问题绝对路径问题 解决os库pathlib库最终解决 问题 环境&#xff1a;python3.7.16 win10 相对路径问题 因为python的执行特殊性&#xff0c;使用相对路径时&#xff0c;在不同路径下用python指令会有不同的索引效果&#xff08;python的项目根目录根据执…

Temp directory ‘C:\WINDOWS\TEMP‘ does not exist

问题描述 解决方法 管理员权限问题&#xff0c;进入temp文件夹更改访问权限即可。 点击 temp文件夹 属性 -> 安全 -> 高级 -> 更改主体Users权限 给读取和写入权限 参考博客 开发springboot项目时无法启动Temp directory ‘C: \WINDOWS\TEMP‘ does not exist

Python库学习(十二):数据分析Pandas[下篇]

接着上篇《Python库学习(十一):数据分析Pandas[上篇]》,继续学习Pandas 1.数据过滤 在数据处理中&#xff0c;我们经常会对数据进行过滤&#xff0c;为此Pandas中提供mask()和where()两个函数&#xff1b; mask(): 在 满足条件的情况下替换数据&#xff0c;而不满足条件的部分…

leetcode-经典面/笔试题目

1.消失的数字 面试题 17.04. 消失的数字 - 力扣&#xff08;LeetCode&#xff09;https://leetcode.cn/problems/missing-number-lcci/ 这个题目当然有好几种解法&#xff0c;这里我推荐一种比较优秀的思路&#xff0c;也就是单身狗思路&#xff1a;异或。 异或的特点是相异…

账户权限控制

1.首先配置一个单群组4节点的链 1.1创建操作目录 cd ~ && mkdir -p fisco && cd fisco 1.2下载国内脚本 curl -#LO https://osp-1257653870.cos.ap-guangzhou.myqcloud.com/FISCO-BCOS/FISCO-BCOS/releases/v2.9.1/build_chain.sh && chmod ux bu…

【KVM】软件虚拟化和硬件虚拟化类型

前言 大家好&#xff0c;我是秋意零。 今天介绍的内容是虚拟化技术以及软件虚拟化和硬件虚拟化。 &#x1f47f; 简介 &#x1f3e0; 个人主页&#xff1a; 秋意零&#x1f525; 账号&#xff1a;全平台同名&#xff0c; 秋意零 账号创作者、 云社区 创建者&#x1f9d1; 个…

系统提示缺少或找不到d3dcompiler_43.dll文件的详细修复教程

今天我来给大家分享一下关于d3dcompiler_43.dll缺失的4个修复方法。 首先&#xff0c;我们来了解一下d3dcompiler_43.dll的作用。它是DirectX中的一个组件&#xff0c;用于编译Shader和Pixel着色器代码。如果缺少了这个文件&#xff0c;就会导致游戏或应用程序无法正常运行。 …

全能数据分析软件 Tableau Desktop 2019 mac中文版功能亮点

Tableau Desktop 2019 mac是一款专业的全能数据分析工具&#xff0c;可以让用户将海量数据导入并记性汇总&#xff0c;并且支持多种数据类型&#xff0c;比如像是编程常用的键值对、哈希MAP、JSON类型数据等&#xff0c;因此用户可以将很多常用数据库文件直接导入Tableau Deskt…

适合新手自学的网络安全基础技能“蓝宝书”:《CTF那些事儿》

文章目录 内容简介读者对象专家推荐目录赠书活动 CTF比赛是快速提升网络安全实战技能的重要途径&#xff0c;已成为各个行业选拔网络安全人才的通用方法。但是&#xff0c;本书作者在从事CTF培训的过程中&#xff0c;发现存在几个突出的问题&#xff1a; 线下CTF比赛培训中存在…

力扣:149. 直线上最多的点数(Python3)

题目&#xff1a; 给你一个数组 points &#xff0c;其中 points[i] [xi, yi] 表示 X-Y 平面上的一个点。求最多有多少个点在同一条直线上。 来源&#xff1a;力扣&#xff08;LeetCode&#xff09; 链接&#xff1a;力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱…

stm32整理(三)ADC

1 ADC简介 1.1 ADC 简介 12 位 ADC 是逐次趋近型模数转换器。它具有多达 19 个复用通道&#xff0c;可测量来自 16 个外部 源、两个内部源和 VBAT 通道的信号。这些通道的 A/D 转换可在单次、连续、扫描或不连续 采样模式下进行。ADC 的结果存储在一个左对齐或右对齐的 16 位…

农业中的机器学习

机器学习训练模型推荐&#xff1a; UnrealSynth虚幻合成数据生成器 - NSDT 机器学习是一个不断发展的领域&#xff0c;在农业中有许多潜在的应用。农民和农业科学家正在探索如何转向机器学习开发来提高作物产量、减少用水量和预测病虫害。未来&#xff0c;机器学习可以帮助农民…

Proteus仿真--12864LCD显示计算器键盘按键实验(仿真文件+程序)

本文主要介绍基于51单片机的12864LCD液晶显示电话拨号键盘按键实验&#xff08;完整仿真源文件及代码见文末链接&#xff09; 仿真图如下 本设计主要介绍计算器键盘仿真&#xff0c;按键按下后在12864液晶上显示对应按键键值 仿真运行视频 Proteus仿真--12864LCD显示计算器…

设计模式_访问者模式

访问者模式 介绍 设计模式定义案例问题堆积在哪里访问模式访问模式是行为型设计模式 从对象中分类出算法 这些算法封装为对象&#xff0c; 这样这些算法类很容易扩展&#xff0c;添加新的算法类就可以了不同的VIP用户 在不同的节日 领取不同的礼物if else太多 解决办法小技巧…

[自定义 Vue 组件] 小尾巴顶部导航栏(2.0) TailTopNav

文章归档&#xff1a;https://www.yuque.com/u27599042/coding_star/oglrqteg8fzvvzn0 [自定义 Vue 组件] 响应式顶部导航栏(1.0) TopNav&#xff1a;https://www.yuque.com/u27599042/coding_star/hzltsltxgavwx8u2 组件效果示例 组件所依赖的子组件 [自定义 Vue 组件] 小尾巴…