我理解的数据结构(一)—— 数组(Array)

我理解的数据结构(一)—— 数组(Array)

首先,我是一个phper,可是毕竟php是一个脚本语言,若是使用脚本语言去理解数据结构具备必定的局限性。由于脚本语言是不须要编译的,若是你的语法写的不错,可能执行起来会要比用一个更好的数据结构来的更快、更高效(在数据量不大的状况下)。并且数据结构是脱离任何一门语言存在的。因此,下面会选用java去更深刻的理解数据结构。

注:这里不会去过多的解释java的语法。php

1、定义一个数组的两种方式

  • int[] arr = new int[10];
  • int[] arr = new int[] {10, 20, 30};

2、数组基础

  • 数组的容量在数组一开始定义的时候就固定了。
  • 数组最大的优势:根据索引快速查询。如:arr[2]
  • 数组最好应用于“索引有语意”的状况下。
  • 但并不是全部有语意的索引都适用于数组:好比索引是一我的的身份证号,会开辟过大的空间,不现实。
  • 下面会讨论数组“索引没有语意”的状况,基于java数组,二次封装属于咱们本身的数组类,更深刻的理解数组。

3、建立一个最基本的数组类

学习任何一个数据结构, CRUD必不可少。下面,让咱们来一块儿一步步完善属于咱们本身的数组的增、删、改、查
public class Array {

    // 数组的实际大小
    private int size;
    // 数组
    private int[] data;

    // 构造函数,根据传入的容纳量定义一个int类型的数组
    public Array(int capacity) {
        data = new int[capacity];
        size = 0;
    }

    // 重载,没有传入容纳量,定义一个长度为10的int类型数组
    public Array() {
        this(10);
    }

    // 数组的实际大小
    public int getSize() {
        return size;
    }

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

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

4、增

//往数组的任意位置插入
public void add(int index, int ele) {

    // 数组已满
    if (size == data.length) {
        throw new IllegalArgumentException("add failed. arr is full");
    }

    // 插入的索引位不合法
    if (index < 0 || index >= size) {
        throw new IllegalArgumentException("add failed. index < 0 or index >= size");
    }

    // 从index向后的全部元素均向后赋值
    for (int i = size - 1; i >= index; i--) {
        data[i + 1] = data[i];
    }
    data[index] = ele;
    size++;
}

// 第一个位置插入
public void addFirst(int ele) {
    add(0, ele);
}

// 最后一个位置插入
public void addLast(int ele) {
    add(size, ele);
}

5、查和改

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

// 查询ele元素的索引,不存在返回-1
public int find(int ele) {
    for (int i = 0; i < size; i++) {
        if (data[i] == ele) {
            return i;
        }
    }
    return  -1;
}

// 更新Index的元素
public void set(int index, int ele) {
    if (index < 0 || index >= size) {
        throw new IllegalArgumentException("get failed. index is illegal");
    }
    data[index] = ele;
}

6、删

// 根据索引删除数组中的第一个ele,返回ele
public int remove(int index) {
    if (index < 0 || index >= size) {
        throw new IllegalArgumentException("remove failed. index is illegal");
    }

    for (int i = index + 1; i < size; i++) {
        data[i - 1] = data[i];
    }
    size--;

    return data[index];
}

// 删除第一个元素
public int removeFirst() {
    return remove(0);
}

// 删除最后一个
public int removeLast() {
    return remove(size - 1);
}

// 删除指定元素
public void removeElement(int ele) {
    int index = find(ele);
    if (index != -1) {
        remove(index);
    }
}

7、包含和重写toString

Override
public String toString() {
    StringBuffer res = new StringBuffer();
    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();
}

// 查询数组中是否包含元素ele
public boolean contain(int ele) {
    for (int i = 0; i < size; i++) {
        if (data[i] == ele) {
            return true;
        }
    }
    return  false;
}

注:经过以上方法咱们已经建立了一个最最最最最基本的数组类(见下图)。固然,你也能够去添加一些本身须要的方法,例如:removeAllfindAll之类的。
类中的方法java

可是,咱们如今的数组只支持int类型,太过局限。接下来,咱们去给咱们的数组升华一哈~

8、使用泛型让咱们的数组支持“任意”数据类型

首先,为何我要在 任意这两个字加上引号,由于java的泛型不支持基本数据类型,只能是类的对象。
可是,这并不表明若是咱们使用了泛型,就不可使用基本数据类型了,由于每个基本数据类型都有一个对应的 包装类
使用泛型的时候,咱们只须要传入对应的包装类便可。

java的基本数据类型

基本数据类型 包装类
boolean Boolean
byte Byte
char Char
short Short
int Int
long Long
float Float
double Double

因此,咱们的代码只须要进行极小的改动便可:

public class ArrayNew<E> {
    // 数组的实际大小
    private int size;
    // 数组
    private E[] data;

