三:数据结构基础:1:数组;(Java提供的数组分析;二次封装数组;复杂度分析;均摊时间复杂度和时间复杂度震荡;)

这篇博客深入探讨了数据结构中的数组,特别是基于Java的数组进行二次封装,以实现动态数组,并讨论了添加、删除、修改和查询操作的复杂度。同时,博客介绍了均摊时间复杂度的概念,防止时间复杂度的震荡,提倡在实际应用中采用更有效的策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

说明:

(1)本篇博客介绍数组,通过基于Java提供的数组,进行二次封装的过程,来加深对数组这种基本的数据结构的理解;

(2)本篇博客涉及的代码,还是比价容易理解的;重要的是,对这个封装的思路、数据结构的套路有个整体的感知;

目录

一:为什么要学习数据结构;

1.数据结构是什么;

2.数据结构,三种结构;

3.数据结构,应用案例; 

二:Java中的数组;

1.数组,基础;

2.数组,演示;

插:数组补充简介;

1.数组的索引,可以有语义,也可以没语义;

2.Java提供的数组,不支持扩容和缩容; 

三:基于Java提供的数组,二次封装,得到自己的数组;  

四:改进1:支持泛型的数组;

五:改进2:动态数组;(数组可以扩容和缩容)

六:复杂度分析;

1.添加元素的操作:

2.删除元素的操作:

3.修改元素的操作:

4.查询元素的操作;

七:引申:均摊时间复杂度,和,防止时间复杂度的震荡;

1.均摊时间复杂度;

2.时间复杂度的震荡;


一:为什么要学习数据结构;

1.数据结构是什么;

          ●这儿主要突出的是【高效】;

2.数据结构,三种结构;

          ● 可以通过邻接矩阵或邻接表,来存储图结构数据;;其实,邻接矩阵本质就是一个二维数组,邻接表本质就是一个链表的数组;

          ● 本专栏,会涉及图论领域的算法实现,而不会介绍图这种数据结构的本身的实现;;;即,本专栏不会介绍图这种数据结构;

          ● 我们在实际开发阿忠,需要根据具体应用场景和业务的不同,灵活选择最合适的数据结构;

3.数据结构,应用案例; 

在【一:线性查找法:2:算法和数据结构有什么用?】也已经介绍过;

(1)数据库;

          ● 数据库就是用来存储数据的,所以在设计数据库的时候,一定会使用到数据结构;;只是,对于诸如MySQL这种数据库来说,我们是在外存(比如硬盘等)上作数据存储;;;;;然后,本专栏中介绍数据结构时,都是在内存上操作的; 

(2)操作系统;

          ● 优先队列,内存管理,文件管理:这些功能本身,其实就是“组织和存储数据”;

(3)文件压缩;

          ● 文件压缩是一种算法,而对于很多算法,我们要想执行它,就需要选择合理的数据结构去支撑;

          ● 比如,一种基础的压缩算法就需要使用哈夫曼树;

(4)游戏;

          ● 比如游戏中的寻路算法;;我们可以借助DFS(深度优先遍历)或BFS(广度优先遍历)算法来实现;

在本专栏中,我们主要关注的是【在内存中世界中的增删改查】;但是,不要忘记数据结构的基础是【对数据的组织】; 


二:Java中的数组;

1.数组,基础;

(1)在Java语言中,要求数组中不同索引上的数据类型必须统一;;;在有的语言中,则没这个规定;

(2)在实际业务开发中,数组的名字一般起一个可以见名知意的名字;

2.数组,演示;

public class Test1 {

    public static void main(String[] args) {

        //案例1:定义数组时,需要指定数组的大小;
        int[] arr = new int[10];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = i;
        }

        //案例2:定义数组的第二种方式;在声明的时候,直接初始化;
        int[] scores1 = new int[]{100, 99, 96};
        for (int i = 0; i < scores1.length; i++) {
            System.out.println(scores1[i]);
        }

        //案例3:案例2的简写形式,在实际开发中自己也使用这种简写方式;
        int[] scores2 = {8, 9, 10};
        for (int i = 0; i < scores2.length; i++) {
            System.out.println(scores2[i]);
        }

        //案例4:java中的数组是可迭代的;
        int[] scores3 = {11, 12, 13};
        for (int score : scores3) {//java中的数组,有可遍历(或者说可迭代)的能力;
            System.out.println(score);
        }
    }

}

(1)有关Java中的数组,可以参考【Java一维数组:创建、初始化;增强for循环;冒泡排序】;


插:数组补充简介;

1.数组的索引,可以有语义,也可以没语义;

