数据结构#数组

前言

写过一些程序,搞过一些项目我们或许会发现,数组其实还是很牛逼的,Java中的集合框架就是用到了数组的封装,比如Arraylist使用到了Object数组作为底层进行了封装,再比如String这个类底层使用了char数组进行了封装,这些东西在我们开发中可谓是屡见不鲜。俗话说万丈高楼平地起,数组作为这些东西的基石我们有必要来探索下。(以下都是围绕自定义数组来搞事的)

探究内容

image.png

科普

image.png

数组基础性的东西我们就不在这唠叨了,今天我们就看一些数组的一些重要的概念:
1 length(capacity)

length我们都知道就是数组的长度,代表数组当前最大容量。他也是基本类型数组中的唯一一个成员变量,其实在一些封装的集合框架中使用capacity(容量),他俩都是一个概念,都代表当前数组的容量。

2 size(自定义数组中)
  • 数组的大小,代表当前容器中存的真实元素数。
  • 代表最后一个有效元素下一位置。

size为什么可以代表最后一个有效元素下一位置?
如上文的科普中的图片中,数组中存在2个元素,则第一个元素为data[0],最后一个有效元素为data[1],最后一个有效元素下一位置元素为data[size]

3 数组的语义
  • 如上文假如数组具备语义data数组存的就是学生分数时,想知道成绩时data[索引] 就获得了当前索引的成绩。如此我们知道数组具有查询快的优点(可以与链表比较,后续会探究原因)

数组有语义那我们使用就简单了,通过索引查询,通过索引修改,简直是so easy。

  • 数组没语义时就产生一些问题了,还是上图,data[2],data[3]…是没有意义的,我们是不允许操作这些位置的(面向程序端的程序员来说)。这些索引的删除、修改数据是不允许的。

我们的动态数组就是研究数组的没有语义情况

基本数组的封装

基本数组我们就以int 类型为例封装一个可以存取int类型的数组,来探讨一下数组底层是如何封装的。同时本部分也是动态数组,通用动态数组的基础。
1 类的设计
package array;

/**
 * Create by SunnyDay on 2019/01/29
 */
public class Array {
    private int[] data; // 数组
    private int size;// 数组大小
    // capacity (数组的容量) 与size的区别:  size代表数组内存储元素的数量,capacity 能存储元素的最大量,
    // capacity 作用等同length

    /**
     * @param capacity 根据传入的容量声明数组大小
     */
    public Array(int capacity) {
        data = new int[capacity];
    }

    /**
     * 默认数组大小
     */
    public Array() {
        this(10);
    }
}

我们自定义的具有int功能的数组就叫Array,中间有两个成员。可以看出我们首先设计了一个参数的构造,当你声明对象时是必须传入一个int型的容量的,然而当你不传参数(使用无参数构造)时我们默认的有数值的。这些都是常识接下来我们看看一些方法的设计。

2 增删改查

容器的操作无非是增删改查,接下来我们就具体探索增删改查的设计

2.1 增的api设计

  • addLast
  • add
  • addFirst
/**
     * 向数组的最后一个元素之后添加元素
     */
 public void addLast(int e) {
        if (size == data.length) {
            throw new IllegalArgumentException("addLast element fail ,Array is full.");
        }
        data[size] = e;
        size++;
}

addLast 往数组末尾添加元素,添加前我们首先判断下数组是否满,满了抛异常,反之添加元素,数组大小增加。

 /**
     * 数组任意位置添加元素
     *
     * @param index 要插入的索引
     * @param e     要插入的元素
     */
    public void add(int index, int e) {
        // 首先还是检查数组是否满了
        if (size == data.length) {
            throw new IllegalArgumentException("add element fail ,Array is full.");
        }
        // 1、上面判断了size==arr.length的状况,要想插入元素位置合法,能够插入元素,size<arr.length即可
        // 2、最不理想的状况就是还能插入一个元素,此时size=arr.length -1
        // 3、这时index==size时就是插入末尾(size代表当前有效元素下一位的索引)
        // 4、index>size就是插入位置不合法了,超出了数组的界限。
        if (index < 0 || index > size) {
            throw new IllegalArgumentException("add element fail ,require index>0  or index<=size");
        }
        // 数据先向后挪,再添加。 否则会发生数据覆盖

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

    }

add往指定位置添加数据,添加数据时我们还是要考虑意外因素的,比如数组是否满了,添加时索引是否合法。这些处理过后我们就看添加的思路了:
image.png

1 遍历要插入索引位置元素及其以后的有效元素
2 插入位置元素及后面元素向后移动一位
ps:移动顺序从后向前否则数据会覆盖丢失

