数据结构入门学习(三):数组进阶修炼

C++ 学习路线:快速入门到进阶 C/C++ 是一门底层、细粒度、功能强大、学习曲线陡峭的语言,掌握这门语言的程序员通常有着更长的生命周期以及更深的护城河。但入门门槛高也是不争的事实,这篇文章把C++学习划分为入门、进阶、深入三个阶段,每步提供相应的学习方法和资源,帮助大家更好地掌握这门语言。 阅读详情

关于博主

努力与运动兼备~~~有任何问题可以加我好友或者关注微信公众号,欢迎交流,我们一起进步!

                                      微信公众号:  啃饼思录

                                    QQ: 2810706745(i思录)

写在前面

因为我们上篇实现的数组只能存放int类型,但是我们需要的是可以承载多种类型,甚至自定义对象的容器,所以本篇就来通过介绍数组的进阶修炼,来实现这个目的。

使用泛型

泛型是Java SE 1.5的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。使用泛型可以让我们的数据结构可以放置“任何”数据类型,注意既然是使用了泛型,因此不可以是基本数据类型,只能是类对象,要想使用基本数据类型就要使用它们对于的包装类对象。

基本数据类型是指byte,short,int,long,float,double,boolean,char。每个基本数据类型都有对应的包装类: Byte , Short, Integer , Long , Float , Double,Boolean,Character。

注意:只有int 和char其基本数据类型与对应的包装类有一个变形以外,其余的都是其首字母的大写而已。

我们可以通过装箱和拆箱来实现其基本数据类型和对应包装类之间的转换。

我们现在将前面的Array.java文件,复制一份,名字为ArrayElement.java,里面修改泛型E(Element这只是一个元素的简称,你可以自己随意定义)。

泛型是不能通过new来创建一个数组的data = new E[capacity];这是错误的做法。我们知道java中的类都是隐式的继承Object类,因此可以先通过new Object[]这样的方式先创建Object数组,然后再进行强制类型转换为E[]即可。也就是这样的:

data = (E[]) new Object[capacity];

然后还要注意因为使用了泛型,因此对象的类型就都要修改为E了。当然对象的比较更不能使用==了,应该使用equals()方法:

    public int find(E e){
        for(int i =0;i<size;i++){
            if(data[i].equals(e)){
                return i;
            }
        }
        return -1;
    }

前面我们好像说过在删除指定元素的时候,就是假如你删除的不是最后一个元素,那么那个元素向前挪了一步,把前面的元素给覆盖了,但是后面的却没有元素将它(就是最后一个元素(data[size]),删除的是最后一个元素就更不必说了)给覆盖,而是依然存留在那里,但是由于元素的数量减少了,因此你永远也访问不到它。

现在在使用泛型的使用,我们尽管也可以这么做(不去管它,任其流浪,最后被java的垃圾回收机制收回,过程很漫长),但是建议这里使用data[size] = null;立即调用java回收机制进行处理。

为什么呢?我们知道8大基本数据类型,在内存中存放的是数据本身,而引用数据类型在内存中存放的数据的引用地址。数据本身是不可以被修改的,但是引用地址却是可以修改的,因此如果你能把该空间进行释放那就可以继续使用了。尽管Java有自动的垃圾回收机制,但如果data[size]依旧存放着对对象的地址引用,就使得它不会被自动的垃圾回收机制给处理掉。因此使用data[size] = null;就能立即调用java回收机制进行处理,从而释放资源。

如果data[size]不置为null,它会被称为loitering Objects(游荡对象),一点用都没有,而且垃圾回收机制是不回收的。loitering Objects != memory leak只是为了程序优化而已,如果能手动去除它就更好了,因此最后添加data[size] = null;,你是在不想添加也是可以的。

现在我附上完整的ArrayElement.java文件里面的代码:

package com.suanfa.test.Array;

public class ArrayElement<E> {
    private E[] data;   //数组
    private int size;    //数组中元素的个数

    /**
     * 带容量参数构造函数
     *
     * @param capacity 数组容量
     * **/
    public ArrayElement(int capacity){
        data = (E[]) new Object[capacity];
        size =0;
    }



    /***
     * 默认的构造函数
     * */
    public ArrayElement(){
        this(10);
    }



    /**
     * 静态数组入参构造函数
     * @param data 传入静态数组
     */
    public ArrayElement(E[] data) {
        this.data = data;
    }


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


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


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


    /**
     * 向所有元素末尾添加一个新元素。
     *
     * @param e 添加的元素
     */
    public void addList(E e){
//        if(size ==data.length){
//            throw new IllegalArgumentException("对不起,数组容量已经满了,添加元素操作失败!");
//        }
////        data[size++]=e;  //这个和下面两行代码表达的意思一样,但还是建议分开写。
//        data[size] =e;
//        size++;

        add(size,e);
    }


    /**
     * 向所有元素开头添加一个新元素。
     *
     * @param e 添加的元素
     */
    public void addFirst(E e){
        add(0,e);
    }