(1)数组的索引,可以有语义,也可以没语义;

          ● 索引是可以有语义的:比如存储学生的数组,如果把学号作为索引;那么,scores[2]就表示学号为2的那个学生;

          ● 自然,索引是可以没有语义的;

(2)单纯从【充分利用数组优点】的技术角度,来说,索引有语义是很好的;

          ● 比如,我们想查学号为10046的学生,直接scores[10046]就行了,十分方便;

(3)但是,很多场景中,不应该使用有语义的索引;

          ● 我们为了以身份证号为索引,那么我们就要开辟很大的空间;对于计算机来说,这是不值当的、甚至是不可能的;

          ● 而且,空间开辟那么多,也很可能会造成浪费;;;比如,目前就有10个人;如果以IDNumber为索引的话,就会造成很大很大的浪费;

          ● 而且,自己在实际开发中,使用数组时,一般索引都没有语义;

2.Java提供的数组,不支持扩容和缩容; 

Java提供的数组,不支持扩容和缩容(即,Java提供的数组是静态数组);为此,我们要基于Java提供的数组,进行二次封装,得到一个动态数组;


三:基于Java提供的数组,二次封装,得到自己的数组;  

import com.sun.org.apache.xpath.internal.objects.XNull;
import sun.awt.windows.WingDings;

public class Array {
    private int[] data;
    private int size;//数组中,实际元素个数;;;capacity我们就不定义了,因为data.length就是;

    /**
     * 自己写一个构造函数,传入数组容量capacity,去创建一个数组;
     * @param capacity
     */
    public Array(int capacity) {
        data = new int[capacity];
        size = 0;
    }

    /**
     * 定义无参构造
     */
    public Array() {
        this(10);//如果用户在创建Array时,没有传入参数,那么我们就默认其capacity是10;
    }

    /**
     * 查看数组中,元素个数,即查看size;
     * @return
     */
    public int getSize() {
        return size;
    }

    /**
     * 查看数组容量,即查看capacity;
     * @return
     */
    public int getCapacity() {
        return data.length;
    }

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

    /**
     * 向数组中追加一个元素;
     * 比如一个数组capacity是10,目前size是6;那么我们就是添加第7个元素,其实也就是在索引为6的位置上添加一个元素;
     * 比如数组原先是:66 88 99 100 0 0 0;;我们想要追加77,变成66 88 99 100 77 0 0;
     *
     * @param e
     */
    public void addLast(int e) {

        //如果此时,数组已经满了,就抛出一个异常;
        if (size == data.length) {
            throw new IllegalArgumentException("AddLast failed. Array is full.");
        }

        data[size] = e;
        size++;//自然,添加完元素后,数组的size要记得加1;

//        add(size, e);//我们开发了add()方法后,其实这儿就可以使用add()方法;
    }


    /**
     * 向数组指定位置,添加一个元素;
     * 比如数组原先是:66 88 99 100 0 0 0;;我们想要在index=1的位置上添加77,变成66 77 88 99 100 0 0;
     * @param index
     * @param e
     */
    public void add(int index,int e) {

        //如果此时,数组已经满了,就抛出一个异常;
        if (size == data.length) {
            throw new IllegalArgumentException("AddLast failed. Array is full.");
        }
        //要求,索引不能为负;索引不能大于size,否则就会出现66 88 99 100 0 77 0这种情况了,这与【数组中的元素需要连续】相违背;
        if (index < 0 || index > size) {
            throw new IllegalArgumentException("AddLast failed. Require index < 0 and index > size.");
        }

        //把88 99 100都向后平移一位;
        for (int i = size - 1; i >= index; i--) {
            data[i + 1] = data[i];
        }
        data[index] = e;
        size++;//自然,添加完元素后,数组的size要记得加1;

    }

    /**
     * 向数组第一个位置,添加一个元素;
     * 比如数组原先是:66 88 99 100 0 0 0;;我们想要在i数组第一个位置上添加77,变成77 66 88 99 100 0 0;
     * @param e
     */
    public void addFirst(int e) {
        add(0, e);//这儿就可以直接调用add()方法了;
    }


