数据结构与算法之线性表(超详细顺序表、链表)
原创公众号:bigsai
文章已收录在 全网都在关注的数据结构与算法学习仓库 欢迎star
前言
通过前面数据结构与算法基础知识我么知道了数据结构的一些概念和重要性,那么我们今天总结下线性表相关的内容。当然,我用自己的理解解分享给大家。
其实说实话,可能很多人依然分不清线性表,顺序表,和链表之间的区别和联系!
- 线性表:逻辑结构, 就是对外暴露数据之间的关系,不关心底层如何实现,数据结构的逻辑结构大分类就是线性结构和非线性结构而顺序表、链表都是一种线性表。
- 顺序表、链表:物理结构,他是实现一个结构实际物理地址上的结构。比如顺序表就是用数组实现。而链表用指针完成主要工作。不同的结构在不同的场景有不同的区别。
在Java中,大家都知道List
接口类型,这就是逻辑结构,因为他就是封装了一个线性关系的一系列方法和数据。而具体的实现其实就是跟物理结构相关的内容。比如顺序表的内容存储使用数组的,然后一个get,set,add方法都要基于数组来完成,而链表是基于指针的。当我们考虑对象中的数据关系就要考虑指针的属性。指针的指向和value。
下面用一个图来浅析线性表的关系。可能有些不太确切,但是其中可以参考,并且后面也会根据这个图举例。
线性表基本架构
对于一个线性表来说。不管它的具体实现如何,但是它们的方法函数名和实现效果应该一致(即使用方法相同、达成逻辑上效果相同,差别的是运行效率)。线性表的概念与Java的接口/抽象类有那么几分相似。最著名的就是List的Arraylist和LinkedList,List是一种逻辑上的结构,表示这种结构为线性表,而ArrayList,LinkedList更多的是一种物理结构(数组和链表)。
所以基于面向对象的编程思维,我们可以将线性表写成一个接口,而具体实现的顺序表和链表的类可以实现这个线性表的方法,提高程序的可读性,还有一点比较重要的,记得初学数据结构与算法时候实现的线性表都是固定类型
(int),随着知识的进步,我们应当采用泛型
来实现更合理。至于接口的具体设计如下:
package LinerList; public interface ListInterface<T> { void Init(int initsize);//初始化表 int length(); boolean isEmpty();//是否为空 int ElemIndex(T t);//找到编号 T getElem(int index) throws Exception;//根据index获取数据 void add(int index,T t) throws Exception;//根据index插入数据 void delete(int index) throws Exception; void add(T t) throws Exception;//尾部插入 void set(int index,T t) throws Exception; String toString();//转成String输出 }
顺序表
顺序表是基于数组实现的,所以所有实现需要基于数组特性。对于顺序表的结构应该有一个存储数据的数组data和有效使用长度length.
还有需要注意的是初始化数组的大小,你可以固定大小,但是笔者为了可用性如果内存不够将扩大二倍。
下面着重讲解一些初学者容易混淆的概念和方法实现。这里把顺序表比作一队坐在板凳上的人。
插入操作
add(int index,T t)
其中index为插入的编号位置,t为插入的数据,插入的流程为:
(1)从后(最后一个有数据位)向前到index依次后移一位,腾出index位置的空间
(2)将待插入数据赋值到index位置上,完成插入操作
可以看得出如果顺序表很长,在靠前的地方如果插入效率比较低(插入时间复杂度为O(n)),如果频繁的插入那么复杂度挺高的。
删除操作
同理,删除也是非常占用资源的。原理和插入类似,删除index位置的操作就是从index+1开始向后依次将数据赋值到前面位置上,具体可以看这张图:
代码实现
这里我实现一个顺序表给大家作为参考学习:
package LinerList; public class seqlist<T> implements ListInterface<T> { private Object[] date;//数组存放数据 private int lenth; public seqlist() {//初始大小默认为10 Init(10); } public void Init(int initsize) {//初始化 this.date=new Object[initsize]; lenth=0; } public int length() { return this.lenth; } public boolean isEmpty() {//是否为空 if(this.lenth==0) return true; return false; } /* * * @param t * 返回相等结果,为-1为false */ public int ElemIndex(T t) { // TODO Auto-generated method stub for(int i=0;i<date.length;i++) { if(date[i].equals(t)) { return i; } } return -1; } /* *获得第几个元素 */ public T getElem(int index) throws Exception { // TODO Auto-generated method stub if(index<0||index>lenth-1) throw new Exception("数值越界"); return (T) date[index]; } public void add(T t) throws Exception {//尾部插入 add(lenth,t); } /* *根据编号插入 */ public void add(int index, T t) throws Exception { if(index<0||index>lenth) throw new Exception("数值越界"); if (lenth==date.length)//扩容 { Object newdate[]= new Object[lenth*2]; for(int i=0;i<lenth;i++) { newdate[i]=date[i]; } date=newdate; } for(int i=lenth-1;i>=index;i--)//后面元素后移动 { date[i+1]=date[i]; } date[index]=t;//插入元素 lenth++;//顺序表长度+1 } public void delete(int index) throws Exception { if(index<0||index>lenth-1) throw new Exception("数值越界"); for(int i=index;i<lenth;i++)//index之后元素前移动 { date[i]=date[i+1]; } lenth--;//长度-1 } @Override public void set(int index, T t) throws Exception { if(index<0||index>lenth-1) throw new Exception("数值越界"); date[index]=t; } public String toString() { String vaString=""; for(int i=0;i<lenth;i++) { vaString+=date[i].toString()+" "; } return vaString; } }
链表
学习c/c++的时候链表应该是很多人感觉很绕的东西,这个很大原因可能因为指针,Java虽然不直接使用指针但是我们也要理解指针的原理和运用。链表不同于顺序表(数组)它的结构像一条链一样链接成一个线性结构,而链表中每一个节点都存在不同的地址中,链表你可以理解为它存储了指向节点(区域)的地址,能够通过这个指针找到对应节点。
对于物理存储结构,地址之间的联系是无法更改的,相邻就是相邻。但对于链式存储,下一位的地址是上一个主动记录的,可以进行更改。这就好比亲兄弟从出生就是同姓兄弟,而我们在成长途中最好的朋友可能会由于阶段性发生一些变化!
就如西天取经的唐僧、悟空、八戒、沙和尚。他们本无联系,但结拜为师徒兄弟,你问悟空他的师父它会立马想到唐僧,因为五指山下的约定。
基本结构
对于线性表,我们只需要一个data数组和length就能表示基本信息。而对于链表,我们需要一个node(head头节点),和length分别表示存储的节点数据和链表长度,这个节点有数据域和指针域。数据域就是存放真实的数据,而指针域就是存放下一个node的指针,其具体结构为:
class node<T>{ T data;//节点的结果 node next;//下一个连接的节点 public node(){} public node(T data) { this.data=data; } public node(T data, node next) { this.data = data; this.next = next; } }
带头结点链表VS不带头结点链表
有很多人会不清楚带头结点和不带头结点链表的区别,甚至搞不懂什么是带头结点和不带头结点,我给大家阐述一下:
带头结点:head指针始终指向一个节点,这个节点不存储有效值仅仅起到一个标识作用(相当于班主任带学生)
不带头结点:head指针始终指向第一个有效节点,这个节点储存有效数值。
那么带头结点和不带头结点的链表有啥区别呢?
查找上:无大区别,带头结点需要多找一次。
插入上:非第0个位置插入区别不大,不带头结点的插入第0号位置之后需要重新改变head头的指向。
删除上:非第0个位置删除区别不大,不带头结点的删除第0号位置之后需要重新改变head头的指向。
头部删除(带头节点):带头节点的删除和普通删除一样。直接head.next=head.next.next
,这样head.next就直接指向第二个元素了。第一个就被删除了
头部删除(不带头节点):不带头节点的第一个节点(head)就存储有效数据。不带头节点删除也很简单,直接将head指向链表中第二个node节点就行了。即:head=head.next
总而言之:带头结点通过一个固定的头可以使链表中任意一个节点都同等的插入、删除。而不带头结点的链表在插入、删除第0号位置时候需要特殊处理,最后还要改变head指向。两者区别就是插入删除首位(尤其插入)当然我是建议你以后在使用链表时候尽量用带头结点的链表避免不必要的麻烦。
带头指针VS带尾指针
基本上是个链表都是要有头指针的,那么头尾指针是个啥呢?
头指针: 其实头指针就是链表中head节点,成为头指针。
尾指针: 尾指针就是多一个tail节点的链表,尾指针的好处就是进行尾插入的时候可以直接插在尾指针的后面,然后再改变一下尾指针的顺序即可。
但是带尾指针的单链表如果删除尾的话效率不高,需要枚举整个链表找到tail前面的那个节点进行删除。
插入操作
add(int index,T t)
其中index为插入的编号位置,t为插入的数据,在带头结点的链表中插入在任何位置都是等效的。
加入插入一个节点node
,根据index找到插入的前一个节点叫pre
。那么操作流程为
1. `node.next=pre.next`,将插入节点后面先与链表对应部分联系起来。此时node.next和pre.next一致。 2. `pre.next=node` 将node节点插入到链表中。
当然,很多时候链表需要插入在尾部,如果频繁的插入在尾部每次枚举到尾部的话效率可能比较低,可能会借助一个尾指针去实现尾部插入。
删除操作
按照index移除(主要掌握):delete(int index)
本方法为带头结点普通链表的通用方法(删除尾也一样),找到该index的前一个节点pre,pre.next=pre.next.next
代码实现
在这里我也实现一个单链表给大家作为参考使用:
package LinerList; class node<T>{ T data;//节点的结果 node next;//下一个连接的节点 public node(){} public node(T data) { this.data=data; } public node(T data, node next) { this.data = data; this.next = next; } } public class Linkedlist<T> implements ListInterface<T>{ node head; private int length; public Linkedlist() { head=new node(); length=0; } public void Init(int initsize) { head.next=null; } public int length() { return this.length; } public boolean isEmpty() { if(length==0)return true; else return false; } /* * 获取元素编号 */ public int ElemIndex(T t) { node team=head.next; int index=0; while(team.next!=null) { if(team.data.equals(t)) { return index; } index++; team=team.next; } return -1;//如果找不到 } @Override public T getElem(int index) throws Exception { node team=head.next; if(index<0||index>length-1) { throw new Exception("数值越界"); } for(int i=0;i<index;i++) { team=team.next; } return (T) team.data; } public void add(T t) throws Exception { add(length,t); } //带头节点的插入,第一个和最后一个一样操作 public void add(int index, T value) throws Exception { if(index<0||index>length) { throw new Exception("数值越界"); } node<T> team=head;//team 找到当前位置node for(int i=0;i<index;i++) { team=team.next; } node<T>node =new node(value);//新建一个node node.next=team.next;//指向index前位置的下一个指针 team.next=node;//自己变成index位置 length++; } @Override public void delete(int index) throws Exception { if(index<0||index>length-1) { throw new Exception("数值越界"); } node<T> team=head;//team 找到当前位置node for(int i=0;i<index;i++)//标记team 前一个节点 { team=team.next; } //team.next节点就是我们要删除的节点 team.next=team.next.next; length--; } @Override public void set(int index, T t) throws Exception { // TODO Auto-generated method stub if(index<0||index>length-1) { throw new Exception("数值越界"); } node<T> team=head;//team 找到当前位置node for(int i=0;i<index;i++) { team=team.next; } team.data=t;//将数值赋值,其他不变 } public String toString() { String va=""; node team=head.next; while(team!=null) { va+=team.data+" "; team=team.next; } return va; } }
总结
你可能会问这个是否正确啊,那我来测试一下:
这里的只是简单实现,实现基本方法。链表也只是单链表。完善程度还可以优化。能力有限, 如果有错误或者优化的地方还请大佬指正。
单链表查询速度较慢,因为他需要从头遍历,如果在尾部插入,可以考虑设计带尾指针的链表。而顺序表查询速度虽然快但是插入很费时费力,实际应用根据需求选择!
Java中的Arraylist和LinkedList就是两种方式的代表,不过LinkedList使用双向链表优化,并且JDK也做了大量优化。所以大家不用造轮子,可以直接用,但是手写顺序表、单链表还是很有学习价值的。
单链表搞懂了,双链表也就不远了(下集预告)!
原创不易,bigsai请朋友们帮两件事帮忙一下:
-
点赞、在看、分享支持一下, 您的肯定是我创作的源源动力。
- 微信搜索「bigsai」,2021我需要你的支持!
咱们下次再见!

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
深度剖析 | 关于数据锁定和读取一致性问题
1 背景介绍 传统的 RDBMS 系统在三件事上值得注意:锁定:即锁定记录的功能,以便其他人无法更改它们。 阅读一致性:您(仅您自己)可以看到您所做的更改,直到您提交为止,这时其他人才可以看到它们。 遗憾的是,其较差的可伸缩性:在线事务处理(OLTP)工作负载无法很好地扩展。在现实情况下,将CPU内核数量加倍不会使您的容量翻倍。 这种失败导致许多人尝试使用NoSQL解决方案,但是随着时间的流逝,越来越明显的是,如果没有这些解决方案,锁定和读取一致性将很难实现。VOLTDB是唯一的数据平台,可让企业按比例进行锁定和读取一致性。 本文解释了为什么很难做到这一点,以及VOLTDB是如何做到这一点的。 2 为什么缩放OLTP很难? 为了让大家更直观地了解为什么扩展OLTP很难,先来让我们看一下简单的白板交易的典型生命周期: 在表格“ a”中插入一行 更新表“ b”中的行 更新表“ c”中的行 做外部活动 更新表“ a”中的行 犯罪 乍一看,可能很难看出这是如何扩展的。但是,让我们看一下使用旧版RDBMS的实际实现: 在此图中,有这样几件事变得显而易见: 1.此操作不会立即完成 您需要12次网络...
- 下一篇
【死磕JVM】用Arthas排查JVM内存 真爽!我从小用到大
Arthas是啥 当我们系统遇到JVM或者内存溢出等问题的时候,如何对我们的程序进行有效的监控和排查,就发现了几个比较常用的工具,比如JDK自带的 jconsole、jvisualvm还有一个最好用的工具——jprofiler,但是这个是收费的,或者除了很有钱的公司,一般很少人会用这个,还有一个就是我们今天的主角——Arthas ,为什么今天会重点讲这个呢? 官网地址:http://arthas.gitee.io/GitHub地址:https://github.com/alibaba/arthas/ Arthas 是Alibaba开源的Java诊断工具,采用命令行交互模式,提供了较为丰富的功能,主要还是他是免费里面的算是好用且功能比较强大的一个JVM排查的插件,在了解这个利器之后,发现还是挺好用的,而且支持的功能也比较全面,那么Arthas到底可以为我们做哪些事情呢? 提供性能看板,包括线程、cpu、内存等信息,并且会定时的刷新。 根据各种条件查看线程快照。找出cpu占用率最高的n个线程 输出jvm的各种信息,如gc算法、jdk版本、ClassPath等 遇到问题无法在线上 debug...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Red5直播服务器,属于Java语言的直播服务器
- CentOS7,8上快速安装Gitea,搭建Git服务器
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- 2048小游戏-低调大师作品
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- CentOS7,CentOS8安装Elasticsearch6.8.6
- CentOS关闭SELinux安全模块
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池