线性表的顺序存储结构是指用一组地址连续的存储单元依次存放线性表的元素。当程序采用顺序存储结构来实现线性表时,线性表中相邻元素的两个元素ai和an+1对应的存储地址loc(ai)和loc(an+1)也是相邻的。
换句话来说,顺序结构线性表中数据元素的物理关系和逻辑关系是一致的,线性表中数据元素的存储地址可按如下公式计算:
loc(ai) = loc(a0)+ i * b; (0 < i < n)
上面的 b 代表每个数据元素的存储单元。从上面的公式可以看出,程序获取线性表中每个元素的存储起始地址的时间相同,读取表中数据元素的时间也相同,而且顺序表中每个元素都可随机存取,因此顺序存储的线性表是一种随机存取的存储结构。
为了使用顺序存储结构实现线性表,程序通常会采用数组来保存线性表中的数据元素。
顺序线性表的实现难点在于 插入数据元素和删除数据元素。
插入元素示意图:
对于这个插入操作,还有一个必须要考虑的问题:由于顺序结构线性表底层采用数组来存储数据元素,因此插入数据元素是必须要保证不会超出底层数组的容量。如果线性表中元素的个数超出了底层数组的长度,那么就必须为该线性表扩充底层数组的长度。
删除元素示意图:
下面是代码实现:
import java.util.Arrays;
public class SequenceList<T> {
private int DEFAULT_SIZE = 16;
//集合容量
private int capacity;
//底层存放数据元素的数组
private Object[] elementData;
//集合当前存放元素的个数
private int size = 0;
public SequenceList(){
capacity = DEFAULT_SIZE;
elementData = new Object[capacity];
}
public SequenceList(T element){
this();
elementData[0] = element;
size ++;
}
/**
* 以指定长度来初始化集合
* @param element
* @param initSize
*/
public SequenceList(T element, int initSize){
//将capacity的大小扩大到大于initSize的最小2的n次方
capacity = 1;
while(capacity < initSize){
capacity <<= 1;
}
elementData = new Object[capacity];
elementData[0] = element;
size ++;
}
//获取顺序线性表的长度
public int length(){
return size;
}
//获取顺序线性表在索引 i 处的元素
@SuppressWarnings("unchecked")
public T get(int i){
if(i < 0 || i > size - 1){
throw new IndexOutOfBoundsException("线性表数组越界:"+ i);
}
return (T) elementData[i];
}
//查询顺序线性表中指定元素的索引
public int locate(T element){
for(int i = 0; i < size; i ++){
if(elementData[i].equals(element)){
return i;
}
}
return -1;
}
//向顺序线性表中的指定位置插入某数据元素
public void insert(int index, T element){
/**
* 这里看源码写的是if(index < 0 || index > size){。。。}
* 虽然它是放在另外单独的一个叫rangeCheckForAdd(int index)的方法里,但实质是一样的。
* 为什么这么写呢?size为底层存储数据元素的数组的当前数据元素的个数 index > size;
* 说明 index = size 也是不满足此条件的,那么方法不会抛出数组越界异常,而是数组去扩容,插入数据
* 我想它这样做就是为了保证顺序线性表的一个特性:用一组地址连续的存储单元依次存放线性表的元素。
* 所以它如果要在指定位置插入新元素,那么这个位置索引必须在底层数组已有元素索引的之间,也就是 0 ~ size - 1。
* 那索引值为刚好为size时,为什么也可以插入数据?答案显而易见,都是为了满足顺序线性表的用一组地址连续的存储单元依次存放线性表的元素的特性。
*/
if(index < 0 || index > size){
throw new IndexOutOfBoundsException("线性表数组越界:"+ index);
}else{
ensureCapacity(size + 1);
System.arraycopy(elementData, index, elementData, index + 1, size-index);
elementData[index] = element;
size ++;
}
}
//在线性表的开始位置添加新数据元素
public void add(T element){
insert(size, element);
}
//删除线性表中指定索引位置处的数据元素并返回
@SuppressWarnings("unchecked")
public T delete(int index){
if(index < 0 || index > size - 1){
throw new IndexOutOfBoundsException("线性表数组越界:"+ index);
}else{
T oldObject = (T) elementData[index];
int coryLength = size - index - 1;
if(coryLength > 0){
System.arraycopy(elementData, index + 1, elementData, index, coryLength);
}
//将数组的最后一个索引指空
elementData[-- size] = null;
return oldObject;
}
}
//删除线性表中的最后一个数据元素并得到它
public T remove(){
return delete(size - 1);
}
//判断线性表是否为空
public boolean isEmpty(){
return size == 0;
}
//请空线性表
public void clear(){
/*for(int i = 0; i < size; i ++){
elementData[i] = null;
}*/
Arrays.fill(elementData, null);
size = 0;
}
//toString 方法
public String toString(){
if(size == 0){
return "[]";
}else{
StringBuffer sb = new StringBuffer();
sb.append("[");
for(int i = 0; i < size; i ++){
sb.append(elementData[i].toString()+ ",");
}
return sb.toString().substring(0,sb.length() - 1)+"]";
}
}
//扩容
private void ensureCapacity(int minCapacity){
if(minCapacity > capacity){
while(minCapacity < capacity){
capacity <<= 1;
}
}
}
}
测试代码如下:
import com.yc.list.SequenceList;
public class SequenceListTest {
public static void main(String[] args) {
SequenceList<String> list = new SequenceList<String>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
System.out.println( list + "\tccc的位置为:"+list.locate("ccc"));
list.insert(1, "ddd");
System.out.println( list + "\tccc的位置为:"+list.locate("ccc"));
list.remove();
System.out.println( list +"\tccc的位置为:"+list.locate("ccc"));
list.add("ccc");
list.delete(1);
System.out.println( list + "\tccc的位置为:"+list.locate("ccc"));
}
}
输出结果为:
由上面的运行结果来看,这个SequenceList类部分实现了ArryatList的功能。或者说他就是一个简易版本的ArrayList。
但是要注意的是:这个SequenceList类是线程不安全的,在单线程的情况下他是完全可以胜任的。但如果放在多线程环境下,它可能会引起线程安全问题。