线性表的顺序存储
线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素。
线性表(a1,a2,a3,…,an)的顺序存储示意图如上图。
首先,介绍一下线性表的顺序存储结构:
Java语言(其他语言一样)种用一维数组来实现顺序存储结构,即第一个元素存放到数组中下标为0的位置上,当然也可以是下标为1,这个都可以。然后把线性表相邻的元素存储在数组中相邻的位置。
在本文,不管是我们的ArrayList和LinkedList,我们都会继承我们事先准备好的List接口,让这两个类去实现List接口中的方法。下面先看一下List接口代码。
import java.util.Iterator;
/**
* @author 七夏
* @param <E>
*/
public interface List<E> extends Iterable<E> {
int getSize(); //获取线性表中元素的有效个数
boolean isEmpty(); //判断线性表是否为空表
void add(int index,E e); //在线性表指定的index角标出插入元素e
void addFirst(E e); //在线性表的表头处插入元素e
void addLast(E e); //在线性表的表尾处插入元素
E get(int index); //获取线性表中的指定角标index处的元素
E getFirst(); //获取表头的元素
E getLast(); //获取表尾元素
void set(int index,E e); //修改线性表中指定角标index处的元素
boolean contains(E e); //判断线性表中是否包含元素e
int find(E e); //查找元素e的角标(从左到右默认第一个出现的元素角标)
E remove(int index); //删除并返回线性表中指定角标index的元素
E removeFirst(); //删除并返回表头元素
E removeLast(); //删除并返回表尾元素
void removeElement(E e); //删除指定元素
void clear(); //清空线性表
}
该接口我们继承了Iterable接口,所以在我们的ArrayList类中,我们要实现其Iterable中的方法Iterator(),即迭代器。并且我们在实现Iterator这个方法的时候是写一个内部类来实现Iterator这个接口中的haxNext和next方法。
线性表的顺序存储结构代码
package DS01.动态数组;
import java.util.Iterator;
/**
* @author 七夏
* @version 1.0
* @param <E>
* 创建线性表List的顺序存储结构实现类ArrayList
*/
public class ArrayList<E>implements List<E>{
//声明一个E类型的一维数组
private E[] data;
//维护元素个数
private int size;
//默认最大容量为10
private static int DEFAULT_CAPACITY = 10;
//创建一个为默认容量的顺序表
public ArrayList(){
this(DEFAULT_CAPACITY);
}
//创建一个容量由用户指定的顺序表
public ArrayList(int capacity){
//如果用户指定的容量小于0的话,将会抛出一个异常
//下面的代码中的抛出异常大致类似
if(capacity < 0){
throw new IllegalArgumentException("初始容量不能为负数:"+capacity);
}
data = (E[])(new Object[capacity]);
this.size = 0;
}
//用户传入一个数组,将该数组封装成一个顺序表
public ArrayList(E[] data){
if(data == null){
throw new IllegalArgumentException("数组不能为空:null");
}
this.data = (E[])(new Object[data.length]);
for (int i = 0; i < data.length; i++) {
this.data[i] = data[i];
}
this.size = data.length;
}
}
对线性表有很多操作,文章中主要讲述增,删,查和扩容(修改和查找类似,只要“查”到对应元素将其修改即可)。
- 增
**线性表中,增的最好情况,时间复杂度为O(1);最坏情况下为O(n),因为它平均移动一半的元素。**插入数据时的实现过程如下图:
插入前:
插入后:
@Override
public void add(int index, E e) {
if(index < 0 || index > size){
throw new IllegalArgumentException("角标越界");
}
if(size == data.length){
//扩容,扩为原来的2倍
resize(data.length*2);
}
for(int i = this.size;i > index;i--){
data[i] = data[i-1];
}
data[index] = e;
size++;
}
- 删
**线性表中,增的最好情况,时间复杂度为O(1);最坏情况下为O(n),因为它平均移动一半的元素。**删除的过程和插入相似,只是元素移动的位置相反而已。
@Override
public E remove(int index) {
if(isEmpty()){
throw new IllegalArgumentException("线性表为空");
}
//判断传入的角标是否越界
if(index < 0 || index >=size){
throw new IllegalArgumentException("角标越界");
}
E res = data[index];
for (int i = index+1; i < size; i++) {
data[i-1] = data[i];
}
size--;
if(size <= data.length/4 && data.length/2 >= 10){
resize(data.length/2);
}
return res;
}
- 查
线性表对于查找来说很简单,因为是数组,所以就要看我们需要查找的“标志”是什么。比如,我们想要找到下标为i的元素,或者是我们想要找到一个确定的元素。在此,文章就先说说一下寻找一个确定的元素的情况。
@Override
public int find(E e) {
if(isEmpty()){
throw new IllegalArgumentException("线性表为空");
}
for (int i = 0; i < size; i++) {
if(data[i].equals(e)){
return i;
}
}
return -1;
}
- “扩容”
对于顺序存储的线性表,是用数组来实现的,因为数组在开始创建的时候,其空间的大小可能会远远大于需要存储的元素,所以,我们需要根据存储的元素来大概确定一下数组的容量。并且扩容的方法我们不对外界提供,因此设置为private。
private void resize(int newLength) {
E[] newData = (E[])(new Object[newLength]);
for (int i = 0; i < size; i++) {
newData[i] = data[i];
}
//将新的newData数组的地址赋予原先data数组
data = newData;
}
以上,我们不仅仅有增、删和查,我们还有查找元素,判空,获取有效元素个数,toString等等方法,我们在下面将一一列出(toString方法的重写中,我们用到了StringBuilder)。
这里我们的迭代器是,先定义一个index=-1,让index像遍历数组一样的方式去重写hasNext和next这两个方法:
- ①hasNext函数中,先判断index是否小于size-1,意思就是,index角标是否在顺序表的范围之内(如果index等于size-1时,也就是说此时的index已经是该顺序表的最后一个元素,则说面已经没有下一个元素了);
- ②next方法是,获取index++后的元素(如果,index=-1时,我们就获取它的index++后的元素值,即角标为0的元素)。
@Override
public int getSize() {
return this.size;
}
@Override
public boolean isEmpty() {
return this.size == 0;
}
@Override
public void addFirst(E e) {
add(0,e);
}
@Override
public void addLast(E e) {
add(size,e);
}
@Override
public E get(int index) {
if(isEmpty()){
throw new IllegalArgumentException("线性表为空");
}
if(index < 0 || index >size){
throw new IllegalArgumentException("角标越界");
}
return data[index];
}
@Override
public E getFirst() {
return get(0);
}
@Override
public E getLast() {
return get(size-1);
}
@Override
public void set(int index, E e) {
if(isEmpty()){
throw new IllegalArgumentException("线性表为空");
}
if(index < 0 || index >=size){
throw new IllegalArgumentException("角标越界");
}
data[index] = e;
}
@Override
public boolean contains(E e) {
return find(e) != -1;
}
@Override
public int find(E e) {
if(isEmpty()){
throw new IllegalArgumentException("线性表为空");
}
for (int i = 0; i < size; i++) {
if(data[i].equals(e)){
return i;
}
}
return -1;
}
@Override
public E removeFirst() {
return remove(0);
}
@Override
public E removeLast() {
return remove(size-1);
}
@Override
public void removeElement(E e) {
int index = find(e);
if(index != -1){
remove(index);
}else{
throw new IllegalArgumentException("元素不存在");
}
}
@Override
public void clear() {
size = 0;
data = (E[])(new Object[DEFAULT_CAPACITY]);
}
public int getCapacity(){
return data.length;
}
@Override
public String toString() {
/**
* ArrayList:10/20
* [1,2,3,4,5,6,7,8,9,10]
*/
StringBuilder sb = new StringBuilder();
sb.append(String.format("ArrayList: %d/%d \n",size,data.length));
sb.append('[');
if(isEmpty()){
sb.append(']');
}else{
for (int i = 0; i < size; i++) {
sb.append(data[i]);
if(i == size-1){
sb.append(']');
}else{
sb.append(',');
}
}
}
return sb.toString();
}
@Override
public Iterator<E> iterator(){
return new ArrayListIterator();
}
private class ArrayListIterator implements Iterator{
private int index = -1;
@Override
public boolean hasNext() {
return index < size-1;
}
@Override
public E next() {
index++;
return data[index];
}
}
在顺序存储结构的线性表中,为什么说删除和增加元素都是平均O(n)的时间复杂度呢?
这是因为,如果要插入到第i个位置或者删除第i个位置的元素,则需要移动n-i个元素位置,在概率论中,我们删除或者增加的概率都是相同的,增和删的位置在前,则移动的多,相反移动的少,最终,平均移动的次数和最中间那个元素移动的次数相等,为(n-1)/2。所以时间复杂度为O(n)。