一、什么是顺序表
概念:顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
如下图:
优点:访问速度比较快,在给定下标的情况下时间复杂度低至O(1)
因此,顺序表适用于经常进行 下标查找 或者 更新 的操作
那么下面我们就来试着自己实现一个顺序表MyArrayList,并实现基础的增删查改的操作吧
二、MyArrayList的实现
1、定义MyList接口
public interface MyList {
// 新增元素,默认在数组最后新增
void add(int data);
// 在 pos 位置新增元素
void add(int pos, int data);
// 判定是否包含某个元素
boolean contains(int toFind);
// 查找某个元素对应的位置
int indexOf(int toFind);
// 获取 pos 位置的元素
int get(int pos);
// 给 pos 位置的元素设为 value -> 更新
void set(int pos, int value);
//删除第一次出现的关键字key
void remove(int toRemove);
// 获取顺序表长度
int size();
// 清空顺序表
void clear();
// 打印顺序表,
void display();
boolean isFull();
}
2、MyArrayList实现接口
1、定义成员变量与构造方法
public int[] elem;
public int usedSize;
public MyArrayList() {
this.elem = new int[10];
}
2、添加元素add
(1)尾部添加元素
public void add(int data) {
//判断是否满了,如果满了就扩容
if(isFull()) {
//扩容
elem = Arrays.copyOf(elem,2*elem.length);
}
this.elem[usedSize] = data;
this.usedSize++;
}
(2)指定下标添加元素
public void add(int pos, int data) {
try {
checkPosOfAdd(pos);
}catch (PosNotLegalException e) {
e.printStackTrace();
}
if(isFull()) {
elem = Arrays.copyOf(elem,2*elem.length);
}
//移动元素
for (int i = usedSize-1; i >= pos; i--) {
elem[i+1] = elem[i];
}
//插入元素
elem[pos] = data;
usedSize++;
}
/*
该方法来 判断 添加元素时 pos是否合法
*/
private void checkPosOfAdd(int pos) throws PosNotLegalException{
if(pos < 0 || pos > usedSize) {
throw new PosNotLegalException("pos位置不合法!");
}
}
3、判断是否包含某个元素
public boolean contains(int toFind) {
//只需要寻找usedSize次
for (int i = 0; i < usedSize; i++) {
if(elem[i] == toFind) {
return true;
}
}
return false;
}
4、查找某个元素对应的位置
public int indexOf(int toFind) {
for (int i = 0; i < usedSize; i++) {
if(elem[i] == toFind) {
return i;
}
}
return -1;
}
5、获取或修改某一下标的值
注意:在获取或修改某一下标元素值时,要判断所给下标是否合法,如果不合法应给出相应的错误提示
(1)判断下标合法性
private void checkPosOfGetAndSet(int pos) throws PosNotLegalException{
if(pos < 0 || pos >= usedSize) {
throw new PosNotLegalException("get/set获取元素的时候" +
"pos位置不合法!");
}
}
(2)获取
public int get(int pos) {
try {
checkPosOfGetAndSet(pos);
}catch (PosNotLegalException e) {
e.printStackTrace();
}
return elem[pos];
}
(3)修改
public void set(int pos, int value) {
try {
checkPosOfGetAndSet(pos);
}catch (PosNotLegalException e) {
e.printStackTrace();
}
elem[pos] = value;
}
7、删除某一下标元素的值
public void remove(int toRemove) {
//1、要查找是否存在要删除的关键字 toRemove
int pos = indexOf(toRemove);
if(pos == -1) {
System.out.println("没有要删除的数字!");
return;
}
for (int i = pos; i < usedSize-1; i++) {
elem[i] = elem[i+1];
}
usedSize--;
}
8、清空顺序表
public void clear() {
/*for (int i = 0; i < usedSize; i++) {
elem[i] = null;
}*/
usedSize = 0;
}
9、打印顺序表
public void display() {
for (int i = 0; i < usedSize; i++) {
System.out.print(elem[i] +" ");
}
System.out.println();
}
10、其它方法
public int size() {
return usedSize;
}
public boolean isFull() {
return usedSize == elem.length;
}
public boolean isEmpty() {
return usedSize == 0;
}
三、ArrayList
1、简介
实际上像MyArrayList那样自己定义一个顺序表也是不难的,不过在java中已经帮我们写好了ArrayList类了,我们只需要会调用即可。像前文中编写的MyArrayList类只是为了让我们更好地去理解ArrayList的底层实现,从而在应用时可以更加得心应手。
注意:
1. ArrayList 是以泛型方式实现的,使用时必须要先实例化 2. ArrayList 实现了 RandomAccess 接口,表明 ArrayList 支持随机访问 3. ArrayList 实现了 Cloneable 接口,表明 ArrayList 是可以 clone 的 4. ArrayList 实现了 Serializable 接口,表明 ArrayList 是支持序列化的 5. 和 Vector 不同, ArrayList 不是线程安全的,在单线程下可以使用,在多线程中可以选择 Vector 或者CopyOnWriteArrayList 6. ArrayList 底层是一段连续的空间,并且可以动态扩容,是一个动态类型的顺序表
2、构造
public static void main(String[] args) {
// ArrayList创建,推荐写法
// 构造一个空的列表
List<Integer> list1 = new ArrayList<>();
// 构造一个具有10个容量的列表
List<Integer> list2 = new ArrayList<>(10);
list2.add(1);
list2.add(2);
list2.add(3);
// list2.add("hello"); // 编译失败,List<Integer>已经限定了,list2中只能存储整形元素
// list3构造好之后,与list中的元素一致
ArrayList<Integer> list3 = new ArrayList<>(list2);
// 避免省略类型,否则:任意类型的元素都可以存放,使用时将是一场灾难
List list4 = new ArrayList();
list4.add("111");
list4.add(100);
}
3、常见方法
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("JavaSE");
list.add("JavaWeb");
list.add("JavaEE");
list.add("JVM");
list.add("测试课程");
System.out.println(list);
// 获取list中有效元素个数
System.out.println(list.size());
// 获取和设置index位置上的元素,注意index必须介于[0, size)间
System.out.println(list.get(1));
list.set(1, "JavaWEB");
System.out.println(list.get(1));
// 在list的index位置插入指定元素,index及后续的元素统一往后搬移一个位置
list.add(1, "Java数据结构");
System.out.println(list);
// 删除指定元素,找到了就删除,该元素之后的元素统一往前搬移一个位置
list.remove("JVM");
System.out.println(list);
// 删除list中index位置上的元素,注意index不要超过list中有效元素个数,否则会抛出下标越界异常
list.remove(list.size()-1);
System.out.println(list);
// 检测list中是否包含指定元素,包含返回true,否则返回false
if(list.contains("测试课程")){
list.add("测试课程");
}
// 查找指定元素第一次出现的位置:indexOf从前往后找,lastIndexOf从后往前找
list.add("JavaSE");
System.out.println(list.indexOf("JavaSE"));
System.out.println(list.lastIndexOf("JavaSE"));
// 使用list中[0, 4)之间的元素构成一个新的SubList返回,但是和ArrayList共用一个elementData数组
List<String> ret = list.subList(0, 4);
System.out.println(ret);
list.clear();
System.out.println(list.size());
}
4、遍历
ArrayList 可以使用三方方式遍历: for 循环 + 下标、 foreach 、使用迭代器public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);
// 使用下标+for遍历
for (int i = 0; i < list.size(); i++) {
System.out.print(list.get(i) + " ");
}
System.out.println();
// 借助foreach遍历
for (Integer integer : list) {
System.out.print(integer + " ");
}
System.out.println();
Iterator<Integer> it = list.listIterator();
while(it.hasNext()){
System.out.print(it.next() + " ");
}
System.out.println();
}
注意:
1. ArrayList 最长使用的遍历方式是: for 循环 + 下标 以及 foreach 2. 迭代器是设计模式的一种,后序容器接触多了再给大家铺垫
5、扩容机制
ArrayList 是一个动态类型的顺序表,即:在插入元素的过程中会自动扩容。以下是 ArrayList 源码中扩容方式:Object[] elementData; // 存放元素的空间
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; // 默认空间
private static final int DEFAULT_CAPACITY = 10; // 默认容量大小
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private void grow(int minCapacity) {
// 获取旧空间大小
int oldCapacity = elementData.length;
// 预计按照1.5倍方式扩容
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果用户需要扩容大小 超过 原空间1.5倍,按照用户所需大小扩容
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果需要扩容大小超过MAX_ARRAY_SIZE,重新计算容量大小
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 调用copyOf扩容
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
// 如果minCapacity小于0,抛出OutOfMemoryError异常
if (minCapacity < 0)
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}
总结:
1. 检测是否真正需要扩容,如果是调用 grow 准备扩容 2. 预估需要库容的大小 初步预估按照1.5 倍大小扩容 如果用户所需大小超过预估1.5 倍大小,则按照用户所需大小扩容 真正扩容之前检测是否能扩容成功,防止太大导致扩容失败 3. 使用 copyOf 进行扩容
四、总结
顺序表的优点:
- 适合下标查找和更新的场景
缺点:
- 1、不方便进行插入和删除操作,因为要移动数组元素,最坏情况下时间复杂度会达到O(n)
- 2、扩容可能会浪费空间,例如长度为100的顺序表放满了,这时插入1个元素,顺序表就会扩容1.5倍,即多50个位置但实际只存储了1个元素,造成空间浪费
那么本篇文章就到此为止了,如果觉得这篇文章对你有帮助的话,可以点一下关注和点赞来支持作者哦。作者还是一个萌新,如果有什么讲的不对的地方欢迎在评论区指出,希望能够和你们一起进步✊