【底层实现 | 数据结构与算法笔记01】数组

简单概述

本篇文章的代码实现主要是学习算法与数据结构课程后的一个总结内容, 如果喜欢的话, 希望大家支持一下bobo老师的课程.

数组

定义

数组是本质上是一个数据容器, 其主要是用来存放数据类型一致的数据, 将其整体存放存储起来, 方便后续的使用.

如何实现

由于我们需要实现的数组并不是数组长度不可变的, 因此在最开始的设计的时候我们需要考虑成员变量, 构造参数, 成员方法, 以及重写方法, 其结构如下所示:

ArrysList结构图

成员变量

成员变量

虽然我们设计时想要实现的是可变数组, 但是其最终的数据存放依然是使用长度固定的数组进行存放, 我们只是会在其空间不够的时候, 将其进行扩容操作; 同时, 由于数组data中的元素我们需要知道其长度, 所以这里还有一个size变量.

构造参数

构造参数

在这里我们提供了两个构造参数, 一个是有参构造public Array(int capacity) 和无参构造public Array(), 我们通过有参构造传入容量capacity来进行最开始的数组初始化操作, 因此这里整体的实现就是对data创建一个新的数组, 然后初始化size为0即可.

// 有参构造函数, 通过传入数组的容量, 并初始化size, 来构造一个Array对象
public Array(int capacity){
    // 初始化一个容量的数组
    data = (E[]) new Object[capacity];
    // 初始化size
    size = 0;
}

之后, 我们提供的无参构造, 可以直接调用有参构造进行初始化, 这里我们默认设置容量为10即可.

// 无参构造函数, 传入数组容量capacity构造一个Array
public Array(){
    this(10);
}

成员方法

成员方法结构图

对于可变数组, 我们需要实现其最基本的几个操作CRUD, 也就是常说的增加, 修改, 查找以及删除操作, 除了这几个操作之外, 我们可能还需要知道可变数组的长度, 容量等一些方便的方法, 用于辅助CRUD的构建.

修改 set(int index, E e)

修改方法

修改方法实现是最简单的一个, 我们只需要输入索引, 以及要修改的值, 就可以对对应索引位置的元素进行修改值的操作, 但是这里需要注意的是, 我们要检查一下用户输入的索引是否在我们许可的范围内, 如果它的索引不存在, 这个时候我们没有办法修改值, 而这里索引的范围正好是[0, size), 这里因为size代表数组的长度, 所以size本身是取不到的, 只能取到size - 1.

public void set(int index, E e){
    // 判断index是否在0<=index<size的范围内
    if (index < 0 || index >= size) {
        throw new IllegalArgumentException("Set failed. Index is illegal.");
    }

    data[index] = e;
}

查找

查找方法

查找方法是增删改查当中最容易实现的内容, 我们可以通过索引的方式来查找到我们需要的元素, 也可以通过传入元素来判断我们的数组中是否包含某个元素或者找到元素索引首次出现的位置.

get(int index), getLast(), getFirst()

public E get(int index)通过传入索引来获取到我们需要的元素, 这个方法的实现直接返回数组的索引就行了, 但是需要主要的就是我们要判断一下索引是否在输入的范围内, 这里index的范围是[0, size), 因为size代表数组的长度, 所以size本身是取不到的, 而public E getLast()public E getFirst()我们只需要调用public E get(int index) 方法就可以了, 一个索引位置为0, 一个索引位置为size - 1.

// 获取index索引位置的元素
public E get(int index){
    // 判断索引是否在0<=index<size的范围内
    if (index < 0 || index >= size) {
        // 报错
        throw new IllegalArgumentException("Get failed. Index is illegal.");
    }
    return data[index];
}
// 获取数组中最后一个元素
public E getLast(){
    return get(size - 1);
}

// 获取数组中第一个元素
public E getFirst(){
    return get(0);
}
contains(E e)与find(E e)

public boolean contains(E e)public int find(E e)两者其实本质上是一样的, 都是对数组进行遍历, 不同的是contains() 最终返回的是一个布尔值, find()返回的则是首次出现的索引值.

// 是否包含元素e
public boolean contains(E e){
    for (int i = 0; i < size; i++) {
        if (data[i].equals(e)) {
            return true;
        }
    }
    return false;
}

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

增加

增加方法

add(int index, E e)