    /**
     * 根据索引,获取数据
     *
     * @param index
     * @return
     */
    public int get(int index) {
        // (1)已经知道,如果数组capacity=10,然后目前数组只存了3个数,即size=3;;;那么此时这个数组是没有存满的;通
        // 过get方法的逻辑,可以看到,用户是永远无法访问到那些还没存元素的索引位置的;
        // (2)而且,还可以发现,我们在Array类中定义的data[]数组,对外是不可见的;;即用户只能感受到Array,而感受不到我
        // 们底层利用了data[]的;
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("Get failed. Index is illegal.");
        }
        return data[index];
    }

    /**
     * 根据所有,修改某个元素;(注意是修改,而不是新增)
     * @param index
     * @param e
     * @return
     */
    public void set(int index,int e) {
        // 已经知道,如果数组capacity=10,然后目前数组只存了3个数,即size=3;;;那么此时这个数组是没有存满的;
        // 通过set方法的逻辑,可以看到,用户是永远无法修改到那些还没存元素的索引位置的;
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("Get failed. Index is illegal.");
        }
        data[index] = e;
    }

    /**
     * 判断,数组中是否包含某个元素;
     * @param e
     * @return
     */
    public boolean contains(int e) {
        for (int i = 0; i < size; i++) {
            if (data[i] == e) {
                return true;
            }
        }
        return false;
    }

    /**
     * 判断,数组中是否包含某个元素;如果有,返回这个元素在数组中的索引;(其实,这儿查询的是第一个;比如数组中有三个位置都是77,
     * 如果我们查找77的话,返回的是第一个的索引)
     * @param e
     * @return
     */
    public int find(int e) {
        for (int i = 0; i < size; i++) {
            if (data[i] == e) {
                return i;
            }
        }
        return -1;
    }

    /**
     * 从数组中,删除指定索引的元素,并返回该元素的值;
     * 比如一个数组是:66 77 88 99 100 0 0 0,我们要删除index=1那个元素,那么删除后就是:66 88 99 100 0 0 0 0;
     * 然后返回77;
     * @param index
     * @return
     */
    public int remove(int index) {

        //先判断index是否OK,这其中自然涵盖了数组是否为空,索引是否过大等情况;
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("Delete failed. Index is illegal.");
        }
        int ret = data[index];
        for (int i = index + 1; i < size; i++) {
            data[i-1] = data[i];
        }
        size--;//别忘了,维护一下size;
        return ret;
        //data[size] = 0;//其实,我们可以不做一步操作,因为我们的编程实现,用户根本访问不到index=size及之后的位置;
    }

    /**
     * 删除第一个元素,并返回这个删除的元素;
     * @return
     */
    public int removeFirst() {
        return remove(0);
    }

    /**
     * 删除最后一个元素,并返回这个删除的元素;
     * @return
     */
    public int removeLast() {
        return remove(size - 1);
    }

    /**
     * 判断数组中是否包含某个元素,如果有就删除;(其实,这儿删除的是第一个;比如数组中有三个位置都是77,如果我们删除77的话,
     * 删除的是第一个)
     * @param e
     */
    public void removeElement(int e) {
        int index = find(e);
        if (index != -1) {
            remove(index);
        }
    }

    /**
     * 判断数组中是否包含某个元素,如果有就删除;同时,如果有,而且我们删除了的话,就返回true;如果没有就返回false;
     * @param e
     * @return
     */
    public boolean removeElementIsOK(int e) {
        int index = find(e);
        if (index != -1) {
            remove(index);
            return true;
        }
        return false;
    }








    /**
     * 重写toString方法,打印数组的基本信息和元素内容;
     * @return
     */
    @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();
    }
}

说明:

(1)上面是基于Java的数组,封装自己的数组;内容挺好理解的,看注释;

(2)但是,上面的数组还有两个需要改进的地方:

          ● 目前Array只能是int类型的,后面要支持泛型;

          ● 数组无法扩容和缩容,后面需要动态数组;

(3)也可以测试;

public class Test2 {

    public static void main(String[] args) {
        Array array = new Array(20);
        for (int i = 0; i < 10; i++) {
            array.addLast(i);
        }

        System.out.println(array);

        array.add(1, 100);
        System.out.println(array);

        array.addFirst(9);
        System.out.println(array);

        array.remove(2);
        System.out.println(array);
        //…………
    }
}

四:改进1:支持泛型的数组;

(1)数组应该能够盛放int,String这种java原本就有的类型;也要能够存放自定义的,如Students这种类型;

(2)为此,就需要把上面的Array类,设为泛型类;有关泛型的内容,如有需要可以参考【Java泛型二:自定义泛型类】及附近相关文章;

自定义支持泛型的数组;



/**
 * 支持泛型
 */
public class Array2<E> { //Array2盛放的数据类型是E,那么具体E是什么,我们在使用Array2的时候再声明;
    private E[] data;
    private int size;//数组中,实际元素个数;;;capacity我们就不定义了,因为data.length就是;

    /**
     * 自己写一个构造函数,传入数组容量capacity,去创建一个数组;
     * @param capacity
     */
    public Array2(int capacity) {
//        data = new E[capacity];//java本身不支持:"new 泛型类型[]",这种直接new一个泛型数组;这条语句会报错;
        data = (E[]) new Object[capacity];//因为,上面会报错,所以对于java来说,我们需要这么做;
        size = 0;
    }