    /**
     * 在第Index的位置,添加e元素
     *
     * @param e 添加的元素,index 待添加元素的索引号
     */
    public void add(int index, E e){
        if(size ==data.length){
            throw new IllegalArgumentException("对不起,数组容量已经满了,添加元素操作失败!");
        }
        if(index<0 || index>size){
            throw new IllegalArgumentException("对不起,索引号必须在0和size之间");
        }

        //注意数组的索引范围为0-length-1;
        //开始位置: size也就是最后一个元素(索引号size-1),目标位置:index(这个元素也是要挪移的),然后新的元素将其进行覆盖
        for(int i =size-1;i>=index;i--){
            data[i+1]=data[i];
        }
        //将新的元素添加到index位置处
        data[index]=e;
        //size每次都需要+1
        size++;
    }



    /***
     * 打印数组信息及遍历元素
     *
     *  @return 数组信息和元素遍历结果
     * */
    @Override
    public String toString() {
        StringBuilder res =new StringBuilder();
        res.append(String.format("ArrayElement: 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();  //这里必须使用StringBuilder的toString方法
    }




    /**
     * 根据index来获取对应位置的元素
     *
     * @param index 索引号
     * **/
    public E get(int index){
        if(index<0 || index>size){
            throw new IllegalArgumentException("对不起,索引号必须在0和size之间");
        }
        return data[index];
    }



    /**
     * 根据index来修改对应位置的元素
     *
     * @param index 索引号,e  元素
     * **/
    public E set(int index,E e){
        if(index<0 || index>size){
            throw new IllegalArgumentException("对不起,索引号必须在0和size之间");
        }
        return data[index]=e;
    }


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



    /***
     *
     *查找元素e在数组中的位置(索引号),一般找不到该元素就返回-1
     *
     * @param  e  元素
     * */
    public int find(E e){
        for(int i =0;i<size;i++){
            if(data[i].equals(e)){
                return i;
            }
        }
        return -1;
    }


//size既表示数组中元素的个数,也表示数组中第一个为空的元素的位置,用户只对它进行操作的数组元素具有可视化操作,其他的不可视元素对用户没有任何影响
    //就像一个数组容量20,长度却是10,其余10个默认都是0,但是对用户是没有影响的,因为你不使用它,要理解这里面的挪位(其实就是覆盖思想)


    /**
     * 删除第Index位置的元素,并返回删除的元素
     *
     * @param index 待删除元素的索引号
     */
    public E remove(int index){
        if(index<0 || index>=size){
            throw new IllegalArgumentException("对不起,索引号必须在0和size之间");
        }

        //注意数组的索引范围为0-length-1;
        //开始位置: index后面一个元素(索引号index+1),目标位置:index(这个元素也是要挪移的),然后新的元素将其进行覆盖
        for(int i =index+1;i< size;i++){
            data[i-1]=data[i];   //记住这里不能使用data[i+1]=data[i];
        }
        //size每次都需要-1
        size--;
        data[size] =null;  //这里就是说快速将后面的元素进行垃圾回收处理
        //将删除的元素进行返回
        return data[index];
    }



    /**
     * 删除数组开头的元素,并返回删除的元素
     *
     * @param index 待删除元素的索引号
     */
    public E removeFirst(){
        return remove(0);
    }




    /**
     * 删除数组末尾的元素,并返回删除的元素
     *
     * @param index  待删除元素的索引号
     */
    public E removeLast(){
        return remove(size-1);
    }



    /**
     * 删除数组中指定的元素
     *
     * @param  e 待删除的元素
     */
    public void removeElement(E e){
        int index =find(e);
        //判断元素是否存在,存在的话
        if(index != -1){
            remove(index);
        }
    }
}

后面新建一个ArrayElementTest.java文件,这个也是复制原来的ArrayTest.java里面的代码,注意现在使用了泛型,因此应该在调用的时候声明泛型表示的具体类型,这里使用Integer:ArrayElement<Integer> ArrayElement=new ArrayElement<>(20);,这里附上完整的ArrayElementTest.java文件里面的代码:

package com.suanfa.test.Array;


public class ArrayElementTest {
    public static void main(String [] args){
        //从java 1.7开始后面<>就可以不写前面的类型了
        ArrayElement<Integer> ArrayElement=new ArrayElement<>(20);
        for(int i =0;i<10;i++){
            ArrayElement.addList(i);
        }
        System.out.println(ArrayElement);

        System.out.println("**************************************************");

        ArrayElement.add(2,100);
        System.out.println(ArrayElement);

        System.out.println("**************************************************");
        ArrayElement.addFirst(-1);
        System.out.println(ArrayElement);

        //[-1,0,1,100,2,3,4,5,6,7,8,9]
        System.out.println("**************************************************");

        int index =ArrayElement.find(4);
        System.out.println(index);

        System.out.println("**************************************************");

        ArrayElement.remove(3);
        System.out.println(ArrayElement);
        System.out.println("**************************************************");

        ArrayElement.removeFirst();
        System.out.println(ArrayElement);

        System.out.println("**************************************************");

        ArrayElement.removeLast();
        System.out.println(ArrayElement);

        System.out.println("**************************************************");

        ArrayElement.removeElement(4);
        System.out.println(ArrayElement);

        System.out.println("**************************************************");

    }   
    }


//运行结果
ArrayElement: size = 10, capacity = 20
[0,1,2,3,4,5,6,7,8,9]
**************************************************
ArrayElement: size = 11, capacity = 20
[0,1,100,2,3,4,5,6,7,8,9]
**************************************************
ArrayElement: size = 12, capacity = 20
[-1,0,1,100,2,3,4,5,6,7,8,9]
**************************************************
6
**************************************************
ArrayElement: size = 11, capacity = 20
[-1,0,1,2,3,4,5,6,7,8,9]
**************************************************
ArrayElement: size = 10, capacity = 20
[0,1,2,3,4,5,6,7,8,9]
**************************************************
ArrayElement: size = 9, capacity = 20
[0,1,2,3,4,5,6,7,8]
**************************************************
ArrayElement: size = 8, capacity = 20
[0,1,2,3,5,6,7,8]
**************************************************

你很好奇这里为啥只需要声明Integer,而不需要像前面那样修改e的类型,那是因为Integer可以通过自动拆箱的方式转变为int,同样int可以通过自动装箱的方式转变为Integer类型。

为了更好的理解这个,我们可以先建一个Student.java文件,里面写入代码:

package com.suanfa.test.Array;

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){
        ArrayElement<Student> studentArrayElement =new ArrayElement<>();
        studentArrayElement.addList(new Student("小明",100));
        studentArrayElement.addList(new Student("小红",99));
        studentArrayElement.addList(new Student("小白",90));
        studentArrayElement.addList(new Student("小明",100));
        
        System.out.println(studentArrayElement);
    }
}

