一、前言
上一篇中内容中,我们从存储结构、性能以及试用场景几个方面分析了数组这种最基本的数据结构。顺序表的也是由数组实现的。在实际的开发过程中,我们也经常使用一个与数组类似的顺集合容器—ArrayList。ArrayList的底层实现就是数组,但是它把数组的特性进行了封装,调用起来更方便,同时优化了数组的痛点(不支持自动扩容)。下面我们就利用上一篇学到的知识,试着完成一个简单的顺序表。如图:
先简单回顾下数组的特点:数组是一种线性表数据结构,是一组连续的内存空间(这也是随机访问的重要切入点),存储的数据类型相同。(https://blog.csdn.net/anyway8090/article/details/89428804)
下面来看一个接口MyList,接口中包含了容器类的大部分方法,包括增删改查的方法也就是我们自己手写的手写的MyArrayList的顶级接口。
package cn.com.arrayList;
/**
* 顺序表顶级接口
* @author Administrator
*/
public interface MyList<T> {
/**
* 判断顺序表是否为空
*/
boolean isEmpty();
/**
* 顺序表大小
*/
int size();
/**
* 获取元素
* @param index
* @return
*/
T get(int index);
/**
* 设置某个元素的值
* @param index
* @param data
* @return
*/
T set(int index,T data);
/**
*添加元素
* @param data
* @return
*/
boolean add(T data);
/**
* 在指定位置添加元素
* @param index
* @param data
* @return
*/
boolean add(int index,T data);
/**
* 根据index移除元素
* @param index
* @return
*/
T remove(int index);
/**
* 根据data移除元素
* @param data
* @return
*/
boolean remove(T data);
/**
* 清空链表
*/
void clear();
/**
* 是否包含data元素
* @param data
* @return
*/
boolean contains(T data);
/**
* 根据值查询下标
* @param data
* @return
*/
int indexOf(T data);
/**
* 根据data值查询最后一个出现在顺序表中下标
* @param data
* @return
*/
int lastIndexOf(T data);
/**
* 输出格式
* @return
*/
String toString();
}
首先实现接口先实现接口MyList,从写接口中的方法,声明当前数组,当前数组大小及默认数组的长度大小为10。
//当前数组
private Object[] table;
//当前数组长度
private int size;
//默认数组长度
public final static int DEFULT_LENTHT=10;
此处声明两个构造方法,一个指定数据的长度,一个默认无参构造方法,而无参构造方法调指定默认数组长度大小为10;
/**
*指定容器大小
* @param capacity
*/
public MyArrayList(int capacity){
this.table=new Object[capacity];
}
/**
* 无参构造方法,设置默认大小10
*/
public MyArrayList(){
this(DEFULT_LENTHT);
}
来先看下几个简单的方法
判断是容器是否为空,只需判断当前数组的长度是否为0;
/**
* 判断是否为空,判断当前容器大小是否为0
*/
@Override
public boolean isEmpty() {
return this.size==0;
}
获得元素在容器的中的位置:由于容器本身是个数组,所有元素的位置也就是元素在数组中的下标,而确定元素的下标需要遍历所有元素从而找到元素的下标位置。
/**
* 根据下标寻址,默认返回-1,表示该元素在容器中不存在
*/
@Override
public int indexOf(T data) {
if (data != null) {
for (int i = 0; i < table.length; i++) {
if (this.table[i].equals(data)) {
return i;
}
}
}
return -1;
}
判断元素最后一次出现的位置:与indexOf的方法相似,只是遍历的顺序会和indexOf相反(代码就省略了)
容器指定位置添加元素:因为数组的连续性,在未指定条件的情况,新加入顺序表的元素都会放入的数组的末端。当数组长度无法满足当前添加元素时,触发容器的动态扩容机制。
@Override
public void add(int index, T data) {
//边界校验,插入元素失败
if (index < 0 || index > size()) {
return false;
}
//动态扩容
ensureCapacity(index);
// 插入新值
this.table[index] = data;
// 长度加1
size++;
return true;
}
/**
* 判断数据是否需要扩容,当容器大小与数组的大于等于数组大小时,
* 将原来的数据扩容到之前的2倍,并且搬移数组到新数组中
* @param index
*/
public void ensureCapacity(int index){
if (size() == table.length) {
//将当前数组赋值给临时数组
Object[] temp = this.table;
// 将原来的数组扩容到原来的两倍
this.table = new Object[size() *2];
// 把原来的数组赋值复制到新的数组中
for (int i = 0; i < size(); i++) {
this.table[i] = temp[i];
}
}
//index后面的元素向后移动一位
for (int j = size; j > index; j--) {
this.table[j] = this.table[j-1];
}
}
不指定位置添加元素时,默认在容器末尾添加元素:
/**
* 在尾部插入元素
*/
@Override
public boolean add(T data) {
return this.add(size(),data);
}
获取指定位置的元素:由于顺序表的底层是由数组实现,所以根据数组下标获取元素值,如果边界不合法返回null
/**
* 获取元素(根据数组下标获取元素)
*/
@Override
public T get(int index) {
if(index> 0 && index<this.size){
return (T) this.table[index];
}
return null;
}
删除指定位置的元素:由于数组中的数据具有连续性,在删除数据组时,需要进行数据搬移才能保证数据的连续性,在大规模的数据搬移过程中,此时的性能消耗是不可忽略的。
/**
* 删除指定位置的元素
*/
@Override
public T remove(int index) {
if(index > 0 && index<size){
//记录删除的元素并返回
T old=(T) this.table[index];
//从被删除的元素开始,后面的元素依次上前移动一位
for (int j = index; j < size-1; j++) {
this.table[j]=this.table[j+1];
}
//将末尾元素设置为null
this.table[size-1]=null;
//顺序表长度-1
--size;
return old;
}
return null;
}
删除指定元素:删除不为null的元素,首先找到在数组中找到指定元素的位置,然后将该元素后的元素进行搬移,将删除的元素赋值为null;
public boolean remove(T data) {
if (data != null) {
for (int index = size; index > 0; index--) {
if (data.equals(table[index])) {
// 从被删除的元素开始,后面的元素依次上前移动一位
for (int j = index; j < size - 1; j++) {
this.table[j] = this.table[j - 1];
}
// 末尾的元素设置为null
this.table[size - 1] = null;
// 顺序表长度-1
--size;
return true;
}
}
}
return false;
}
修改某个元素的值:校验边界合法性,根据下标获取该元素,用新元素替换旧元素
/**
* 设置某个节点的值返回旧的节点
*/
@Override
public T set(int index, T data) {
if(index>0 && index < size ){
T olddata = (T) this.table[index];
this.table[index]=data;
return olddata;
}
return null;
}
以上就是顺序表最基本的增删改查方法,从上面的方法中,可以看出,顺序表结构在增删情况下由于要涉及数据搬移效率要明显低于读写,不过在已经数据较少同时对于性能不敏感的情况下,顺序表基本能满足需求,譬如ArrayList在我们实际系统中应用是比较广泛的。