    /**
     * 定义无参构造
     */
    public Array2() {
        this(10);//如果用户在创建Array时,没有传入参数,那么我们就默认其capacity是10;
    }

    /**
     * 查看数组中,元素个数,即查看size;
     * @return
     */
    public int getSize() {
        return size;
    }

    /**
     * 查看数组容量,即查看capacity;
     * @return
     */
    public int getCapacity() {
        return data.length;
    }

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

    /**
     * 向数组中追加一个元素;
     * 比如一个数组capacity是10,目前size是6;那么我们就是添加第7个元素,其实也就是在索引为6的位置上添加一个元素;
     * 比如数组原先是:66 88 99 100 0 0 0;;我们想要追加77,变成66 88 99 100 77 0 0;
     *
     * @param e
     */
    public void addLast(E e) {

        //如果此时,数组已经满了,就抛出一个异常;
        if (size == data.length) {
            throw new IllegalArgumentException("AddLast failed. Array is full.");
        }

        data[size] = e;
        size++;//自然,添加完元素后,数组的size要记得加1;

//        add(size, e);//我们开发了add()方法后,其实这儿就可以使用add()方法;
    }


    /**
     * 向数组指定位置,添加一个元素;
     * 比如数组原先是:66 88 99 100 0 0 0;;我们想要在index=1的位置上添加77,变成66 77 88 99 100 0 0;
     * @param index
     * @param e
     */
    public void add(int index,E e) {

        //如果此时,数组已经满了,就抛出一个异常;
        if (size == data.length) {
            throw new IllegalArgumentException("AddLast failed. Array is full.");
        }
        //要求,索引不能为负;索引不能大于size,否则就会出现66 88 99 100 0 77 0这种情况了,这与【数组中的元素需要连续】相违背;
        if (index < 0 || index > size) {
            throw new IllegalArgumentException("AddLast failed. Require index < 0 and index > size.");
        }

        //把88 99 100都向后平移一位;
        for (int i = size - 1; i >= index; i--) {
            data[i + 1] = data[i];
        }
        data[index] = e;
        size++;//自然,添加完元素后,数组的size要记得加1;

    }

    /**
     * 向数组第一个位置,添加一个元素;
     * 比如数组原先是:66 88 99 100 0 0 0;;我们想要在i数组第一个位置上添加77,变成77 66 88 99 100 0 0;
     * @param e
     */
    public void addFirst(E e) {
        add(0, e);//这儿就可以直接调用add()方法了;
    }