我们遍历的元素为6,5 而6表示为data[size-1],5为插入位置index,表示为data[index],故循环条件便出来了,然后再进行移位。

有了插入指定位置等方法我们就可以复用其代码了,修改addLast。

size代表末尾索引,也就是当前索引,正好应用使代码简洁多了。

 public void addLast(int e) {
       add(size, e);
    }

addFirst和addLast类似复用就行(参考附录源代码)

2.2查、修,的设计

  /**
     * 根据索引查询元素
     * @param index 查询的索引
     * @return data[index] 直接返回元素
     *              静态数组必须开辟一定空间  没使用的不让访问,这就是直接返回元素,而不是返回静态数组的原因
     *              好处:用户永远只能查询已经使用空间上的数据
     * */
    public int get(int index){
        if (index<0||index>size){
            throw new IllegalArgumentException("get failed index is illegal");
        }
        return data[index];
    }


 /**
     * 元素的修改
     * @param index 修改的索引
     * @param e 修改为的元素
     *
     * */
    public void set(int index,int e){
        if (index<0||index>size){
            throw new IllegalArgumentException("set failed index is illegal");
        }
        data[index] = e;
    }

参看代码不在讲解,你懂得哈哈!!!

2.3 删 的api设计

  • remove
  • removeFirst
  • removeLast
  • removeElement
/**
     * 按照索引删除元素
     * @param index 索引
     * @return  int type   返回删除的元素
     * */
    public int remove(int index){
        if (index<0||index>=size){
            throw new IllegalArgumentException("remove failed index is illegal");
        }
        int tempElement = data[index];
        // index+1<=size
        for (int i=index+1;i<size;i++){
            data[i-1] = data[i];
        }
        size--;
        return tempElement;
    }

image.png

删除的核心思路:
如图我们要删除索引为index(2)的元素,此时我们要遍历索引为index以及他后面有效的元素,让他们逐个向前移即可。
data[index]后一个元素为data[index+1],当循环到size位置终止,所以遍历的条件就出来了。
ps:删除时元素要从前开始移动哦

removeFirst,removeLast参考源码嘿嘿!!!

以上是根据索引删除元素的,当然我们也可以根据元素名删除元素,这就是removeElement的功能,其实其内部也是根据索引删除的(参考源码)

通用数组

有了基本数组的设计思路,我们再运行泛型就设计出了通用数组(全部源码)

package array;

/**
 * Create by SunnyDay on 2019/02/01
 */
public class UseGeneric <E>{
    private E[] data; // 数组
    private int size;// 数组大小

    /**
     * @param capacity 根据传入的容量声明数组大小
     */
    public UseGeneric(int capacity) {
        // 由于java 不支持泛型数组 我们可以使用Obj 在强转
        data = (E[]) new Object[capacity];
    }

    /**
     * 默认数组大小
     */
    public UseGeneric() {
        this(10);
    }

    /**
     * 返回数组大小
     */
    public int getSize() {
        return size;
    }

    /**
     * 返回数组容量
     * length java数组的唯一属性数组长度
     */
    public int getCapacity() {
        return data.length;
    }

    /**
     * 数组判断空
     */
    public boolean isEmpty() {
        return size == 0;
    }

    /**
     * 向数组的最后一个元素之后添加元素
     */
    public void addLast(E e) {
        add(size, e);
    }

    /**
     * 数组任意位置添加元素
     *
     * @param index 要插入的索引
     * @param e     要插入的元素
     */
    public void add(int index, E e) {
        // 首先还是检查数组是否满了
        if (size == data.length) {
            throw new IllegalArgumentException("add element fail ,Array is full.");
        }
        // 判断插入数据的合法性(数组中数据是紧密排列,索引大于零)
        if (index < 0 || index > size) {
            throw new IllegalArgumentException("add element fail ,require index>0  or index<=size");
        }
        // 数据先向后挪,再添加。 否则会发生数据覆盖

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

    }

    /**
     * 数组的头部添加元素(复用上面代码)
     */
    public void addFirst(E e) {
        add(0, e);
    }
    /**
     * 根据索引查询元素
     * @param index 查询的索引
     * @return data[index] 直接返回元素
     *              静态数组必须开辟一定空间  没使用的不让访问,这就是直接返回元素,而不是返回静态数组的原因
     *              好处:用户永远只能查询已经使用空间上的数据
     * */
    public E get(int index){
        if (index<0||index>size){
            throw new IllegalArgumentException("get failed index is illegal");
        }
        return data[index];
    }

