ArrayList
基本概念:
- 顺序表的底层是顺序存储结构, 也就是数组
- 顺序表的最直观理解是变长数组,动态数组
- 顺序表充分体现了封装与抽象之美
java 语言实现
ArrayList 需要实现下面的接口
/**顺序表的增删改查*/
public interface List<T>
{
/** 第一个位置插入 */
void addFirst(T data);
/** 在最后一个位置插入 */
void addLast(T data);
/** 删除index对应位置的节点, 并返回该节点的数据 */
T delete(int index);
/** 在index 对应位置加入数据 */
void add(int index, T data);
/** 更改index对应位置的数据为data */
void set(int index, T data);
/** 查询index对应位置的数据 */
T get(int index);
/** 获取data元素第一次出现时候的index */
int getIndex(T data);
/**遍历顺序表*/
void traverse();
/**获取顺序表大小*/
int size();
}
成员变量与构造函数
ArrayList 底层维护着一个数组对象, 它的最大容量是capacity, size 用来表示ArrayList 插入元素的数目
package com.weinijuan;
public class ArrayList<T> implements List<T>
{
private int size;
private int capacity = 16;
private T[] ar;
public ArrayList()
{
ar = (T[]) new Object[capacity];
}
}
实现addLast
这是ArrayList 类用的最多的方法, 乍一看貌似会数组下标访问越界, 实则不然, capacity才是数组真正的容量, size 是当前对象最后一个元素下标(index - 1)的后面一个位置的下标。
因为插入了一个元素, 所以size需要加一。
这段代码虽然简单,但几乎是ArrayList 最重要的方法
@Override
public void addLast(T data)
{
ar[size] = data;
size++;
}
实现扩容机制
上面的缺陷就是最多只能有16个元素,可以在size == capacity 的时候进行扩容.
扩容原理:
- 新创建一个更大容量的数组
- 将原先数组的内容拷贝到 新创建的数组中
(System.arraycopy方法,第一个参数是被拷贝的数组, 第二个是开始拷贝的位置,
第三个是被拷入的数组, 第四个是开始拷入的位置, 第五个是拷贝的个数 ) - 更新ar, 令ar 指向新数组
- 更新capacity
System.arraycopy 经常出现在ArrayList 中,需要掌握
private void resize(int newCapacity)
{
T[] temp = (T[]) new Object[newCapacity];
System.arraycopy(ar, 0, temp, 0, size);
ar = temp;
capacity = newCapacity;
}
@Override
public void addLast(T data)
{
if (size == capacity)
{
resize(2*capacity);
}
ar[size] = data;
size++;
}
删除最后一个元素和缩小容量
- 一旦size < capacity/4, 就缩小容量为二分之一, 从而保证ArrayList 的利用率大于等于百分之五十。这个参数可以自己调
- 取得返回数据
- 逻辑大小减一
- 将原先位置的最后一个元素置为null , 从而防止对象游离, 对于原始数据类型这一步可以省略
- 返回数据
对象游离: 因为通过数组可以访问到最后一个本应该被删除的对象, 所以垃圾回收器不会回收这个对象, 导致对象游离
public T deleteLast(int index)
{
if (size < capacity/4)
{
resize(capacity/2);
}
T retData = ar[size - 1];
size--;
ar[size] = null;
return retData;
}
上面的几个方法非常简单,非常重要,后面的几个方法可能复杂,但是实际中并不是经常使用
实现add(index, data)
- 数组边界检查, 🐶 ,
- 数组扩容检查
- 因为index处必然已经有对象在那了, 所以需要将[index, size - 1 ]处的所有对象向后挪一个位置, 然后让index位置放置data.
- 更新成员变量 size
List: add 方法的index 范围是【0,index】, 而删除、查找、更新都是【0, index-1】
@Override
public void add(int index, T data)
{
if (index < 0 || index > size)
{
throw new IllegalArgumentException();
}
if (size == capacity)
{
resize(2 * capacity);
}
for (int i = size - 1; i >= index; i--)
{
ar[i + 1] = ar[i];
}
ar[index] = data;
size++;
}
实现delete(index)
核心:
将[index, size - 2]的内容用[index + 1, size - 1]的内容覆盖, 即可实现删除ar[index]
不能简单的讲ar[index] = null, 举个例子
原本arrayList 中存有 0, 1, 2, 3, 4, 5, 6
现在delete(3), 显然剩下的应该是0, 1, 2, 4, 5, 6
而不是0, 1, 2, null, 4, 5, 6
@Override
public T delete(int index)
{
if (index < 0 || index >= size)
{
throw new IllegalArgumentException();
}
if (size < capacity/4)
{
resize( capacity/2);
}
T retData = ar[index];
for (int i = index; i < size - 1; i++)
{
ar[i] = ar[i + 1];
}
size--;
return retData;
}
get/set
和数组访问一摸一样, 只不过它是IndexOutOfBoundsException异常, 我们用的是不合法参数异常, 我猜 java 底层就是在c++对象的基础之上加了这样一层异常检查
@Override
public void set(int index, T data)
{
if (index < 0 || index >= size)
{
throw new IllegalArgumentException();
}
ar[index] = data;
}
@Override
public T get(int index)
{
if (index < 0 || index >= size)
{
throw new IllegalArgumentException();
}
return ar[index];
}
traverse / size
@Override
public void traverse()
{
for (int i = 0; i < size; i++)
{
System.out.print(ar[i] + " ");
}
}
@Override
public int size()
{
return size;
}
getIndex
这里第一个 == 等号是用来检测当data 和ar[i] 均为null 或者是完全相同对象的时候。
@Override
public int getIndex(T data)
{
for (int i = 0; i < size; i++)
{
if (data == ar[i] || data != null && data.equals(ar[i]))
{
return i;
}
}
return -1;
}
测试
使用之前测试LinkedList 的测试文件 🐶 , 真好啊,
测试List 的其他实现类时只需要将 new 后面的类名改一下就可以了。
package com.weinijuan;
import org.junit.Assert;
import org.junit.Test;
public class ListTest
{
@Test
public void testAddFirst()
{
List<Integer> list = new ArrayList<>();
int[] test = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int[] ans = {9, 8, 7, 6, 5, 4, 3, 2, 1, 0};
for (int i = 0; i < test.length; i++)
{
list.addFirst(test[i]);
}
list.traverse();
for (int i = 0; i < ans.length; i++)
{
if (i == 5)
{
System.out.println();
}
Assert.assertEquals(ans[i],(int) list.get(i));
}
}
@Test
public void testAddLast()
{
List<Integer> list = new ArrayList<>();
int[] test = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int[] ans = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
for (int i = 0; i < test.length; i++)
{
list.addLast(test[i]);
}
list.traverse();
for (int i = 0; i < ans.length; i++)
{
Assert.assertEquals(ans[i],(int) list.get(i));
}
}
@Test
public void testDelete()
{
List<Integer> listFirst = new ArrayList<>();
List<Integer> listMid = new ArrayList<>();
List<Integer> listEnd = new ArrayList<>();
int[] test = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int[] ansFirst = {1, 2, 3, 4, 5, 6, 7, 8, 9};
int[] ansMid = {0, 1, 2, 3, 4, 6, 7, 8, 9};
int[] ansEnd = {0, 1, 2, 3, 4, 5, 6, 7, 8};
for (int i = 0; i < test.length; i++)
{
listFirst.addLast(test[i]);
listMid.addLast(test[i]);
listEnd.addLast(test[i]);
}
listFirst.delete(0);
listMid.delete(test.length/2);
listEnd.delete(test.length - 1);
for (int i = 0; i < ansFirst.length; i++)
{
Assert.assertEquals(ansFirst[i],(int) listFirst.get(i));
Assert.assertEquals(ansMid[i],(int) listMid.get(i));
Assert.assertEquals(ansEnd[i],(int) listEnd.get(i));
}
}
@Test
public void testSize()
{
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 100; i++)
{
list.addLast(i);
}
Assert.assertEquals(100, list.size());
}
@Test
public void testSet()
{
List<Integer> list = new ArrayList<>();
int[] test = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int[] ans = {10, 1, 2, 3, 4, 20, 6, 7, 8, 30};
for (int i = 0; i < test.length; i++)
{
list.addLast(test[i]);
}
list.set(0, 10);
list.set(test.length/2, 20);
list.set(test.length - 1, 30);
list.traverse();
for (int i = 0; i < ans.length; i++)
{
Assert.assertEquals(ans[i],(int) list.get(i));
}
}
@Test
public void testGetIndex()
{
List<Integer> list = new ArrayList<>();
int[] test = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 5};
int ans = 5;
for (int i = 0; i < test.length; i++)
{
list.addLast(test[i]);
}
Assert.assertEquals(5, list.getIndex(5));
Assert.assertEquals(-1, list.getIndex(100));
}
}
性能估计
操作 | 时间复杂度 | 备注 |
---|---|---|
addFirst | O (N) | |
addLast | O(1) | |
add | O(N) | 需要额外移动其他对象,很慢 |
set | O(1) | |
get | O(1) | |
getIndex | O(N) | |
traverse | O(N) | |
size | O(1) | |
delete | O(N) | 删除最后第一个是O(1) 删除第一个是O(N) |
经常在开头中间插入删除使用链表, 经常查找更新使用ArrayList
一般情况下:用ArrayList 更好, 因为它充分利用了空间,也快