    /**
     * 根据索引,获取数据
     *
     * @param index
     * @return
     */
    public E get(int index) {
        // (1)已经知道,如果数组capacity=10,然后目前数组只存了3个数,即size=3;;;那么此时这个数组是没有存满的;通
        // 过get方法的逻辑,可以看到,用户是永远无法访问到那些还没存元素的索引位置的;
        // (2)而且,还可以发现,我们在Array类中定义的data[]数组,对外是不可见的;;即用户只能感受到Array,而感受不到我
        // 们底层利用了data[]的;
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("Get failed. Index is illegal.");
        }
        return data[index];
    }

    /**
     * 根据所有,修改某个元素;(注意是修改,而不是新增)
     * @param index
     * @param e
     * @return
     */
    public void set(int index,E e) {
        // 已经知道,如果数组capacity=10,然后目前数组只存了3个数,即size=3;;;那么此时这个数组是没有存满的;
        // 通过set方法的逻辑,可以看到,用户是永远无法修改到那些还没存元素的索引位置的;
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("Get failed. Index is illegal.");
        }
        data[index] = e;
    }

    /**
     * 判断,数组中是否包含某个元素;
     * @param e
     * @return
     */
    public boolean contains(E e) {
        for (int i = 0; i < size; i++) {
            if (data[i].equals(e)) {  //改为E之后,这儿就是比较对象了,我么就不能使用"if (data[i] == e)"了;
                return true;
            }
        }
        return false;
    }

    /**
     * 判断,数组中是否包含某个元素;如果有,返回这个元素在数组中的索引;(其实,这儿查询的是第一个;比如数组中有三个位置都是77,
     * 如果我们查找77的话,返回的是第一个的索引)
     * @param e
     * @return
     */
    public int find(E e) {
        for (int i = 0; i < size; i++) {
            if (data[i].equals(e)) {//改为E之后,这儿就是比较对象了,我么就不能使用"if (data[i] == e)"了;
                return i;
            }
        }
        return -1;
    }

    /**
     * 从数组中,删除指定索引的元素,并返回该元素的值;
     * 比如一个数组是:66 77 88 99 100 0 0 0,我们要删除index=1那个元素,那么删除后就是:66 88 99 100 0 0 0 0;
     * 然后返回77;
     * @param index
     * @return
     */
    public E remove(int index) {

        //先判断index是否OK,这其中自然涵盖了数组是否为空,索引是否过大等情况;
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("Delete failed. Index is illegal.");
        }
        E ret = data[index];
        for (int i = index + 1; i < size; i++) {
            data[i-1] = data[i];
        }
        size--;//别忘了,维护一下size;
        //data[size] = 0;//其实,我们可以不做一步操作,因为我们的编程实现,用户根本访问不到index=size及之后的位置;
        //但是,如果E是一个复杂对象的话,因为data[size]因为有应用,所以其不会被垃圾回收,这就涉及到了空间释放的问题;
        //所以,此时,可以增加下面一句话,释放空间;;;;自然,不加也行;
        //PS:对于这种data[size]处的对象,有个术语loitering objects,即闲逛的对象;即这个对象本身已经没有用了,但其并
        // 没有被回收,还在程序内存中;;;;但是loitering objects并不代表memory leak内存泄露;
        data[size] = null;
        return ret;
    }

    /**
     * 删除第一个元素,并返回这个删除的元素;
     * @return
     */
    public E removeFirst() {
        return remove(0);
    }

    /**
     * 删除最后一个元素,并返回这个删除的元素;
     * @return
     */
    public E removeLast() {
        return remove(size - 1);
    }

    /**
     * 判断数组中是否包含某个元素,如果有就删除;(其实,这儿删除的是第一个;比如数组中有三个位置都是77,如果我们删除77的话,
     * 删除的是第一个)
     * @param e
     */
    public void removeElement(E e) {
        int index = find(e);
        if (index != -1) {
            remove(index);
        }
    }

    /**
     * 判断数组中是否包含某个元素,如果有就删除;同时,如果有,而且我们删除了的话,就返回true;如果没有就返回false;
     * @param e
     * @return
     */
    public boolean removeElementIsOK(E e) {
        int index = find(e);
        if (index != -1) {
            remove(index);
            return true;
        }
        return false;
    }








    /**
     * 重写toString方法,打印数组的基本信息和元素内容;
     * @return
     */
    @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();
    }
}

说明:

(1)泛型类不难理解,挺简单的;上面的改造过程也挺简单的,看注释;

(2)需要注意两点:

          ● Java语言,不允许直接创建泛型数组;(这算是一个有争议的,遗留问题)

          ● loitering objects,闲逛对象(或游荡对象);


五:改进2:动态数组;(数组可以扩容和缩容)

自然,当删除数组中的元素,删除到一定程度的时候,也可以采取类似的思路来缩容;

可以扩容的动态数组;

import javafx.scene.chart.PieChart;

/**
 * 支持泛型
 */
public class Array3<E> { //Array2盛放的数据类型是E,那么具体E是什么,我们在使用Array2的时候再声明;
    private E[] data;
    private int size;//数组中,实际元素个数;;;capacity我们就不定义了,因为data.length就是;

    /**
     * 自己写一个构造函数,传入数组容量capacity,去创建一个数组;
     * @param capacity
     */
    public Array3(int capacity) {
//        data = new E[capacity];//java本身不支持:"new 泛型类型[]",这种直接new一个泛型数组;这条语句会报错;
        data = (E[]) new Object[capacity];//因为,上面会报错,所以对于java来说,我们需要这么做;
        size = 0;
    }

    /**
     * 定义无参构造
     */
    public Array3() {
        this(10);//如果用户在创建Array时,没有传入参数,那么我们就默认其capacity是10;
    }

    /**
     * 查看数组中,元素个数,即查看size;
     * @return
     */
    public int getSize() {
        return size;
    }

    /**
     * 查看数组容量,即查看capacity;
     * @return
     */
    public int getCapacity() {
        return data.length;
    }

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

    /**
     * 向数组中追加一个元素;
     * 比如一个数组capacity是10,目前size是6;那么我们就是添加第7个元素,其实也就是在索引为6的位置上添加一个元素;
     * 比如数组原先是:66 88 99 100 0 0 0;;我们想要追加77,变成66 88 99 100 77 0 0;
     *
     * @param e
     */
    public void addLast(E e) {

//        //如果此时,数组已经满了,就抛出一个异常;
//        if (size == data.length) {
//            throw new IllegalArgumentException("AddLast failed. Array is full.");
//        }
//
//        data[size] = e;
//        size++;//自然,添加完元素后,数组的size要记得加1;

        add(size, e);//我们开发了add()方法后,其实这儿就可以使用add()方法;
    }


