数组

数组的作用:把数据码成一排进行存放

数组初始化的两种方法

  • 指定容量
int[] arr = new int[20];
  • 指定初始值
 int[] scores = new int[]{100, 99, 66};

数据结构之数组详解

二次封装我们自己的数组

/**
 * 二次封装我们自己的数组
 * Created by binzhang on 2019/3/15.
 */
public class Array {
    private int[] data;
    private int size;

    // 构造函数,传入数组的容量capacity构造Array
    public Array(int capacity){
        data = new int[capacity];
        size = 0;
    }

    // 无参数的构造函数,默认数组的容量capacity=10
    public Array(){
        this(10);
    }

    // 获取数组中的元素个数
    public int getSize(){
        return size;
    }

    // 获取数组的容量
    public int getCapacity(){
        return data.length;
    }

    // 返回数组是否为空
    public boolean isEmpty(){
        return size == 0;
    }

}

添加方法

数据结构之数组详解
size是数组的第一个空元素的定位符。

// 向所有元素后添加一个新元素
public void addList(int e){
    if(size == data.length)
        throw new IllegalArgumentException("AddLast failed . Array is full.");
    data[size] = e;
    size ++;
}

当遇到下面场景即向指定位置添加元素时要怎么办呢?
数据结构之数组详解
首先将100后移一个位置,再依次将99,88后移一个位置,然后将77插入到索引为1的位置。

// 在第index个位置插入一个新元素e
public void add(int index, int e){
    if(size == data.length)
        throw new IllegalArgumentException("AddLast failed . Array is full.");
    if(index < 0 || index > size)
        throw new IllegalArgumentException("Add failed. Require index >= 0 and index <= size.");
    for (int i = size -1; i>= index; i --)
        data[i + 1] = data[i];

    data[index] = e;
    size ++;
}

这里其实可以在addLast里面复用我们的add方法了。

// 向所有元素后添加一个新元素
public void addList(int e){
    add(size, e);
}

在所有元素前添加一个新元素

// 在所有元素前添加一个新元素
public void addFirst(int e){
    add(0,e);
}

重写toString方法

//重新toString方法输出数组信息
@Override
public String toString(){
    StringBuilder res = new StringBuilder();
    res.append(String.format("Array: size = %d , capacity = %d\n", size, data.length));
    res.append('[');
    for (int i = 0 ; i < size ; i ++){
        res.append(data[i]);
        if(i != size-1)
            res.append(", ");
    }
    res.append(']');
    return res.toString();
}

验证toString方法和在指定位置插入元素方法输出:

public class Main {

    public static void main(String[] args) {

        Array arr = new Array(20);
        for (int i = 0 ; i < 10 ; i ++){
            arr.addLast(i);
        }
        System.out.println(arr);
    }
}

输出:

Array: size = 10 , capacity = 20
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Array: size = 11 , capacity = 20
[0, 100, 1, 2, 3, 4, 5, 6, 7, 8, 9]

数组中查询元素和修改元素

// 获取index索引位置的元素
public int get(int index){
    if(index < 0 || index >= size)
        throw new IllegalArgumentException("Add failed. Index is illegal.");
    return data[index];
}

public E getLast(){
    return get(size - 1);
}

public E getFirst(){
    return get(0);
}
    
// 修改index索引位置的元素为e
public void set(int index, int e){
    if(index < 0 || index >= size)
        throw new IllegalArgumentException("Add failed. Index is illegal.");
    data[index] = e;
}

数组中的包含、搜索和删除元素

// 查找数组中是否有元素e
public boolean contains(int e){
    for (int i = 0 ; i < size ; i ++){
        if (data[i] == e)
            return true;
    }
    return false;
}

// 查找数组中元素e所在的索引,如果不存在元素e,则返回-1
public int find(int e){
    for (int i = 0 ; i < size ; i ++){
        if(data[i] == e)
            return i;
    }
    return -1;
}

如下场景,想要删除索引为1的值为77的元素
数据结构之数组详解
将77后的元素都向左移一位即可,最后将size–

 // 从数组中删除index位置的元素,返回删除的元素
public int remove(int index){
    if(index < 0 || index >= size)
        throw new IllegalArgumentException("Add failed. Index is illegal.");
    int res = data[index];
    for (int i = index + 1 ; i < size ; i ++)
        data[i - 1] = data[i];
    size --;
    data[size] = null;
    return res;
}

对应的可以写出相应的扩展方法

// 从数组中删除第一个元素,返回删除的元素
public int removeFirst(){
    return remove(0);
}

// 从数组中删除最后一个元素,返回删除的元素
public int removeLast(){
    return remove(size - 1);
}

// 从数组中删除元素e
public void removeElement(int e){
    int index = find(e);
    if(index != -1)
        remove(index);
}

