【算法与数据结构体系课笔记】动态数组的时间复杂度

数组最大的优点:快速查询。比如 scores[2]

数组最好应用于“索引有意义”的情况。比如学生学号。

但并非所有有语意的索引都适用于数组。比如身份证号。

 

(一)自定义数组

size表示现在数组内的元素数量,size <= capacity;

capacity表示数组容量,即最大可容纳的元素数量,capacity=数组.length;

public class Array<E> {
    private E[] data;
    private int size;//当前数组内元素数量

    public Array(int capacity){
        data = (E[])new Object[capacity];
        size = 0;
    }

    public Array(){
        data = (E[])new Object[10];
        size = 0;
    }

    public void add(int index,E e){
        if (index < 0 || index > size)
            throw new RuntimeException("Add failed! Index is illegal.");
        if (size == data.length)
            throw new RuntimeException("Add failed! Array is full.");

        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);
    }

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

    public E remove(int index){
        if (index < 0 || index >= size)
            throw new RuntimeException("Remove failed! Index is illegal.");

        E e = data[index];
        for (int i = index + 1; i < size ;i ++)
            data[i - 1] = data[i];
        size --;
        data[size] = null;//loitering objects != memory leak,这句可写可不写

        return e;
    }

    public E removeFirst(){
        return remove(0);
    }
    
    public E removeLast(){
        return remove(size - 1);
    }

    public E get(int index){
        if ( index < 0 || index >= size)
            throw new RuntimeException("Get failed! Index is illegal.");
        return data[index];
    }

    public E getFirst(){
        return get(0);
    }

    public E getLast(){
        return get(size - 1);
    }

    public void set(int index){
        if ( index < 0 || index >= size)
            throw new RuntimeException("Set failed! Index is illegal.");
    }

    @Override
    public String toString(){
        StringBuilder res = new StringBuilder();
        res.append("[ ");
        for (int i = 0; i < data.size ; i ++){
            res.append(data[i]);
            if (i != size - 1)
                res.append(",");
        }
        res.append(" ]");
        return res.toString();
    }

    public boolean isEmpty(){
        return size == 0;
    }

    public int getCapacity(){
        return data.length;
    }

    public int getSize(){
        return size;
    }

    public boolean contains(E e){
        for (int i = 0; i < data.length ;i ++)
            if (data[i].equals(e))
                return true;
        return false;
    }

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

    private void resize(int newCapacity){
        E[] newData = (E[])new Object[newCapacity];
        for (int i = 0; i < size ; i ++)
            newData[i] = data[i];
        data = newData;
    }

}

add方法

在索引index的位置添加元素e,当数组没有空间(size==data.length)时,会抛出错误。

但不能一直这样,毕竟咱这是“动态”数组!所以,我们就考虑没有空间时为数组扩容。

扩大为原来数组容量的2倍,即 2 * data.length。

 

  • 那么可能会有疑问说为什么是2倍呢?为什么不是+5或是+1000呢?

我们可以这么想,如果当前数组容量是10000满了的话,大概率是大量添加数据的,一次只增加个位数容量的话,操作频繁很耗时;增加太多可能用不了。

如果当前容量是50满了的话,太少操作频繁耗时,一下增加1000可能太多用不了浪费空间。

所以不能把额外增加的容量设为一个定值,要根据当先数组容量进行变化。

2 * data.length就很合适,3 * data.length当然也可以,但也不要太大了。

那么add方法就可以修改为:

    public void add(int index,E e){
        if (index < 0 || index > size)
            throw new RuntimeException("Add failed! Index is illegal.");
        if (size == data.length)
            resize( 2 * data.length);

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

    private void resize(int newCapacity){//只在这个类的内部使用,用户不能调用,所以是private
        E[] newData = (E[])new Object[newCapacity];
        for (int i = 0; i < size ; i ++)
            newData[i] = data[i];
        data = newData;
    }

既然是动态数组,add要扩容,remove到一定程度肯定要缩容。这里肯定也不能减少一个定值,也是要根据数组当前的容量决定的。

前面是增大2倍,这里也就缩小到原来的二分之一吧!也就是:

public void remove(int index){

...

if(size == data.length / 2)

       resize(data.length / 2);

...

}

但这样写也是有问题的,我们接着向下看。

 

(二)时间复杂度

 

add和remove操作的是末尾元素的情况下,时间复杂度就是O(1),最坏情况下就是O(n)。

但并不是每一次增删操作都能触发resize方法。

所以对于resize来说,完全使用这种最坏情况下的分析是不合理的。

 

(三)均摊复杂度和防止复杂度的震荡

9次addLast操作,触发一次resize操作,总共进行了17(8+8+1)次操作。

实际,不可能每次addLast都会触发resize操作。

 

这意味着,这时的时间复杂度跟有多少个元素是没有关系的。

这种情况下计算均摊时间是有意义的,因为最坏情况并不会每次都出现

 

均摊复杂度(amortized time complexity):一个相对比较耗时的操作,如果我们能保证他不会每次都触发的话,那么这个比较耗时的操作是可以分摊到其他操作中的。

 

同理,removeLast操作,均摊复杂度也为O(1)。

 

  • 复杂度震荡

但是有一种特殊情况要注意,会有复杂度震荡

复杂度震荡演示动图

在一些特殊的情况下,size=capacity=n时(如上图),addLast和removeLast操作都会扩容或缩容,时间复杂度为O(n)。

 

添加元素容量不够时只能进行扩容,但删除元素时,可以稍微“懒惰”一点,不着急处理。

当size==capacity / 4时,才将capacity = data.length / 2;

所以remove方法应改为:

    public E remove(int index){
        if (index < 0 || index >= size)
            throw new RuntimeException("Remove failed! Index is illegal.");

        E e = data[index];
        for (int i = index + 1; i < size ;i ++)
            data[i - 1] = data[i];
        size --;
        data[size] = null;//这句可写可不写,都不影响的。loitering objects != leak memory

        if (size == data.length / 4 && data.length / 2 != 0)
            resize(data.length / 2);

        return e;
    }

    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个

红包金额最低5元

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

抵扣说明:

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

余额充值