    /**
     * 向数组指定位置,添加一个元素;
     * 比如数组原先是:66 88 99 100 0 0 0;;我们想要在index=1的位置上添加77,变成66 77 88 99 100 0 0;
     * @param index
     * @param e
     */
    public void add(int index,E e) {

        //要求,索引不能为负;索引不能大于size,否则就会出现66 88 99 100 0 77 0这种情况了,这与【数组中的元素需要连续】相违背;
        if (index < 0 || index > size) {
            throw new IllegalArgumentException("AddLast failed. Require index < 0 and index > size.");
        }

        //如果此时,数组已经满了,就去扩容;
        if (size == data.length) {
            //当数组已经满了,需要扩容的时候,这儿采取的策略是,在原有长度的基础上扩大一倍;(Java提供的动态数组ArrayList是扩1.5倍)
            //这儿我们之所以不扩容一个常数(比如扩容10个长度)的原因是:数组的实际存储情况不同,这个常数不好确定;比如数组容
            // 量是一万,如果扩容10的话,就有点不痛不痒、杯水车薪;;如果数组容量是10的话,如果扩容1000,就会太猛了;
            resize(2 * data.length);
        }

        //把88 99 100都向后平移一位;
        for (int i = size - 1; i >= index; i--) {
            data[i + 1] = data[i];
        }
        data[index] = e;
        size++;//自然,添加完元素后,数组的size要记得加1;

    }

    /**
     * 向数组第一个位置,添加一个元素;
     * 比如数组原先是:66 88 99 100 0 0 0;;我们想要在i数组第一个位置上添加77,变成77 66 88 99 100 0 0;
     * @param e
     */
    public void addFirst(E e) {
        add(0, e);//这儿就可以直接调用add()方法了;
    }


    /**
     * 根据索引,获取数据
     *
     * @param index
     * @return
     */
    public E get(int index) {
        // (1)已经知道,如果数组capacity=10,然后目前数组只存了3个数,即size=3;;;那么此时这个数组是没有存满的;通
        // 过get方法的逻辑,可以看到,用户是永远无法访问到那些还没存元素的索引位置的;
        // (2)而且,还可以发现,我们在Array类中定义的data[]数组,对外是不可见的;;即用户只能感受到Array,而感受不到我
        // 们底层利用了data[]的;
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("Get failed. Index is illegal.");
        }
        return data[index];
    }

    /**
     * 根据所有,修改某个元素;(注意是修改,而不是新增)
     * @param index
     * @param e
     * @return
     */
    public void set(int index,E e) {
        // 已经知道,如果数组capacity=10,然后目前数组只存了3个数,即size=3;;;那么此时这个数组是没有存满的;
        // 通过set方法的逻辑,可以看到,用户是永远无法修改到那些还没存元素的索引位置的;
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("Get failed. Index is illegal.");
        }
        data[index] = e;
    }

    /**
     * 判断,数组中是否包含某个元素;
     * @param e
     * @return
     */
    public boolean contains(E e) {
        for (int i = 0; i < size; i++) {
            if (data[i].equals(e)) {  //改为E之后,这儿就是比较对象了,我么就不能使用"if (data[i] == e)"了;
                return true;
            }
        }
        return false;
    }

    /**
     * 判断,数组中是否包含某个元素;如果有,返回这个元素在数组中的索引;(其实,这儿查询的是第一个;比如数组中有三个位置都是77,
     * 如果我们查找77的话,返回的是第一个的索引)
     * @param e
     * @return
     */
    public int find(E e) {
        for (int i = 0; i < size; i++) {
            if (data[i].equals(e)) {//改为E之后,这儿就是比较对象了,我么就不能使用"if (data[i] == e)"了;
                return i;
            }
        }
        return -1;
    }

    /**
     * 从数组中,删除指定索引的元素,并返回该元素的值;
     * 比如一个数组是:66 77 88 99 100 0 0 0,我们要删除index=1那个元素,那么删除后就是:66 88 99 100 0 0 0 0;
     * 然后返回77;
     * @param index
     * @return
     */
    public E remove(int index) {

        //先判断index是否OK,这其中自然涵盖了数组是否为空,索引是否过大等情况;
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("Delete failed. Index is illegal.");
        }
        E ret = data[index];
        for (int i = index + 1; i < size; i++) {
            data[i-1] = data[i];
        }
        size--;//别忘了,维护一下size;
        //data[size] = 0;//其实,我们可以不做一步操作,因为我们的编程实现,用户根本访问不到index=size及之后的位置;
        //但是,如果E是一个复杂对象的话,因为data[size]因为有应用,所以其不会被垃圾回收,这就涉及到了空间释放的问题;
        //所以,此时,可以增加下面一句话,释放空间;;;;自然,不加也行;
        //PS:对于这种data[size]处的对象,有个术语loitering objects,即闲逛的对象;即这个对象本身已经没有用了,但其并
        // 没有被回收,还在程序内存中;;;;但是loitering objects并不代表memory leak内存泄露;
        data[size] = null;

        //当我们删除数组中元素的时候,如果数组此时实际数据数量为数组容量的一般时,我们就执行缩容操作;
        if (size == data.length/2) {
            //而具体,我们可以把数组的capacity缩小为原来的一半;
            resize(data.length/2);
        }
        return ret;
    }

    /**
     * 删除第一个元素,并返回这个删除的元素;
     * @return
     */
    public E removeFirst() {
        return remove(0);
    }

    /**
     * 删除最后一个元素,并返回这个删除的元素;
     * @return
     */
    public E removeLast() {
        return remove(size - 1);
    }

    /**
     * 判断数组中是否包含某个元素,如果有就删除;(其实,这儿删除的是第一个;比如数组中有三个位置都是77,如果我们删除77的话,
     * 删除的是第一个)
     * @param e
     */
    public void removeElement(E e) {
        int index = find(e);
        if (index != -1) {
            remove(index);
        }
    }

    /**
     * 判断数组中是否包含某个元素,如果有就删除;同时,如果有,而且我们删除了的话,就返回true;如果没有就返回false;
     * @param e
     * @return
     */
    public boolean removeElementIsOK(E e) {
        int index = find(e);
        if (index != -1) {
            remove(index);
            return true;
        }
        return false;
    }




    /**
     * 重写toString方法,打印数组的基本信息和元素内容;
     * @return
     */
    @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();
    }

    /**
     * 工具方法:数组扩容或缩容;
     * @param newCapacity
     */
    private void resize(int newCapacity) {
        E[] newDate = (E[]) new Object[newCapacity];//先创建一个新的、新容量的数组;
        for (int i = 0; i < size; i++) {
            newDate[i] = data[i];//把数组中的数据,全部复制到新的数组中去;
        }
        data = newDate;//然后,把数组变量data,指向这个新创建的数组;
        //说明两点:(1)newData是个方法内部属性,这个方法执行完之后,newData这个变量就会被回收;
        // (2)data这个变量原先指向的是原先那个数组的空间,现在data指向新的空间了,那么原先旧数组的空间也会被回收;
    }
}

