文章目录
- 顺序表
- 前置知识
- ArrayList的构造
- ArrayList的常用方法
- ArrayList的遍历
- ArrayList的扩容机制
- ArrayList的模拟实现
- ArrayList的相关练习
顺序表
前置知识
顺序表是线性表的一种(底层是数组),另一种是链表,说到线性表,就得了解 List
接口,站在数据结构的角度上,List
接口就是线性表,即n个具有相同类型元素的有限序列,在该序列上可以执行增删查改以及变量等操作。
List
接口继承自Collection
接口,Collection
接口规范了后序容器常用的一些方法,例如求元素个数size()
、添加新元素add(E)/addAll(Collection<? extends E>)
等等,如下图:
了解完Collection
接口的方法,再来看一下List
接口的方法,有很多,只截取一部分:
很多是吧,我们列举常用的,包括接下来要讲的ArrayList
类,常用方法也是这几个:
boolean add(E e)
:尾插evoid add(int index, E element)
:将 e 插入到index位置boolean addAll(Collection<? extends E> c)
:尾插 c 中的所有元素(参数列表的形式我们会在泛型进阶讲解),这里的参数可以接受任何类型的集合,只要这个集合的元素类型是 E 或其子类。E remove(int index)
:删除 index 位置的元素boolean remove(Object o)
:删除遇到的第一个 oE get(int index)
:获取 index 下标位置元素,E set(int index, E element)
:将 index 下标位置元素设置为 elementvoid clear()
:清空容器boolean contains(Object o)
:判断 o 是否在线性表中int indexOf(Object o)
:返回第一个 o 所在的下标int lastindexOf(Object o)
:返回最后一个 o 所在的下标List<E> subList(int fromIndex, int toIndex)
:截取下标区间 [fromIndex, int toIndex] 的元素
【List的使用】
List
是一个接口,并不能直接用来实例化,使用时必须实例化List
的实现类。 在集合框架中,ArrayList
和LinkedList
都实现了List
接口,而ArrayList
就是本篇要介绍的主角。
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。在集合框架中,对应 ArrayList
类。不过要注意的是,这个数组只能依次存储,不能跳跃式的存储,比如,添加新的元素,不能跳过一个下标,存储在后面,必须依次存储。
我们先看ArrayList
类的部分源码:
ArrayList
是以泛型方式实现的,是一个泛型类,注意实例化ArrayList
实现了RandomAccess
接口,表明ArrayList
支持随机访问ArrayList
实现了Cloneable
接口,表明ArrayList
是可以clone
的ArrayList
实现了Serializable
接口,表明ArrayList
是支持序列化的- 另外,
ArrayList
不是线程安全的,在单线程下可以使用,在多线程中可以选择Vector
或CopyOnWriteArrayList
ArrayList
底层是一段连续的空间,并且可以动态扩容,是一个动态类型的顺序表
ArrayList的构造
ArrayList
的构造方法有三个:
ArrayList()
:无参数构造方法ArrayList(Collection<? extends E> c)
:利用其他实现了Collection
接口的容器构造ArrayList(int initialCapacity)
:指定顺序表初始容量的构造方法
public static void main(String[] args) {
ArrayList<Integer> l1 = new ArrayList<>();//无参构造
System.out.println(l1);
ArrayList<Integer> l2 = new ArrayList<>(10);//指定初始容量构造
l2.add(1);
l2.add(2);
System.out.println(l2);
ArrayList<Integer> l3 = new ArrayList<>(l2);//使用l2构造
System.out.println(l3);
}
- 如上代码,我们使用了其他
ArrayList
对象l2
作为参数构造了l3
,即将l2
的所有元素作为l3
初始数据
当然我们可以定义List
接口的引用接收ArrayList
对象:
List<Integer> list = new ArrayList<>();
- 优点:向上转型,从而可以实现多态
- 缺点:无法调用
ArrayList
特有的方法,只能调用ArrayList
中重写List
接口的方法
ArrayList的常用方法
ArrayList
类中的常用方法与上文列举的List
接口中常用方法一致,这里仅给出演示代码,一些小的注意问题在注释给出:
public static void main(String[] args) {
ArrayList<Integer> list1 = new ArrayList<>();
ArrayList<Integer> list2 = new ArrayList<>();
list1.add(1);
list1.add(2);
list1.add(3);
list1.add(4);
list1.add(5);
list1.remove(2);//删除下标为2位置的元素
list1.remove(Integer.valueOf(5));//删除值为5的元素
System.out.println(list1);//打印结果为[1, 2, 4]
list2.add(6);
list2.add(7);
list2.add(8);
list1.addAll(list2);//将list2中的所有元素尾插到list1中
System.out.println(list1);//打印结果为[1, 2, 4, 6, 7, 8]
list1.set(0, 100);//将0下标位置设置为100
System.out.println(list1.get(0));//打印0下标位置的元素100
// list1.add(100, 100); error,报错!因为顺序表只允许连续存储,不允许跳跃式存储
System.out.println(list1.contains(100));//100在表中,打印true
List<Integer> list3 = list1.subList(0, 3);//截取[0, 3)下标位置的元素,返回一个List<Integer>的对象,用list3接收
System.out.println(list3);//打印结果是[100, 2, 4]
}
ArrayList的遍历
前面的演示代码中我们使用System.out.println(对象的引用);
的方式打印表中的数据,这是因为ArrayList
类中重写了toString
方法
现在我们介绍三种ArrayList
的遍历方法:for
、for-each
以及迭代器
直接看代码:
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);
//for循环 + 下标遍历
System.out.println("=====for循环=====");
for (int i = 0; i < list.size(); i++) {
System.out.print(list.get(i) + " ");
}
System.out.println();
//for-each循环
System.out.println("=====for-each循环=====");
for (int x : list) {
System.out.print(x + " ");
}
System.out.println();
for(Integer x : list) {
System.out.print(x + " ");
}
System.out.println();
//使用迭代器
System.out.println("=====使用迭代器=====");
Iterator<Integer> it = list.iterator();
while(it.hasNext()) {
System.out.print(it.next() + " ");
}
System.out.println();
ListIterator<Integer> lIt = list.listIterator();
while(lIt.hasNext()) {
System.out.print(lIt.next() + " ");
}
System.out.println();
System.out.println("=====逆序输出=====");
ListIterator<Integer> lIt1 = list.listIterator(list.size());
while(lIt1.hasPrevious()) {
System.out.print(lIt1.previous() + " ");
}
}
for
循环:利用方法size()
和get()
即可for-each
:以整型为例,:
左边可以是Integer
包装类也可以是int
(自动拆箱)- 迭代器:
Iterator
和ListIterator
都是接口,ListIterator
接口继承了Iterator
接口,使用时调用指定方法即可 - 对于拓展的逆序输出,
ListIterator
可以做到,但是Iterator
没有相关的方法
ArrayList的扩容机制
抛出几个问题:使用ArrayList
的空构造方法实例化的顺序表初始容量是多少?;向顺序表中添加数据时,如果表满了,怎么扩容?
对于这几个问题,我们要观察ArrayList
的源码:
对于ArrayList
实现的接口以及表示的含义前面已经介绍过了,直接看定义的成员变量:
-
serialVersionUID
是Java序列化机制中的一个特殊字段,用于标识类的版本 -
DEFAULT_CAPACITY
是默认的初始化容量,为10 -
EMPTY_ELEMENTDATA
与DEFAULTCAPACITY_EMPTY_ELEMENTDATA
:共同点:
- 它们都是用来表示空
ArrayList
实例的静态final
常量。 - 它们都是
Object
类型的数组。 - 它们都被声明为
private
,只在ArrayList
内部使用。
区别:
EMPTY_ELEMENTDATA
是一个真正的空数组(长度为0),而DEFAULTCAPACITY_EMPTY_ELEMENTDATA
的长度等于DEFAULT_CAPACITY
。- 当添加第一个元素时,如果使用的是
EMPTY_ELEMENTDATA
,则需要创建一个新的数组并设置其大小为DEFAULT_CAPACITY
;如果使用的是DEFAULTCAPACITY_EMPTY_ELEMENTDATA
,则可以直接在原数组上进行调整。 EMPTY_ELEMENTDATA
用于普通的空ArrayList
实例,而DEFAULTCAPACITY_EMPTY_ELEMENTDATA
用于那些初始容量已经设置为默认容量的空ArrayList
实例。
- 它们都是用来表示空
-
elementData
:即顺序表,存储顺序表的元素 -
size
:当前顺序表的有效数据数量
接着我们看一下三个构造方法:
- 初始化顺序表容量的构造方法:当传入的初始化容量为0时,将
EMPTY_ELEMENTDATA
赋值给顺序表elementData
- 无参构造方法:无参构造方法初始容量其实设置为了默认容量,所以赋值
DEFAULTCAPACITY_EMPTY_ELEMENTDATA
- 利用其他
Collection
的构造方法:先将集合c
转化为Object
类型的数组,如果数组不为空,判断c
的类是否为ArrayList
,为真,直接赋值;为假,则通过copyOf
创建新数组并拷贝,用新数组赋值;如果数组为空,直接赋值EMPTY_ELEMENTDATA
到这里,我们解决了第一个问题并且拓展了一些,并考虑一个新的问题,这个问题将和最初的问题2一并解决:空构造方法实例化的表是个空数组,那么添加时怎么操作的?
从add()
方法入手:
如图第二个add
调用了第一个add
方法,而第一个和第三个add
在表满时,均调用了一个grow()
方法
然后,无参grow
又调用了带有参数的grow
方法
首先,将当前容量赋值给oldCapacity
,如果if
语句判断为假(oldCapacity <= 0 && elementData == DEFAULTCAPACITY_EMPTY_ELEMRNTDATA
),执行else
语句,即:当添加第一个元素时,如果使用的是无参构造方法,ArrayList
会将内部数组的容量从0扩展到10,并赋值;如果if
语句判断为真,则调用ArraysSupport.newLength
方法计算新容量newCapacity
,传入的第一个参数是旧容量,第二个参数是,增长的容量,即带参数的grow
的参数与旧容量的差值(由于参数为size + 1
,所以这里是1),第三个参数是旧容量的一半。
newLength
方法接受三个参数:oldLength
(旧数组长度)、minGrowth
(最小增长量)和prefGrowth
(首选增长量)。它首先计算首选长度prefLength
,即旧数组长度加上minGrowth
和prefGrowth
中的较大值。然后,如果首选长度在0到SOFT_MAX_ARRAY_LENGTH
(数组的最大长度限制)之间,就返回这个首选长度;否则,调用hugeLength
方法来处理较大的长度情况。
hugeLength
方法也接受两个参数:oldLength
和minGrowth
。它首先计算最小所需长度minLength
,即旧数组长度加上最小增长量。然后,如果minLength
小于0(表示溢出),就抛出一个OutOfMemoryError
异常,提示所需的数组长度过大。如果minLength
小于等于SOFT_MAX_ARRAY_LENGTH
,则返回SOFT_MAX_ARRAY_LENGTH
;否则,返回minLength
。
【总结】
回归到最初的grow
方法:
- 检测是否真正需要扩容,如果是调用grow准备扩容
- 预估需要库容的大小 初步预估按照1.5倍大小扩容 如果用户所需大小超过预估1.5倍大小,则按照用户所=需大小扩容,真正扩容之前检测是否能扩容成功,防止太大导致扩容失败
- 使用
copyOf
进行扩容 - 另外,如果使用的是无参构造方法,当添加第一个元素时,
ArrayList
会将内部数组的容量从0扩展到10,此后按照上3步
ArrayList的模拟实现
模拟实现一个ArrayList
类是比较简单的,只需要掌握对数组的增删查改即可。
实现方式多种多样,能实现增删查改等业务即可,这里给出模拟实现的代码,感兴趣的可以看一下:
import java.util.Arrays;
public class MyArrayList {
public int[] elem;
public int capacity;
public int usedSize;//0
//默认容量
private static final int DEFAULT_SIZE = 10;
public MyArrayList() {
this.elem = new int[DEFAULT_SIZE];
this.capacity = 10;
}
/**
* 打印顺序表:
* 根据usedSize判断即可
*/
public void display() {
if(this.isEmpty()) {
return;
}
for(int i = 0; i < this.usedSize; i++) {
System.out.print(elem[i] + " ");
}
System.out.println();
}
// 新增元素,默认在数组最后新增
public void add(int data) {
if(isFull()) {
this.elem = Arrays.copyOf(this.elem, 2 * this.capacity);
}
this.elem[this.usedSize] = data;
this.usedSize++;
}
/**
* 判断当前的顺序表是不是满的!
* @return true:满 false代表空
*/
private boolean isFull() {
if(this.usedSize == this.capacity) {
return true;
}else {
return false;
}
}
private boolean checkPosInAdd(int pos) {
//在0位置添加可以,同时在最后位置也可以添加,但是不可以跳着增加
if(pos >= 0 && pos <= this.usedSize) {
return true;
}else {
throw new PosIllegalException("位置不合法!");
}
}
// 在 pos 位置新增元素
public void add(int pos, int data) throws PosIllegalException {
if(isFull()) {
this.elem = Arrays.copyOf(this.elem, 2 * this.capacity);
}
try{
checkPosInAdd(pos);
for(int i = this.usedSize; i > pos; i--) {
this.elem[i] = this.elem[i - 1];
}
this.elem[pos] = data;
}catch (PosIllegalException e) {
System.out.println("输入的位置不合法!插入失败");
e.printStackTrace();
}
}
// 判定是否包含某个元素
public boolean contains(int toFind) {
if(isEmpty()) {
return false;
}
for(int i = 0; i < this.usedSize; i++) {
if(toFind == this.elem[i]) {
return true;
}
}
return false;
}
// 查找某个元素对应的位置
public int indexOf(int toFind) {
if(isEmpty()) {
System.out.println("表为空!");
return -1;
}
for (int i = 0; i < this.usedSize; i++) {
if(toFind == this.elem[i]) {
return i;
}
}
return -1;
}
// 获取 pos 位置的元素
public int get(int pos) throws PosIllegalException {
if(isEmpty()) {
System.out.println("表为空!返回值无效!");
return -1;
}
try {
checkPosInAdd(pos);
return this.elem[pos];
}catch (PosIllegalException e) {
System.out.println("查找的位置不合法,返回值无效!");
e.printStackTrace();
}
return -1;
}
private boolean isEmpty() {
if(this.usedSize == 0) {
return true;
}else {
return false;
}
}
// 给 pos 位置的元素设为【更新为】 value
public void set(int pos, int value) {
if(isEmpty()) {
System.out.println("表为空!");
return;
}else {
try {
checkPosInAdd(pos);
this.elem[pos] = value;
}catch (PosIllegalException e) {
System.out.println("位置不合法!");
e.printStackTrace();
}
}
}
/**
* 删除第一次出现的关键字key
* @param key
*/
public void remove(int key) {
if(isEmpty()) {
System.out.println("表为空");
}
for(int i = 0; i < this.usedSize; i++) {
if(this.elem[i] == key) {
for(int j = i; j < this.usedSize - 1; j++) {
this.elem[j] = this.elem[j + 1];
}
this.usedSize--;
return;
}
}
System.out.println("没有找到关键字!");
return;
}
// 获取顺序表长度
public int size() {
return this.usedSize;
}
// 清空顺序表
public void clear() {
this.usedSize = 0;
}
}
public class PosIllegalException extends RuntimeException {
public PosIllegalException() {
}
public PosIllegalException(String mes) {
super(mes);
}
}
ArrayList的相关练习
给定一个非负整数 numRows
, 生成「杨辉三角」的前 numRows
行。
class Solution {
public List<List<Integer>> generate(int numRows) {
//补充代码
}
}
杨辉三角大家应该不陌生,如下:
1
1 1
1 2 1
1 3 3 1
… …
题目最大的特点是返回值类型:List<List<Integer>>
,即返回一个实现了List
接口的集合,其每一个元素也是这个集合的类型,这意味着集合中的每个元素代表杨辉三角的一行,每个元素又是一个集合,这个集合的每个元素是杨辉三角每一行的每个元素。我们刚刚学习了ArrayList
,所以我们会使用ArrayList
完成。
杨辉三角的第一行一定是一个1,每一行的第一个和最后一个元素一定是1,基于这一点,我们直接实现:
class Solution {
public List<List<Integer>> generate(int numRows) {
ArrayList<List<Integer>> list = new ArrayList<>(numRows);
//第一行的1
ArrayList<Integer> l1 = new ArrayList<>();
l1.add(1);
list.add(l1);
//后续行
for(int i = 1; i < numRows; i++) {
ArrayList<Integer> lTmp = new ArrayList<>();
lTmp.add(1);//第一个元素一定是1
for(int j = 1; j < i; j++) {
lTmp.add(list.get(i - 1).get(j - 1) + list.get(i - 1).get(j));
}//每一行的中间元素
lTmp.add(1);//最后一个元素一定是1
list.add(lTmp);
}
return list;
}
}
可能会出现以下报错(也是裤儿出现的问题):
Line 16: error: incompatible types: ArrayList<ArrayList<Integer>> cannot be converted to List<List<Integer>> return list;
因为:
ArrayList<ArrayList>
和ArrayList<List>
之间的类型不兼容,不能直接进行互相转换。
在Java泛型中,类型擦除会导致泛型类型的具体信息在编译时被擦除,因此在运行时,泛型类型的实例并不知道它们的类型参数的具体类型。这意味着,尽管ArrayList
是List
的子类,但在泛型类型中,它们被视为不同的类型。
原题链接:118. 杨辉三角 - 力扣(LeetCode)
完