//运行结果
ArrayElement: size = 4, capacity = 10
[Student(name:小明, score:100),Student(name:小红, score:99),Student(name:小白, score:90),Student(name:小明, score:100)]

你知道的,数组中是可以存放重复的元素的,如果你想让它变成和set一样(其实所谓的set不允许存放重复元素,它是指不能存放String类型,因为它重写了针对String元素不重复的hashCode()和equals()方法)不允许存放重复元素,那你是不是只需要自己重写Object类的hashCode()和equals()方法就能实现这个类似的功能呢?**答案是不可以!!!**因为set的底层实现是map,说白了是根据key的值来进行判断是否重复的,因此只有底层实现了map才能实现这个功能。

动态数组

我们假设现在有这么一个场景,一个名为data的数组,里面size为4,容量为4,也就是说该数组已经满了,按照我们之前的逻辑,现在往里面添加元素是不可能的事了。那有没有方法,可以对其进行扩容呢?其实所谓的扩容就是新建另一个容量比它大的数组newData,然后把data数组的元素全部遍历复制过去,最后修改原数组data的指针指向就完成了“扩容”的目的。那你可能会说那原来的数组还存在啊,是存在的,但是因为我们这个数组是定义在类中,当没有对象指向它的时候就会被java的垃圾回收机制给处理掉。同样对于删除元素的时候,你可以使用类似的“缩容”方法来达到目的。

数组扩容

打开ArrayElement.java文件,修改在第Index的位置,添加e元素的代码:

    /**
     * 在第Index的位置,添加e元素
     *
     * @param e 添加的元素,index 待添加元素的索引号
     */
    public void add(int index, E e){
        if(index<0 || index>size){
            throw new IllegalArgumentException("对不起,索引号必须在0和size之间");
        }

        //不使用扩容的方法
//        if(size ==data.length){
//            throw new IllegalArgumentException("对不起,数组容量已经满了,添加元素操作失败!");
//        }

        //使用扩容的方法
        if(size ==data.length){
            resize(2* data.length);
        }
        
        //注意数组的索引范围为0-length-1;
        //开始位置: size也就是最后一个元素(索引号size-1),目标位置:index(这个元素也是要挪移的),然后新的元素将其进行覆盖
        for(int i =size-1;i>=index;i--){
            data[i+1]=data[i];
        }
        //将新的元素添加到index位置处
        data[index]=e;
        //size每次都需要+1
        size++;
    }

同时新增扩容方法的代码:

    /***
     * 对数组进行动态扩容
     *
     *  @return  扩容以后的新的数组
     * */
    private void resize(int capacity){
        E[] newData = (E[]) new Object[capacity];  //创建一个新的数组
        for(int i =0;i<size;i++){  //遍历原来的数组
            newData[i] =data[i];   //原来的数组元素赋值给现在的数组
        }
        data =newData;    //原来数组的指针指向现在新的数组
    }

然后打开ArrayElementTest.java文件,修改里面的代码如下:

        //从java 1.7开始后面<>就可以不写前面的类型了
        ArrayElement<Integer> ArrayElement=new ArrayElement<>();  //默认数组容量为10
        for(int i =0;i<10;i++){
            ArrayElement.addList(i);
        }
        System.out.println(ArrayElement);

        System.out.println("**************************************************");

        ArrayElement.add(2,100);  //已经超出原来数组的容量,自动扩容
        System.out.println(ArrayElement);

        System.out.println("**************************************************");
        ArrayElement.addFirst(-1);
        System.out.println(ArrayElement);


//运行结果

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

数组缩容

缩容和扩容是相反的操作,因此修改删除元素的代码,确定当数组size小于seze/2时,就进行该操作,缩小一半的容量。

