以下内容来自黑马程序员 我摘抄部分关键内容 并进行批注
部分转载于黑马程序员
线性表是最基本、最简单、也是最常用的一种数据结构。一个线性表是n个具有相同特性的数据元素的有限序列。
前驱元素: 若A元素在B元素的前面,则称A为B的前驱元素
后继元素: 若B元素在A元素的后面,则称B为A的后继元素
线性表的特征:数据元素之间具有一种“一对一”的逻辑关系。
-
第一个数据元素没有前驱,这个数据元素被称为头结点;
-
最后一个数据元素没有后继,这个数据元素被称为尾结点;
-
除了第一个和最后一个数据元素外,其他数据元素有且仅有一个前驱和一个后继。 如果把线性表用数学语言来定义,则可以表示为(a1,...ai-1,ai,ai+1,...an),ai-1领先于ai,ai领先于ai+1,称ai-1是ai的 前驱元素,ai+1是ai的后继元素
线性表的分类: 线性表中数据存储的方式可以是顺序存储,也可以是链式存储,按照数据的存储方式不同,可以把线性表分为顺序表和链表。
1.1 顺序表 顺序表是在计算机内存中以数组的形式保存的线性表,线性表的顺序存储是指用一组地址连续的存储单元,依次存储线性表中的各个元素、使得线性表中再逻辑结构上相邻的数据元素存储在相邻的物理存储单元中,即通过数据元素物理存储的相邻关系来反映数据元素之间逻辑上的相邻关系。
1.1.1 顺序表的实现
顺序表API设计:
这里的api设计有一点问题 第5条中并不是往第i个元素之前插入一个值为t的数据元素 而是往i处添加 根据后面的实现方法可以看出这一点。
其次是设计的关键在于N
1.1.2 顺序表的遍历
一般作为容器存储数据,都需要向外部提供遍历的方式,因此我们需要给顺序表提供遍历方式。
在java中,遍历集合的方式一般都是用的是foreach循环,如果想让我们的SequenceList也能支持foreach循环,则 需要做如下操作:
1.让SequenceList实现Iterable接口,重写iterator方法;
2.在SequenceList内部提供一个内部类SIterator,实现Iterator接口,重写hasNext方法和next方法;
1.1.3 顺序表的容量可变
在之前的实现中,当我们使用SequenceList时,先new SequenceList(5)创建一个对象,创建对象时就需要指定容器的大小,初始化指定大小的数组来存储元素,当我们插入元素时,如果已经插入了5个元素,还要继续插入数据,则会报错,就不能插入了。这种设计不符合容器的设计理念,因此我们在设计顺序表时,应该考虑它的容量的伸缩性。 考虑容器的容量伸缩性,其实就是改变存储数据元素的数组的大小,那我们需要考虑什么时候需要改变数组的大小?
1.添加元素时: 添加元素时,应该检查当前数组的大小是否能容纳新的元素,如果不能容纳,则需要创建新的容量更大的数组,我们这里创建一个是原数组两倍容量的新数组存储元素。
2.移除元素时: 移除元素时,应该检查当前数组的大小是否太大,比如正在用100个容量的数组存储10个元素,这样就会造成内存 空间的浪费,应该创建一个容量更小的数组存储元素。如果我们发现数据元素的数量不足数组容量的1/4,则创建 一个是原数组容量的1/2的新数组存储元素。
1.1.4 顺序表的时间复杂度
get(i):不难看出,不论数据元素量N有多大,只需要一次eles[i]就可以获取到对应的元素,所以时间复杂度为O(1);
insert(int i,T t):每一次插入都需要把i位置后面的元素移动一次,随着元素数量N的增大,移动的元素也越多,时间复杂为O(n);
remove(int i):每一次删除都需要把i位置后面的元素移动一次,随着数据量N的增大,移动的元素也越多,时间复杂度为O(n);
由于顺序表的底层由数组实现,数组的长度是固定的,所以在操作的过程中涉及到了容器扩容操作。这样会导致顺序表在使用过程中的时间复杂度不是线性的,在某些需要扩容的结点处,耗时会突增,尤其是元素越多,这个问题越明显。(比如说,当你插入一个元素时容器的内存不够,这是你需要多花时间来进行一系列开辟内存的操作(步骤:先用一线性表暂时存储数组——>申请一块两倍大小的新线性表——>将暂存在临时数组的线性表再重新存入新线性表))
1.1.5 java中ArrayList实现
java中ArrayList集合的底层也是一种顺序表,使用数组实现,同样提供了增删改查以及扩容等功能。 1.是否用数组实现; 采用了数组
2.有没有扩容操作; 进行了扩容操作
3.有没有提供遍历方式; 提供了遍历方式
1.1.6那为什么还需要我们手写线性表呢**
1.ArrayList类为了达到普适性 内部有更多的限制 执行时间可能更长
2.对于封装好的方法我们一般不好改动 灵活性稍差