ArrayList
文章目录
前言: 上文我们学习完了泛型,这里我们就来 学习 List接口底下的 ArrayList 顺序表 。
这里先列出顺序表中常用的方法 。
方法 | 解释 |
---|---|
boolean add(E e) | 尾插 e |
void add(int index, E element) | 将 e 插入到 index 位置 |
boolean addAll(Collection<? extends E> 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 |
1. 线性表
线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列…
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
2. 顺序表
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
这里我们的 顺序表 在一般情况下是使用数组来实现的 , 这里就会有一个问题?
顺序表背后是一个数组,为啥我们不直接使用我们的数组呢?
这里我们就可以举出一个例子, 我们现在有一个数组
下面就来实现我们自己的 ArrayList , 开楼
列出:需要实现的方法
public class MyArrayList {
// 打印顺序表
public void display() { }
// 新增元素,默认在数组最后新增
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) { }
//删除第一次出现的关键字key
public void remove(int toRemove) { }
// 获取顺序表长度
public int size() { return 0; }
// 清空顺序表
public void clear() { }
}
准备工作
1.创建 MyArrayList 类
2.刚刚说我们的ArrayList 是使用数组实现的 , 这里我们就可以添加一个数组。
3.有了数组我们还需要知道我们的元素个数,所以这里我们还需要添加一个usesize来记入元素个数。
4.通过构造方法对我们的数组进行初始化大小
这里我们就创建好 我们的类了 , 接下来就可以对数组进行操作了(添加增删查改等操作)
实现 ArrayList 方法
1. 打印顺序表
这里我们 usesize
记录了我们数组元素个数,那么我们就可以通过for循环,来打印 0 - usesize
的下标即可
这里 第一个方法我们就完成了 , 接下来看第二个 新增元素
2. 新增元素 or 返回元素个数
注意: 这里是在数组的尾部进行添加
此时我们就能够完成下面的代码, 但是只能说太年轻了 。
这里我们要想到特殊的情况 , 数组是有限的, 我们如果添加元素超过了一开始初始化的 10 , 那么是不是会出现问题 ?
这里就需要考虑我们的数组放满了,如何进行扩容。
这里就来解决这个问题, 在此之前先来完成一个方法,返回数组长度
非常简单,直接返回 usesize即可。
完成了这个方法, 那么我们就可以来对 新增方法进行改进了 , 我们的useszie
记录了我们的元素个数 , 这里就可以通过 size()
这个方法来进行判断如果我们的 size() 返回的个数 等于 elem.length
,那么就说明这个数组满了需要扩容。
这里我们满了扩容可以 写成一个方法, 因为 后面我们在头部进行元素插入的时候也需要判断当前数组是否需要扩容 ,你要想使用数组,添加元素就会出现满了的情况,那么扩容是必不可少的
这里就需要使用我们之前学习的 Arrays.copyOf
即可
写完了这几个方法 ,我们就可以 来实现这个类,使用这几个方法感受一下。
创建一个TestList 类 实现我们自己写的MyArrayList
3. isFull : 判断数组是否满了
非常简单: 直接判断当前元素个数是否等于数组的长度即可 ,等于说明满了, 小于说明没有满
这里直接返回 size() >= elem.length 也是可以的 。
写完这个方法,我们就能将之前新增 方法 里面的 判满 改成 isFull 。
下面继续
4. 在 pos位置下新增一个元素
此时我们就区实现代码 ,
代码写完了, 但我想告诉你 ,还是太年轻了、
这里我们还要考虑 一些细节的问题 比如: pos的位置是否合法, 数组是否满了(上面好像已经判判断了) , 还有一点我们不能隔着元素放,啥意思呢?
按照上面的写法就可以 优化我们的 代码
注意: 这里 还没有 完成, 这里 的pos 位置异常 直接打印 一个pos位置不合法,有点太简单了, 回忆一下我们学习过的知识, 异常能否想到呢? 这里我们就可以抛异常 。
5. 判断是否包含某个元素
这里直接一个for 循环 去找即可
细节扩展 : 假如 我们的 toFind 是一个引用类型,此时就不能通过等号进行比较了, 需要使用equals 方法 (这里我们没有 使用泛型所以这里我们无法传入一个引用类型)
返回 某个元素的下标
与上面的同理,for 循环,找到了返回 下标,没找到 返回 -1即可
6. 获取pos位置的元素
相信大家经过之前 在 pos 位置下添加元素,这里会考虑 pos位置的合法性吧 ,同样 需要判断 pos 是 小于0 还是大于 usesize , 如果是 就返回 -1即可
这里我们 判断了 pos 的合法性 ,也解决了我们数组为空的情况, 因为 数组为空那么 size == 0 ,此时 我们 pos 不管 等于几都会进入 if 语句 。
但是为了 严谨 我们还是来写一下 数组为空的情况
7. 给 pos位置的元素跟换成value
会了 获取 pos 位置的元素,那么跟换不就是换汤不换药吗?
注意点 不就是 pos位置的合法性, 加上 数组是否为空 ,那么这里我们就直接来写代码
到此我们就剩下两个方法了, 一个 删除指定元素 , 一个 清空顺序表 ,这里我们继续弄
8. 删除顺序表中指定的元素
此时就完成了我们的删除操作, 注意这里 下标为 6位置的 6 我们并不需要做处理, 这时 因为我们的 usesize --
那么下次 新整加元素 ,就会在 下标为 6的地方整加那么这个6就被覆盖掉了。
注意:
这里我们就来写代码
注意: 我们 可以调用 上面写过的方法来找到我们的 key 下标
9. 清空顺序表
这里就非常简单了, 直接 让 usesize
置为空即可, 下次添加元素的时候就会从 0 下标开始, 就会将之前的值覆盖掉 , 注意:如果我们是引用类型,那么就需要将每个值置为null
附上代码:
import java.util.Arrays;
public class MyArrayList {
private int[] elem; // 存放 我们元素的数组
private int usesize; // 记入有效的数组个数
private static final int DEFAULT_SIZE = 10;
// 设置一个常量
public MyArrayList() {
this.elem = new int[DEFAULT_SIZE]; // 初始化数组大小
usesize = 0; // 此时 我们还没有添加元素,所以size = 0 ;
}
// 打印顺序表
public void display() {
for (int i = 0; i < usesize; i++) {
System.out.printf(this.elem[i] + " ");
}
System.out.println();
}
public void dilatation() {
this.elem = Arrays.copyOf(this.elem, 2 * this.elem.length);
}
public void add(int value) {
if (isFull()) {
// 此时说明满了 ,需要扩容
dilatation();
}
elem[this.usesize++] = value;
}
public int size() {
return this.usesize;
}
public boolean isFull() {
// if(size() >= elem.length){
// return true;
// }
// return false;
return size() >= elem.length;
}
public void add(int pos, int value) throws PosWrongfulException {
if (isFull()) {
dilatation();
}
if (pos < 0 || pos > size()) {
System.out.println("pos位置不合法");
return;
}
for (int i = usesize - 1; i >= pos; i--) {
elem[i + 1] = elem[i];
}
elem[pos] = value;
this.usesize++;
}
public boolean contains(int toFind) {
for (int i = 0; i < usesize; i++) {
if (elem[i] == toFind) {
return true;
}
}
return false;
}
public int indexOf(int toFind) {
for (int i = 0; i < usesize; i++) {
if (elem[i] == toFind) {
return i;
}
}
return -1;
}
public int get(int pos) throws EmptyException {
if (isEmpty()) {
// return - 1; 直接返回 -1 太过于简单 ,那么我们抛出异常 。
throw new EmptyException("当前顺序表为空");
}
if (pos < 0 || pos >= size()) {
// 注意 usesize 如果是 6个元素,那么我们的下标就只能拿到 5的位置
// 那么此时 返回 elem[usesize] 就相当于 越界了 抛出异常即可
throw new PosWrongfulException("pos位置异常!");
}
return elem[pos];
}
public boolean isEmpty() {
return size() == 0;
}
public void set(int pos, int value) {
if (isEmpty()) {
// 通过 isEmpty 判断当前数组是否为空
//为空 抛出异常
throw new EmptyException("数组为空 !");
}
// 判断当前 pos位置是否合法
if (pos < 0 || pos >= size()) {
// 此时 pos位置不合法抛出异常
throw new PosWrongfulException("pos位置异常!");
}
// 此时 pos位置 合法, 数组又不为空 ,将pos位置元素更改为value 即可
elem[pos] = value;
}
// 删除第一次出现的 key
public void remove(int key) {
if (isEmpty()) {
throw new EmptyException("数组为空无法删除!!!");
}
int ret = indexOf(key);
if (ret == -1) {
throw new Exception("无删除的 key");
}
// 此时 ret 存在
for (int i = ret; i < usesize - 1; i++) {
elem[i] = elem[i + 1];
}
this.usesize--;
}
// 清空顺序表
public void clear(){
// 如果是引用类型 那么 循环置 null
// for(int i = 0;i<usesize;i++){
// elem[i] = null;
// }
// 这我们不是一个引用类型, 直接将 usesize 置为 0 即可
this.usesize = 0;
}
}
这里我们就将我们自己的ArrayList的方法写完了, 接下来就来学习一下 ArrayList 的方法,
ArrayList
1.创建 ArrayList
这里可以看到我们的ArrayList
是属于java.util包底下的,另外除了我们的 ArrayList,java集合类一般都是在java.util包里面的,所以以后找具体的集合类可以多看一下util包。
下面我们就可以点击ArrayList源码开一下(鼠标左键点击ArrayList)
补充: ArrayList是以泛型方式实现的,使用必须先实例化
下面继续 , 通过 ALT + 7 查看 ArrayList 的方法 , 这里先来看构造方法
通过帮助手册就能够看到相应的解释了
使用无参构造方法
指定ArrayList容量大小的构造方法
此时我们ArrayList 中数组的初始大小就是 12 , 如果我们不设置那么初始大小就是 10
第三个构造方法
上面只是使用 , 下面就来具体的观察一下源码 ,
ArrayList 默认大小 10
注意: 当使用无参构造的 ArrayList, 并且在添加第一个元素的才会分配 10个大小的空间。
分析 : 为啥我们没有指定 数组大小,能够 存入数据
总流程:
总结: 当我们调用不带参数的构造方法的时候,只有第一个add的时候,才会分配大小为 10的内存
ArrayList 的 1.5倍扩容
此时 就有一个问题 , 当我们放第11个元素的时候 ,是如何扩容的
这里我们就需要 区看grow
是如何实现的 。
找到grow
这个方法(进入 add 的源码, 然后继续进入ensureCapacityInternal ,在进入 ensureExplicitCapacity 此时就能够看到 grow) , 进入 grow ,观察他的实现。
补充 : 我们的数组 最大的大小 就为 MAX_VALUE
下面就来看一下 ArrayList 的方法
方法 | 解释 |
---|---|
boolean add(E e) | 尾插 e |
void add(int index, E element) | 将 e 插入到 index 位置 |
boolean addAll(Collection<? extends E> 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 这个方法 ,
演示: subList 截取 左闭右开
另外 :subList截取后 , 将获取截取部分的首地址 赋值给新的 List
ArrayList 的几种打印方式
最后来看一下 ArrayList 的几种打印方式
之前我们一直是 sout 加 变量名 来直接打印 --> 使用了 重写的 toString 方法, 但 有点不太方便, 如果我们想要对某个下标的值进行 操作呢?
此时就会显得非常麻烦, 那么我们就可以使用 for 循环来打印 ,显示每一个数据
1.for循环
注意: 这里 我们 循环里面如果 有 删除 (remove) 方法, 那么就 不能使用 arrayList.size() 这个 方法了 ,因为我们执行一次就会调用一次这个方法, 那么删除元素, 就会出现问题 , 原先是 5 , 执行一次 变为了 4 , 那么 size 读取到了 4 ,循环就变成了 3 次 (本来 i < 5 , 变为了 i < 4 , 循环是否就少了一次? ) , 后面 类推同样又会少执行。
解决方法 : 这样 size 就 固定了, 不会因为 arrayList 里面元素的减少 就改变了(一开始已经固定了)
2. for-each 循环
同样的 for-each 循环也是可以的
这里写成 for - each中 int x : arrayList
, 涉及到 自动拆包
3.迭代器
这里 需要 arrayList 调用 一个方法 iterator , 使用 Iterator 来接收。
这里的打印原理 :
除了 iterator , 其实还有一个 List的 迭代器 ListItertor
这里我们的 List的 迭代器 ,其实 就是 Iterator
的子类 ,那么 我们打印不就是 一样的情况、
这里我们 可以使用迭代器的 删除 , 注意 remove 会有一个坑 ,这里来看一下
到这里 ArrayList 差不多就学完了, 下面来几个练习题 趁热打铁
ArrayList 练习题
题目一 :
1.一个班级里有 3 学生, 我们需要将学生的 姓名,年龄 分数(浮点类型) 存入 然后输出 。
非常简单 : 就是创建一个学生类,然后 存入ArrayList 即可。
如果我们想要 排序, 针对分数进行排序(换一种说法 对这个集合进行排序) 那么需要怎么做呢 ?
这里就需要介绍一个 类 Collections
点进这个类 : 会发现这里有许多的方法 ,这里我们主要是排序那么就 可以去看 sort方法
通过 快捷键 ctrl + f 搜索 sort 就能 找到我们的 sort方法
熟悉吗 这里 < T extends Comparable<?super T> >
就是 之前泛型讲过的 泛型上界 ,表明 传入的参数必须是 实现 Comparable 这个接口的。
下面就来改造我们的 Student 类
此时 进行排序即可 :
同理 我们传入比较器也是 可以的, 进入 sort 源码, 你能发现他重载的方法。
总结 : 这个 题目 只有两个目的 :
- ArrayList 是可以存放自定义类型的
- 可以对集合进行排序
题目二 :
给定两个字符串 : str1: "welcome to beji" str2 : "come"
请删除第一个字符串当中, 出现的第二个字符串当中的字符 (注意: 这里我们的 空格 也算).
结果 : wl t b ji
要求: 使用用集合来完成 ArrayList来完成。
这我们 不是使用 ArrayList也能完成, 但本文学习的是ArrayList 那么就使用这。
思路 : 遍历字符串 一 , 如果字符 在 字符串 二中 那么就 不放在 ArrayList中 。 最后打印即可
代码如下:
public static String sum(String str1 , String str2){
ArrayList<Character> list = new ArrayList<>();
for(int i = 0; i<str1.length();i++){
// 判断 当前的 i下标的字符是否存在 str2 中,存在
// 就不添加到 list 中
char a = str1.charAt(i);
if(!str2.contains(a+"")){
// contains 查询的是一个字符串是否存在str2中
// a是一个字符 此时就会报错那么就让a 拼接上一个“”变成字符串
list.add(a);
}
}
return list.toString();
}
--------------------------------------------分割线----------------------------------------------------------------
如果 不记得 contains 这个方法, 那么就笨一点 在来一个循环 来判断即可
public static String sum(String str1, String str2) {
ArrayList<Character> list = new ArrayList<>();
char[] a1 = str1.toCharArray();
char[] a2 = str2.toCharArray();
int m = a1.length, n = a2.length;
int j = 0;
for (int i = 0; i < m; i++) {
for (j = 0; j < n; j++) {
if(a1[i] == a2[j]){
break;
}
}
if(j == n){
list.add(a1[i]);
}
}
return list.toString();
}
public static void main(String[] args) {
String str1 = "welcome to beji";
String str2 = "come";
System.out.println(sum(str1, str2));
}
最后实现 一副扑克牌
第一步 : 我们有 一副牌 , 52张牌(去除了大小王) 4 个花色 52 张牌 : 花色和数字 不一样 。
第二步 : 洗牌
第三步 : 揭牌
这里我们就需要两个类 , 一个 表示 每张牌的 如 花色 和 点数 。
第二个 就是 我们的一副牌
这里先来买 一副牌
这可以我们可以将 list中的数据 打印出来 看一下 。
数据就成功的打印出来了, 但是这些数据是 顺序存放的,如果我们就这样的使用这一副牌, 就会出现问题 。
所以这里就需要 再来一个功能洗牌 ,下面就来实现一下。
思路: 从最后一张牌 开始 洗 (这里的洗牌需要使用到随机数) , 每次洗一张牌,让 i – , 让随机数 生成 0 - i 之间的数,那么上次洗的牌就不会在出现在随机数中 ,注意这里我们操作的是下标。
调用方法 : 观察牌是成功的洗了的
最后我们来 写最后一个 功能 ,揭牌
规则 : 3 个人 每个人 轮流的抓 五张牌
这里思考 , 一个人那 5张牌,那么 是不是需要 创建 3 个集合 来 存放 每个人 的牌?
那么 就可以 创建 3个 集合
存放 牌的容器 ,创建好了,那么 又出现问题了, 如何指定每个人 拿牌 ?
这里就可以创建一个 List<List<Poker>> list2 = new ArrayList<>();
来存放我们的 容器, 啥意思呢?
想一想二维数组 ,这样就将 3个 List 建立了关系
看代码:
public static void main(String[] args) {
List<Poker> list = buyPokers();
System.out.println(list);
// 开始洗牌
shuffle(list);
System.out.println(list);
List<List<Poker>> list2 = new ArrayList<>();
// 第一个人的牌
List<Poker> hand1 = new ArrayList<>();
// 第二个人的牌
List<Poker> hand2 = new ArrayList<>();
// 第三个人的牌
List<Poker> hand3 = new ArrayList<>();
list2.add(hand1);
list2.add(hand2);
list2.add(hand3);
// 揭牌 : 3 个人 每个人 轮流抓 5张牌
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 3; j++) {
List<Poker> handTmp = list2.get(j); // 来确定是 谁的手(存放牌的容器)
handTmp.add(list.remove(0));
// 没一个人拿一张牌 , 就少了一张牌
}
}
// 最后将我们的 牌给打印出来
for (int i = 0; i < list2.size(); i++) {
System.out.println("第" + (i + 1) + "个人的牌" + list2.get(i));
}
System.out.println("剩余的牌 :" + list);
}
这里我们就 将揭牌给完成了 ,如果 有兴趣 ,可以 通过 每个人的点数,来写一些玩法, 如 牛牛,炸金花等 (计算点数,然后 比较每个人的点数)。
最后在来一个杨辉三角结束我们的 ArrayList 的学习
杨辉三角
将数组 换成我们的 ArrayList 即可 代码如下
相比大家都和熟悉 杨辉三角形吧 ,不就是一个直角三角形,j == 0 和 i== j 的位置赋值为 1,其他位置 arr[i][j] == arr[i-1][j]+arr[i-1][j-1] 这样的思路
然后这里我们来实践一下
class Solution {
public List<List<Integer>> generate(int numRows) {
// 这里 就 创建了一个ret 变量 相当于 一个二维数组 arr[i][j]
List<List<Integer>> ret = new ArrayList<>();
// 将第一行 的 为一个 元素 j == 0 的 地方放入 1
List<Integer> list = new ArrayList<>();
list.add(1);
// 这一行 放完 就放入我们的二维数组中,
ret.add(list);
for(int i = 1;i<numRows;i++) {
// 每一行的 第一列 就是 j = 0 的 地方放入 1
List<Integer> list2 = new ArrayList<>();
list2.add(1);
//这一步相当于 得到 i - 1 行 的 信息
List<Integer> preRow = ret.get(i-1);
for(int j = 1;j<i;j++) {
// 这里 不就是 arr[i][j] = arr[i-1][j]+arr[i-1][j-1]
int nums = preRow.get(j)+preRow.get(j-1);
将他放入顺序表中
list2.add(nums);
}
最后将 最后一个 1 加入 ,就是 i == j 的 地方
list2.add(1);
别忘记 要放进我们的二维数组中,
ret.add(list2);
}
return ret;
}
}
顺序表的问题及思考 (引出下文内容)
假设我们 顺序表的 大小 为 100 , 但是 我们想要 插入 101 个元素,此时就会顺序表 就会1.5倍扩容,但是我们只需要使用一个那么剩下的 49个空间是不是就浪费了 有什么解决办法吗?
还有 , 我们规定 插入只能在顺序表的尾部插入, 删除只删除头部,那么删除的时间复杂度为O(N) , 每次删除 头部都会挪动 N - 1 个元素, 非常的耗时间,那么有什么办法解决呢?
这里就可以使用另外一种数据结构 链表
来解决 ,下文目标链表 。