本节目标:
- 了解线性表和顺序表
- 能够实现简单的顺序表及其基本操作
- 认识 ArrayList类并且知道如何去使用
本篇文章正式进入数据结构!进入之前,先了解一下什么是线性表和顺序表。
1.线性表与顺序表
线性表
线性表( linear list ) 是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列...
它在逻辑上是线性结构,也就是说是连续的一条直线,但是在物理结构上不一定是连续的。线性表在物理上储存时,通常以数组和链式结构的形式储存,例如:
顺序表
顺序表是用一段物理地址连续的存储单元异常储存数据元素的线性结构,一般情况下采用数组储存。在数组上完成数据的增删查改操作。
2.实现简单的顺序表及其基本操作
在进入 ArrayList 前,我们可以先自己实现一个简单的顺序表和它的一些基本功能,在这个过程中,要特别记住,数据结构的两个特点:
- 抽象,要有大概的结构
- 逻辑非常的严谨
我们实现这个顺序表的过程中,会围绕着这两点展开!
这是我们要实现的顺序表和它的一些操作:
public class SeqList {
private int[] array;
private int size;
// 默认构造方法
SeqList(){ }
// 将顺序表的底层容量设置为initcapacity
SeqList(int initcapacity){ }
// 新增元素,默认在数组最后新增
public void add(int data) { }
// 在 pos 位置新增元素
public void add(int pos, int data) { }
// 判定是否包含某个元素
public boolean contains(int toFind) { return true; }
// 查找某个元素对应的位置
public int indexOf(int toFind) { return -1; }
// 获取 pos 位置的元素
public int get(int pos) { return -1; }
// 给 pos 位置的元素设为 value
public void set(int pos, int value) { }
//删除第一次出现的元素
public void remove(int toRemove) { }
// 获取顺序表长度
public int size() { return 0; }
// 清空顺序表
public void clear() { }
// 打印顺序表
public void display() { }
//这个方法是为了看测试结果才弄的!
}
从易到难,一个一个弄。
构造方法
两个构造方法比较简单:
public SeqList() {}public SeqList(int initcapacity) {this.array = new int[initcapacity];}
接着轮到打印顺序表的方法
打印顺序表
对于打印操作,我们的思路:对存放数据的数组进行遍历,一个个打印,至于数组中存放有多少个数据,用 size 来表示数组中储存的元素个数。
public void display() {for (int i = 0; i < this.size; i++) {System.out.println(array[i] + " ");}}
接着轮到获取顺序表长度的方法
获取顺序表长度
//获取顺序表长度public int getSize() {return this.size;}
接着轮到判定顺序表是否包含某个元素的方法
判定顺序表是否包含某个元素
要求:判断顺序表是否包含某个元素,若包含则返还true,否则返回false。
思路:可以对顺序表进行遍历,如果在遍历的过程中找到了这个元素,那么返回true,如果遍历结束了,还没有找到这个元素,就说明顺序表中不包含这个元素,那么返回false。
//判断顺序表是否包含某个元素,若包含则返还true,否则返回false。public boolean contains(int toFind) {for (int i = 0; i < this.size; i++) {if (array[i] == toFind) {return true;}}return false;}
接着轮到查找某个元素在顺序表的位置的方法
查找某个元素在顺序表的位置
要求:查找某个元素在顺序表的位置,如果顺序表包含这个元素,就返回它在顺序表中对应的位置,否则返回-1。
思路:思路与判定顺序表是否包含某个元素差不多。
//查找某个元素在顺序表的位置,如果顺序表包含这个元素,就返回它在顺序表中对应的位置,否则返回-1;public int indexOf(int toFind) {for (int i = 0; i < this.size; i++) {if (array[i] == toFind) {return i;}}return -1;}
简单的弄完了,现在稍微上上强度了,需要考虑的事变多了。
新增元素1
要求:新增元素,在数组最后的位置新增。
思路:我们前面说过,使用size来记录数组中储存的元素个数,那么size相当于是现在数组的最后一个位置,那么直接将要新增的元素放到 array[size],接着size++就好了。
思路很清晰,但是现在有一个问题,如果数组满了怎么办?这样子可放不进去了,那么我们需要在新增的元素之前,判断一下数组是否满了,如果满了,需要对它进行扩容,这样才能放入新的元素。
//新增元素1public void add(int data) {if (isFull()) {scalingArray();}this.array[size] = data;this.size++;}//判断数组是否满了private boolean isFull() {return this.size == array.length;}//对数组进行扩容private void scalingArray() {this.array = Arrays.copyOf(this.array,2*this.array.length);//扩容为原来的两倍}
这里判断数组是否满了的方法与扩容数组的方法被private修饰,原因是我们不希望通过外部去直接访问它们,在内部使用即可!
在编写新增元素1的方法的过程中,发现:虽然看起来并不难实现,但是存在着许多的小细节,一不小心就会忽视,这就与之前说的数据结构的两个特点对应上了!
新增元素2
要求:新增元素,在指定位置新增元素。
思路:假设指定位置是pos,要在pos位置新增元素,那么只需要将pos及其后面的元素向后移动即可,把pos位置腾出空间,同时进行判满和是否扩容,最后size++即可。
看起来没啥问题合理,但是我们忽略了一件重要的是:pos一定合法吗?如果pos < 0 或者 pos > size(顺序表不允许脱节),那我们不就炸了!因此还得处理pos合不合法的问题。 对应它不合法的情况,我们可以自定义一个异常类,当pos不合法时抛出异常,接着通过try-catch处理即可。
自定义异常类(pos不合法):
public class PosIllegal extends RuntimeException{public PosIllegal() {}public PosIllegal(String str) {super(str);}
}
新增元素2的方法:
//新增元素2public void add(int pos,int data) {try {isIllegal(pos);if (isFull()) {scalingArray();}for (int i = this.size - 1; i >= pos; i--) {array[i + 1] = array[i];}this.array[pos] = data;this.size++;}catch (PosIllegal e) {e.printStackTrace();}}//判断pos是否合法private void isIllegal(int pos) {if(pos < 0 || pos > this.size) {throw new PosIllegal("pos不合法!");}}
}
获取某个位置的元素
要求:通过传入下标pos获取pos位置的元素。
思路:和之前一样,在获取数据之前要对pos进行判断是否合法,但是这次判断pos合不合法与之前的不同,因为我们不能获取pos = size位置的元素,因此pos的范围只能是 pos >= 0 || pos < size。不仅如此,在判断pos是否合法前,我们还要判断数组是不是空的,如果是空的,那没办法获取元素。可以写一个自定义异常类来解决数组为空的情况,当数组为空时,抛出异常;如果数组不为空,则返回要获取的元素,若数组中没有该元素,则返回-1.
自定义异常类(数组为空):
public class EmptyException extends RuntimeException{public EmptyException() {}public EmptyException(String str) {super(str);}
}
获取某个位置元素的方法:
public int get(int pos) {try {isEmpty();isIllegal_1(pos);return this.array[pos];}catch (PosIllegal e) {e.printStackTrace();}catch (EmptyException e) {e.printStackTrace();}return -1;}//判断数组是否为空private void isEmpty() {if(this.size == 0) {throw new EmptyException("数组为空!");}}//判断pos是否合法——新的标准private void isIllegal_1(int pos) {if (pos < 0 || pos >= this.size) {throw new PosIllegal("pos不合法! ");}}
将某个位置的元素进行更改
要求:将下标为pos的元素进行更改。
思路:与上一个操作类似,第一,我们需要判断数组是否为空;第二我们需要对pos判断是否合法,这里pos的范围与上一个操作一样,最后用新的元素将pos位置原来的元素覆盖即可。
//更改某个位置的元素public void set(int pos,int value) {try {isEmpty();isIllegal_1(pos);this.array[pos] = value;}catch (PosIllegal e) {e.printStackTrace();}catch (EmptyException e) {e.printStackTrace();}}
删除第一次出现的元素
要求:将数组中第一次出现的元素删除。
思路:通过查找某个元素在顺序表的位置方法,查找要删除的元素的下标,接着让它后面的元素向前移动,把它覆盖掉,这样就达到了删除的目的,如果数组中没有这个元素,那么就是给出提示“数组中没有该元素”,最后令 size-- 即可。
//删除第一次出现的元素public void remove(int toRemove) {int x = indexOf(toRemove);if (x == -1) { //x 等于-1说明找不到System.out.println("数组中没有该元素!");return;}for (int i = x; i < this.size - 1; i++) {this.array[i] = this.array[i + 1];}this.size--;}
清空顺序表
要求:将顺序表中的所有元素清除。
思路:在这里,我们是通过数组去实现顺序表的,而且决定顺序表中是否存在元素的是 元素个数size,那么我们直接令 size = 0即可!但这仅仅是对于基本数据类型数组而言,对于引用数据类型数组则需要将数组中的元素一个个指向null。
//清空顺序表public void clear() {this.size = 0;}
总结
至此,我们就实现了一个简单的顺序表。
import java.util.Arrays;public class SeqList {private int[] array;private int size;//构造方法public SeqList() {}public SeqList(int initcapacity) {this.array = new int[initcapacity];}//打印顺序表public void display() {for (int i = 0; i < this.size; i++) {System.out.println(array[i] + " ");}}//获取顺序表长度public int getSize() {return this.size;}//判断顺序表是否包含某个元素,若包含则返还true,否则返回false。public boolean contains(int toFind) {for (int i = 0; i < this.size; i++) {if (array[i] == toFind) {return true;}}return false;}//查找某个元素在顺序表的位置,如果顺序表包含这个元素,就返回它在顺序表中对应的位置,否则返回-1;public int indexOf(int toFind) {for (int i = 0; i < this.size; i++) {if (array[i] == toFind) {return i;}}return -1;}//新增元素1public void add(int data) {if (isFull()) {scalingArray();}this.array[size] = data;this.size++;}//判断数组是否满了private boolean isFull() {return this.size == array.length;}//对数组进行扩容private void scalingArray() {this.array = Arrays.copyOf(this.array,2*this.array.length);//扩容为原来的两倍}//新增元素2public void add(int pos,int data) {try {isIllegal(pos);if (isFull()) {scalingArray();}for (int i = this.size - 1; i >= pos; i--) {array[i + 1] = array[i];}this.array[pos] = data;this.size++;}catch (PosIllegal e) {e.printStackTrace();}}//判断pos是否合法private void isIllegal(int pos) {if(pos < 0 || pos > this.size) {throw new PosIllegal("pos不合法!");}}//获取某个位置元素public int get(int pos) {try {isEmpty();isIllegal_1(pos);return this.array[pos];}catch (PosIllegal e) {e.printStackTrace();}catch (EmptyException e) {e.printStackTrace();}return -1;}//判断数组是否为空private void isEmpty() {if(this.size == 0) {throw new EmptyException("数组为空!");}}//判断pos是否合法2private void isIllegal_1(int pos) {if (pos < 0 || pos >= this.size) {throw new PosIllegal("pos不合法! ");}}//更改某个位置的元素public void set(int pos,int value) {try {isEmpty();isIllegal_1(pos);this.array[pos] = value;}catch (PosIllegal e) {e.printStackTrace();}catch (EmptyException e) {e.printStackTrace();}}//删除第一次出现的元素public void remove(int toRemove) {int x = indexOf(toRemove);if (x == -1) { //x 等于-1说明找不到System.out.println("数组中没有该元素!");return;}for (int i = x; i < this.size - 1; i++) {this.array[i] = this.array[i + 1];}this.size--;}//清空顺序表public void clear() {this.size = 0;}
}
3.ArrayList类
3.1 ArrayList的简介
在集合框架中,ArrayList是一个普通的类,它继承了一些抽象类和实现了一些接口,具体如下:
【注意事项】
- ArrayList是以泛型方式实现的,使用时必须要先实例化。
- ArrayList实现了RandomAccess接口,表明ArrayList支持随机访问。
- ArrayList实现了Cloneable接口,表明ArrayList是可以clone的。
- ArrayList实现了Serializable接口,表明ArrayList是支持序列化的。
- 和Vector不同,ArrayList不是线程安全的,在单线程下可以使用,在多线程中可以选择Vector或者 CopyOnWriteArrayList。
- ArrayList底层是一段连续的空间,并且可以动态扩容,是一个动态类型的顺序表。
3.2 ArrayList的使用
ArrayList的构造
方法 | 解释 |
---|---|
ArrayList() | 无参构造 |
ArrayList(Collection<? extends E> c) | 利用其他 Collection 构造 ArrayList |
ArrayList(int initialCapacity) | 指定顺序表的初始容量 |
这里对第二种构造方法做解释:它的参数 Collection<? extends E> c ,说明传入这个方法的参数,必须要实现Collection接口,并且要么是E,要么是E的子类。
它们的使用通常如下:
import java.util.ArrayList;public class Main {public static void main(String[] args) {//创建一个空的顺序表ArrayList<Integer> arrayList1 = new ArrayList<>();//创建一个具有10个初始容量的顺序表ArrayList<Integer> arrayList2 = new ArrayList<>(10);arrayList2.add(1);arrayList2.add(2);arrayList2.add(3);//第二种构造方法的使用ArrayList<Integer> arrayList3 = new ArrayList<>(arrayList2);//因为 arrayList2 的类型也为Integer并且实现了Collection接口,因此它能作为参数传入}
}
ArrayList的常见操作
ArrayList虽然提供的方法比较多,但是常用方法如下所示:
方法 | 解释 |
---|---|
boolean add(E e) | 尾插 e |
void add(int index, E element) | 将 e 插入到 index 位置 |
boolean addAll(Collection c) | 尾插 c 中的元素 |
E remove(int index) | 删除 index 位置元素 |
boolean remove(Object o) | 删除遇到的第一个 o |
E get(int index) | 获取下标 index 位置元素 |
E set(int index, E element) | 将下标 index 位置元素设置为 element |
void clear() | 清空 |
boolean contains(Object o) | 判断 o 是否在线性表中 |
int indexOf(Object o) | 返回第一个 o 所在下标 |
int lastIndexOf(Object o) | 返回最后一个 o 的下标 |
List subList(int fromIndex, int toIndex) | 截取部分 list |
在这些常用方法中,除了 截取部分 的方法,其他的我们在简单顺序表中基本实现了,这里介绍一下截取部分方法。subList的参数还是很好懂的,fromIndex表示开始截取的位置,toIndex表示截取结束的位置,范围是:[fromIndex,toIndex),但是需要注意它的返回值,它的返回值是List,不是ArrayList,而List是一个接口,ArrayList是一个实现了List接口的具体类,因此使用这个方法时,需要用List的引用去接收方法的返回值。举例:
import java.util.ArrayList;
import java.util.List;public class Main_1 {public static void main(String[] args) {ArrayList<Integer> arrayList = new ArrayList<>(10);arrayList.add(1);arrayList.add(2);arrayList.add(3);arrayList.add(4);arrayList.add(5);System.out.println(arrayList);List<Integer> arrayList1 = arrayList.subList(0,2);System.out.println(arrayList1);}
}//运行结果
[1, 2, 3, 4, 5]
[1, 2]
subList需要注意的几点:
- 该方法返回的是原列表中从
fromIndex
(包含)到toIndex
(不包含)的视图,而不是一个新的独立列表- 对返回的子列表进行修改会影响原列表,反之亦然
- 如果原列表发生结构性修改(如添加、删除元素),再操作子列表会抛出
ConcurrentModificationException
比如说这样:
import java.util.ArrayList;
import java.util.List;public class Main_1 {public static void main(String[] args) {ArrayList<Integer> arrayList = new ArrayList<>(10);arrayList.add(1);arrayList.add(2);arrayList.add(3);arrayList.add(4);arrayList.add(5);System.out.println(arrayList);List<Integer> arrayList1 = arrayList.subList(0,2);System.out.println(arrayList1);arrayList1.set(0,99);System.out.println(arrayList);System.out.println(arrayList1);}
}//运行结果
[1, 2, 3, 4, 5]
[1, 2]
[99, 2, 3, 4, 5]
[99, 2]
3.3 ArrayList的遍历
ArrayList可以使用三种方式进行遍历,分别是 for循环+下标、foreach、使用迭代器。
for循环+下标
import java.util.ArrayList;public class Test {public static void main(String[] args) {ArrayList<Integer> arrayList = new ArrayList<>();arrayList.add(1);arrayList.add(2);arrayList.add(3);arrayList.add(4);arrayList.add(5);for (int i = 0; i < arrayList.size(); i++) {System.out.print(arrayList.get(i) + " ");}}
}//运行结果
1 2 3 4 5
foreach
import java.util.ArrayList;public class Test {public static void main(String[] args) {ArrayList<Integer> arrayList = new ArrayList<>();arrayList.add(1);arrayList.add(2);arrayList.add(3);arrayList.add(4);arrayList.add(5);for (Integer e:arrayList) { //这里是 int e也行,因为在循环过程中会发生自动拆箱System.out.print(e + " ");}}
}
使用迭代器
迭代器是一种接口,它定义了遍历集合元素的规范,包含三个核心抽象方法:
boolean hasNext()
:判断是否还有下一个元素E next()
:获取下一个元素default void remove()
:删除最近一次通过next()
获取的元素(JDK 8 后新增的默认方法)
这里我们可以使用分别使用两个迭代器去比遍历顺序表,它们是 Iterator 和 listIterator。具体使用方式如下:
import java.util.ArrayList;
import java.util.Iterator;
import java.util.ListIterator;public class Test {public static void main(String[] args) {ArrayList<Integer> arrayList = new ArrayList<>();arrayList.add(1);arrayList.add(2);arrayList.add(3);arrayList.add(4);arrayList.add(5);Iterator<Integer> it = arrayList.iterator();while(it.hasNext()) {System.out.print(it.next() + " ");}System.out.println();System.out.println("================");ListIterator<Integer> listIterator = arrayList.listIterator();while(listIterator.hasNext()) {System.out.print(listIterator.next() + " ");}}
}//运行结果
1 2 3 4 5
================
1 2 3 4 5
发现这里它们都可以实现遍历顺序表。
3.4 ArrayList的扩容机制
ArrayList是一个动态类型的顺序表,即:在插入元素的过程中会自动扩容。具体过程通常如下:
1.检测是否真正需要扩容,如果是调用grow准备扩容
2.预估需要库容的大小
- 初步预估按照1.5倍大小扩容
- 如果用户所需大小超过预估1.5倍大小,则按照用户所需大小扩容
- 真正扩容之前检测是否能扩容成功,防止太大导致扩容失败
3.使用copyOf进行扩容
3.5 ArrayList的缺点
- ArrayList底层使用连续的空间,任意位置插入或删除元素时,需要将该位置后序元素整体往前或者往后搬移,故时间复杂度为O(N)
- 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
- 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。
到此,ArrayList和顺序表的内容完结!感谢您的阅读,如有错误,还请指出!