打开ArrayElement.java文件,修改删除第Index位置的元素,并返回删除的元素代码:

    /**
     * 删除第Index位置的元素,并返回删除的元素
     *
     * @param index 待删除元素的索引号
     */
    public E remove(int index){
        if(index<0 || index>=size){
            throw new IllegalArgumentException("对不起,索引号必须在0和size之间");
        }

        //注意数组的索引范围为0-length-1;
        //开始位置: index后面一个元素(索引号index+1),目标位置:index(这个元素也是要挪移的),然后新的元素将其进行覆盖
        for(int i =index+1;i< size;i++){
            data[i-1]=data[i];   //记住这里不能使用data[i+1]=data[i];
        }
        //size每次都需要-1
        size--;
        data[size] =null;  //这里就是说快速将后面的元素进行垃圾回收处理
        
        
        //判断是否需要进行缩容,是的话缩容一半
        if(size ==data.length/2){
            resize(data.length/2);
        }
        
        //将删除的元素进行返回
        return data[index];
    }

后面缩容的方法和扩容的方法一样,直接调用即可。

然后打开ArrayElementTest.java文件,直接调用原来的代码:

package com.suanfa.test.Array;


public class ArrayElementTest {
    public static void main(String [] args){
        //从java 1.7开始后面<>就可以不写前面的类型了
        ArrayElement<Integer> ArrayElement=new ArrayElement<>();
        for(int i =0;i<10;i++){
            ArrayElement.addList(i);
        }
        System.out.println(ArrayElement);

        System.out.println("**************************************************");

        ArrayElement.add(2,100);
        System.out.println(ArrayElement);

        System.out.println("**************************************************");
        ArrayElement.addFirst(-1);
        System.out.println(ArrayElement);

//        //[-1,0,1,100,2,3,4,5,6,7,8,9]
//        System.out.println("**************************************************");
//
//        int index =ArrayElement.find(4);
//        System.out.println(index);
//
//        System.out.println("**************************************************");
//
        ArrayElement.remove(3);
        System.out.println(ArrayElement);
        System.out.println("**************************************************");

        ArrayElement.removeFirst();
        System.out.println(ArrayElement);

        System.out.println("**************************************************");

        ArrayElement.removeLast();
        System.out.println(ArrayElement);

        System.out.println("**************************************************");

        ArrayElement.removeElement(4);
        System.out.println(ArrayElement);

        System.out.println("**************************************************");
    }  
    
    }


//运行结果

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

看到没有数组先进行了扩容,然后进行了缩容操作,就可以很好地实现数组的动态功能。

简单的复杂度分析

你可能之前听过O(1), O(n) , O(lgn) , O(nlogn) , O(n^2);等,这些就是大O表示法表示的算法时间复杂度。大O描述的是算法的运行时间和输入数据之间的关系。

我们来看一个非常简单的例子,就是对一个nums数组进行求和:

public static int sum(int[] nums){
    int sum = 0;
    for(int num: nums) sum += num;
    return sum;
}

我们就说上面的算法其时间复杂度是O(n),为什么是这样呢?这里的n是指数组nums中含有n个元素,就是说这个算法的时间与n的个数成线性关系的(n是一次方)。n呈线性关系,线性关系表现在一次方。

  • 那你就有疑问,为什么要用大O,叫做O(n)?

因为我们忽略了常数,实际时间 T=c1*n + c2,大O是一个大概的数。

假设有3个算法,一个是T1=2n+2,另一个是T2=2000n+10000,按照刚才的表述它们的时间复杂度是相同的,都是O(n)。因为无论c1,c2是多少,只要符合线性关系T=c1*n + c2都是O(n)的算法。

第三个算法T3=1nn+1,它的时间复杂度是O(n*2),那么我们就会说第三个算法的性能不如前面两个。

那你肯定又要说了,假设n=10,此时T1=22,T2=30000,T3=101,那么性能最好的不应该是T1么,然后就是T3,怎么也轮不到T2啊。是的,你这样分析很对,但是我们这里所说的时间复杂度其实是指渐进时间复杂度。你肯定学过高等数学,里面有一个同阶无穷小的概念,和这个渐进时间复杂度的概念很相似。

渐进时间复杂度

渐进时间复杂度是指对于一个算法来说,我们常常需要计算其复杂度来决定我们是否选择使用该算法。它的定义是对于一个算法,假设其问题的输入大小为n,那么我们可以用 O(n) 来表示其算法复杂度。那么,渐进时间复杂度就是当n趋于无穷大的时候,O(n) 得到的极限值。

你可以理解为:我们通过计算得出一个算法的运行时间 T(n), 与T(n)同数量级的即幂次最高的O(F(n))即为这个算法的时间复杂度。例如:某算法的运行时间T(n) = n+10与n是同阶的(同数量级的),所以称T(n)=O(n)为该算法的时间复杂度。

算法的渐进分析就是要估计:n逐步增大时资源开销T(n)的增长趋势。

如果你还不了解的话,可以使用这个公式: 当n趋于无穷的时候,T(n)/O(n)=k(常数),那么O(n)就是其渐进时间复杂度。

常见排序算法的时间复杂度一览表:

关于算法,我后面会专门有一套笔记去介绍它,这里你不用担心。