    /**
     * 元素的修改
     * @param index 修改的索引
     * @param e 修改为的元素
     *
     * */
    public void set(int index,E e){
        if (index<0||index>size){
            throw new IllegalArgumentException("set failed index is illegal");
        }
        data[index] = e;
    }
    /**
     * 数组中是否包含此元素
     * @param e 元素
     * @return boolean 是否包含
     * */
    public boolean contain(E e){
        for (int i = 0; i < size; i++) {
            if (e.equals(data[i])){
                return true;
            }
        }
        return false;
    }

    /**
     * 查找元素所在的索引(第一次出现的索引)
     * @param e 元素
     * @return  i 索引 ,-1 异常退出(没有元素)
     * */
    public int findIndex(E e){
        for (int i = 0; i < size; i++) {
            if (e.equals(data[i])){
                return i;
            }
        }
        return -1;
    }
    /**
     * 按照索引删除元素
     * @param index 索引
     * @return  int type   返回删除的元素
     * */
    public E remove(int index){
        if (index<0||index>=size){
            throw new IllegalArgumentException("remove failed index is illegal");
        }
        E tempElement = data[index];
        for (int i=index+1;i<size;i++){
            data[i-1] = data[i];
        }
        size--;
        data[size] = null;
        return tempElement;
    }
    /**
     * 删除第一个元素
     * */
    public E removeFirst(){
        return remove(0);
    }

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

    /**
     * 删除指定的元素
     *
     * 复用查找索引的方法
     * 本质通过索引删除元素
     *
     * 只删除一个元素  存在相同时(可以自定义接口在实现)
     * */
    public void removeElement( E e){
        int index = findIndex(e);
        if (index!=-1){
            remove(index);
        }
    }

    // 加注解的好处 防止覆盖出错,当父类没有此方法时报错
    @Override
    public String toString() {
        //  System.out.println(Arrays.toString(data)); 默认会把没有的元素默认为0

        // 自定义 封装
        StringBuilder sb = new StringBuilder();
        sb.append(String.format("Array size =  %d ,capacity = %d\n", size, getCapacity()));
        sb.append("[");
        // 数字中的有效元素遍历
        for (int i = 0; i < size; i++) {
            sb.append(data[i]);
            // 不是最后一个元素时拼接
            if (i != size - 1) {
                sb.append(",");
            }
        }
        sb.append("]");
        return sb.toString();
    }
}

注意点:
1 泛型不支持泛型数组(boject的替换)
2 contain 等方法中equals的替换(tip:客户端程序员自定义对象时要自己实现equals)
3 remove中引用的回收 (基本类型不考虑,但是引用类型要了考虑)

通用动态数组组

我们都知道普通的数组都是静态的,一旦我们初始化之后,数组容量就定下了,这就产生了弊端,假如我们申请了10容量的数组,我们使用了1个,后面不使用的就造成了浪费。假如我们申请了容量为100的数组,删除了90个这些删除的空间不再使用同样造成了浪费。这时我们就设计动态数组。

image.png

扩容思路:我们再设计个大的数组,拷贝当前的数组,再添加即可。

  */
    public void add(int index, E e) {
        // 首先还是检查数组是否满了

        // 判断插入数据的合法性(数组中数据是紧密排列,索引大于零)
        if (index < 0 || index > size) {
            throw new IllegalArgumentException("add element fail ,require index>0  or index<=size");
        }
        if (size == data.length) {
            // todo 动态扩充  参考ArrayList的扩容倍数
            resize((int) (1.5 * 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;
    }

缩容的原理和扩容一样,删除到一定个数时缩小容器容量。

 /**
     * 按照索引删除元素
     *
     * @param index 索引
     * @return int type   返回删除的元素
     */
    public E remove(int index) {
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("remove failed index is illegal");
        }
        E tempElement = data[index];
        for (int i = index + 1; i < size; i++) {
            data[i - 1] = data[i];
        }
        size--;
        data[size] = null; // 引用制空
        // 当数组元素减少过多时达到容量一半时 开始动态缩小容量
        if (size==data.length/2){
            resize(data.length/2);
        }
        return tempElement;
    }

源码下载:https://github.com/sunnnydaydev/DataStructure

小结:

通过一段短途的数组之旅,相信我们对数组的设计有所了解了,本文中只是简单地进行了一下探讨,诸如删除你会发现只能删除第一个元素(按照元素删除,数组中存在多个相同的时),查找元素索引,只能查找第一次出现的,这些弊端我们想要实现解决只需自己添加方法即可。

The end!!!

书山有路勤为径,学海无涯苦作舟。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值