顺序表
线性表的顺序存储结构
1、数组是实现顺序存储结构的基础。
特点:地址连续确定,容量固定,随机存取T(n) = O(1)
一维数组占用一块内存空间,数组的存储单元个数成为数组容量,也成为数字长度。每个存储单元的地址是连续的,即每个元素连续存储。(反映了顺序表的逻辑结构)。
存取任何一个元素的时间复杂度是O(1)的数据结构成为随机存储结构,数组也是随机存储结构。
2、顺序表
线性表的顺序存储结构成为顺序表。数据元素在内存的物理存储次序反映了线性表数据元素之间的逻辑次序。
当顺序表使用的数组容量不够时,解决数据溢出的办法是,申请另一个更大容量的数组并进行数组元素赋值。(延伸:浅拷贝和深拷贝)
顺序表类声明
package SeqList;
public class SeqList extends Object
{
protected Object[] element; //对象数组,保护成员
protected int n; //顺序表元素个数(长度)
//1. 构造、存取
public SeqList(int length) //构造容量为length的空表
{
this.element = new Object[length]; //申请数组的存储空间,元素为null。
//若length<0,Java抛出负数组长度异常 java.lang.NegativeArraySizeException
this.n = 0;
}
public SeqList() //创建默认容量的空表,构造方法重载
{
this(64); //调用本类已声明的指定参数列表的构造方法
}
public SeqList(T[] values) //构造顺序表,由values数组提供元素,忽略其中空对象
{
this(values.length); //创建容量为values.length的空表
//若values==null,用空对象调用方法,Java抛出空对象异常NullPointerException
for (int i=0; i<values.length; i++) //复制数组元素,O(n)
this.element[i] = values[i]; //对象引用赋值
this.n = element.length;
}
public boolean isEmpty() //判断顺序表是否空,若空返回true,O(1)
{
return this.n==0;
}
public int size() //返回顺序表元素个数,O(1)
{
return this.n;
}
//存取操作
public T get(int i) //返回第i个元素,0≤i<n。若i越界,返回null。O(1)
{
if (i>=0 && i<this.n)
return (T)this.element[i]; //返回数组元素引用的对象,传递对象引用
// return this.element[i]; //编译错,Object对象不能返回T对象
return null;
}
//设置第i个元素为x,0≤i<n。若i越界,抛出序号越界异常;若x==null,抛出空对象异常。O(1)
public void set(int i, T x)
{
if (x==null)
throw new NullPointerException("x==null"); //抛出空对象异常
if (i>=0 && i<this.n)
this.element[i] = x;
else throw new java.lang.IndexOutOfBoundsException(i+"");//抛出序号越界异常
}
//返回顺序表所有元素的描述字符串,形式为“(,)”。覆盖Object类的toString()方法
public String toString()
{
String str=this.getClass().getName()+"("; //返回类名
if (this.n>0)
str += this.element[0].toString(); //执行T类的toString()方法,运行时多态
for (int i=1; i<this.n; i++)
str += ", "+this.element[i].toString(); //执行T类的toString()方法,运行时多态
return str+") "; //空表返回()
}
}
操作的效率分析
O(1):isEmpty(), size(), get(), set()
O(n):toString()需要遍历顺序表
顺序表插入操作
1、对要插入的数据元素进行判断,是否为空
2、插入的数据元素的位置,是否越界
这里有两种处理:①采取容错措施,若 i<0,则插入x在最前面;若 i>n,则插入x在最后面 ②不处理,返回错误信息
3、是否溢出,若溢出,申请更大容量的数组并复制全部数组元素
4、移动数据,空出位置(主要的时间花费)
5、插入数据元素
插入过程图解:
//2. 顺序表的插入操作
//插入x作为第i个元素,x!=null,返回x序号。若x==null,抛出空对象异常。O(n)
//对序号i采取容错措施,若i<0,插入x在最前;若i>n,插入x在最后
public int insert(int i, T x)
{
if (x==null) //判断插入的元素是否为空
throw new NullPointerException("x==null"); //抛出空对象异常
if (i<0) i=0; //插入位置i容错,插入在最前
if (i>this.n) i=this.n; //插入在最后
Object[] source = this.element; //数组变量引用赋值,source也引用element数组
if (this.n==element.length) //若数组满,则扩充顺序表的数组容量
{
this.element = new Object[source.length*2]; //重新申请一个容量更大的数组
for (int j=0; j<i; j++) //复制当前数组前i-1个元素
this.element[j] = source[j];
}
for (int j=this.n-1; j>=i; j--) //从i开始至表尾的元素向后移动,次序从后向前
this.element[j+1] = source[j];
this.element[i] = x;
this.n++;
return i; //返回x序号
}
public int insert(T x) //顺序表尾插入x元素,返回x序号。成员方法重载
{
return this.insert(this.n, x); //插入操作中,this.n加1
}
对顺序表进行插入操作时,算法所花费的时间主要用于移动元素,插入一个元素平均需要移动顺序表元素总量的一半,时间复杂度是O(n)。
顺序表删除操作
顺序表删除元素ai,必须将ai之后的ai+1、ai+2、...、an-1元素依次向前移动
1、先将ai后的数据依次向前移动
2、将最后一个数据元素释放空间
//3. 顺序表的删除操作
public T remove(int i) //删除第i个元素,0≤i<n,返回被删除元素。若i越界,返回null。//??若i越界,抛出序号越界异常
{
if (this.n>0 && i>=0 && i<this.n)
{
T old = (T)this.element[i]; //old中存储被删除元素
for (int j=i; j<this.n-1; j++)
this.element[j] = this.element[j+1]; //元素前移一个位置
this.element[this.n-1]=null; //设置数组元素对象为空,释放原引用实例
this.n--;
return old; //返回old局部变量引用的对象,传递对象引用
}
return null;
//throw new IndexOutOfBoundsException(i+""); //抛出序号越界异常
}
删除元素操作所花费的 时间主要用于移动元素。删除一个元素平均移动n/2个元素,时间复杂度为O(n)。
顺序表查找操作
顺序查找,在查找过程中,需要将key与顺序表元素逐个比较是否相等。
//4. 顺序查找
//顺序查找首次出现的与key相等元素,返回元素序号i,0≤i<n;查找不成功返回-1。
//若key==null,Java抛出空对象异常NullPointerException
public int search(T key)
{
for (int i=0; i<this.n; i++)
{
if (key.equals(this.element[i])) //执行T类的equals(Object)方法,运行时多态
return i;
}
return -1; //空表或未找到时
}
public boolean contains(T key) //判断是否包含关键字为key元素
{
return this.search(key)!=-1;
}
顺序查找的次数取决于元素位置,T(n)=O(n)
顺序表的浅拷贝与深拷贝
浅拷贝:一个类的拷贝构造方法通常实现为成员变量逐域赋值,即将当前对象的各成员变量赋值为实际参数对应的各成员变量值,称为浅拷贝。
public SeqList(SeqList<T> list) //浅拷贝构造方法,复制对象
{
this.n = list.n; //int整数赋值,复制了整数值
this.element = list.element; //数组引用赋值,两个变量共用一个数组,错误
}
数组是引用数据类型,数组引用赋值传递了数组的引用信息,没有申请新的存储空间,this和list引用同一个数组,修改、插入、删除等操作时会同时结果相互影响
e.g:
= new SeqList<String>(lista);
lista.remove(0);
lista = SeqList(B,C,D,E), listb =
Exception in thread "main" java.lang.NullPointerException
在lista中进行remove(0)操作是对数组进行删除,实际上也删除了listb的元素,但是listb的结构没有改变。
深拷贝:不仅要复制对象的所有基本类型成员变量值,还要为引用类型变量申请存储空间,并复制其中所有元素。
①
public SeqList(SeqList<? extends T> list) //拷贝构造方法,深拷贝,复制list
{
this.n = list.n;
this.element = new Object[list.element.length]; //申请一个数组
for (int i=0; i<list.n; i++) //复制list所有元素,O(n)
this.element[i] = list.element[i]; //对象引用赋值,没有创建新对象
}
}
这里的深拷贝申请了新的数组空间,进行插入和删除操作不会影响对方对象。
但是由于对象赋值是引用赋值,深拷贝没有复制元素对象,修改其中的实例,仍会影响对方。 如下图
②
∴深拷贝应该 复制每个引用类型成员变量所引用的数组或对象,直至该对象可达的所有对象。如下图所示。
顺序表比较相等
两个线性表相等是指,它们各对应元素相等并且长度相等。
SeqList<T>类声明覆盖equals(Object)方法
//6. 顺序表比较相等
public boolean equals(Object obj) //比较两个顺序表是否相等。覆盖。O(n)
{
if (this==obj) //若this和obj引用同一个顺序表实例,则相等
return true;
if (obj instanceof SeqList<?>) //若obj引用顺序表实例。SeqList<?>是所有SeqList<T>的父类
{
SeqList<T> list = (SeqList<T>)obj; //声明list也引用obj引用的实例
if (this.n==list.n) //比较两者长度是否相等
{
for (int i=0; i<this.n; i++) //比较两个顺序表的所有元素是否相等
if (!(this.get(i).equals(list.get(i))))//equals(Object)运行时多态
return false;
return true;
}
}
return false;
}
时间复杂度是O(n)。