    // 构造函数,根据传入的容纳量定义一个 E 类型的数组
    public ArrayNew(int capacity) {
        // 强转
        data = (E[]) new Object[capacity];
        size = 0;
    }

    // 重载,没有传入容纳量,定义一个长度为10的int类型数组
    public ArrayNew() {
        this(10);
    }

    // 数组的实际大小
    public int getSize() {
        return size;
    }

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

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

    // 往数组的任意位置插入
    public void add(int index, E ele) {

        // 数组已满
        if (size == data.length) {
            throw new IllegalArgumentException("add failed. arr is full");
        }

        // 插入的索引位不合法
        if (index < 0 || index > size) {
            throw new IllegalArgumentException("add failed. index < 0 or index > size");
        }

        // 从index向后的全部元素均向后赋值
        for (int i = size - 1; i >= index; i--) {
            data[i + 1] = data[i];
        }
        data[index] = ele;
        size++;
    }

    // 第一个位置插入
    public void addFirst(E ele) {
        add(0, ele);
    }

    // 最后一个位置插入
    public void addLast(E ele) {
        add(size, ele);
    }

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

    // 查询ele元素的索引,不存在返回-1
    public int find(E ele) {
        for (int i = 0; i < size; i++) {
            if (data[i].equals(ele)) {
                return i;
            }
        }
        return  -1;
    }

    // 更新Index的元素
    public void set(int index, E ele) {
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("get failed. index is illegal");
        }
        data[index] = ele;
    }

    // 根据索引删除数组中的第一个ele,返回ele
    public E remove(int index) {
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("remove failed. index is illegal");
        }
        
        E result = data[index];
        for (int i = index + 1; i < size; i++) {
            data[i - 1] = (data[i]);
        }
        // 空间释放,垃圾回收会自动回收
        data[--size] = null;

        return result;
    }

    // 删除第一个元素
    public E removeFirst() {
        return remove(0);
    }

    // 删除最后一个
    public E removeLast() {
        return remove(size - 1);
    }

    // 删除指定元素
    public void removeElement(E ele) {
        int index = find(ele);
        if (index != -1) {
            remove(index);
        }
    }

    // 查询数组中是否包含元素ele
    public boolean contain(E ele) {
        for (int i = 0; i < size; i++) {
            if (data[i].equals(ele)) {
                return true;
            }
        }
        return  false;
    }

    @Override
    public String toString() {
        StringBuffer res = new StringBuffer();
        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();
    }

}

注:建立数组时,只需ArrayNew<Student> arr = new ArrayNew<>(20);便可。数组

9、动态数组

原理:其实,动态数组的原理很是简单,若是咱们但愿咱们的数组具备可伸缩性,只须要咱们在添加或者删除元素时判断 size是否到达临界。而后去建立一个新 capacity的数组,而后把旧数组的引用指向新数组便可。
因此,咱们上述代码的改变极小,只须要改变 addremove便可。而后添加一个 resize方法。
// 往数组的任意位置插入
public void add(int index, E ele) {
    // 插入的索引位不合法
    if (index < 0 || index > size) {
        throw new IllegalArgumentException("add failed. index < 0 or index > size");
    }

    // 若是size == data.length,数组长度已满
    if (size == data.length) {
        resize(data.length * 2);
    }

    // 从index向后的全部元素均向后赋值
    for (int i = size - 1; i >= index; i--) {
        data[i + 1] = data[i];
    }
    data[index] = ele;
    size++;
}

