数据结构
由于现实的数据元素之间存在着纷繁芜杂的逻辑关系,应用程序则需要分析这些数据的逻辑结构,并采用合适的物理结构来存储(在内存中存储,并非数据库存储)这些数据,并以此为基础对这些数据进行相应的操作。当然,还要分析各种数据结构在时间开销、空间开销上的优劣。这种专门研究应用程序中数据之间的逻辑关系、存储方式及其操作的学问就是所谓的数据结构。
从数据的逻辑结构来分,数据元素之间存在的关联关系被称为数据的逻辑结构。
-
应用程序中的数据大致有如下四种基本的逻辑结构:
-
➢集合:数据元素之间只有"同属于一个集合"的关系。
➢线性结构:数据元素之间存在一一个对一个的关系。
➢树形结构:数据元素之间存在一一个对多个的关系。
➢图状结构或网状结构:数据元素之间存在多个对多个的关系。
对于数据不同的逻辑结构,在底层通常有两种物理存储结构:
-
➢顺序存储结构。
➢链式存储结构。
对于常用的数据结构,可以将其简单地分为线性结构和非线性结构,其中线性结构主要是线性表,而非线性结构则主要是树和图。
线性表
线性表(Linear List)是由n (n≥0)个数据元素(节点) a1,a2,a3,…,an组成的有限序列。线性表中每个元素必须具有相同的结构(即拥有相同的数据项)。线性表是线性结构中最常用而又最简单的一种数据结构。
线性表的基本操作
➢初始化:通常是一个构造器,用于创建一个空的线性表。
➢返回线性表的长度:该方法用于返回线性表中数据元素的个数。
➢获取指定索引处的元素:根据索引返回线性表中的数据元素。
➢按值查找数据元素的位置:如果线性表中存在一个或多个与查找值相等的数据元素,那么该方法返回第一个搜索到的值相等的数据元素的索引,否则返回-1。
➢直接插入数据元素:向线性表的头部插入一个数据元素,线性表长度+1。
➢向指定位置插入数据元素:向线性表的指定索引处插入一个数据元素,线性表长度+1。
➢直接删除数据元素:删除线性表头部的数据元素,线性表长度-1。
➢删除线性表中指定位置的数据元素:删除线性表中指定索引处的数据元素,线性表长度-1。
➢判断线性表是否为空:该方法判断线性表是否为空,如果线性表为空,则返回true,否则返回false。
➢清空线性表:将线性表清空。
线性表的顺序存储结构
线性表的顺序存储结构是指用一组地址连续的存储单元依次存放线性表的元素。当程序采用顺序存储结构来实现线性表时,线性表中相邻元素的两个元素ai和ai+1对应的存储地址loc(ai)和loc(ai+1)也是相邻的。程序获取顺序表中每个元素的存储起始地址的时间相同,读取表中数据元素的时间也相同。而且顺序表中每个元素都可随机存取,因此顺序存储的线性表是一种随机存取的存储结构。为了使用顺序结构实现线性表,程序通常会采用数组来保存线性表中的数据元素。
顺序表的插入操作
顺序表的插入元素操作是将指定插入元素的位置的原元素以及后面的元素向后依次移动一位,然后才插入。由于顺序结构线性表底层采用数组来存储数据元素,因此插入数据元素时必须保证不会超出底层数组的容量。如果线性表中元素的个数超出了底层数组的长度,那么就必须为该线性表扩充底层数组的长度。这个对数组的扩容机制很麻烦,而且性能很差。
顺序表的删除操作
顺序表的插入元素操作是将指定的删除元素删除,出现一个空位之后,指定的删除位置后面所有的元素向前依次移动一位。
@Test
public void t9() {
List<Integer> l=new ArrayList<>();
l.add(12);
l.add(13);
l.add(14);
l.add(0, 10);//在index下标为0的位置添加一个10元素
System.out.println(l.toString());
System.out.println("14在顺序线性表中的位置"+l.indexOf(14));
l.remove(2);
System.out.println(l.toString());
System.out.println("14在顺序线性表中的位置"+l.indexOf(14));
}
线性表的链式存储结构
链式存储结构的线性表(简称为链表)将采用一组地址任意的存储单元存放线性表中的数据元素。链式存储结构的线性表不会按线性的逻辑顺序来保存数据元素,它需要在每一个数据元素里保存一个引用下一个数据元素的引用(或者叫指针)。由于不是必须按顺序存储,链表在插入、删除数据元素时比顺序线性表快得多,但是查找一个节点或者访问特定编号的节点则比顺序线性表慢很多。 使用链表结构可以克服顺序线性表(基于数组)需要预先知道数据大小的缺点,链表结构可以充分利用计算机的内存空间,实现灵活的内存动态管理。但是链表结构失去了数组随机存取的优点,同时链表由于增加了节点的指针域,空间开销比较大。
对于链式存储结构的线性表而言,它的每个节点都必须包含数据元素本身和一个或两个用来引用上一个下一个节点的引用。
节点 = 数据元素 + 引用下一个节点的引用 + 引用上一个节点的引用
双向链表节点中每个节点中的prev代表对前一个节点的引用,只有双向链表的节点才存在prev引用。
链表是多个相互引用的节点的集合,整个链表总是从头节点开始,然后依次向后指向每个节点。空链表就是头节点为null的链表。
单链表上的基本运算
单链表指的是每个节点只保留一个引用,该引用指向当前节点的下一个节点,没有引用指向头节点,尾节点的next引用为null。
对于单链表,系统建立单链表的过程就是不断添加节点的过程。
-
动态建立单链表有以下两种方式:
-
➢头插法建表:该方法从一个空表开始,不断地创建新节点,将数据元素存入节点的data域中,然后不断地以新节点为头节点,让新节点指向原有的头节点。(生成的链表中节点的次序和输入的顺序相反)
➢尾插法建表:该方法是将新节点插入到当前链表的表尾上,因此需要为链表定义一个引用变量来保存链表的最后一个节点。(生成的链表中节点的次序和输入的顺序一致)
- 查找:
➢按序号查找第index个节点:从header节点依次向下在单链表中查找第index个节点。算法为,设header为头,current 为当前节点(初始时current从header开始),0为头节点序号,i为计数器,则可使current依次下移寻找节点,并使i同时递增记录节点序号,直到返回指定节点。
➢在链表中查找指定的element元素:查找是否有等于给定值element的节点。若有,则返回首次找到的其值为element的节点的索引;否则,返回-1。查找过程从开始节点出发,顺着链表逐个将节点的值和给定值element做比较。 - 插入:
插入操作是将值为element 的新节点插入到链表的第index个节点的位置上。因此,首先找到索引为index-1的节点,然后生成一个数据域为element的新节点newNode,并令index-1处节点的next引用新节点,新节点的next引用原来index处的节点。
- 删除:
删除操作是将链表的第index个节点删去。因为在单链表中,第index个节点是由index-1处的节点引用的,因此删除index处的节点将先获取index-1处节点,然后让index-1处节点的next引用到原index+1处的节点,并释放index处节点即可。
对于链表而言,和顺序线性表基本相同,因为它们都是线性表,只是底层实现不同而已。因此,链表和顺序表只是性能上的差异:顺序表在随机存取时性能很好,但插入、删除时性能就不如链表;链表在插入、删除时性能很好,但随机存取时性能就不如顺序表。
循环链表
循环链表是一种首尾相接的链表。将单链表的尾节点next指针改为引用单链表header节点,这个单链表就成了循环链表。循环链表具有一个显著特征:从链表的任一节点出发均可找到表中的其他所有节点,因此循环链表可以被视为“无头无尾”。
循环链表中的第一个节点之前就是最后一个节点,反之亦然。循环链表的无边界使得它实现许多方法时会更容易,在这样的链表上设计算法会比普通链表更加容易。新加入的节点应该是在第一个节点之前(采用头插法插入),还是最后一个节点之后(采用尾插法插入),可以根据实际要求灵活处理,具体的实现区别不大。就程序实现来说,循环链表与普通单链表差别并不大,保证链表中tail.next = header即可,因此此处不再给出循环链表的代码。
除此之外,还有一种伪循环链表, 就是在访问到最后一个节 点之后的时候,手工地跳转到第一个节点,访问到第一个节点之前的时候也一样。这样也可以实现循环链表的功能,当直接用循环链表比较麻烦或者可能有问题时,可以考虑使用这种伪循环链表。
双向链表
如果为每个节点保留两个引用prev和next,让prev指向当前节点的上一个节点,让next指向当前节点的下一个节点,此时的链表既可以向后依次访问每个节点,也可以向前依次访问每个节点,这种形式的链表被称为双向链表。
双向链表是一种对称结构,它克服了单链表上指针单向性的缺点,其中每个节点既可以向前引用,也可以向后引用,这样可以更方便地插入、删除数据元素。与单链表类似的是,如果将链表的header节点与tail节点链在一起就构成了双向循环链表。
- 双向链表的查找:
由于双向链表既可以从header节点开始依次向后搜索每个节点,也可以从tail节点开始依次向前搜索每个节点,因此当程序试图从双向链表中搜索指定索引处的节点时,既可以从该链表的header节点开始搜索,也可以从该链表的tail 节点开始搜索。至于到底应该从header开始搜索,还是应该从tail开始搜索,则取决于被搜索节点是更靠近header,还是更靠近tail。一般来说,可以通过被搜索index 的值来判断它更靠近header,还是更靠近tail。如果index<size/2,则可判断该位置更靠近header,应从header开始搜索;反之,则可判断该位置更靠近tail,那就应从tail开始搜索。 - 双向链表的插入:
双向链表的插入操作更复杂,向双向链表中插入一个新节点必须同时修改两个方向的指针(即引用)。
- 双向链表的删除:
在双向链表中,删除一个节点也需要同时修改两个方向的指针。