既然理解了这个概念,我们再来看一下这个算法T(n)=2*n*n+300*n+10,这里需要注意让含有高阶幂次项的时候,低阶幂次项可以忽略,而各个幂次项前面的常数以及后面的常数都是可以忽略的,因此这个算法的渐进时间复杂度用大O表示的也是O(n*2)。

动态数组的时间复杂度

我们现在来分析前面我们自己创建的动态数组的时间复杂度,体验一下。

添加操作

addLast(e) // O(1) 末尾添加,直接赋值,仅此而已。

O(1)就意味着它与我们的数组规模无关,不论数组n有多大,addLast都能在常数时间内完成,此时不考虑数组size的变化。

addFirst(e) // O(n) 首位添加,所有元素往后挪位

O(n)就意味着它与我们的数组规模有关,n越大,所需时间也就越长。n个元素,就要后挪n-1次。

add(index, e)  //这个因为不知道index的位置,因此不确定,是一个概率问题。

尽管add(index, e)与index的取值有关,但是我们可以这样分析:在数轴上取出任何一个在[0,n]之间的一个数的概率都是相等的为1/n,然后根据概率论知识,就能求出时间期望n/2。或者平均的来看就是O(n/2),也就是O(n)。(前面的常数1/2可以去掉)

(就是最后一个,这里a=0,b=n,因此期望E(x)=n/2)

因此添加操作综合来看,整体都是O(n)级别的。为什么呢?这里的addLast(e)它就是O(1)啊,是的没错,如果你每次都调用这个这个算法的复杂度确实是O(1)。但是那是在非常幸运的条件下得到的结果,而我们大O表示法往往考虑的则是最糟糕的情况。

别忘了如果数组容量已满的情况下,那么就需要扩容,其时间复杂度就是O(n)

resize // O(n),触发扩容,一次添加n个元素

删除操作

removeLast()  // O(1) 末尾删除,仅此而已。
removeFirst()   // O(n) 首位删除,所有元素往前挪位
remove(index, e)  //和add(index, e)一样,都是O(n/2)=O(n)

因此删除操作综合来看,整体也都是O(n)级别的。别忘了如果数组容量小于1/2的情况下,那么就需要缩容,其时间复杂度也是O(n)。

修改操作

set(int index,E e)  // O(1),无论多少,根据索引就能立即找到进行替换,数组的优势

数组的优势就是支持随机访问,只要知道索引号,就能立即找到该元素。

查询操作

get(index)   // O(1),因为是根据index来获取对应位置的元素
contains(e)    // O(n),因为首先需要遍历数组,然后才进行比较
find(e)    // O(n,)根据查找元素e在数组中的位置(索引号),一般找不到该元素就返回-1

因此这个需要看实际情况,因为不同的场景下的O(n)就是不一样,这不是概率问题。

总结一下动态数组

增加和删除都是O(n)级别操作;改:已知索引O(1)级别操作,未知索引O(n)级别操作;查:已知索引O(1)级别操作,未知索引O(n)级别操作。

但是我们对于增加中,假如是末尾操作呢?我们说它依旧是O(n),而不是O(1),是因为里面涉及到了resize扩容,所以我们认为它是O(n)而不是O(1)。那看到这里这个resize好像是一个性能很差的操作,到底是不是呢?其实不是,因为对于resize的分析,我们依旧使用最差情况来进行判断这是不合理的,应当使用均摊复杂度进行分析。

均摊复杂度

关于均摊复杂度,其实在很多算法书中都不会进行介绍,但是在实际工程中,这样的一个思想是蛮有意义的:就是一个相对比较耗时的操作,如果我们能保证他不会每次都被触发的话,那么这个相对比较耗时的操作它相应的时间是可以分摊到其它的操作中来的。

我们现在以增加操作为例,进行均摊复杂度的分析,删除的操作类似。

前面说过增加不是有三个操作么:

addLast(e) // O(1) 末尾添加,直接赋值,仅此而已。
addFirst(e) // O(n) 首位添加,所有元素往后挪位
add(index, e)  //O(n)

现在我仅仅只使用addLast(e)这个算法,那么它的渐进时间复杂度就是O(1),在涉及到了resize操作(扩容)的情况下,渐进时间复杂度就变成了O(n)。所以综合来看就是O(n)级别的操作。其实这里我们忽略了这么一个问题,就是我们不可能每次都进行resize操作。我们设置的数组初始容量为10,也就是说必须先装满10个元素以后,这个数组才会进行扩容操作变成20,然后又需要装满后面10个元素才能进行扩容。

resize的时间复杂度分析

我们前面知道resize的时间的复杂度是O(n),假设现在当前数组的capacity(容量)为10,并且每一次添加操作都是使用addLast()。这样前面10个的复杂度都是O(1),在增加第11个元素的时候,触发resize,然后先将前面10个元素进行复制,接着添加第11个元素。11次addLast()操作,10次复制元素操作,一次resize,一共进行了21次基本操作。

21/11大概等于2,平均下来相当于每次addLast操作,进行2次基本操作。

推广一下,假设capacity为n, 第n+1次addLast()操作,触发resize,总共进行2n+1次基本操作,平均每次addLast操作,进行2次基本操作。

