数据结构-线性表
- 定义
线性表是最基本、最简单、也是最常用的一种数据结构。线性表(linear list)是数据结构的一种,一个线性表是n个具有相同特性的数据元素的有限序列。
线性表中数据元素之间的关系是一对一的关系,即除了第一个和最后一个数据元素之外,其它数据元素都是首尾相接的(注意,这句话只适用大部分线性表,而不是全部。比如,循环链表逻辑层次上也是一种线性表(存储层次上属于链式存储),但是把最后一个数据元素的尾指针指向了首位结点)。
线性表中的个数n定义为线性表的长度,n=0时称为空表。在非空表中每个数据元素都有一个确定的位置,如用ai表示数据元素,则i称为数据元素ai在线性表中的位序。
线性表的相邻元素之间存在着序偶关系。如用(a1,…,ai-1,ai,ai+1,…,an)表示一个顺序表,则表中ai-1领先于ai,ai领先于ai+1,称ai-1是ai的直接前驱元素,ai+1是ai的直接后继元素。当i=1,2,…,n-1时,ai有且仅有一个直接后继,当i=2,3,…,n时,ai有且仅有一个直接前驱 。
- 分类
我们说“线性”和“非线性”,只在逻辑层次上讨论,而不考虑存储层次,所以双向链表和循环链表依旧是线性表。
在数据结构逻辑层次上细分,线性表可分为一般线性表和受限线性表。一般线性表也就是我们通常所说的“线性表”,可以自由的删除或添加结点。受限线性表主要包括栈和队列,受限表示对结点的操作受限制。逻辑结构上相邻的数据在实际的物理存储中有两种形式:分散存储和集中存储。
考虑到这两种情况,线性表分为两种,分别解决两种情况下数据的存储问题: 数据元素在内存中集中存储,采用顺序表示结构,简称“顺序存储”;
数据元素在内存中分散存储,采用链式表示结构,简称“链式存储”。
线性表的逻辑结构简单,便于实现和操作。因此,线性表这种数据结构在实际应用中是广泛采用的一种数据结构。
- 存储结构
线性表主要由顺序表示或链式表示。在实际应用中,常以栈、队列、字符串等特殊形式使用。
顺序表示指的是用一组地址连续的存储单元依次存储线性表的数据元素,称为线性表的顺序存储结构或顺序映像(sequential mapping)。它以“物理位置相邻”来表示线性表中数据元素间的逻辑关系,可随机存取表中任一元素。
链式表示指的是用一组任意的存储单元存储线性表中的数据元素,称为线性表的链式存储结构。它的存储单元可以是连续的,也可以是不连续的。在表示数据元素之间的逻辑关系时,除了存储其本身的信息之外,还需存储一个指示其直接后继的信息(即直接后继的存储位置),这两部分信息组成数据元素的存储映像,称为结点(node)。它包括两个域;存储数据元素信息的域称为数据域;存储直接后继存储位置的域称为指针域。指针域中存储的信息称为指针或链 。
-
特征
- 1.集合中必存在唯一的一个“第一元素”。
- 2.集合中必存在唯一的一个 “最后元素” 。
- 3.除最后一个元素之外,均有唯一的后继(后件)。
- 4.除第一个元素之外,均有唯一的前驱(前件)。
- 5.存储的数据本身的类型一定保持相同,是int型就都是int型,是结构体就都是一种结构体。
- 6.数据一旦用线性表存储,各个数据元素之间的相对位置就固定了。
以上就是关于线性表的概述,接下来真正的去了解它:
- 顺序存储
首先,我们要知道线性表:具有“一对一”逻辑关系的数据,最佳的存储方式就是使用线性表。
线性表,全名为线性存储结构,特征就是把数据排列到一起、一条线上,在去存储到物理空间。示例:
上图中的链表表示顺序链表(顺序存储),用一组地址连续的存储单元依次存储线性表的数据元素。
特点:逻辑上相邻的数据元素,物理次序也是相邻的。
只要确定了存储线性表的初始位置,线性表中的任意数据都可以随意存取,线性表的顺序存储结构是一种随机存取的储存结构,同时我们可以通过索引快速找到某一个索引的值,具有读取快的特点,所以通常我们都使用数组来描述数据结构中的顺序储存结构,用动态分配的一维数组表示线性表。
举例:我们平时在开发中所使用的ArrayList的数据结构就是一维数组,所以可以理解为顺序链表。
List<String> list = new ArrayList<String>();
list.add("A");
list.add("B");
list.add("C");
list.add("D");
图解说明:
首先,我们需要知道ArrayList初始值大小是10, 每次扩容是增加2倍。
当我们添加A元素时,首先进入,分配内存空间,下标为0;添加B时,A需要向后移动一位,所以此时需要去做一个复制操作,把A复制过去,下标加1;此时下标为0保存的是数据B,下标为1的时候保存的是B,当在添加下一个操作,继续重复上面的操作。回过头来我们看看索引,他们都是依次按照进入的数据连接起来的,因为这种数据结构(顺序链表)具有查询快,可以根据索引快速进行读取数据,而增加删除操作每次都会进行一次元素复制,浪费了大量的时间,比较耗费性能。
上面是对顺序存储结构的介绍,接下来是非顺序的存储结构,数据元素在内存中分散存储,采用链式表示结构,简称“链式存储”。
- 链式存储
链式存储结构,一般情况下我们遇到最多且最常用的大概有单向链表,双向链表,循环链表三种。
链式存储是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针(Pointer)。比顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而顺序表相应的时间复杂度分别是O(logn)和O(1)。
对于链式存储结构的线性表而言,它的每个节点都必须包含数据元素本身和一个或者两个用来引用上一个或者下一个的引用,也就是说,有如下公式:
节点 = 数据元素 + 引用下一个节点的引用 + 引用上一个节点的引用
单链表:
单链表是指每个节点只保留一个引用,该引用指向当节点的下一个节点,没有引用指向头节点,尾节点的next引用为null。如下图所示:
在单链表中的每个节点中,除了数据区域外,还有一个区域存储了当前节点的下一节点的地址,我们把这个记录下个结点地址的指针或引用叫作后继指针或引用Next。
单链表结构中,有两个节点比较特殊,那就是第一个节点和最后一个节点。在链式存储结构中,我们将第一个节点称为头结点Head,将最后一个节点称为尾节点Tail。头节点记录链表的起始地址,有了这个地址,我们就可以遍历整个链表。尾节点的后继指针或者引用不是指向一个具体的节点,而是指向一个空地址NULL,从而表示该节点为链表的尾节点。
与数组一样,链表也支持数据的插入、查找、删除。
数组在进行数据的插入,删除操作时,为了保证内存数据的连续性,往往需要做大量的数据搬移工作,所以时间复杂度是O(n)。而在链表中插入或删除数据时,因为链表结构中的节点并不需要连续的存储空间,所以在链表中进行数据的插入和删除时并不需要搬移节点。对于链表的删除和插入操作,我们只需要调整相邻节点的后继指针即可,所以对应的时间复杂度是O(1)。
增加节点:
删除节点:
比如我们创建的集合LinkedList的数据结构就是基于双向链表存储结构,采用链表结构 适合插入和删除操作 只用改变一下前后节点引用 ,但是要是查询某一个元素就远不如数组那么方便了。
链表中每个元素都包含了 上一个和下一个元素的引用,所以add/remove 只会影响到上一个和下一个元素,但是get/set就慢了,get(int) 传入索引与 size的1/2比较,大于一半则从最后一个元素开始遍历挨个查找,小于一半则从第一个元素开始遍历挨个查找。
然后在来解释一下为什么LinkedList为什么查询元素比较慢,看代码:
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
Node<E> node(int index) {
// assert isElementIndex(index);
//从链表的前段开始查找
if (index < (size >> 1)) { //传入进来的索引的大小,与集合尺寸 size/2的结果进行比较。判断要查找的元素距离首末两端的距离
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next; //由于是链表数据结构,所以是不停的进行迭代查找下一个元素,这就是慢的原因
return x;
//从链表的后面开始查找
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
可以看到传入进来的索引的大小,与集合尺寸 size/2的结果进行比较。判断要查找的元素距离首末两端的距离,由于是链表数据结构,所以是不停的进行迭代查找下一个元素,这就是慢的原因。
循环链表
循环链表是一种特殊的单链表,特殊之处在于,我们在单链表中,尾节点的后继指针或者引用不是指向一个具体的节点,而是指向一个空地址NULL,表示这就是最后一个节点。而将单链表的尾节点从指向空地址NULL调整为指向头结点Head,就形成了循环链表。
和单链表相比,循环链表的优点是从链尾到链头比较方便。当要处理的数据具有环型结构特点时,就特别适合采用循环链表。
双向链表
双向链表,指的是一个链表结构,它支持两个方向,每个节点不止只有一个后继指针或者引用Next指向后继节点,还有一个前驱指针或者引用Prev指向前面的节点
从图中可以得知,双向链表需要额外的空间来存储后继节点和前驱节点的地址,所以,存储同样多的数据,双向链表要比单向链表需要的存储空间要多。虽然两个指针或者引用比较浪费存储空间,但可以支持双向遍历,这样也带来了双向链表操作的灵活性。
从双向链表的结构看,双向链表可以在O(1)的时间复杂度下找到前驱节点,基于此特性,在某些特殊的场景下,对节点的删除和插入操作,双向链表比单向链表会更高效。
举例:LinkedHashMap 的实现原理也用到了双向链表。
本文到了这里就是该说结束的时候了,最后我们在总结一下顺序表与链接表的区别:
顺序存储结构和链式存储结构的区别
-
链表存储结构的内存地址不一定是连续的,但顺序存储结构的内存地址一定是连续的;
-
链式存储适用于在较频繁地插入、删除、更新元素时,而顺序存储结构适用于频繁查询时使用。
顺序存储结构和链式存储结构的优缺点:
-
空间上 :顺序比链式节约空间。是因为链式结构每一个节点都有一个指针存储域。
-
存储操作上: 顺序支持随机存取,方便操作
-
插入和删除上:链式的要比顺序的方便(因为插入的话顺序表也很方便,问题是顺序表的插入要执行更大的空间复杂度,包括一个从表头索引以及索引后的元素后移,而链表是索引后,插入就完成了)。