1.线性表和顺序表以及链表的关系
2.ArrayList的基础自定义实现
3.ArrayList的简单介绍和常用API
4.ArrayList的扩容机制
1.啥是线性表,啥又是顺序表和链表?它们之间有什么区别?
线性表:是n个具有相同性质的元素的有限序列;比如说什么顺序表,链表,栈,队列等……这些都属于线性表的范畴
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
简单来说线性表包含顺序表和链表。顺序表和链表统称为线性表
然后我们区分一下链表和顺序表
顺序表:逻辑上和物理上都连续,逻辑上就是说,上一个元素跟下一个元素是有联系的,然后物理上也就是储存空间地址上,是连续的
链表:链表在逻辑上是连续的,但在物理上不一定连续,就是说通过上一个节点是可以找到下一个节点的,但两个节点的存储位置不一定在存储空间中是连续的
以上就是线性表,顺序表,链表的介绍,以及三者的区别和联系~
接下来我们来简单实现一个顺序表~
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
下面是代码展示
public class MyArrayList {
public int[] elem;//基于数组,对数组进行操作
public int usedSize;//有效元素个数
public static final int DEFAULT_CAPACITY = 10;//数组初始化长度
public MyArrayList(){
this.elem = new int[DEFAULT_CAPACITY];//构造函数开辟数组长度
}
//下面是对数组进行操作的API
//打印操作
public void display(){
for(int i = 0; i < this.usedSize; i++){
System.out.print(elem[i] + " ");
}
System.out.println();
}
//新增元素,默认在数组末尾新增,要考虑扩容问题
public void add(int data){
if(isFull()){
elem = Arrays.copyOf(elem,2*elem.length);
}
elem[usedSize++] = data;
}
//判断数组是否为满
public boolean isFull(){
return usedSize == elem.length;
}
//在指定pos位置插入元素,注意的是pos位置是否是合法的
private void checkAddPos(int pos){
if(pos < 0 || pos > usedSize){
throw new PosIndexNotLegalException("pos位置不合法");//自定义异常
}
}
public void addPos(int pos, int data){
try {
checkAddPos(pos);
if(isFull()){
elem = Arrays.copyOf(elem,2*elem.length);
}
for(int i = usedSize-1; i >= pos; i--){
elem[i+1] = elem[i];
}
elem[pos] = data;
usedSize++;
}catch (PosIndexNotLegalException e){
e.printStackTrace();
}
}
//判断数组是否包含某个元素
public boolean contains(int toFind){
for(int i = 0; i < elem.length; i++){
if(toFind == elem[i]){
return true;
}
}
return false;
}
//找到某个元素对应的位置//排除元素重复的情况
public int indexOf(int toFind){
for(int i = 0; i < elem.length; i++){
if(toFind == elem[i]){
return i;
}
}
return -1;
}
//找到pos位置对应的元素,要判断这个pos是否是合法的,必须是有效元素
private void checkGetPos(int pos){
if(pos < 0 || pos >= usedSize){
//等于pos的时候,pos里没有元素
throw new PosIndexNotLegalException("pos位置不合法");//自定义异常
}
}
public int get(int pos){
/*int retVal = -1;
try {
checkGetPos(pos);
retVal = elem[pos];
}catch (PosIndexNotLegalException e){
e.printStackTrace();
}
return retVal;*/
checkGetPos(pos);
return elem[pos];
}
//获取顺序表长度
public int size(){
return usedSize;
}
//更新pos位置的元素
public void set(int pos, int value){
checkGetPos(pos);
elem[pos] = value;
}
//删除第一次出现的关键字key
public void remove(int key){
int index = indexOf(key);
if(index == -1){
return;
}
for(int i = index; i < usedSize-1; i++){
elem[i] = elem[i+1];
}
//elem[i] = null 如果最后一个元素是引用类型的话,要制空,否则就一直占用空间了
usedSize--;
}
//清空顺序表
public void clear(){
/*for(int i = 0; i < elem.length; i++){
elem[i] = null;
}*/
usedSize = 0;
}
}
首先定义了一个数组,和usedSize,我们用usedSize来记录当前数组里有效元素的个数
在调用构造方法我们就直接给数组10个元素大小的长度
接下来我们要实现打印操作,那就是遍历当前数组里面的所有元素,打印出来即可~
重点是在于实现插入元素add方法,要注意的是数组的扩容,先判断当前数组里面是否满了,满了就要扩容,然后元素的插入是尾插,我们不能隔着一个空间在插入,这是不允许的,必须紧挨着最后一个元素的下一个位置插入,因为上一个元素的必须要包含下一个元素的信息,不管是顺序表还是链表都一样,不能跳着插入
剩下的方法都是挺简单的,看注释就能看明白了,过多的就不说了~
ArrayList的简单介绍和常用API
1. ArrayList实现了RandomAccess接口,表明ArrayList支持随机访问
2. ArrayList实现了Cloneable接口,表明ArrayList是可以clone的
3. ArrayList实现了Serializable接口,表明ArrayList是支持序列化的
4. 和Vector不同,ArrayList不是线程安全的,在单线程下可以使用,在多线程中可以选择Vector或者CopyOnWriteArrayList
5. ArrayList底层是一段连续的空间,并且可以动态扩容,是一个动态类型的顺序表
ArrayList的三种构造方式
/**
* ArrayList的三种构造方式
* @param rgs
*/
public static void main2(String[] rgs) {
//1.构造一个空的ArrayList
List list1 = new ArrayList<>();
//2.构造一个长度为10的ArrayList
List list2 = new ArrayList<>(10);
list2.add(1);
list2.add(2);
list2.add(3);
//list2.add("hello");
//3.构造一个和list2一样的ArrayList
List list3 = new ArrayList<>(list2);
}
ArrAyList常用方法(用法注释写的很清楚)
/**
* ArrayList的基本方法调用
* @param args
*/
public static void main3(String[] args) {
List list = new ArrayList<>();
list.add("javaSe");
list.add("javaWab");
list.add("javaEE");
list.add("JVM");
list.add("测试");
System.out.println(list);//可以直接打印出list里面的所有元素
//获取list中有效元素个数
System.out.println(list.size());
//获取元素的下标位置
System.out.println(list.indexOf("javaSe"));
System.out.println(list.indexOf("JVM"));
//获取和设置index位置上的元素,index介于[0,size)间
System.out.println(list.get(0));
System.out.println(list.get(4));
/*list.set(4,"hello");
System.out.println(list.get(4));
System.out.println(list);*/
//在list的index位置插入指定元素:index和后续元素统一往后移动一个位置
list.add(1,"数据结构");
System.out.println(list);
//删除指定元素,找到就删除,该元素的后面元素统一往前移动一位
list.remove("数据结构");
System.out.println(list);
//删除list中index位置元素的时候,注意不要超过list中有效元素个数,否则会越界
list.remove(list.size()-1);
System.out.println(list);
//检测list中是否包含指定元素,包含返回true,不包含返回false
//System.out.println(list.contains("数据结构"));
if(!list.contains("数据结构")){
list.add("数据结构");
}
System.out.println(list);
//查找指定元素第一次出现的位置:indexOf从前往后找 lastIndexOf从后往前找
list.add("javaSe");
System.out.println(list);
System.out.println(list.indexOf("javaSe"));
System.out.println(list.lastIndexOf("javaSe"));
//使用list中[0,4)之间的元素构成一个新的ArrayList返回 0 1 2 3
//List ret = new ArrayList<>(list.subList(0,4));
List ret = list.subList(0,4);
System.out.println(ret);
System.out.println(list.subList(0,6));
//清空元素
list.clear();
System.out.println(list.size());
System.out.println(list);
}
ArrayList三种打印方法(注释很清楚)
/**
* ArrayList的三种遍历方式
* @param args
*/
public static void main(String[] args) {
List list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
System.out.println("======for=========");
//1.使用下标+for遍历
for(int i = 0; i < list.size(); i++){
System.out.print(list.get(i) + " ");
}
System.out.println();
System.out.println("=====foreach========");
//使用foreach遍历
for(Integer x : list){
System.out.print(x + " ");
}
System.out.println();
System.out.println("======迭代器打印==========");
Iterator it = list.listIterator();
//Iterator it = list.iterator();
Iterator it2 = list.listIterator(2);//指定从哪个位置开始迭代打印
while (it2.hasNext()){
System.out.print(it2.next() + " ");
}
}
接下来简单介绍一下ArrayList的扩容机制,深度理解得去看源码
首先,我们最开始要是先创建一个不带参数的ArrayList对象,此时这个对象底层的数组长度为0,那么什么时候才能有长度呢?那就是当你要添加元素的时候,也就是要调用add方法的时候,当你调用add,底层源码中会给数组赋予10个长度的数组大小,所以我们创建好对象,是没有大小的,只是当我们开始add了后,此时数组才会有大小~
我们现在知道了,但第一次add的时候,会给不带参数的ArrayList10个大小的空间,那么当10个大小空间用完了后呢?当我们要添加第11个元素的时候,底层又会去调用grow方法来进行原素组大小的1.5倍的扩容(也就是在10的基础上扩容到15)
如果我们刚开始创建对象的时候就带了参数,给数组赋值15的大小,拿它最开始就是15的长度~
小结:
1. 检测是否真正需要扩容,如果是调用grow准备扩容
2. 预估需要库容的大小初步预估按照1.5倍大小扩容如果用户所需大小超过预估1.5倍大小,则按照用户所需大小扩容真正扩容之前检测是否能扩容成功,防止太大导致扩容失败
3. 使用copyOf进行扩容