泛型数组

泛型数组

  • 让我们的数据结构可以放置“任何”数据类型
  • 不可以是基本数据类型,只能是类对象
    • boolean byte char short int long float double
  • 每个基本数据类型都有对应的包装类(可以使用)
    • Boolean Byte Char Short Int Long Float Double

定义泛型数组:

/**
 * 定义泛型数组
 * Created by binzhang on 2019/3/15.
 */
public class Array<E> {
    private E[] data;
    private int size;

    // 构造函数,传入数组的容量capacity构造Array
    public Array(int capacity){
        data = (E[])new Object[capacity];
        size = 0;
    }
	...
}

声明泛型数组要传入要定义的类型
Array<Integer> arr = new Array<>(20);

泛型数组使用示例:

/**
 * Created by binzhang on 2019/3/16.
 */
public class Student {
    private String name;
    private int score;

    public Student(String studentName, int studentScore){
        this.name = studentName;
        this.score = studentScore;
    }

    @Override
    public String toString() {
        return String.format("Student(name: %s, score: %d)", name, score);
    }

    public static void main(String[] args) {
        Array<Student> arr = new Array<>();
        arr.addLast(new Student("Alice", 100));
        arr.addLast(new Student("Bob", 99));
        arr.addLast(new Student("chili", 88));
        System.out.println(arr);
    }
}

输出:

ArrayOld: size = 3 , capacity = 10
[Student(name: Alice, score: 100), Student(name: Bob, score: 99), Student(name: chili, score: 88)]

动态数组

动态数组可以简单理解为动态扩容,如下图所示当我们的data数组原容量只有4,且里面已经有四个元素,无法再次添加,我们可以先定义一个新数组newData,他的容量是data的二倍,将data中的数据拷贝到newData中,再将data指向newData,释放原数组的空间和newData引用,就完成了动态扩容,这就是动态数组。要注意的是,旧数组拷贝到新数组是要进行遍历的。
数据结构之数组详解
要实现上述过程,我们要改造之前写好了的add方法,加入的部分就是当要插入的位置size等于数组容量capacity/data.length时,进行动态扩容。

// 在第index个位置插入一个新元素e
public void add(int index, E e){
    if(index < 0 || index > size)
        throw new IllegalArgumentException("Add failed. Require index >= 0 and index <= size.");

    if(size == data.length)
        resize(2 * data.length);

    for (int i = size -1; i>= index; i --)
        data[i + 1] = data[i];

    data[index] = e;
    size ++;
}

// 扩容方法设置为私有的,用户不可在外部调用
private void resize(int newCapacity){
    E[] newData = (E[]) new Object[newCapacity];
    for (int i = 0 ; i < size ; i ++)
        newData[i] = data[i];
    data = newData;
}

这样我们就再也不用担心数组空间不够用了。
同理我们也可以考虑在remove方法中加入动态减少数组空间的操作了。

// 从数组中删除index位置的元素,返回删除的元素
public E remove(int index){
    if(index < 0 || index >= size)
        throw new IllegalArgumentException("Add failed. Index is illegal.");

    E res = data[index];
    for (int i = index + 1 ; i < size ; i ++)
        data[i - 1] = data[i];
    size --;
    data[size] = null;

    // 动态减少数组容量
    if (size == data.length / 2 && data.length / 2 != 0)
        resize(data.length / 2);

    return res;
}

数组的时间复杂度

  • O(1),O(n),O(nlogn),O(n^2)
  • 大O描述的是算法的运行时间和输入数据之间的关系

分析动态数组的时间复杂度

  • 添加操作 O(n)
    • addLast(e) O(1)
    • addFirst(e) O(n)
    • add(index, e) O(n/2) = O(n)

时间复杂度我们通常考虑最糟糕的情况,所以添加操作的时间复杂度就是O(n)

  • 删除操作 O(n)
    • removeLast(e) O(1)
    • removeFirst(e) O(n)
    • remove(index, e) O(n/2) = O(n)
  • 修改操作
    • set(index, e) O(1)
  • 查找操作 O(n)
    • get(index) O(1)
    • contains(e) O(n)
    • find(e) O(n)

总结:

  • 增:O(n)
  • 删:O(n)
  • 改:已知索引O(1);未知索引O(n)
  • 查:已知索引O(1);未知索引O(n)

注意对于增删最后一条元素,因为要考虑到resize所以时间复杂度依然是O(n)

复杂度震荡

复杂度震荡问题:当我们的数组容量满的时候,再次添加一个元素,会触发扩容机制,然后再将这个元素删除,又会触发缩减容量的机制,明显这样对我们系统的资源是很大的浪费。

出现问题的原因:removeLast时resize过于着急(Eager)

解决方法:当 size == capacity / 4 时,才将capacity减半(Lazy)

相关文章: