线性表
线性表(List):零个或多个数据元素的有限序列
首先明确几个概念
1. 线性表是一个序列,元素之间是有顺序的
2. 若元素存在多个,第一个元素无前驱,最后一个元素无后继,其他每个元素有且只有一个前驱和后继
3. 线性表是强调有限的
如上图所示一个线性表,将线性表记为(a1,a2,a3…an)则ai+1领先于ai,ai+1成为ai的直接前驱元素,ai-1称为ai的直接后驱元素。
普通线性表
普通线性表内部封装了一个数组对象用来接收线性表的值。整体特性和数组类似,是比较简单且容易理解的一种数据结构。
模拟实现普通线性表
我们来自己模拟实现一个线性表的类,首先要明确该类的属性和方法
分析如下:
1. 增删改查必不可少
2. 表的属性,总容量,当前大小,最大容量
1. 搭建框架
先把准备实现的功能大概搭建起来
/**
* 带有泛型,表示该线性表接受不同类型的对象
*/
public class MyList <T> {
private static final int MAXLENGTH = 100;//最大列表长度
private static final int DEFAULT = 10;
private int size = 0;//当前列表长度初始值
private Object[] data ;//存储list数据的数组
private Object[] emptyObj = {};//空表,clear的时候使用
/**
* 默认构造函数
*/
public MyList(){
initList();
}
/**
* 指定容量的构造函数
*/
public MyList(int capacity){
ensure(capacity);
}
/**
* 初始化操作,建立默认大小为10的列表
*/
private void initList(){
data = new Object[DEFAULT];
}
/**
* 数组扩容
* @param capacity
*/
private void ensure(int capacity){
}
/**
* 清空
*/
void clear(){
data = emptyObj;//data引用指向空列表
}
/**
* 判断list是否为空,true表示空否则false
* @return
*/
boolean isEmpty(){
return this.size == 0;
}
/**
* 添加新元素到list
* @param t
*/
void add(T t){
}
/**
* 返回指定索引位置的元素
* @param idx
* @return
*/
T get(int idx){
return null;
}
/**
* 判断list中是否包含 t 元素
* @param t
* @return
*/
boolean contain(T t){
return false;
}
/**
* 向 list 指定 idx 位置插入 t 元素
* @param indx
* @param t
* @return
*/
boolean insert(int idx,T t){
return null;
}
/**
* 返回list的长度
* @return
*/
int length(){
return this.size;
}
/**
* A∪B操作,把旧的list没有的lst元素插入到list中,返回组合之后的新list
* @param lst
* @return
*/
MyList<T> union(MyList<T> lst){
return this;
}
}
框架的搭建参考了java.util.List接口的一些方法,总之先把需要或者说准备实现的功能先写个框框出来,然后再一个一个的分析并实现。
1. insert():添加新元素
增加新元素这时最基本的功能了,我们先来实现这个方法:
首先的分析一下,我们最终是要向MyArrayList中添加的值是放到该类的private Object[] data数组中去的,所以增加元素的时候就像向数组增加新元素一样了。
/**
* 添加新元素到list
* @param t
*/
void add(T t){
//判断是否越界
if(this.size > MAXLENGTH){
throw new RuntimeException("超过界限");
}
//当前size大于data数组长度的时候
if(this.size > data.length-1){
ensure(this.size+1);//数组扩容
}
data[this.size++]=t;//数组中插入新元素的值
}
注意该方法里面用到了ensure(int capacity);
方法,当数组容量不够的时候,用来动态增加数组的容量,而其他的就向在数组里面插入新数据一样简单明了了。
2. ensure():数组扩容
当当前线性表的容量不够的时候我们就要给内部数组扩容,用来接收更多的值,就向上一个insert方法中用到的情况一样。
首先分析一下:
因为是内部数组扩容,其实原理是简单粗暴的,就是把新创建一个需要的大小的数组容量,然后把旧的数组的值一个一个赋值给新数组就行了,说白了就是两个数组的拷贝,把小容量数组内容拷贝到大容量手中,然后让类内部的数组引用指向新创建的数组,达到扩容的效果。明白了原理实现起来就简单了。
/**
* 数组扩容
* @param capacity
*/
private void ensure(int capacity){
Object[] oldData = data;//旧的数组
int oldLen = data.length;//旧数组的长度
data = new Object[capacity];//创建新的数组
//遍历旧的数组,赋值给新数组
for(int i=0;i<oldLen;i++){
data[i] = oldData[i];
}
}
3. T get(int idx):返回指定索引位置的值
返回指定索引位置的数值,这个是比较简单而且容易理解的,本质就是返回内部数组的指定索引出的数值,相当于arr[idx],
需要注意的是当idx索引位置不合法的情况要主动抛出异常。
/**
* 返回指定索引位置的元素
* @param idx
* @return
*/
T get(int idx){
//先判断
if(idx > data.length || idx < 0){
throw new RuntimeException("idx越界或者小于零!");
}
Object obj = data[idx];
return (T)obj;
}
4. boolean contain(T t):检测是否包含指定对象
本质就是,遍历整个数组,一个一个比较看里面是否有对应的t,如果有就返回true否则返回false。
/**
* 判断list中是否包含 t 元素
* @param t
* @return
*/
boolean contain(T t){
for(int i=0;i<this.size;i++){
if(data[i] == t){
return true;
}
}
return false;
}
5. boolean insert(int idx,T t):指定位置出插入数值
这里要注意一个问题:
1. 插入元素,数组的长度肯定会比之前的长度增加一,首先要扩容。
2. 当idx在最后一个索引位置(数组的末尾)的时候直接扩容,插入即可。
3. 否则就要把插入位置之后的所有元素向后移动一位
/**
* 向 list 指定 idx 位置插入 t 元素
* @param indx
* @param t
* @return
*/
boolean insert(int idx,T t){
//判断是否合法
if( idx < 0 || idx > data.length || idx == this.MAXLENGTH ){
throw new RuntimeException("idx越界或者小于零!");
}
ensure(this.size+1);//数组扩大容量
//如果在末尾
if(idx == this.size){
data[idx] = t;
this.size++;
return true;
}
//把要插入元素之后的所有元素向后移动一位
for(int i=this.size-1;i >= idx;i--){
data[i+1] = data[i];//每一个元素向后移动一位
}
this.size++;
data[idx] = t;
return true;
}
6.T remove(int idx):删除指定idx位置并返回该位置的元素
首先分析:
1. 如果idx在数组末尾那么直接删除返回就可以了
2. 否则要把删除的值先赋给中间变量,然后idx之后的元素向前移动一位
/**
* 删除指定idx位置并返回该位置的元素
* @param indx
* @return
*/
T remove(int idx){
T t = null;
//判断是否合法
if( idx < 0 || idx > data.length || idx == this.MAXLENGTH ){
throw new RuntimeException("idx越界或者小于零!");
}
//返回指定索引位置的值
t = (T) data[idx];
//如果在末尾
if(idx == this.size){
this.size--;
}
//不在末尾,把idx元素向前移动
for(int i=idx;i<this.size;i++){
data[i] = data[i+1];
}
return t;
}
7. MyList union(MyList lst):对两个表U操作,返回一个新的表
分析:
也就是创建一个新的数组,长度为两个表的当前容量,然后把表1和表2的值一个一个赋值给新表,最后返回新表
/**
* A∪B操作,把旧的list没有的lst元素插入到list中,返回组合之后的新list
* @param lst
* @return
*/
MyList<T> union(MyList<T> lst){
int len_o = this.length();//旧list的长度
int len_n = lst.length();//新的list长度
T t;//接收旧的list中没有的元素
for(int i=0;i<len_n;i++){
t = lst.get(i);//得到新lst中的元素
if( !this.contain(t) ){//如果旧的list中没有,就在list尾部添加
this.insert(len_o++,t);
}
}
return this;
}
测试
我们把一个list线性表的基本操作方法完成,下面进行测试,看是否正确?
public static void main(String[] args) {
MyList<String>list = new MyList<String>();
list.add("A");
list.add("B");
list.add("C");
System.out.println(list.length());//3
System.out.println(list.get(0));//A
System.out.println(list.insert(0, "AA"));//true
System.out.println(list.length());//4
for(int i=0;i<90;i++){
list.add("new"+i);
}
System.out.println(list.length());
}
输出:
3
A
true
4
94
与预期符合。
总结
完成了类的创建,运行一切良好,但是我们发现这个程序是有缺陷的,在数组扩容unsure
的之后,每次都要一次一次的移动元素,该方法的时间复杂度为
O(n),在进行insert
操作的时候不仅要扩容还要再次遍历数组把数组的值向后移动一位,时间复杂度是O(n),两个O(n)在小数据量的处理的时候察觉不到
,但是当数据量比较大的时候就会花费很长时间了。链表轻松了解决该问题。下一篇博客详细叙述。