说明:

(1)添加元素的时候,如果数组空间不够了,我们就去扩容;

(2)删除元素的时候,如果数组闲置空间过多,我们就去缩小容;

(3)数组扩容或缩容的方法:resize()方法;

 

(4)上面的扩容和缩容的过程,对于用户来说是不可见的;


六:复杂度分析;

1.添加元素的操作:

(1)在数组后面增加一个元素,addLast(E e)方法:O(1):这个操作的时间复杂度,和数据的规模无关;

(2)在数组的第一个位置增加一个元素,addFirst(E e)方法:O(n):因为这个操作,需要把数组中的数据全部向后平移一位;

(3)在数组的指定位置增加一个元素,add(int index,E e):O(n/2)=O(n):这个具体复杂度是多少与index有关;

          ● 引入概率论的内容,认为index取0-size的可能性都是一样的;所以,可以得到其时间复杂度的期望;这儿就不详细介绍了;

因为,通常考虑时间复杂度时候,我们考虑最坏的情况;所以,对于添加元素来说,整体来看,其时间复杂度就是O(n);

(4)添加时,如果需要,还需要对数组进行扩容,resize(int newCapacity)方法:O(n);

2.删除元素的操作:

(1)删除数组后面的一个元素,removeLast()方法:O(1);

(2)删除数组的第一个元素,removeFirst()方法:O(n);

(3)删除指定位置的元素,remove(int index)方法:O(n/2)=O(n)

(4)删除时,如果需要,还需要对数组进行缩容,resize(int newCapacity)方法:O(n);

因为,通常考虑时间复杂度时候,我们考虑最坏的情况;所以,对于查询元素来说,整体来看,其时间复杂度就是O(n);

3.修改元素的操作:

(1)修改指定位置的元素,set(int index,E e):O(1);

因为,通常考虑时间复杂度时候,我们考虑最坏的情况;所以,对于修改元素来说,整体来看,如果已知索引其时间复杂是O(1),如果未知索引其时间复杂度就是O(n);

4.查询元素的操作;

(1)根据索引去查元素,get(int index):O(1);

(2)判断数组中是否包含某个元素,contains(E e):O(n);

(3)判断数组中是否包含某个元素,如果有,返回这个元素在数组中的索引,find(E e):O(n);

