目录
一、List介绍
【本节目标】
- 什么是List
- List常见接口介绍
- List的使用
1. 什么是List
在集合框架中,List是一个接口,继承自Collection
Collection也是一个接口,该接口中规范了后序容器中常用的一些方法,具体如下所示
Iterable也是一个接口,表示实现该接口的类是可以逐个元素进行遍历的,具体如下
站在数据结构的角度来看,List就是一个线性表,即n个具有相同类型元素的有限序列,在该序列上可以执行增删改查以及变量等操作。
【面试题】Collection中有那些方法?
2. 常见接口介绍
List中提供了好的方法, 虽然方法比较多,但是常用方法如下:
方法 | 解释 |
---|---|
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 |
3. List的使用
注意:List是个接口,并不能直接用来实例化。
如果要使用,必须去实例化List的实现类。在集合框架中,ArrayList和LinkedList都实现了List接口。
二、ArrayList与顺序表
【本节目标】
- 线性表
- 顺序表
- ArrayList的简介
- ArrayList使用
- ArrayList的扩容机制
- 扑克牌
1. 线性表
线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列…
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储
2. 顺序表
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储,在数组上完成数据的增删查改
2.1 接口的实现
public interface IList {
//新增元素
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();
//判断是否为空
boolean isEmpty();
}
2.2 顺序表具体的实现
//顺序表具体的实现
import java.util.Arrays;
public class MyArrayList implements IList {
public int[] elem;
public int usedSize;
//默认的容量
public static final int DEFAULT_CAPACITY = 5;
public MyArrayList () {
elem = new int[DEFAULT_CAPACITY];
}
/**
* 打印顺序表当中所有的元素
*/
@Override
public void display() {
for (int i = 0; i < this.usedSize; i++) {
System.out.print(this.elem[i] + " ");
}
System.out.println();
}
/**
* 添加元素,默认添加到数组的最后位置
* @param data
*/
@Override
public void add(int data) {
//1. 判断数组是否满了,满了要扩容
if (isFull()) {
this.elem = Arrays.copyOf(elem, 2 * elem.length);
}
//2.添加数据
this.elem[usedSize] = data;
this.usedSize++;
}
/**
* 给pos位置添加一个元素
* 1.移动元素,从后往前
* 2.元素放进去
* @param pos
* @param data
*/
@Override
public void add(int pos, int data) {
// 1. 判断pos位置是否小于0,是否超过usedSide
checkPosOfAdd(pos);
// 2. 判断数组是否满了,满了要扩容
if (isFull()) {
this.elem = Arrays.copyOf(this.elem, 2 * this.elem.length);
}
//3.给pos位置添加一个元素
for (int i = this.usedSize - 1 ; i >= pos ; i--) {
this.elem[i + 1] = this.elem[i];
}
this.elem[pos] = data;
this.usedSize++;
}
public void checkPosOfAdd(int pos) {
if(pos < 0 || pos > this.usedSize) {
throw new PosException("pos的位置为:" + pos);
}
}
/**
* 查找当前元素,是否存在
* @param toFind
* @return
*/
@Override
public boolean contains(int toFind) {
for (int i = 0; i < this.usedSize; i++) {
if (toFind == this.elem[i]) {
return true;
}
}
return false;
}
/**
* 查找当前元素的下标
* @param toFind
* @return
*/
@Override
public int indexOf(int toFind) {
for (int i = 0; i < this.usedSize; i++) {
if (toFind == this.elem[i]) {
return i;
}
}
return -1;
}
/**
* 获取pos位置的值
* @param pos
* @return
*/
@Override
public int get(int pos) {
//1. pos不合法怎么办
checkPosOfGet(pos);
//2. pos为空怎么办
if(isEmpty()) {
throw new EmptyException("顺序表为空");
}
return this.elem[pos];
}
private void checkPosOfGet(int pos) {
if (pos < 0 || pos >= this.usedSize) {
throw new PosException("pos位置不合法 " + pos);
}
}
@Override
public boolean isEmpty() {
return usedSize == 0;
}
/**
* 更新pos位置的值为value
* @param pos
* @param value
*/
@Override
public void set(int pos, int value) {
//1. pos合不合法?
checkPosOfGet(pos);
//2. 判断空不空
if(isEmpty()) {
throw new EmptyException("顺序表为空");
}
this.elem[pos] = value;
}
/**
* 删除toRemove这个数字
* @param toRemove
*/
@Override
public void remove(int toRemove) {
//1. 判断是不是为空
if(isEmpty()) {
throw new EmptyException("顺序表为空,不能删除");
}
//2.判断是否能找到这个数字
if(contains(toRemove)){
//3. 找到下标
int pos = indexOf(toRemove);
//4.删除
for (int i = pos; i < this.usedSize; i++){
this.elem[i] = this.elem[i+1];
}
this.usedSize--;
}else {
System.out.println("找不到该元素" + toRemove);
}
}
@Override
public int size() {
return this.usedSize;
}
/**
* 清空顺序表,防止内存泄露
*/
@Override
public void clear() {
this.usedSize = 0;
}
@Override
public boolean isFull() {
return usedSize == elem.length;
}
}
public class EmptyException extends RuntimeException{
public EmptyException() {
}
public EmptyException(String msg) {
super(msg);
}
}
public class PosException extends RuntimeException {
public PosException() {
}
public PosException(String msg) {
super(msg);
}
}
public class TestDemo {
public static void main(String[] args) {
MyArrayList myArrayList = new MyArrayList();
myArrayList.add(1);
myArrayList.add(2);
myArrayList.add(3);
myArrayList.add(4);
myArrayList.add(5);
myArrayList.add(6);
myArrayList.display();
System.out.println(myArrayList.contains(5));
System.out.println(myArrayList.indexOf(5));
myArrayList.remove(3);
myArrayList.display();
}
}
3. ArrayList简介
在集合框架中,ArrayList是一个普通的类,实现了List接口,具体框架图如下
【说明】
- ArrayList是以泛型方式实现的,使用时必须要先实例化
- ArrayList实现了RandomAccess接口,表明ArrayList支持随机访问
- ArrayList实现了Cloneable接口,表明ArrayList是可以clone的
- ArrayList实现了Serializable接口,表明ArrayList是支持序列化的
- 和Vector不同,ArrayList不是线程安全的,在单线程下可以使用,在多线程中可以选择Vector或者CopyOnWriteArrayList
- ArrayList底层是一段连续的空间,并且可以动态扩容,是一个动态类型的顺序表
4. ArrayList使用
4.1 ArrayList的构造
方法 | 解释 |
---|---|
ArrayList() | 无参构造 |
ArrayList(Collection<? extends E> c) | 利用其他 Collection 构建 ArrayList |
ArrayList(int initialCapacity) | 指定顺序表初始容量 |
public static void main(String[] args) {
ArrayList<Integer> arrayList = new ArrayList<>();//这里没有分配内存
arrayList.add(10);
arrayList.add(20);
arrayList.add(30);
arrayList.add(0,99);
//结论一:第一次add的时候,分配了内存,大小为10
//结论二:扩容的时候,是1.5倍进行扩容
List<Integer> arrayList1 = new ArrayList<>();
ArrayList<Integer> arrayList2 = new ArrayList<>(arrayList);
System.out.println(arrayList2);
ArrayList<Integer> arrayList3 = new ArrayList<>(10);
//分配了内存
}
4.2 ArrayList常见操作
ArrayList虽然提供的方法比较多,但是常用方法如下所示,需要用到其他方法时,同学们自行查看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 |
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.3 ArrayList的遍历
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);
System.out.println(List);
// 使用下标+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.iterator();
while(it.hasNext()){
System.out.print(it.next() + " ");
}
System.out.println();
ListIterator<Integer> it1 = list.listIterator();
while(it1.hasNext()){
System.out.print(it1.next() + " ");
}
System.out.println();
//从后向前打印
ListIterator<Integer> it2 = list.listIterator(list.size());
while(it2.hasPrevious()){
System.out.print(it2.previous() + " ");
}
System.out.println();
}
注意
- ArrayList最长使用的遍历方式是:for循环+下标 以及 foreach
- 迭代器是设计模式的一种,后序容器接触多了再给大家铺垫
4.4 ArrayList的扩容机制
下面代码有缺陷吗?为什么?
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 100; i++) {
list.add(i);
}
}
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;
}
【总结】
- 检测是否真正需要扩容,如果是调用grow准备扩容
- 预估需要扩容的大小
- 初步预估按照1.5倍大小扩容
- 如果用户所需大小超过预估1.5倍大小,则按照用户所需大小扩容
- 真正扩容之前检测是否能扩容成功,防止太大导致扩容失败
- 使用copyOf进行扩容
5. ArrayList的具体使用
5.1 简单的洗牌算法
public class Card {
public String suit;//花色
public int num;//数字
public Card(String suit, int num) {
this.suit = suit;
this.num = num;
}
@Override
public String toString() {
return suit + num;
}
}
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class CardGame {
//定义四种花色
public static final String[] suits = {"♠", "♥", "♣", "♦"};
/**
* 生成一副扑克牌
* 52张
* @return
*/
public List<Card> buyCard() {
List<Card> cardList = new ArrayList<>();
//四种花色
for (int i = 0; i < 4; i++) {
//同一花色,13张牌
for (int j = 1; j <= 13 ; j++) {
String suit = suits[i];
Card card = new Card(suit, j);
cardList.add(card);
//cardList.add(new Card(suits[i],j ));
}
}
return cardList;
}
/**
* 洗牌
* @param cardList
*/
public void shuffle(List<Card> cardList) {
Random random = new Random();
for (int i = cardList.size() - 1; i > 0 ; i--) {
int index = random.nextInt(i);
swap(cardList, i, index);
}
}
/**
* 交换的方式进行洗牌
* 每一个下表的数据和其他下标的数据进行交换
* @param cardList
* @param i
* @param j
*/
public static void swap(List<Card> cardList, int i, int j) {
Card tmp = cardList.get(i);
cardList.set(i,cardList.get(j));
cardList.set(j,tmp);
}
/**
* 发牌
* 3个人,每个人轮流去抓5张牌
* 1.每个人拿到的牌放到哪里?
* 2.什么叫做每人轮流抓5张牌?
* @param cardList
*/
public List<List<Card>> getCard(List<Card> cardList) {
List<List<Card>> hand = new ArrayList<>();
List<Card> hand1 = new ArrayList<>();
List<Card> hand2 = new ArrayList<>();
List<Card> hand3 = new ArrayList<>();
hand.add(hand1);
hand.add(hand2);
hand.add(hand3);
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 3; j++) {
//怎么揭牌:相当于每次删除0下标这个牌
Card card = cardList.remove(0);
//怎么放到对应的人的手中
hand.get(j).add(card);
}
}
return hand;
}
}
import java.util.List;
public class Main {
public static void main(String[] args) {
CardGame cardGame = new CardGame();
System.out.println("买牌");
List<Card> ret = cardGame.buyCard();
System.out.println(ret);
System.out.println("洗牌");
cardGame.shuffle(ret);
System.out.println(ret);
System.out.println("揭牌");
List<List<Card>> hand = cardGame.getCard(ret);
for (int i = 0; i < hand.size(); i++) {
System.out.println("第" + i + "个人的牌:" + hand.get(i));
}
System.out.println("剩下的牌");
System.out.println(ret);
}
}
//运行结果
买牌
[♠1, ♠2, ♠3, ♠4, ♠5, ♠6, ♠7, ♠8, ♠9, ♠10, ♠11, ♠12, ♠13, ♥1, ♥2, ♥3, ♥4, ♥5, ♥6, ♥7, ♥8, ♥9, ♥10, ♥11, ♥12, ♥13, ♣1, ♣2, ♣3, ♣4, ♣5, ♣6, ♣7, ♣8, ♣9, ♣10, ♣11, ♣12, ♣13, ♦1, ♦2, ♦3, ♦4, ♦5, ♦6, ♦7, ♦8, ♦9, ♦10, ♦11, ♦12, ♦13]
洗牌
[♥6, ♥7, ♣13, ♠12, ♠3, ♥10, ♥3, ♣1, ♥4, ♦10, ♣12, ♠2, ♥5, ♦12, ♥9, ♦1, ♣2, ♥12, ♦4, ♦11, ♠5, ♦2, ♠1, ♠11, ♦5, ♦6, ♠10, ♦7, ♠7, ♣6, ♥13, ♦8, ♥11, ♣11, ♣10, ♥2, ♣7, ♦3, ♣5, ♥8, ♠6, ♠4, ♦13, ♠9, ♦9, ♣9, ♥1, ♣8, ♠13, ♣4, ♠8, ♣3]
揭牌
第0个人的牌:[♥6, ♠12, ♥3, ♦10, ♥5]
第1个人的牌:[♥7, ♠3, ♣1, ♣12, ♦12]
第2个人的牌:[♣13, ♥10, ♥4, ♠2, ♥9]
剩下的牌
[♦1, ♣2, ♥12, ♦4, ♦11, ♠5, ♦2, ♠1, ♠11, ♦5, ♦6, ♠10, ♦7, ♠7, ♣6, ♥13, ♦8, ♥11, ♣11, ♣10, ♥2, ♣7, ♦3, ♣5, ♥8, ♠6, ♠4, ♦13, ♠9, ♦9, ♣9, ♥1, ♣8, ♠13, ♣4, ♠8, ♣3]
5.2 杨辉三角
[杨辉三角][https://leetcode.cn/problems/pascals-triangle/submissions/]
- 每一行的第一个数字为1,每一行的最后一个数字是1
- 公式:
[i][j] = [i-1][j] + [i-1][j-1]
class Solution {
public List<List<Integer>> generate(int numRows) {
//定义了一个二维数组
List<List<Integer>> ret = new ArrayList<>();
List<Integer> list = new ArrayList<>();
list.add(1);
ret.add(list);
for (int i = 1; i < numRows ; i++) {
//每循环一次就是一行
List<Integer> curRow = new ArrayList<>();
curRow.add(1);//每一行的第一个元素
List<Integer> prevRow = ret.get(i-1);
//中间元素!
for (int j = 1; j < i; j++) {
int x = prevRow.get(j) + prevRow.get(j-1);
curRow.add(x);
}
curRow.add(1);//每一行的最后一个元素
ret.add(curRow);
}
return ret;
}
}
6. ArrayList的问题及思考
- ArrayList底层使用连续的空间,任意位置插入或删除元素时,需要将该位置后序元素整体往前或者往后搬移,故时间复杂度为O(N),比较好的是给定一个下标,可以快速查找对应的元素,所以时间复杂度为O(1)
- 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗
- 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间
思考: 如何解决以上问题呢
- 随用随取
- 插入/删除元素是否可以不移动元素
链表
【例题】
str1:"welcome to cvte"
str2:"come"
删除字符串1中出现的所有字符串2中的字符,意味着这个结果为"wl t vt"
要求使用ArrayList来解决问题
//遍历str1当中的每个字符是否存在str2的字符
//不存在则存放到arrayList中
public static List<Character> func(String str1, String str2){
List<Character> arrayList = new ArrayList<>();
for (int i = 0; i < str1.length(); i++) {
char ch = str1.charAt(i);
if (!str2.contains(ch + "")) {
arrayList.add(ch);
}
}
return arrayList;
}
public static void main(String[] args) {
String str1 = "welcome to cvte";
String str2 = "come";
for (char ch :func(str1,str2)) {
System.out.print(ch);
}
}