把玩算法 | 数组
基础
数组是由相同类型的元素的集合所组成的数据结构,分配一块连续的内存来存储。数组是使用索引来访问里面的元素的。如果我们有n个值,那么数组索引的范围为0至n-1。对于0到n-1之间的任意的i,我们就能在Java代码中用arr[i]
来访问第i个元素的值。
下面的代码创建了一个人名的数组,然后打印数组中的第二个元素:
String[] names = {"张三", "李四", "王五"};
System.out.println(names[1]);
但是数组创建完成后,其大小也就不能改变了。如果我们想要动态的改变数组的大小,那么就需要先创建足够大的同类型的数组,然后再将原始的数组中的数据拷贝到新创建的数组中。例如原先创建了一个长度为3的数组存放了3个人的名字,后来需要再存放"赵六"和"钱七"这两个人名,那么就需要重新创建长度为5的数组来存放这5个人名。相关的代码如下(注:数组的拷贝可以使用System.arraycopy
方法来进行拷贝):
String[] names = {"张三", "李四", "王五"}; // 原先的数组
// 现在需要存放"赵六"和"钱七"这两个人名
String[] newNames = new String[5];
// 拷贝之前的数据
for (int i = 0; i < names.length; i++) {
newNames[i] = names[i];
}
// 存放"赵六"和"钱七"
newNames[3] = "赵六";
newNames[4] = "钱七";
每次都要自己手动的来对数组进行扩容确实挺麻烦的,我们可以将添加、删除、访问数组中的元素的方法进行封装,在需要用到时直接用就行了。
API
下面是我们为数组定义的相关API:
数组
pubic class Array<Element> implements Iterable<Element>
Array() 创建一个空数组
Array(int capacity) 创建一个初始化容量的数组
void add(Element e) 添加一个元素
Element get(int index) 获取指定位置的元素
void set(int index, Element e) 设置指定位置的元素
Element remove(int index) 移除指定位置的元素
int size() 获取数组的大小
-
泛型:一种特别的Java的机制,也叫参数化类型。在API中,类名后面的
<Element>
将Element
定义为一个类型参数,它是一个占位符,表示的是该类将会使用到的某个具体的数据类型。Array<Element>
可以理解为某种元素的数组。我们这里的数组是能存放任意类型的数据的,例如,可以编写下面的代码来存放String类型的对象:Array<String> arr = new Array<String>(); arr.add("张三"); ...
-
可迭代的集合:在很多情况下,只需要用某种方式来处理集合中的每个元素,这种模式一般叫做
迭代器模式
。有了它,我们就能写出很清晰的代码而不必依赖于具体的集合类型的实现。只要实现了Iterable
,就能使用for-each
来遍历集合中的每一个元素,例如下面的代码打印出所有的人的姓名:Array<String> names = new Array<String>(); ... for (String name : names) { System.out.println(name); }
上面的
for-each
等同于下面的代码(很明显,使用for-each
要简介方便很多):for (Iterator<String> iterator = names.iterator(); iterator.hasNext();) { String name = iterator.next(); System.out.println(name); }
有了上面的API后,就能将上面的代码改为使用Array
来编写。在这里只需要关注所需要实现的逻辑,而不必关注具体的内部实现细节:
public static void main(String[] args) {
Array<String> names = new Array<>(3);
names.add("张三");
names.add("李四");
names.add("王五");
System.out.println(names);
// 存放"赵六"和"钱七"
names.add("赵六");
names.add("钱七");
System.out.println(names);
}
实现
更加详细的实现见github:Array.java
public class Array<Element> implements Iterable<Element> {
private Object[] elements; // 元素
private int size; // 元素的个数
public Array() { this(0); }
public Array(int capacity) { elements = new Object[capacity]; }
public void add(Element e) {
if (size == elements.length) {
resize(size * 2 + 1);
}
elements[size++] = e;
}
public Element get(int index) { return (Element) elements[index]; }
public void set(int index, Element e) { elements[index] = e; }
public int size() { return size; }
// 下面这几个方法的实现,请看接下来的实现说明
private void resize(int capacity)
public void remove(int index)
public Iterator<Element> iterator()
}
默认的构造器创建了一个空白的数组(长度为0),也提供了一个能指定初始化容量的版本,这样就可以根据自己的使用场景提供一个恰当的容量,避免在添加元素的过程中,数组内部频繁的进行扩容,影响性能。内部维护了一个数组Object[]
用来存放元素,使用实例变量size
来记录数组的大小。
为了能更加直观的看到数组存放的元素,还需要重写toString
方法:
public String toString() {
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < size; i++) {
if (i > 0) {
sb.append(",");
}
sb.append(elements[i]);
}
sb.append("]");
return sb.toString();
}
add, get, set这几个方法的实现都比较简单,都是直接操作存放元素的数组来实现,还提供了size()方法来获取数组的大小。如果元素数组满了,add方法会在添加元素之前会数组进行扩展,相关的实现如下:
数组扩容
elements的初始化长度是固定的,当elements满了之后就没有空余的空间来存放后续添加的元素了,这时就需要进行扩容了。这里的实现也比较简单,创建一个新的数组,再将原先数组中的值复制过去,再将新的数组赋值给elements就可以了。
private void resize(int capacity) {
Object[] newElements = new Object[capacity];
for (int i = 0; i < size; i++) {
newElements[i] = elements[i];
}
elements = newElements;
}
在add方法中,将元素数组的长度调整为原先的2倍再加1(size * 2 + 1
)
移除
移除数组中指定索引i处的元素时,需要将索引i之后的所有元素往前挪一格,并且还需要将数组中最后的一个元素设置为null,避免对失效位置对元素的引用。
例如,数组中存放了5个元素:[张三,李四,王五,赵六,钱七]
,如果要删除第二个元素,调用remove(1)
方法的示例图如下:
移除过程(灰色方格表示还未处理到的位置):
实现如下:
public Element remove(int index) {
for (int i = index + 1; i < size; i++) {
elements[i - 1] = elements[i];
}
size--;
Element oldValue = (Element) elements[size];
elements[size] = null;
if (size > 0 && size < elements.length / 4) {
resize(elements.length / 2);
}
return oldValue;
}
当数组的大小为其容量的四分之一时,将数组的容量缩减为数组容量的二分之一。
迭代
iterator()
方法的实现如下:
public Iterator<Element> iterator() {
return new Iterator<Element>() {
int cursor;
@Override
public boolean hasNext() {
return cursor < size;
}
@Override
public Element next() {
return (Element) elements[cursor++];
}
@Override
public void remove() {
throw new UnsupportedOperationException("remove");
}
};
}
内部维护了一个当前迭代器的指针cursor
,相关方法的实现说明如下:
hasNext()
:表示是否还有下一个元素,如果cursor指针未超过数组的大小,那么说明还有下一个元素next()
:获取下一个元素,并将cursor指针指向下一个元素remove()
:这里不支持移除操作,直接抛出了UnsupportedOperationException
异常
迭代示意图如下:
本文来自博客园,作者:coder-qi,转载请注明原文链接:https://www.cnblogs.com/coder-qi/p/algs-array.html