你看到了,平均每次addLast操作,进行2次基本操作,这样均摊计算时间复杂度是O(1)的。在这个例子中,均摊计算是比考虑最槽糕的情况是有意义的,因为只有最好的和最坏的出现的概率一样,我们考虑最坏的情况才是有意义的。这里明显概率不一样。所以addLast()的均摊复杂度是O(1),自然removeLast的均摊时间复杂度也是O(1)。

复杂度的震荡

前面我们分开看addLast()和removeLast(),我们发现它们的均摊时间复杂度都是O(1)。但是当我们同时看addLast()和removeLast()操作的时候就会出现一个问题了 !

假设我们现在有一个数组,它的容量为n,并且已经装满了元素,那么现在我们再调用一下addLast操作,显然在添加一个新的元素的时候需要resize扩容操作(扩容会耗费O(n)的时间),之后我们立即进行removeLast操作(根据我们之前的逻辑,在上一个操作里通过扩容,容量变为了2n,在我们删除1个元素之后,元素又变为了n = 2n/2。因此根据我们代码中的逻辑,又会触发缩容操作,同样耗费了O(n)的时间)

继续使上面的addLast、removeLast操作相继进行,你可能就有疑问了,前面我们均摊是进行了n个元素以后才会进行resize操作,这里似乎没有,仅仅只增加或者删除了一个元素就触发了resize操作,这就是复杂度的震荡。(同时看addLast和removeLast的时候,每一次都会耗费O(n)的复杂度)

为什么会出现这种情况,原因在于我们进行removeLast操作的时候,resize过于着急,就把容量缩了(Eager)。

防止复杂度的震荡

为了解决上面的那种问题,我们可以使用 Lazy懒惰的方法。记住添加元素时,容量不足的情况下,只能通过扩容来实现,这是没有疑问的。但是删除元素的时候,我们就不一定是当容量小于1/2的时候就开始缩容了,我们完全可以等一等,等到只需要1/4的时候再进行缩容1/2(此时剩余1/2,但是只使用了1/4),这样就能解决震荡问题。

也就是capacity1/4,我们就缩capacity1/2,剩余capacity1/2(其中占用capacity1/4,未使用capacity==1/4)。

这样懒一下,就使得算法性能变得更好,后面在介绍线段树的时候还有更懒的例子。但是并不是说,越懒代码越少,逻辑越简单,但是实际上并不是这样,相反则是更难。

现在用代码实现刚才的分析,打开ArrayElement.java文件,修改删除第Index位置的元素,并返回删除的元素代码,将原来的这段代码

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

修改为:

//判断是否需要进行缩容,是的话缩容一半
//设置缩容条件为1/4,而且数组长度不能为0
if(size ==data.length/4 && data.length/2 !=0){
   resize(data.length/2);
}

注意一下整数除法,当length为1时等于0,而我们是无法new一个容量为0的数组,因此需要过滤掉。

这样关于数组的介绍就到此为止了,不知你发现没有,我们研究一种数据结构的时候,都是先研究它如何存取数据。在存取的基础上,然后再考虑如何进行增删改查操作,最后就是时间复杂度的分析,后面我们也是采取这种模式来研究其余的数据结构。

C语言书籍推荐从入门到进阶再到封神全套(2021年整理) 一、C语言书籍推荐入门,初学,编程基础系列 1、《C语言程序设计:现代方法》(第2版) 时至今日, C语言仍然是计算机领域的通用语言之一,但今天的 C语言已经和最初的时候大不相同了。本书最主要的一个目的就是通过一种“现代方法”来介绍 C语言,书中强调标准 C,强调软件工程,不再强调“手工优化”。这一版中紧密结合了 C99标准,并与 C89标准进行对照,补充了 C99中的最新特性。本书分为 C语言的基础特性、 C语言的高级特性、 C语言标准库和参考资料 4个部分。每章末尾都有一个“问与答”小节给出一系列与 阅读详情

相关推荐

怎样学习数据结构? 伯克利神课CS61B 总结感悟,学习指南和避坑建议

目录 【概述】 【课程传送门】 【课程评价】 【课程版本选择】 【课程教材选择】 【课程难度与作业量】 【课程简介】 1)了解数据结构类型 2)根据场景应用数据结构 3)学习了Java这门语言 【如果时光倒流,我再次上这门课时我会注意什么?】 1.读英文原版教材。 2.注重课本,注重一手资料。 3.写作业,要多做题目。 4.坚持。 文章6900字,阅读时间约7分钟 【概述】 学习编程有两门基础课程,即数据结构和算法。数据结构中网络公开课中评价一直居高不下的就是U...

lindaicoding的博客 1万+

魔方阵:3阶、4阶、8阶算法与实现

魔方阵:一个N阶魔方阵即N行N列的数组,其每一行的和==每一列的和==对角线的和。 例如一个3阶魔方阵: 8  1  6 3  5  7 4  9  2 一:3阶魔方阵 这里我们用两种方法实现: 方法一:遍历三阶数组的所有条件,将符合条件的(行的和==列的和==对角线和)挑出来。这种方法可以打印出多种情况。 方法二: 1.将1放在第一行的中间 2.后面的数字放在当前数字的的上一行后一列  ...

