文章目录
ArrayList和顺序表
引言:本篇文章讲述了顺序表的代码原理和如何使用Java自己的库方法。虽然Java提供了数据结构的方法,但是只会用是不行的,还应该知道它的原理,这样学习数据结构就事半功倍。在学习数据结构的过程中,我个人建议应当多思考,多画图,这样对数据结构的理解会更深刻。如果有一定的基础可以看看ArrayList
的源代码。
一、线性表
线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结
构,常见的线性表:顺序表、链表、栈、队列…
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物
理上存储时,通常以数组和链式结构的形式存储。
二、顺序表
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成
数据的增删查改。
2.1、顺序表的代码逻辑
1、定义顺序表的接口
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();
boolean isFull();
public boolean isEmpty();
}
2、实现方法
首先是初始化成员变量
public class ArrayListDemo implements IList {
private int[] elem;//初始化数组用来存放元素
private static final int DEFAULT_SIZE = 10;//默认容量
private int usedSize = 0;//顺序表元素个数
public ArrayListDemo() {
this.elem = new int[DEFAULT_SIZE];
}
public ArrayListDemo(int pos) {
this.elem = new int[pos];
}
}
-
打印顺序表(在Java库方法中并没有此方法,此处仅仅是为了演示)
@Override public void display() { for (int i = 0; i < this.usedSize; i++) { System.out.print(elem[i] + " "); } System.out.println(); }
-
获得顺序表长度
public int size() { return this.usedSize; }
因为我们定义的
usedSize
为顺序表的长度,因此只需要返回usedSize
的值就行。 -
默认在数组最后新增元素
在写的时候思考一下,当默认容量不够用时应该怎么办?
我们可以定义一个
checkCapacity()
来检查容量,如果容量满了就扩容。//TODO:检查容量是否需要扩容 public void checkCapacity() { //扩容 if (isFull()) { elem = Arrays.copyOf(elem,elem.length*2); } }
//TODO:新增元素,默认在数组最后新增 @Override public void add(int data) { checkCapacity(); this.elem[this.usedSize] = data; this.usedSize++; }
-
在pos位置新增元素
//TODO:在 pos 位置新增元素 @Override public void add(int pos, int data) { //检查插入位置是否合适 try { checkPost(pos); }catch (PosIllegal posIllegal) { posIllegal.printStackTrace(); return; } //检查容量 checkCapacity(); //元素后移 for (int i = usedSize-1; i >= pos; i--) { this.elem[i+1] = this.elem[i]; } //存放 this.elem[pos] = data; this.usedSize++; }
这里定义一个自定义异常类
PosIllegal
,若是插入pos不合法就抛出异常。pos检查:
//TODO:检查下标是否异常 private void checkPost(int pos) throws PosIllegal{ if (pos < 0 || pos > usedSize) { throw new PosIllegal("插入位置不合法:"+pos); } }
-
判定是否包含某个元素
//TODO:判定是否包含某个元素 @Override public boolean contains(int toFind) { if (isEmpty()) { return false; } for (int i = 0; i < this.usedSize; i++) { if (this.elem[i] == toFind) { return true; } } return false; }
-
查找某个元素对应位置
//TODO:查找某个元素对应的位置 @Override public int indexOf(int toFind) { if (isEmpty()) { return -1; } for (int i = 0; i < usedSize; i++) { if (this.elem[i] == toFind) {//注意,若是引用数据类型应该使用equals方法 return i; } } //没找到 return -1; }
-
获取pos位置元素
//TODO:获取 pos 位置的元素 @Override public int get(int pos) { try { checkPost(pos); }catch (PosIllegal posIllegal) { posIllegal.printStackTrace(); } if (isEmpty()) { return -1; } return this.elem[pos]; }
-
给pos位置元素设置value值 更新
//TODO:给 pos 位置的元素设为 value 更新 @Override public void set(int pos, int value) { checkPost(pos); elem[pos] = value; }
-
删除第一次出现的关键字key
//TODO:删除第一次出现的关键字key @Override public void remove(int toRemove) { int index = indexOf(toRemove); if (index == -1) { System.out.println("没有这个数字"); return; } for (int i = index; i < usedSize; i++) { this.elem[i] = this.elem[i+1]; } this.usedSize--; }
-
清空顺序表
//TODO:清空顺序表 @Override public void clear() { this.usedSize = 0; }
-
判满
//TODO:判满 @Override public boolean isFull() { return usedSize == elem.length; }
什么时候顺序表满了?顺序表长度等于数据元素个数时顺序表满
-
判空
//TODO:判空 @Override public boolean isEmpty() { return usedSize == 0; }
3、自定义异常类
public class PosIllegal extends RuntimeException{
public PosIllegal(String msg) {
super(msg);
}
}
public class MyArrayListException extends RuntimeException{
public MyArrayListException(String message) {
super(message);
}
}
4、main方法测试
public class MainTest {
public static void main(String[] args) {
ArrayListDemo arrayList = new ArrayListDemo();
//设置初始元素
arrayList.add(1);
arrayList.add(2);
arrayList.add(3);
arrayList.add(4);
arrayList.add(5);
arrayList.display();
System.out.println("=================");
//在pos位置新增元素
arrayList.add(1,991);
arrayList.add(3,992);
arrayList.add(0,993);
arrayList.add(4,994);
arrayList.display();
System.out.println("=================");
//测试扩容情况
arrayList.add(100);
arrayList.add(101);
arrayList.display();
System.out.println("=================");
//删除数据
arrayList.remove(1000);
arrayList.remove(100);
arrayList.display();
System.out.println("=================");
//获取数据
int num = arrayList.get(8);
System.out.println(num);
System.out.println("=================");
//更改数据
arrayList.set(0,0);
arrayList.display();
System.out.println("=================");
//查找对应下标数据
int tem = arrayList.indexOf(100);
System.out.println(tem);
System.out.println("=================");
//判断是否包含某个元素
System.out.println(arrayList.contains(100));
System.out.println(arrayList.contains(101));
System.out.println("=================");
//获取顺序表长度
System.out.println("顺序表长度为:"+arrayList.size());
System.out.println("=================");
//清空顺序表
arrayList.clear();
arrayList.display();
}
}
三、Java库方法实现顺序表
1、ArrayList简介
在集合框架中,ArrayList是一个普通的类,实现了List接口,具体框架图如下:
【说明】
- ArrayList是以泛型方式实现的,使用时必须要先实例化。
- ArrayList实现了RandomAccess接口,表明ArrayList支持随机访问
- ArrayList实现了Cloneable接口,表明ArrayList是可以clone的
- ArrayList实现了Serializable接口,表明ArrayList是支持序列化的
- 和Vector不同,ArrayList不是线程安全的,在单线程下可以使用,在多线程中可以选择Vector或者CopyOnWriteArrayList
- ArrayList底层是一段连续的空间,并且可以动态扩容,是一个动态类型的顺序表
2、ArrayList的使用
2.1、ArrayList的构造方法
方法 | 解释 |
---|---|
ArrayList() | 无参构造 |
ArrayList(Conllection <? extends E> c) | 利用其他 Collection 构建 ArrayList |
ArrayList(int initialCapacity) | 指定顺序表初始容量 |
构造一个空列表(整形为例)
ArrayList<Integer> list = new ArrayList<>();
构建一个容量为10的列表
ArrayList<Integer> list = new ArrayList<>(10);
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add("Hello");//编译失败,Integer限定了list里面只能为整形
关于ArrayList(Conllection <? extends E> c)的讲解:
举例如下
ArrayList<Number> list = new ArrayList<>(10);
ArrayList<Integer> list2 = new ArrayList<>(list);
使用该方法首先要满足两个条件:
-
实现Collection接口
ArrayList<Integer> list = new ArrayList<>(10); ArrayList<Integer> list2 = new ArrayList<>(list);
传入的list是ArrayList类型,而ArrayList实现了Collection接口,所以满足条件
再比如:
LinkedList<Integer> list1 = new LinkedList<>(); ArrayList<Integer> list2 = new ArrayList<>(list1);
LinkedList实现了Collection接口,所以也能传入
-
? extends E
这个代码的意思是?必须是E的子类或者它本身,比如:
ArrayList<Integer> list = new ArrayList<>(10); ArrayList<Number> list2 = new ArrayList<>(list);
list是Integer,而Integer是Number的子类,所以能够传入
但若是:
ArrayList<String> list1 = new ArrayList<>(10); ArrayList<Number> list2 = new ArrayList<>(list1);//编译失败
因为String不是Number的子类,所以不能传入.
补充说明
这种构造方法可以实现把一个集合内的元素给另一个集合
ArrayList<Integer> list = new ArrayList<>(10); list.add(1); list.add(2); list.add(3); list.add(4); ArrayList<Number> list2 = new ArrayList<>(list); list2.add(99); System.out.println(list1); System.out.println(list2); //结果为: //[1,2,3,4] //[1,2,3,4,99]
2.2、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 |
addAll方法:
ArrayList<Integer> list1 = new ArrayList<>(10);
list1.add(1);
list1.add(2);
list1.add(3);
ArrayList<Number> list2 = new ArrayList<>();
list2.add(99);
list2.addAll(list1);
System.out.println(list2);
//结果为:[99,1,2,3]
这个相当于把另一个集合的元素放到自己的后面
[重点说明]
remove方法
//list2:[99,1,2,3]
//删除指定下标元素
list2.remove(0);
System.out.println(list2);
//结果为:[1,2,3];
//list2:[99,1,2,3]
//删除第一个指定元素:
list2.remove((Integer)2);
System.out.println(list2);
//结果为:[99,1,3]
subList方法
public static void main(String[] args) {
ArrayList<Integer> list1 = new ArrayList<>();
list1.add(1);
list1.add(2);
list1.add(3);
list1.add(4);
list1.add(5);
System.out.println(list1);
List<Integer> list2 = list1.subList(1,3);//左闭右开
System.out.println(list2);
}
//结果为:
//[1,2,3,4,5]
//[2,3]
注意!!!
list2.set(1,99);
System.out.println(list2);
System.out.println(list1);
这是我们预期的结果是:
//[2,99]
//[,1,2,3,4,5]
但实际上运行的结果为:
为什么list1里面的元素也被更改了呢?
原因是list2并没有创建新的对象,而是指向list1被截取的部分
因此,修改list2时也会修改list1里面的值。subList的截取,并不会产生新的对象!!!
3、ArrayList的遍历
ArrayList有三种遍历方式:for循环+下标、for-each循环、使用迭代器
第一种:for循环+下标
for (int i = 0; i < list.size(); i++) {
System.out.print(list.get(i) + " ");
}
第二种:for-each循环
for (Integer integer:list2) {
System.out.print(integer + " ");
}
第三种:使用迭代器
Iterator<Integer> iterator = list2.listIterator();
while (iterator.hasNext()) {
System.out.print(iterator.next() + " ");
}
4、ArrayList的扩容机制
ArrayList是一个动态类型的顺序表,即:在插入元素的过程中会自动扩容。源代码请自行查看
【总结】
- 检测是否真正需要扩容,如果是调用grow准备扩容
- 预估需要库容的大小
- 初步预估按照1.5倍大小扩容
- 如果用户所需大小超过预估1.5倍大小,则按照用户所需大小扩容
- 真正扩容之前检测是否能扩容成功,防止太大导致扩容失败
- 使用copyOf进行扩容
5、ArrayList的相关总结
- ArrayList底层使用连续的空间,任意位置插入或删除元素时,需要将该位置后序元素整体往前或者往后搬移,故时间复杂度为O(N)
- 扩容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
:list2) {
System.out.print(integer + " ");
}
第三种:使用迭代器
```Java
Iterator<Integer> iterator = list2.listIterator();
while (iterator.hasNext()) {
System.out.print(iterator.next() + " ");
}
4、ArrayList的扩容机制
ArrayList是一个动态类型的顺序表,即:在插入元素的过程中会自动扩容。源代码请自行查看
【总结】
- 检测是否真正需要扩容,如果是调用grow准备扩容
- 预估需要库容的大小
- 初步预估按照1.5倍大小扩容
- 如果用户所需大小超过预估1.5倍大小,则按照用户所需大小扩容
- 真正扩容之前检测是否能扩容成功,防止太大导致扩容失败
- 使用copyOf进行扩容
5、ArrayList的相关总结
- ArrayList底层使用连续的空间,任意位置插入或删除元素时,需要将该位置后序元素整体往前或者往后搬移,故时间复杂度为O(N)
- 扩容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
- 扩容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。