越努力,越幸运!!
如果本文有哪些不对的地方,欢迎各位大佬指正,谢谢!
一、何为顺序表
线性表的顺序表示指的是用一组地址连续的存储单元依次存储线性表的数据元素,这种表示也称作线性表的顺序存储结构或顺序映像。通常,称这种存储结构的线性表为顺序表(Sequential List)。
顺序表的特点就是逻辑上相邻的元素,其物理次序也是相邻。
由于顺序表的特点就是物理地址连续,那么其查找和遍历都是很方便的,但是其删除和新增操作就会很麻烦,尤其是顺序表的长度很大的时候。
二、顺序表的具体实现
1、线性表接口
public interface List<E> extends Iterable<E> {
/**
* <h>判断线性表是否为空</h>为空返回true,不为空返回 fasle
*
* @return 返回boolean值
*/
boolean empty();
/**
* <h>获取线性表的长度</h>
*
* @return 返回线性表的实际有效长度
*/
int size();
/**
* <h>新增线性表元素</h>
*
* @param data 需加入到线性表中的元素
*/
void add(E data);
/**
* <h>在索引位置出插入元素</h>
*
* @param index 插入元素的索引位置
* @param data 需要插入的元素值
*/
void insert(int index, E data);
/**
* <h>获取指定索引位置的元素值</h>
*
* @param index 索引位置
* @return 返回索引对应的元素值
*/
E get(int index);
/**
* <h>获取匹配的元素值的第一个索引值</h>
*
* @param data 需要匹配的元素值
* @return 返回第一个匹配元素的索引值
*/
int getFirstIndex(E data);
/**
* <h>获取匹配的元素值的最后一个索引值</h>
*
* @param data 需要匹配的元素值
* @return 返回最后一个匹配元素的索引值
*/
int getLastIndex(E data);
/**
* <h>删除指定元素</h>
*
* @param data 需要删除的元素
* @return 返回删除的元素值
*/
void delete(E data);
/**
* <h>删除指定索引位置的元素</h>
*
* @param index 需要删除的元素所在的索引位置
* @return 返回删除的元素值
*/
E delete(int index);
}
2、顺序表的具体实现
package com.shadow.list;
import java.util.Arrays;
import java.util.Iterator;
import java.util.NoSuchElementException;
/**
* <h>顺序表<h/>
*
* 实现了线性结构接口List。底层使用数组实现,索引从0开始。
* 顺序表允许存入多个null元素,并且元素按照存入的顺序存储。
*
* @author Shadow
* @date 2021/3/15 15:10
*/
public class ArrayList<E> implements List<E> {
/** 顺序表的长度,顺序表中的有效元素个数 **/
private int size;
/** 用于存储顺序表中元素的数组 **/
private Object[] elementData;
/**
* 构造方法,用于创建一个空顺序表。
* @param initialSize 顺序表的初始容量
*/
public ArrayList(int initialSize) {
elementData = new Object[initialSize];
}
/**
* 构造方法,用于创建一个空顺序表。
* 顺序表的初始容量默认为10
*/
public ArrayList() {
this(10);
}
/**
* 校验顺序表是否为空
* @return 为空,返回true
*/
@Override
public boolean empty() {
return size() == 0;
}
/**
* 获取顺序表的长度
* @return 返回顺序表中的元素个数
*/
@Override
public int size() {
return size;
}
/**
* 在顺序表的末尾追加元素
* @param data 需加入到线性表中的元素
*/
@Override
public void add(E data) {
// 判断是否需要扩容
ensureCapacityHelper(size + 1);
elementData[size++] = data;
}
/**
* 顺序表扩容
* @param minimumSize 顺序表新增/插入元素所需的最小容量
*/
private void ensureCapacityHelper(int minimumSize) {
// 新增元素所需最小容量大于存储数组的长度
if (minimumSize > elementData.length) {
grow(minimumSize);
}
}
private void grow(int minimumSize) {
int oldSize = elementData.length;
int newSize = oldSize + oldSize >> 1;
if (minimumSize > newSize) {
newSize = minimumSize;
}
elementData = Arrays.copyOf(elementData, newSize);
}
/**
* <h>在指定的索引位置处插入元素</h>
* 允许插入的位置[0, size + 1]
* @param index 插入元素的索引位置
* @param data 需要插入的元素值
*/
@Override
public void insert(int index, E data) {
// index处于[0, size]
checkIndex(index);
ensureCapacityHelper(size + 1);
// 插入的是第一个元素或追加到最后一个元素的末尾
if ((index == 0 && size() == 0) || index == size()) {
add(data);
} else {
// 顺序表不为空,并且插入在顺序表的中间位置
System.arraycopy(elementData, index, elementData, index + 1, size - index);
elementData[index] = data;
size ++;
}
}
/**
* 获取指定索引位置的元素值
* @param index 索引位置
* @return 返回索引对应的元素值
*/
@Override
@SuppressWarnings("unchecked")
public E get(int index) {
// 校验索引
checkIndex(index);
return (E) elementData[index];
}
/**
* 获取元素在顺序表中首次出现的索引位置
* @param data 需要匹配的元素值
* @return 如果存在,则返回首次出现的索引位置,不存在,则返回-1
*/
@Override
public int getFirstIndex(E data) {
if (data == null) {
for (int i = 0; i < size; i++) {
if (elementData[i] == null) {
return i;
}
}
} else {
for (int i = 0; i < size; i++) {
if (data.equals(elementData[i])) {
return i;
}
}
}
return -1;
}
/**
* 获取元素在顺序表中最后一次出现的索引位置
* @param data 需要匹配的元素值
* @return 如果存在,则返回首次出现的索引位置,不存在,则返回-1
*/
@Override
public int getLastIndex(E data) {
if (data == null) {
for (int i = size - 1; i > 0; i--) {
if (elementData[i] == null) {
return i;
}
}
} else {
for (int i = size - 1; i > 0; i--) {
if (data.equals(elementData[i])) {
return i;
}
}
}
return -1;
}
/**
* 校验索引是否合法
* @param index 需要校验的索引
*/
private void checkIndex(int index) {
if (index < 0 || index > size) {
throw new IndexOutOfBoundsException("索引不合法 : " + index);
}
}
/**
* 删除指定的元素值,在顺序表中出现的该元素值会被全部删除
* @param data 需要删除的元素
*/
@Override
public void delete(E data) {
if (data == null) {
for (int i = 0; i < size; i++) {
if (elementData[i] == null) {
delete(i);
}
}
} else {
for (int i = 0; i < size; i++) {
if (elementData[i].equals(data)) {
delete(i);
}
}
}
}
/**
* 删除指定索引位置处的元素值
* @param index 需要删除的元素所在的索引位置
* @return 返回被删除的元素值
*/
@Override
public E delete(int index) {
checkIndex(index);
E obj = get(index);
System.arraycopy(elementData, index + 1, elementData, index, size - index - 1);
elementData[--size] = null;
return obj;
}
/**
* 迭代器,用于遍历顺序表
* @return 返回一个迭代器实现类
*/
@Override
public Iterator<E> iterator() {
return new Iter();
}
/**
* <h>迭代器内部类</h>
* 该内部类只实现Iterator接口中的hasNext和next方法,默认方法不进行重写
*/
private class Iter implements Iterator<E> {
/** 下一个元素的索引位置 **/
private int cursor;
@Override
public boolean hasNext() {
return cursor < size;
}
@Override
public E next() {
if (cursor > size) {
throw new NoSuchElementException();
}
return get(cursor++);
}
}
}
三、Java集合中的顺序表ArrayList
我们自己实现了一个顺序表后,我们再来看看Java 集合框架中是如何对顺序表这个数据结构进行实现的。
由于 ArrayList 是实现于 List 接口,在List 接口的基础上又新增了很多功能,这里我们就看最主要的方法的实现,我们只看 List 中有的部分方法。
1、List 接口结构
2、ArrayList 底层实现源码解读
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
// 列表容器的长度,有效元素的个数
private int size;
// 存储列表元素的数组
transient Object[] elementData;
// 构造方法,用于创建一个空的列表
// initialCapacity 为列表的初始容量,可以为0
public ArrayList(int initialCapacity) {
// 如果大于0,则直接初始化列表数组的容量为initialCapacity
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
// 如果初始化容量为0,则直接赋值一个空数组
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
// 构造方法,用于创建一个空的列表
// 直接赋值一个空数组
// 从这里可以看出,如果在不对列表进行设置初始容量的时候,列表的初始容量为0
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// 新增元素,在列表的末尾追加
// 从扩容方法中,可以看出,列表初始的容量为0,
// 第一次加入元素时,对列表进行了扩容,长度为DEFAULT_CAPACITY,即为10
public boolean add(E e) {
// 校验是否需要扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
// 在指定的位置插入元素
public void add(int index, E element) {
// 校验index的合法性
rangeCheckForAdd(index);
// 校验是否需要扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
// 将插入位置及后面的元素全部向后移动一位
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
// 插入元素
elementData[index] = element;
size++;
}
// 校验插入元素时的index的合法性
private void rangeCheckForAdd(int index) {
// index必须在[0,index]范围内
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
// 校验扩容
private void ensureCapacityInternal(int minCapacity) {
// 进一步判断是否需要扩容,以及扩容长度
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 如果列表数组为空,则说明第一次新增元素,扩容到列表默认长度10
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 列表数组不为空,则返回新增所需的最小容量
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
// 存储列表长度修改记录,用于fail-fast机制
modCount++;
// 新增所需的最小容量大于列表数组的长度,则需要扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
// 初始扩容到原容量的一半,即原先为10,则现预扩容到15
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果预扩容的容量依旧不满足新增所需的最小容量,则预扩容偏移向量为新增所需的最小容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果预扩容的容量比数组最大值还要大,那么需要判断是否需要直接扩容到Integer的最大值
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 将列表元素拷贝到新数组,并赋值给elementData
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
// 获取指定元素的索引位置
public E get(int index) {
// 校验索引是否合法
rangeCheck(index);
return elementData(index);
}
// 校验索引
private void rangeCheck(int index) {
// 索引值范围在[0,size)范围内
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
// 删除指定索引位置的元素
public E remove(int index) {
// 校验索引位置
rangeCheck(index);
// 记录当前列表结构修改的次数
modCount++;
E oldValue = elementData(index);
// 需要移动的元素个数
int numMoved = size - index - 1;
if (numMoved > 0)
// 将index + 1往后的列表元素向前移动一位
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 列表size-1,并且将未删除前的最后一位置为null
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
// 删除列表集合中指定的元素值,所有的该元素都会被删除
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
// 删除指定索引位置处的元素
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}
// 将列表集合转为数组
public Object[] toArray() {
return Arrays.copyOf(elementData, size);
}
// 将泛型数组转为数组
// 传入的参数a用来存放列表集合中的元素
@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
// 如果传入的数组a的长度小于当前列表元素个数
if (a.length < size)
// Make a new array of a's runtime type, but my contents:
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
// 判断是否包含指定的元素
public boolean contains(Object o) {
// 查找指定元素值在列表第一次出现的索引位置
return indexOf(o) >= 0;
}
// 查找指定元素值在列表第一次出现的索引位置
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
public Iterator<E> iterator() {
return new Itr();
}
/**
* An optimized version of AbstractList.Itr
*/
private class Itr implements Iterator<E> {
int cursor; // 下一个元素返回的光标位置
int lastRet = -1; // 上一个元素返回的索引位置
int expectedModCount = modCount;
Itr() {}
// 判断是否还有下一个元素
public boolean hasNext() {
// 如果最后一个元素的光标位置等于列表元素个数,
// 则说明已经到列表末尾,没有下一个元素值,此时返回false
return cursor != size;
}
// 获取下一个元素值
@SuppressWarnings("unchecked")
public E next() {
// 校验当前列表在迭代器遍历时,列表结构是否被其他人修改
checkForComodification();
int i = cursor; // 将下一个元素的索引位置赋值给i
// 校验索引值是否合法
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
// 校验索引值是否合法
if (i >= elementData.length)
throw new ConcurrentModificationException();
// 光标移动到下一个元素的索引处
cursor = i + 1;
// 将要获取的元素的索引值赋值给lastRet,并取出元素返回
return (E) elementData[lastRet = i];
}
final void checkForComodification() {
// 如果第一次记录的列表修改记录数与现在的列表的修改记录数不一致,则会抛出异常
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
}
四、总结
1、顺序表的特点
- 顺序表其逻辑次序相邻的元素,在物理次序也一样相邻。即顺序表是存放在一个地址连续的存储单元中。
- 根据索引查找速度快,时间复杂度为O(1),增加和删除速度慢,时间复杂度为O(n)。
2、ArrayList 总结
- ArrayList 从名字就知道其底层是基于数组实现,数组的长度是固定的,为了满足集合长度不固定的特点,会在列表容量不足时,进行扩容。ArrayList 的扩容规则是,每次扩容为当前容量的1.5倍。
- ArrayList 如果没有设置初始容量,那么其初始容量就是0,在第一次新增的时候,扩容为默认容量10。
- 迭代器中每次获取下一个元素都会判断当前列表的结构有没有被修改,即用modCount来进行判断,如果列表结构被改变,即modCount值被改变,则会触发fail-fast机制,抛出ConcurrentModificationException异常。
您都看到这了?如何觉得写的还行,不妨给个赞?
参考文献:数据结构(C语言版)(第2版)-- 2.4 线性表的顺序表示和实现