zDavid_2018的博客 9484

C语言之三维数组

前言: 之前学习C语言的时候仅仅是掌握了二维数组,但是并没有对三维数组进行研究,今天的代码提前搞完了,摸摸鱼,对去年研究的三维数组的相关知识发布一下,期待能够帮助到有缘人!实际上,当你阅读此篇文章时,我假设你已经对C语言的二维数组非常熟悉了,并且非常熟悉数组与指针之间的关系,如果没有达到此要求,那直接翻篇,不要看,等待基础掌握扎实后再来学习此篇文章! 一,看图说话,一眼看三维数组 二,首先看下三维数组的遍历 #include "stdio.h" void main(){ //三维数组 .

十三阿哥的博客 5万+

C语言初阶——3.数组

C语言初阶——3.数组

Sunrise的博客 493

3阶矩阵转置

编写并测试3*3矩阵转置函数,使用数组保存3*3矩阵,在使用循环语句实现矩阵元素的行列对调,注意在循环语句中究竟需要对哪些元素进行操作。

宇树机器狗go2—slam建图(1)点云格式

上一篇番外文章教大家如何在宇树机器狗go2的gazebo仿真环境中实现简单的导航运动,本期文章会教大家如何让宇树的机器狗go2在仿真环境中进行slam建图时经常会遇到的一些点云格式,在后续的slam建图和slam算法解析的时候会经常与这些点云信息打交道。宇树机器狗go2仿真会作为一个长期项目来进行不定期更新,仿真不只限于在gazebo上实现,后面还会在isaac sim等仿真平台上教大家如何搭建,其目的还是帮助各位都能以较低的成本加入到宇树机器狗go2的学习中。

古月居 3273

初识多维数组—三维数组

三维数组,是指维数为三的数组结构。三维数组是最常见的多维数组,由于其可以用来描述三维空间中的位置或状态而被广泛使用。简单说,一维数组就是坐标轴上的x轴,只有一个方向。二维数组就是x轴+y轴,组成了一个平面。三维数组就是x+y+z的坐标系,可以用来描述空间。...

m0_72727628的博客 1万+

手把手教你用TBtools+MCScanX完成玉米基因家族共线性分析(附完整代码)

本文提供了一套完整的玉米基因家族共线性分析实战流程,详细讲解了如何从数据准备、格式处理开始,利用DIAMOND进行快速比对,再结合MCScanX进行核心共线性区块鉴定,最后通过TBtools实现从全基因组圈图到特定基因家族微共线性图的可视化。教程包含可直接运行的代码与常见问题解决方案,帮助研究者高效完成从命令行计算到图形化展示的全过程。

tech5的博客 591

JavaSE 学习第三阶段(数组)

