Loading

把玩算法 | 数组

基础

数组是由相同类型的元素的集合所组成的数据结构,分配一块连续的内存来存储。数组是使用索引来访问里面的元素的。如果我们有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异常

迭代示意图如下:

posted @ 2021-08-15 12:40  coder-qi  阅读(201)  评论(0编辑  收藏  举报