【数据结构与算法】ArrayList 和 顺序表
文章目录
- 🌲List
- 🌲1. 线性表
- 🌲2. 顺序表
- 🌿2.1 MyArrayList
- 2.1.1 类中重写所有接口方法
- 1.新增元素
- 2.在pos位置新增元素(指定位置)
- 3.判定是否包含了某个特定元素
- 4.查找特定元素对应的位置
- 5.获取pos下标的元素
- 6.给pos位置的元素替换成value
- 7.删除数据
- 8.获取顺序表长度
- 9.清空顺序表
- 10.打印顺序表
- 需要的自定义异常
- 🌲3.ArrayList 简介
- 🌲4. ArrayList 的使用
- 🌿4.1 ArrayList 的构造
- 🌿4.2 ArrayList常见操作
- 🌿4.3 ArrayList的遍历
- 🌿4.4 ArrayList的扩容机制(java8源码实现讲解)
- 🌲5. ArrayList的具体使用
- 🌿5.1 删除字符 (要求使用集合)
- 🌿5.2 杨辉三角
- 🌿5.3 扑克牌洗牌
- 6.🌸ArrayList的问题
在正式学习顺序表前,先简单了解一下 List
🌲List
🌲1. 线性表
线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列…
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储
🌲2. 顺序表
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
顺序表底层是一个数组,为什么不直接操作数组就好了,还需要单独写个类?
这个数组里有几个有效数据?-----6个
在Java里数组没有元素默认为0,判断的时候遇到0就停止,然后总数就是元素个数,那如果这6个元素中加了一个0呢?还是6个元素,但是数组认为只有5个元素,这就是为什么我们需要顺序表
在使用ArrayList之前,我们自己实现一个顺序表,方便我们更加深入的理解顺序表,才能熟练使用它
🌿2.1 MyArrayList
public class MyArrayList implements IList {
public int[] array;
public int usedSize;//成员变量默认为0
public static final int DEFAULT_CAPACITY = 10;
public MyArrayList() {
this.array = new int[DEFAULT_CAPACITY];
}
成员变量包含数组,数组里的有效元素个数,数组长度(可以使用final修饰让数据不被改变)
###🌿 2.1.1 接口IList
//10个方法
public interface IList {
// 新增元素,默认在数组最后新增
public void add(int data);
// 在 pos 位置新增元素
public void add(int pos, int data);
// 判定是否包含某个元素
public boolean contains(int toFind);
// 查找某个元素对应的位置
public int indexOf(int toFind);
// 获取 pos 位置的元素
public int get(int pos);
// 给 pos 位置的元素设为 value
public void set(int pos, int value);
//删除第一次出现的关键字key
public void remove(int toRemove);
// 获取顺序表长度
public int size();
// 清空顺序表
public void clear();
// 打印顺序表,注意:该方法并不是顺序表中的方法,为了方便看测试结果给出的
public void display();
}
注意:接口中的方法默认为public abstract 修饰
这些是我们要实现的所有方法,自己实现一遍,能更好的理解ArrayList
2.1.1 类中重写所有接口方法
1.新增元素
// 新增元素,默认在数组最后新增
public void add(int data) { //默认在尾部插入数据
//判满
if(isFull()){
grow();//扩容
}
this.array[this.usedSize]=data;
this.usedSize++;
}
public void grow(){
this.array= Arrays.copyOf(this.array,2*this.array.length);
}
public boolean isFull(){
return this.usedSize== array.length;
}
默认在数组尾部新增
①需要判断数组是否是满的,满的就需要扩容,
②不满的话就进行元素插入,判断数组满不满状态的函数实现
③最后数组长度+1
2.在pos位置新增元素(指定位置)
public void add(int pos, int data) { //插入到指定位置
//pos合法性
try{
checkPos(pos);
if(isFull()){
grow();//扩容
}
//挪动数据
for (int i = usedSize-1; i >=pos ; i--) {
array[i+1]=array[i];
}
array[pos]=data;
this.usedSize++;
}catch (PosIllegal e){
System.out.println("插入元素pos位置不合法");
e.printStackTrace();//提示异常位置
}
}
private void checkPos(int pos) throws PosIllegal{
if(pos < 0 || pos > usedSize) { //注意:usedSize能插入,只要前驱存在就可以插入
throw new PosIllegal("Pos位置不合法!!");
}
}
①先判断pos位置合法性,既不能是负数,又必须要有前驱信息的支持
②判断数组元素是否满了,继续调用isFull()函数
③我们插入数据的时候,需要先把插入元素后面的元素都往后挪一位,挪数据实现从数组的最后一个元素开始往后挪,一次挪到当pos位置空出,没有元素的时候即可
④挪完数据之后,我们把pos位置赋值为data,并且把数组大小扩容一位,方便再进行新增元素
3.判定是否包含了某个特定元素
public boolean contains(int toFind) {
for (int i = 0; i <this.usedSize ; i++) {
if(array[i]==toFind){
return true;
}
}
return false;
}
4.查找特定元素对应的位置
// 查找某个元素对应的位置
public int indexOf(int toFind) {
for (int i = 0; i < this.usedSize; i++) {
if(this.elem[i] == toFind){
return i;
}
}
return -1;
}
5.获取pos下标的元素
private void checkPos2(int pos) throws PosIllegal{
if(pos < 0 || pos >= usedSize) {
throw new PosIllegal("Pos位置不合法!!");
}
}
private void checkEmpty() {
if(isEmpty()) {
throw new EmptyException("顺序表为空!");
}
}
public boolean isEmpty(){
return usedSize==0;
}
@Override
public int get(int pos) {
try{
checkEmpty();
checkPos2(pos);
return array[pos];
}catch (PosIllegal e){
System.out.println("插入位置pos不合法");
e.printStackTrace();
}catch (EmptyException e){
System.out.println("顺序表位空");
e.printStackTrace();
}
return -1;
}
①先判断pos位置合法性
②判断数组是否为空(可有可无)
6.给pos位置的元素替换成value
①先要进行合法性判断再替换
public void set(int pos, int value) { //更新 pos位置更新为value
try{
checkEmpty();
checkPos2(pos);
array[pos]=value;
}catch (PosIllegal e){
System.out.println("插入位置pos不合法");
e.printStackTrace();
}catch (EmptyException e){
System.out.println("顺序表位空");
e.printStackTrace();
}
}
7.删除数据
public void remove(int toRemove) {
try{
checkEmpty();
int pos=indexOf(toRemove);
if (pos==-1)
return ;
for (int i = pos; i <this.usedSize-1 ; i++) {
array[i]=array[i+1];
}
this.usedSize--;
}catch (EmptyException e){
e.printStackTrace();
}
}
①顺序表不为空
②顺序表当中有我们要删除的元素
③找到它的下标
④把i+1的值赋给i,i还要小于usedSize-1(只要涉及到删除数据,如果是引用数据类型,那么就要把elem[i] = null;否则就会发生内存泄漏)
8.获取顺序表长度
// 获取顺序表长度
public int size() {
return this.usedSize;
}
9.清空顺序表
// 清空顺序表
public void clear() {
//因为是基本类型,所以置为0即可
this.usedSize = 0;
/*当它是引用类型时
for (int i = 0; i < this.usedSize; i++) {
this.elem[i] = null;
}
this.usedSize = 0;
*/
}
①基本类型置为0即可,若是引用类型则循环打印置为null,再置为0
注意:
此处可以把elem置为null可以吗?可以,但是很暴力,数组直接被回收了,顺序表只执行了一次就没了,再次使用的时候还需开辟新的数组,相当于我们每次使用的时候还需new一次,很麻烦也没必要
10.打印顺序表
public void display() {
for (int i = 0; i < this.usedSize; i++) {
System.out.print(array[i]+" ");
}
/*
这里面所有都是 0
for (int x : array) {
System.out.print(x+" ");
}*/
}
}
需要的自定义异常
在这里面添加,获取pos下标不合法的时候我们也可以写我们需要的异常类来更好的实现我们需要的异常,实现异常的抛出是我们赋值命名的异常名:
public class MyArrayListEmptyException extends RuntimeException{
public MyArrayListEmptyException(){
}
public MyArrayListEmptyException(String message){
super(message);
}
}
public class MyArraylistIndexOutofException extends RuntimeException{
public MyArraylistIndexOutofException(){
}
public MyArraylistIndexOutofException(String message){
super(message);
}
}
好,自己实现一遍顺序表是不是思路清晰了很多,我们正式进入顺序表介绍
🌲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的源码,我们来详细了解
提问:都是空数组,那我们main函数中add的值都是存在哪里的?
答:add函数过程中会扩容1.5倍 下面我们讲到扩容机制再详细说
方法一ArrayList()不带参数的构造方法的使用:
ArrayList<Integer> arrayList = new ArrayList<>();
arrayList.add(1);//往数组最后的一个位置存元素
arrayList.add(2);
arrayList.add(3);
arrayList.add(4);
System.out.println(arrayList);//用字符串的形式打印出来所有的元素
System.out.println(arrayList.size());//获取当前有效数据的个数
System.out.println(arrayList.get(1));//获取指定下标的元素
方法二的使用:
ArrayList<Integer> arrayList2 = new ArrayList<>(arrayList);
arrayList2.add(99);
arrayList2.add(199);
System.out.println(arrayList2);
arrayList2承接了arrayList1的数据(使用其他的集合 来构造当前的List,底层源码实现是数组的拷贝)
方法三的使用:
ArrayList<Integer> arrayList3 = new ArrayList<>(15)
指定初始化数组容量大小
在源码的实现里:
①:第一次add的时候,我们底层的数组才变成了10,如果只是调用了不带参数的构造方法,默认还是0
②:grow函数就是扩容函数,扩容的方式是1.5倍的扩容
例如整体的举例使用:
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);
}
🌿4.2 ArrayList常见操作
add方法
ArrayList<Integer> arrayList = new ArrayList<>();
arrayList.add(0,1);
arrayList.add(1,2);
arrayList.add(2,99);
System.out.println(arrayList);
需要注意的是:在我们实现元素插入赋值的时候,前驱必须存在
addAll方法:
ArrayList<Integer> arrayList2 = new ArrayList<>();
arrayList2.addAll(arrayList);
arrayList2.add(19);
System.out.println(arrayList2);
remove方法:
public class Main{
public static void main(String[] args) {
MyArrayList array=new MyArrayList();
ArrayList<Integer> list=new ArrayList<Integer>();
list.add(0,1);
list.add(1,2);
list.add(2,3);
list.add(3,4);
list.add(4,5);
ArrayList<Integer> list2=new ArrayList<Integer>();
list2.addAll(list);
list2.add(99999999);
list2.remove(new Integer(5));
System.out.println(list2);
}
}
lastIndexOf方法:
int index = arrayList.lastIndexOf(new Integer(99));
System.out.println(index);
subList方法 和 set方法:
ArrayList<Integer> arrayList = new ArrayList<>();
arrayList.add(0,1);
arrayList.add(1,2);
arrayList.add(2,99);
arrayList.add(3,199);
arrayList.add(4,299);
System.out.println(arrayList);
List<Integer> list = arrayList.subList(1,3);//区间左闭右开
System.out.println(list);
相当于字符串的截取,区间左闭右开
这里面我们如果把list里面的数值改变了,是否会影响原来的arraylist数组呢?-----会改变
🌿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);
// 使用下标+for遍历
for (int i = 0; i < list.size(); i++) {
System.out.print(list.get(i) + " ");
}
System.out.println();
// 借助foreach遍历
for (Integer integer : list) { //也可以用int 这个过程相当于拆箱
System.out.print(integer + " ");
}
System.out.println();
Iterator<Integer> it = list.listIterator();
while(it.hasNext()){
System.out.print(it.next() + " ");
}
System.out.println();
}
两种迭代器打印代码如下:
注意:
- ArrayList最常使用的遍历方式是:for循环+下标 以及 foreach
- 迭代器是设计模式的一种,后序容器接触多了在仔细学一学,讲一讲
感兴趣的同学可以在IDEA中点到源码,读源码学习
🌿4.4 ArrayList的扩容机制(java8源码实现讲解)
我们这里用的是 java8 源码,更好理解,java17 重写了,但是效果并不影响
ArrayList是一个动态类型的顺序表,即:在插入元素的过程中会自动扩容
Object[] elementData; // 存放元素的空间
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; // 默认空间为0
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进行扩容
④.授予copeOf进行扩容
🌲5. ArrayList的具体使用
🌿5.1 删除字符 (要求使用集合)
🌿5.2 杨辉三角
杨辉三角
我们把图形抽象为一个直角三角形
要求的返回值是这样的–二维数组
ret是整个数组
public List<List<Integer>> generate(int numRows) {
List<List<Integer>> ret=new ArrayList<>();
//第一行
List<Integer> list0=new ArrayList<>();
list0.add(1);
ret.add(list0);
//从第2行开始求每个元素
for (int i = 1; i <numRows ; i++) {
List<Integer> curRow = new ArrayList<>();//每一行
curRow.add(1); //第一列
//中间
List<Integer> preRow = ret.get(i - 1);//获得上一行
for (int j = 1; j < i; j++) {
int val1 = preRow.get(j);
int val2 = preRow.get(j - 1);
curRow.add(val1 + val2);
}
//尾巴
curRow.add(1);
ret.add(curRow);
}
return ret;//返回目标数组
}
}
🌿5.3 扑克牌洗牌
自己写一副扑克牌
①:完成刚买牌的顺序打印出来
②:我们再完成洗牌随机打乱顺序
③:三个人轮流每个人揭5张牌
④:输出最后剩余的牌
1.我们需要先单独创建一个Card类,定义花色和数字,添加构造方法以及getter和setter方法,重写ToString方法
class Card {
private String suit;
private int rank;
public Card(String suit, int rank) {
this.suit = suit;
this.rank = rank;
}
public String getSuit() {
return suit;
}
public void setSuit(String suit) {
this.suit = suit;
}
public int getRank() {
return rank;
}
public void setRank(int rank) {
this.rank = rank;
}
@Override
public String toString() {
return "[ " + suit+" "+rank+" ]";
}
}
2.牌按照花色和顺序打印出来,再把花色和序号拼接在一起组成每个牌每个花色
public static final String[] suits = {"♥","♠","♣","♦"};
public static List<Card> buyCard() {
List<Card> desk = new ArrayList<>();
for (int i = 0; i < 4; i++) {
for (int j = 1; j <= 13 ; j++) {
String suit = suits[i];
Card card = new Card(suit,j);
desk.add(card);
}
}
return desk;
}
3.洗牌使用random类中的nextInt函数并且交换每个元素的下标,让它遍历的时候随机和其他元素下标交换,让下标随机数取元素长度里的任何一个下标,正向遍历也可以,反向遍历更好
public static void shuffle(List<Card> cardList) {
for (int i = cardList.size()-1; i > 0 ; i--) {
Random random = new Random();
int index = random.nextInt(i);
swap(cardList,i,index);
}
}
private 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);
}
4.三个人每个人都轮流揭五张牌,最后揭的牌需要删除,方便后面我们打印剩余的牌
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 3; j++) {
//每次揭牌都去获取 cardList的0下标的数据【删除】
Card card = cardList.remove(0);
List<Card> hand = hands.get(j);
hand.add(i,card);//这里使用add 不能是set
/*hands.get(j).add(card);*/
}
}
5.当我们此时打印cardList就是剩余的牌了,然后把所有的代码执行结果打印出来
附上代码:
CardDemo
public class CardDemo {
public static final String[] suits = {"♥","♠","♣","♦"};
public List<Card> buyCard() {
List<Card> cardList = new ArrayList<>();
for (int i = 1; i <= 13; i++) {
for (int j = 0; j < 4; j++) {
int rank = i;
String suit = suits[j];
Card card = new Card(suit,rank);
cardList.add(card);
}
}
return 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);
}
}
private void swap(List<Card> cardList,int i,int j) {
/*
Card tmp = cardList[i];
cardList[i] = cardList[j];
cardList[j] = tmp;
*/
Card tmp = cardList.get(i);
cardList.set(i,cardList.get(j));
cardList.set(j,tmp);
}
public List<List<Card>> play(List<Card> cardList) {
List<Card> hand0 = new ArrayList<>();
List<Card> hand1 = new ArrayList<>();
List<Card> hand2 = new ArrayList<>();
List<List<Card>> hand = new ArrayList<>();
hand.add(hand0);
hand.add(hand1);
hand.add(hand2);
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 3; j++) {
Card card = cardList.remove(0);
//怎么把对应的牌 放到对应的人的手里面
hand.get(j).add(card);
}
}
return hand;
}
}
Card
public class Card {
private String suit;
private int rank;
public Card(String suit, int rank) {
this.suit = suit;
this.rank = rank;
}
@Override
public String toString() {
/*return "Card{" +
"suit='" + suit + '\'' +
", rank=" + rank +
'}';*/
return "{"+suit + rank +"} ";
}
}
Main
public static void main(String[] args) {
CardDemo cardDemo = new CardDemo();
//1. 买52张牌
List<Card> cardList = cardDemo.buyCard();
System.out.println(cardList);
//2. 洗牌
cardDemo.shuffle(cardList);
System.out.println(cardList);
//3. 3个人 每个人 轮流揭牌5张
List<List<Card>> ret = cardDemo.play(cardList);
for (int i = 0; i < ret.size(); i++) {
System.out.println("第"+(i+1)+"个人的牌:"+ret.get(i));
}
System.out.println("剩下的牌:");
System.out.println(cardList);
}
6.🌸ArrayList的问题
- ArrayList底层使用连续的空间,任意位置插入或删除元素时,需要将该位置后序元素整体往前或者往后搬移,故时间复杂度为O(N)
- 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
- 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间
这些问题该如何解决呢?
使用链表就可以解决
下一篇文章揭晓链表的神奇应用