JavaSE 学习第三阶段(数组) 1. 数组的定义 数组是相同类型的有序集合。 数组描述的是相同类型的若干个数据,按照一定的先后次序排序组合而成。 其中,每一个数据称作一个数组元素,每个数组元素可以通过一个下标来访问他们。 2. 数组的声明和定义 package com.mango.method; public class ArrayDemo01 { // 变量的类型 变量的名字 = 变量的值 // 数组类型 public static void main(String

weixin_44694641的博客 190

【实战指南】Green Hills MULTI-IDE 从零安装到工程创建全流程

本文详细介绍了Green Hills MULTI-IDE从零安装到工程创建的全流程,包括系统要求检查、安装步骤、工程配置及调试技巧。特别适合嵌入式开发新手快速上手这款专业IDE工具,提升开发效率。

weixin_28740299的博客 478

ST-CNN实战:用Python实现交通流量预测(附完整代码)

本文详细介绍了如何利用时空卷积神经网络(ST-CNN)进行交通流量预测的实战过程。通过Python和PyTorch,从数据预处理、构建邻接矩阵,到ST-CNN核心模块的代码实现与模型训练,提供了完整的解决方案,帮助开发者掌握处理时空序列数据的核心算法。

rust6ferris的博客 928

ACM修炼指南(bestsort个人版 -----持续更新ing)

从接触ACM到现在学过的所有东西都在这里面了,虽然我还是noob的不行,但是还是希望能籍此给才接触ACM的新人们一点参考,希望看了这篇后不像我才踏入ACM之初那么迷茫。并且让自己有一个清晰的学习记录

bestsort 3261

从零开始之stm32之CAN通信

CAN通信极简教程(小白友好版),从小白的视角了解并实现简单的STM32F103的CAN通信,直接上手。

qq_30993593的博客 1309

「算法与数据结构」从入门到进阶整理推荐书单

一.入门系列 这些书籍通过图片、打比方等通俗易懂的方法来讲述,让你能达到懂一些基础算法,线性表,堆栈,队列,树,图,DP算法,背包问题等,不要求会实现,但是看过以下这些书对于之后实现算法打下坚实的思维基础。 很适合在闲暇之余拿出来阅读一番。 1.1 《啊哈!算法》 阅读链接:https://book.douban.com/subject/25894685/ 这不过是一本有趣的算法书而已。和别的算...

weixin_33704591的博客 2223

用示波器抓取同步Buck自举电容波形:从充电回路到SW节点振铃的完整分析

本文通过实测TI TPS54561和MPS MPQ4572同步Buck芯片,详细解析了自举电容在充电、维持和放电三个工作阶段的波形特征。重点揭示了电感泄放回路与充电回路重叠时,在SW节点引发的振铃现象及其产生机理,并提供了串联阻尼电阻等有效的工程抑制方法。文章结合示波器实测,为硬件工程师提供了从波形观测到问题调试的完整实践指南。

weixin_29276847的博客 374

TI DSP的 FFT与IFFT库函数的使用测试

/** * Copyright (C) 2013 Guangzhou Tronlong Electronic Technology Co., Ltd. - www.tronlong.com * * @file dsplib_fft.c * * @brief Example application main file. * This application will run FFT performance test. * * @author Tronlong <support@tron.

ToneChip的专栏,一个传统的嵌入式码农,裸机编程 2678

python数据结构推荐书-「算法与数据结构」从入门到进阶吐血整理推荐书单

推荐一下「算法与数据结构」从入门到进阶的书单。一.入门系列这些书籍通过图片、打比方等通俗易懂的方法来讲述,让你能达到懂一些基础算法,线性表,堆栈,队列,树,图,DP算法,背包问题等,不要求会实现,但是看过以下这些书对于之后实现算法打下坚实的思维基础。很适合在闲暇之余拿出来阅读一番。1.1 《啊哈!算法》image这不过是一本有趣的算法书而已。和别的算法书比较,如果硬要说它有什么特点的话,那就是你能...

weixin_37988176的博客 1412

flink新版本无bat启动文件的解决办法

flink

6 4933

数据结构 python的书推荐-学习数据结构,有哪些值得推荐的好书?

刚好在学算法,正好回答一波:)推荐一下「算法与数据结构」从入门到进阶的书单。一.入门系列这些书籍通过图片、打比方等通俗易懂的方法来讲述,让你能达到懂一些基础算法,线性表,堆栈,队列,树,图,DP算法,背包问题等,不要求会实现,但是看过以下这些书对于之后实现算法打下坚实的思维基础。很适合在闲暇之余拿出来阅读一番。1.1 《啊哈!算法》这不过是一本有趣的算法书而已。和别的算法书比较,如果硬要说它有什么...

weixin_37988176的博客 1099

怀旧版电脑GBC模拟器

再现原版GBC掌机的界面,电脑运行连游戏界面都一样大小,很怀旧,很真实,(英文版)强烈推荐,很小的文件,很难才找到的。

visio安装包

Microsoft Office Visio 是一款专业的制图软件。利用经过更新的形状、协作工具和数据链接图表,创建专业级图表以简化复杂的信息。

「算法与数据结构」从入门到进阶吐血整理推荐书单

一.入门系列 这些书籍通过图片、打比方等通俗易懂的方法来讲述,让你能达到懂一些基础算法,线性表,堆栈,队列,树,图,DP算法,背包问题等,不要求会实现,但是看过以下这些书对于之后实现算法打下坚实的思维基础。 很适合在闲暇之余拿出来阅读一番。 1.1 《啊哈!算法》 阅读链接:https://book.douban.com/s...

墨鱼菜鸡 1782

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

数据结构与算法这么难,为什么我们还要学习?

提到数据结构与算法,就一定会伴随着诸多所谓的坚持和抱怨。同时,还有两个词总是出现,一个是内功,是对知识的定位,一个是吃透,是对自己的期待。可是,我们是不是被这两个词束缚太久了,以至于出现了很多的问题

Edison's 小宇宙 4万+

SAP B1中文培訓教材_SDK

公司内部培训资料,对开发工具 ,DIAPI, UIAPI,Add-ON有详细解说

《Java小子怒闯数据结构九重天》第一重天——数组

本专栏文章主要用于帮助Java使用者快速上手数据结构,刷算法题!

apple_51673523的博客 4872

crc8校验算法 c语言_算法与数据结构从入门到进阶整理推荐书单

转载自:五分钟算法小吴花了几天时间整理了一下学习「数据结构与算法」可以参考的书籍,希望能在学习的道路上帮到你,文末提供收集的PDF版。一.入门系列 这些书籍通过图片、打比方等通俗易懂的方法来讲述,让你能达到懂一些基础算法,线性表,堆栈,队列,树,图,DP算法,背包问题等,不要求会实现,但是看过以下这些书对于之后实现算法打下坚实的思维基础。很适合在闲暇之余拿出来阅读一番。1.1 《啊哈!算...

weixin_39979245的博客 281

【算法基础三】算法如何入门?零基础入门算法应该学些什么?

算法如何入门以及零基础入门算法应该学些什么?​ ✅作者简介:大家好,我是新小白2022,让我们一起学习,共同进步吧!🏆 📃个人主页:新小白2022的优快云博客 🔥系列专栏:算法基础入门 💖如果觉得博主的文章还不错的话,请点赞👍+收藏⭐️+留言📝支持一下博主哦🤞 ​...

新小白 1万+
上一篇: 数据结构入门学习(二):数组基础介绍
下一篇: SSM搭建二手市场交易平台(一):开发流程与环境搭建
啃饼思录
博客等级 码龄9年 11粉丝 18原创
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值