// 根据索引删除数组中的第一个ele,返回ele
public E remove(int index) {
    if (index < 0 || index >= size) {
        throw new IllegalArgumentException("remove failed. index is illegal");
    }

    E result = data[index];
    for (int i = index + 1; i < size; i++) {
        data[i - 1] = (data[i]);
    }
    // 空间释放,垃圾回收会自动回收
    data[--size] = null;

    // 减少数组长度,不要浪费空间
    if (size == data.length / 2 && size != 0) {
        resize(size);
    }

    return result;
}

// 自动伸缩数组
private void resize(int newCapacity) {
    E[] newData = (E[])new Object[newCapacity];
    for (int i = 0; i < size; i++) {
        newData[i] = data[i];
    }
    data = newData;
}

10、简单复杂度分析咱们封装的数组

经过上面的分析和代码实现,咱们封装了一个本身的数组,而且实现了一些数组 最基本的功能,包括支持增、删、改、查、支持任意数据类型以及动态数组。那么咱们就来分析一下咱们本身封装数组的复杂度。
操做 复杂度
O(n)
O(n)
已知索引O(1);未知索引O(n)
已知索引O(1);未知索引O(n)

可是:在咱们的数组中,增和删咱们都调用了resize方法,若是size < data.length,其实咱们执行addLast复杂度只是O(1)而已(removeLast同理)。因此,咱们应该怎么去分析resize方法所带来的复杂度呢?数据结构

11、均摊复杂度和防止复杂度的震荡

(1)均摊复杂度

让咱们拿 来举例
方法 复杂度
addLast(ele) O(1)
addFirst(ele) O(n)
add(index, ele) O(n/2) = O(n)
resize(newCapacity) O(n)

其实,在执行addLast的时候,咱们并非每次都会触发resize方法,更多的时候,复杂度只是O(1)而已。
比方说:
当前的capacity = 8,而且每一次添加操做都使用addLast,第9次addLast操做,触发resize,总共17次基本操做(resize方法会进行8次操做,addLast方法进行9次操做)。平均,每次addLast操做,进行2次基本操做(17 / 9 ≈ 2)。
假设:
capacity = nn + 1addLast,触发resize,总共进行了2n + 1次操做,平均每次addLast操做,进行了2次基本操做。app

这样均摊计算,时间复杂度是O(1)!ide

(2)防止复杂度的震荡

让咱们来假设这样一种状况:
size == data.length时,咱们执行了 addLast方法添加一个元素,这个时候咱们须要去执行 resize方法,此时, addLast的复杂度为 O(n)
而后,我去 removeLast,此时的 removeLast复杂度也是 O(n)
再而后,我再去执行 addLast
.
.
.

有没有发现,在这样一种极端状况下,addLastremoveLast的复杂度变成了O(n),其实,这个就是复杂度的震荡函数

  • 为何咱们会产生这种震荡?学习

    • add状况下,咱们去扩容数组无可厚非。可是remove状况下,咱们马上去缩容数组就有点不合适了。
  • 怎么去解决这种状况?this

    • 由于咱们以前采起的措施是Eager
    • 因此,咱们采起一种Lazy的方式:当size == data.length / 2,咱们不要马上缩容,当size == data.length / 4时,咱们才去缩容,就能够很好的解决这种震荡。
具体代码以下,其实只是对 remove进行了极小的改变
public E remove(int index) {
    if (index < 0 || index >= size) {
        throw new IllegalArgumentException("remove failed. index is illegal");
    }
    
    E result = data[index];
    for (int i = index + 1; i < size; i++) {
        data[i - 1] = data[i];
    }
    // 空间释放,垃圾回收会自动回收
    data[--size] = null;

    // 减少数组长度,不要浪费空间,防止震荡
    if (size == data.length / 4 && data.length / 2 != 0) {
        resize(data.length / 2);
    }

    return result;
}
相关文章
相关标签/搜索