增加方法主要有public void add(int index, E e), public void addLast(E e), public void addFirst(E e)构成, 其中public void addLast(E e)public void addFirst(E e)并不需要重新构建, 只需要调用写好的public void add(int index, E e)方法就可以了, 那么这里如何实现利用索引index,添加增加的元素呢, 由于我们这里使用的是数组, 因此, 我们并不可能通过增加空间来增加元素, 所以我们只能通过覆盖的方式来完成对元素的增加, 而这里获取到我们需要的索引位置的空间, 只有将其后面的元素进行移动, 然后才能够将我们所需要的索引位置空间获取到, 因此我们最开始写出来的方式如下:

 // 在第index个位置上插入一个新的元素e
    public void add(int index, E e){
        for (int i = size - 1; i >= index; i--) {
            data[i + 1] = data[i];
        }

        // 在index位置上放入元素
        data[index] = e;
        // 数组的长度+1
        this.size += 1;
    }

但是当我们实现了这个方法时, 我们可能会出现两个问题, 一个就是同之前的修改和查找方法一样, 确定好正确的索引位置, 那么这个时候我们就需要知道在哪个范围内可以插入元素, 对于增加其索引index的范围就是[0, size], 这里的size可以取到的原因是因为我们可以在数组的末尾添加元素; 另一个问题就是, 如果数组空间不够了, 我们该怎么办, 因此在这里我们需要实现一下容量修改方法resize()才可以完成相关的操作, 同时我们也需要获取长度getSize()以及获取容量操作getCapacity()(也可以直接使用size, data.length两者进行代替), 最终实现如下:

// 在第index个位置上插入一个新的元素e
public void add(int index, E e){
    // 如果index的值不在0<=index<=size中, 此时就报错
    if (index < 0 || index > this.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];
    }

    // 在index位置上放入元素
    data[index] = e;
    // 数组的长度+1
    this.size += 1;
}
addFirst(E e)和addLast(E e)

public void addLast(E e)public void addFirst(E e)方法只需要调用一下public void add(int index, E e)就可以进行操作了, 而不需要自己重复造轮子, 这里同样也说明了, 好的代码扩展性和可复用性都是特别强的, 因此我们后面也需要努力学习如何写出扩展性和复用性强的代码.

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

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

删除

删除方法

删除方法主要分为publice E remove(int index)public void removeElement(E e)两个, remove()方法主要是通过索引来删除元素, 而removeElement(E e)则是删除数组中存在的元素, 虽然两者功能不一样, 但是其实removeElement(E e)同样也是对之前写的方法的复用.

remove(int index)

public E remove(int index)方法, 有了上面public void add(int index, E e)方法的基础, 整体实现思路就很容易理解了:

  1. 确定删除元素的索引范围
  2. 进行元素的删除操作
  3. 元素的缩容操作

对于删除元素的索引范围, 此部分同set()get()方法的范围一样, index的范围也是左闭右开: [0, size), 元素删除的操作则是同添加的操作相反, 是将元素往前移动来进行覆盖模拟删除元素的操作, 完成操作后将长度减少1, 而缩容操作, 则是当数组容量大于长度2倍进行的操作, 这个操作也可以不做, 但这里考虑到后面或出现数组元素特别大的情况, 所以有了缩容操作, 具体代码实现方式如下:

// 从数组中删除index位置的元素, 返回删除的元素
public E remove(int index){
    // 判断index的返回是否在0<=index<size的范围内
    if (index < 0 || index >= size) {
        throw new IllegalArgumentException("Remove failed. Index is illegal.");
    }

    E ret = data[index];

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

    if (size == data.length / 2) {
        resize(data.length / 2);
    }

    return ret;
}
removeFirst()和removeLast()

public E removeFirst()public E removeLast()两者可以直接调用remove(int index)的方法来实现, 避免重复造轮子的行为, 这里两者的索引为0和size -1即可.

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

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

public void removeElement(E e)在有了之前实现的find(E e)remove(int index)实现就变得很简单, 只需要获取到索引后, 然后将对应索引的处的元素删除就可以了.

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

其他

其他方法

其他方法部分主要是一些没办法进行简单归纳分类的内容, 基本上是上面方法的支撑方法, 整体实现当中, 只有resize(int newCapacity)需要进行逻辑实现, 其余方法更多的是对属性的封装.

isEmpty(), getSize()和getCapacity()
// 获取数组的长度
public int getSize(){
    return this.size;
}

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

// 判断数组是否为空
public boolean isEmpty(){
    return size == 0;
}
resize(int newCapacity)

private void resize(int newCapacity)方法由于是支撑方法, 所以需要对其进行私有化操作, 我们通过创建一个新的数组newData将旧数组data的值遍历赋值给自己后, 将data的地址重新赋值为newData后, 就完成了新数组容量的构造.

// 对数组进行扩容
public void resize(int newCapacity){
    E[] newData = (E[]) new Object[newCapacity];
    for (int i = 0; i < size; i++) {
        newData[i] = data[i];
    }
    data = newData;
}

重写方法

重写方法