因为,通常考虑时间复杂度时候,我们考虑最坏的情况;所以,对于查询元素来说,整体来看,如果已知索引其时间复杂是O(1),如果未知索引其时间复杂度就是O(n);


七:引申:均摊时间复杂度,和,防止时间复杂度的震荡;

1.均摊时间复杂度;

(1)问题阐述:

          ● 逻辑阐述:【前面在添加操作的时间复杂度时,因为通常考虑时间复杂度时候,我们考虑最坏的情况;】→【但,在实际应用中,对于添加来说,我们可以只调用addLast( E e),尽量不调用addFirst(E e)和add(int index,E e);;那么此时,添加操作的时间复杂度就是O(1);】→【即使如此,因为在添加的时候,有时是需要扩容的,而介绍resize(int newCapacity)方法的时间复杂度是O(n);】→【索引,鉴于通常考虑时间复杂度时候,我们考虑最坏的情况】→【所以,即使是调用addLast(E e)方法,最坏的情况下,其时间复杂度依旧是O(n);】→【所以,我们认为添加操作时间复杂度,整体上看为O(n);】

          ● 但是,在实际上,我么调用addLast(E e)的时候,不可能每一次都会需要调用resize(int newCapacity)方法的;所以,对于addLast(E e)来说,依旧考虑最坏的、需要调用resize(int newCapacity)的情况,是稍微有些不合理的;

(2)均摊时间复杂度:

          ● 案例阐述:【假如我们创建了一数组,其capacity是8;然后,我们使用addLast(E e)向其中添加元素;】→【那么我们需要添加8次,直到第九次添加的时候,才会去调用resize(int newCapacity)方法;】→【那么,其实上面的过程,我们调用了9次addLast(E e),触发了一次resize(int newCapacity),总共进行了17次基本操作;平均来看,每次调用了addLast(E e),进行两次基本操作】;

          ● 案例阐述:一般化处理:【假如我们创建了一数组,其capacity是n;然后,我们使用addLast(E e)向其中添加元素;】→【那么我们需要添加n次,直到第n+1次添加的时候,才会去调用resize(int newCapacity)方法;】→【那么,其实上面的过程,我们调用了n+1次addLast(E e),触发了一次resize(int newCapacity),总共进行了2n+1次基本操作;平均来看,每次调用了addLast(E e),进行两次基本操作】;

          ● 那么,这样一看,均摊计算的话,addLast(E e)方法的时间复杂度是O(2),即O(1);

          ● 在这个案例中,均摊计算比计算最坏的情况,更加有意义;而,这种计算复杂度,就是均摊时间复杂度;在实际工程中,均摊时间复杂度还是有意义的;

          ● 自然,removeLast(E e)方法的均摊时间复杂度也是O(1);

2.时间复杂度的震荡;

(1)问题阐述:

          ● 【假如我们创建了一数组,其capacity是8;然后,此时数组已经满了】→【如果,此时我们使用addLast(E e)向其中添加元素;就会调用resize(int newCapacity)方法,那么此时addLast(E e)的时间复杂度就是O(n)】→【此时数组的capacity是16,然后size是9】→【然后,我们马上再去调用removeLast(E e),自然也会去触发resize(int newCapacity)方法,那么此时removeLast(E e)的时间复杂度就是O(n)】→【此时数组的capacity是8,然后size是8】→【然后,我们又去调用addLast(E e),……】→【然后,我们又去调用removeLast(E e),……】;

(2)问题原因分析:

          ● 调用removeLast(E e)的时候,我们过于着急的去调用resize(int newCapacity)方法,即采用了一种Eager的策略;

(3)解决方案:

          ● 调用removeLast(E e)的时候,可以采用一种lazy策略;比如:【假如我们创建了一数组,其capacity是8;然后,此时数组已经满了】→【如果,此时我们使用addLast(E e)向其中添加元素;就会调用resize(int newCapacity)方法,那么此时addLast(E e)的时间复杂度就是O(n)】→【此时数组的capacity是16,然后size是9】→【然后,我们马上再去调用removeLast(E e),此时数组的capacity是16,然后size是8】→【此时,我们不着急调用resize(int newCapacity),去缩容;】→【而是再等等,如果一直调用removeLast(E e),直到数组的capacity是16,size是4,即size是capacity是四分之一时,我们再来调用resize(int newCapacity),去缩容】→【此时,缩容依旧是缩为二分之一,即数组缩容后,capacity是8,然后size是4】;

          ● 这种lazy的策略,还是比较有意思的;在算法领域,有时我们懒一些,返回会使算法的整体性能会更好;;;后面介绍到线段树,也会介绍到类似的策略;

          ● removeLast(E e)采用lazy策略的实现:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值