引言
在Java编程世界中,顺序表是一种基础且强大的数据结构,它提供了一种高效的方式来存储和管理数据。顺序表又称为动态数组,是数组的一种灵活扩展,允许进行动态的插入和删除操作。今天,我们将深入了解Java中顺序表的内部工作原理和实现方法。
顺序表的概念
顺序表是一种线性表,但它通过一段连续的存储空间来存储元素,这使得数据元素可以通过索引快速访问。在Java中,顺序表通常由数组实现,但与普通数组不同,顺序表可以动态调整其大小以适应更多的数据元素。
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
ArrayList与顺序表简介
在集合框架中,ArrayList是一个普通的类,实现了List接口。
具体框架图如下:
注意:
- ArrayList是以泛型方式实现的,使用时必须要先实例化。
- ArrayList实现了RandomAccess接口,表明ArrayList支持随机访问。
- ArrayList实现了Cloneable接口,表明ArrayList是可以clone的。
- ArrayList实现了Serializable接口,表明ArrayList是支持序列化的。
- 和Vector不同,ArrayList不是线程安全的,在单线程下可以使用,在多线程中可以选择Vector或者CopyOnWriteArrayList。
- ArrayList底层是一段连续的空间,并且可以动态扩容,是一个动态类型的顺序表。
顺序表的特点:
- 动态扩容:当顺序表中的元素超过其初始容量时,它可以自动扩展以容纳更多元素。
- 随机访问:顺序表允许通过索引直接访问任何位置的元素,提供了快速的检索能力。
- 连续存储:所有元素都存储在连续的内存空间中,这有助于优化访问速度。
ArrayList的实现
在Java中,ArrayList可以通过创建一个类来实现,该类包含一个数组以及相关的方法来执行各种操作。
以下是一个简单的ArrayList实现示例(未使用泛型):
1.先创建一个接口类IList
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();
}
这个接口类也可以不创建,但为了避免漏写功能还是建议创建。
2.创建MyArrayList类并让它实现IList类中的方法
public class MyArrayList implements IList {
private int[] elem;
private int usedSize;
//顺序表默认大小
public static final int DEFAULT_SIZE = 10;
//有参构造
public MyArrayList(int elem) {
this.elem = new int[elem];
}
//无参构造
public MyArrayList() {
this.elem = new int[DEFAULT_SIZE];
}
//新增元素,默认在数组最后新增
@Override
public void add(int data) {
checkCapacity();
this.elem[this.usedSize] = data;
++this.usedSize;
}
/**
* 判断是否已满(仅做内部支持)
*/
private boolean isFull() {
// if (this.usedSize == this.elem.length) {
// return true;
// }
// return false;
return this.usedSize == this.elem.length;
}
/**
* 检查是否需要进行扩容,需要则扩容(仅做内部支持)
*/
private void checkCapacity() {
if (isFull()) {
this.elem = Arrays.copyOf(this.elem, this.elem.length * 2);
}
}
//在pos位置新增元素
@Override
public void add(int pos, int data) {
checkCapacity();
try {
checkPosOnAdd(pos);
} catch (PosIllegality e) {
e.printStackTrace();
return;
}
for (int i = this.usedSize; i > pos; --i) {//把pos后的数据往后移动一格
this.elem[i] = this.elem[i - 1];
}
this.elem[pos] = data;
++this.usedSize;
}
/**
* 检查传入add的pos是否合法(仅做内部支持)
*/
private void checkPosOnAdd(int pos) {
if (pos < 0 || pos > usedSize) {
throw new PosIllegality("插入元素下标异常:" + pos);
}
}
//查找某个元素是否存在,注意:该方法并不是顺序表中的方法
@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;
}
/**
* 检查是否尚未存入数据(仅做内部支持)
*/
private boolean isEmpty() {
return this.usedSize == 0;
}
//查找某个元素对应的位置
@Override
public int indexOf(int toFind) {
if (isEmpty()) {
return -1;
}
for (int i = 0; i < this.usedSize; i++) {
if (this.elem[i] == toFind) {
return i;
}
}
return -1;
}
//获取pos位置的元素
@Override
public int get(int pos) {
checkPosOnGetAndSet(pos);//判断pos是否合法
if (isEmpty()) {//判断顺序表是否为空
throw new MyArrayEmpty("获取指定下标元素时,顺序表为空!");
}
return elem[pos];
}
/**
* 检查传入get的pos是否合法(仅做内部支持)
*/
private void checkPosOnGetAndSet(int pos) {
if (pos < 0 || pos >= usedSize) {
throw new PosIllegality("获取指定下标元素异常:" + pos);
}
}
//将pos位置的元素设为value
@Override
public void set(int pos, int value) {
checkPosOnGetAndSet(pos);
elem[pos] = value;
}
//删除第一次出现的关键字toRemove
@Override
public void remove(int toRemove) {
int index = indexOf(toRemove);
if (index == -1) {
System.out.println("未找到指定元素!");
return;
}
for (int i = index; i < this.usedSize - 1; i++) {
this.elem[i] = this.elem[i + 1];
}
--this.usedSize;
}
//获取顺序表长度
@Override
public int size() {
return this.usedSize;
}
//清空顺序表
@Override
public void clear() {
this.usedSize = 0;
}
//打印顺序表,注意:该方法并不是顺序表中的方法,为了方便看测试结果给出的
@Override
public void display() {
for (int i = 0; i < this.usedSize; i++) {
System.out.print(this.elem[i] + " ");
}
System.out.println();
}
}
在上面的代码中,MyArrayList类使用int类型来存储元素,但是在实际的顺序表中是用泛型来存储数据的,这里用int类型是为了方便,毕竟目的是让大家理解顺序表的工作原理。
注意:每个方法有什么作用都有注释,请大家自行查看。异常类的代码没给出是因为,我认为都学到这了,独立写一个异常类的能力应该是要有的,所以请大家自行完成,实在不行可以把报异常改成打印。
ArrayList的使用
构造方法
方法 | 解释 |
ArrayList() | 无参构造 |
ArrayList(Collection<? extends E> c) | 利用其他Collection构建ArrayList |
ArrayList(int initialCapacity) | 指定顺序表初始容量 |
常见操作
方法 | 解释 |
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<E> subList(int fromIndex, int toIndex) | 截取部分list |
遍历
ArrayList可以使用三种方式遍历:for循环+下标、for-each(增强for循环)和使用迭代器。
代码演示如下:
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();//通过List接口来new一个ArrayList
list.add(1);
list.add(2);
list.add(3);
//使用for循环
for (int i = 0; i < list.size(); i++) {
System.out.print(list.get(i) + " ");
}
System.out.println();
//使用for-each(增强for循环)
for (Integer integer : list) {
System.out.print(integer + " ");
}
System.out.println();
//使用迭代器
Iterator<Integer> it = list.listIterator();//迭代器
while (it.hasNext()) {
System.out.print(it.next() + " ");
}
}
运行结果如下:
由于ArrayList是用数组来存储数据的,因此更推荐使用for循环和for-each(增强for循环)来进行遍历操作。
补充:
关于ArrayList的扩容问题
- 默认按照1.5倍大小扩容。如果用户所需大小超过1.5倍大小,则按照用户所需大小扩容。真正扩容之前会检测是否能扩容成功,防止太大导致扩容失败。
- 使用copyOf进行扩容。
关于ArrayList在某些情况下的性能问题
- ArrayList底层使用连续的空间,任意位置插入或删除元素时,需要将该位置后序元素整体往前或者往后搬移,效率较低,且时间复杂度会从尾插时的O(1)变为O(N)。
- 增容需要申请新空间,拷贝数据,释放旧空间。
- 由于数组是满了就扩容的,因此基本上不存在刚好够用的情况,大多数情况下都会造成大量浪费(例如,增加了很大一块空间,但多增加的空间确只使用了很小一部分),即使大部分空间都使用了也还是会造成一定的空间浪费。
针对以上问题,Java又提供了另一种线性表,它就是链表。下一次将会给大家介绍Java的LinkedList与链表。
结语
Java顺序表是一种灵活且高效的数据结构,适用于需要快速访问和动态调整大小的场景。通过对顺序表的深入了解,我们可以更好地利用Java提供的强大功能来管理和操作数据集合。无论是在学术研究还是实际开发中,顺序表都是一个不可或缺的工具。
以上,就是的本次要带大家深度剖析的Java中的String类的全部内容,感谢大家愿意花时间阅读本文!
如有错误,建议,或问题均可在评论区指出!