对于数组我们最终还需要对其实现打印操作, 如果不进行重写操作, 那么最终调用System.out.println()方法执行打印操作时, 只会打印其地址值, 而不是显示出数组的情况, 这种方式可能会让我们想要查看数组当中的内容时还需要进行遍历操作, 比较麻烦.

在进行打印操作时, 由于我们的打印结果类似于[1, 2, 3], 所以这里我们要注意到对最后一个值的特殊处理.

// 打印数组的内部信息
@Override
public String toString() {
    StringBuilder res= new StringBuilder();
    res.append(String.format("Array: size = %d, capacity = %d\n", this.size, data.length));
    res.append('[');
    for (int i = 0; i < size; i++) {
        // 如果不为最后一个元素那么此时都会加,
        if (i != size - 1) {
            res.append(data[i] + ", ");
        } else {
            res.append(data[i]);
        }
    }
    res.append(']');
    // 输出最终的打印信息
    return res.toString();
}

整体实现代码

// 定义一个泛型数组, 泛型数组的好处就是可以存放各种类型的元素
public class Array<E>{
    // 成员变量
    // 数据存储data
    private E[] data;
    // 数组长度
    private int size;
    
    // 构造函数
   	// 有参构造函数, 通过传入数组的容量, 并初始化size, 来构造一个Array对象
    public Array(int capacity){
        // 初始化一个容量的数组
        data = (E[]) new Object[capacity];
        // 初始化size
        size = 0;
    }
    // 无参构造函数, 传入数组容量capacity构造一个Array
    public Array(){
        this(10);
    }
    
    // 成员方法
    // 获取数组的长度
    public int getSize(){
        return this.size;
    }
    // 获取数组的容量
    public int getCapacity(){
        return data.length;
    }
   	// 判断数组是否为空
    public boolean isEmpty(){
        return this.size == 0;
    }
    // 在第index个位置上插入一个新的元素e
    public void add(int index, E e){
        // index的范围[0, size], 超过这个范围出错
        if (index < 0 || index > this.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];
        }
        // 在index的位置上放入元素
        data[index] = e;
        // 数组的长度+1
        size++;
    }
    // 向所有元素后添加一个元素
    public void addLast(E e){
        add(size, e);
    }
    // 向所有元素钱添加一个元素
    public void addFirst(E e){
        add(0, e);
    }
    // 获取index索引位置的值
    public E get(int index){
        // index的范围在[0, index)
        if (index < 0 || index >= size){
            throw new IllegalArgumentException("Get 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, E e){
        // index的范围为[0, size)
        if (index < 0 || index >= size){
            throw new IllegalArgumentException("Set failed. Index is illegal.");
        }
        data[index] = e;
    }
    // 是否包含元素e
    public boolean contains(E e){
        for (int i = 0; i < size; i++){
            if (data[i].equals(e)){
                return true;
            }
        }
        return false;
    }
    // 查找数组中元素e所在的索引,如果不存在元素e,则返回-1
    public int find(E e){
        for(int i = 0; i < size; i++){
            if(data[i].equals(e)){
                return i;
            }
        }
        return -1;
    }
    // 从数组中删除index位置的元素, 返回删除的元素
    public E remove(int index){
        // index的范围[0, size)
        if(index < 0 || index >= size){
            throw new IllegalArgumentException("Remove failed. Index is illegal.");
        }
        E ret = data[index];
        for(int i = index + 1; i < size; i++){
            data[i-1] = data[i];
        }
        this.size -= 1;
        data[size] = null;
        if(size == data.length / 2){
            resize(data.length / 2);
        }
        return ret;
    }
    // 从数组中删除第一个位置的元素, 返回删除的元素
    public E removeFirst(){
        return remove(0);
    }
    // 从数组中删除最后一个位置的元素, 返回删除的元素
    public E removeLast(){
        return remove(size - 1);
    }
    // 从数组中删除元素e
    public void removeElement(E e){
        int index = find(e);
        if (index != -1){
            remove(index);
        }
    }
    // 打印数组的内部信息
    @Override
    public String toString(){
        StringBuilder res = new StringBuilder();
        res.append(String.format("Array: size = %d, capactiy = %d\n", this.size, data.length));
        res.append('[');
        for (int i = 0; i < size; i++){
            // 如果不为最后一个元素, 那么此时都会加一个,
            if (i != size - 1){
                res.append(data[i] + ", ");
            } else {
                res.append(data[i]);
            }
        }
        res.append(']');
        // 输出最终打印的信息
        return res.toString();
    }
    
    // 对数组进行扩容操作
    private void resize(int newCapacity){
        E[] newData = (E[]) new Object[newCapacity];
        // 数组的复制
        for (int i = 0; i < size; i++){
            newData[i] = data[i];
        }
        data = newData;
    }
}

参考文献

  1. 算法与数据结构
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值