支持动态扩容的数组及复杂度分析
零 引言
我们实现了自己的数组,并且将其改造成了可以支持任意类型的泛型数组。但是,这还不算完,我们的数组还不支持动态扩容。如果我们初始化时,指定容量小了,则没法扩容,反之,如果我们初始化容量偏大,后面用不到那么多,则造成空间的浪费。
一 改造add()为支持动态扩容的方法
改造前:
public void add(E e, int index) { if(size==data.length){ throw new IllegalArgumentException("add failed.array is full"); } if (index < 0 || index > size) { throw new IllegalArgumentException("add failed.index is invalid"); } for (int i = size; i>index; i--) { data[i] = data[i - 1]; } data[index] = e; size++; }
改造后:
public void add(E e,int index){ if (index<0 || index>size){ throw new IllegalArgumentException("add failed.index is invalid"); } //如果数组满了,即size==data.length,则扩容为原来的2倍大小 if(size==data.length){ resize(2*data.length); } for(int i=size;i>index;i--){ data[i]=data[i-1]; } data[index]=e; size++; } //添加新的扩容方法,将元素组的元素copy到新数组,然后原数组的引用指向新数组。 private void resize(int newCapacity){ E[] newData=(E[])new Object[newCapactiy]; for(int i=0;i<size;i++){ newData[i]=data[i]; } data=newData; }
二 改造remove()为支持动态扩容的方法
改造前:
publiv E remove(int index){ if(index<0||index>=size){ throw new IllegalArgumentException("remove failed.index is invalid"); } E temp=data[index]; for(int i=index;i+1<size;i++){ data[i]=data[i+1]; } size--; retrun temp; }
改造后:
public E remove(int index){ if(index<0||index>=size){ throw new IllegalArgumentException("remove failed.index is invalid"); } E temp=data[index]; for(int i=index;i+1<size;i++){ data[i]=data[i+1]; } size--; //如果实际元素个数是总容量的一般,且总容量的二分之一大小不等于1的时候,执行缩容,所谓原来的1/2大小 if(size==data.length/2 && data.length/2 ! = 1){ resize(data.length/2); } }
三 动态扩容的测试验证
public static void main(String[] args) { //初始化容量为1的数组 Array<Student> studentArray = new Array<Student>(1); studentArray.addLast(new Student("Jim", 10)); //数组的容量和大小都是1 System.out.println(studentArray); //继续插入元素,数组就会自动扩容,扩容为原来的2倍 studentArray.addFirst(new Student("Kate", 8)); //数组的容量和大小都是2 System.out.println(studentArray); //再次插入,出发扩容,容量是4,大小变为3 studentArray.add(new Student("Jerry", 9), 1); System.out.println(studentArray); //继续插入,容量,大小都是4 studentArray.add(new Student("Parker", 7), 2); System.out.println(studentArray); //再次执行插入,触发扩容,容量是8,大小为5; studentArray.add(new Student("Charlie", 11), 0); System.out.println(studentArray); System.out.println(studentArray.contains(new Student("Jerry", 9))); //执行删除操作,容量是8,大小为4,触发缩容操作,变为容量和大小都是4的数组 studentArray.removeElement(new Student("Jim", 10)); System.out.println(studentArray); } //Array size: 1 ,length: 1 Student{name='Jim', age=10}] Array size: 2 ,length: 2 Student{name='Kate', age=8} , Student{name='Jim', age=10}] Array size: 3 ,length: 4 Student{name='Kate', age=8} , Student{name='Jerry', age=9} , Student{name='Jim', age=10}] Array size: 4 ,length: 4 Student{name='Kate', age=8} , Student{name='Jerry', age=9} , Student{name='Parker', age=7} , Student{name='Jim', age=10}] Array size: 5 ,length: 8 Student{name='Charlie', age=11} , Student{name='Kate', age=8} , Student{name='Jerry', age=9} , Student{name='Parker', age=7} , Student{name='Jim', age=10}] true Array size: 4 ,length: 4 Student{name='Charlie', age=11} , Student{name='Kate', age=8} , Student{name='Jerry', age=9} , Student{name='Parker', age=7}]
四 复杂度分析
1 增加元素复杂度
addLast(E e), O(1)
addFirst(E e); O(n)
add(E e,int index); O(n/2),看做O(n),因为每次插入的位置index不确定,所以,每次需要移动的元素个数也不确定。从概率上讲,可以认为每次需要移动n/2个元素,进而复杂度是O(n)。
2 删除元素复杂度
removeLast() O(1)
removeFirst() O(n)
remove(int index) O(n/2),看做是O(n),处理方式跟任意位置添加元素一样。
3 查找元素复杂度
find(E e) O(n)
get(int index) O(1)
contains(E e) O(n)
如果有索引下标的查找,复杂度为O(1),没有索引下标的查找,复杂度是O(n)。
4 修改元素复杂度
modify(E e,E newValue) O(n)
set(E e,int index) O(1)
5 均摊复杂度分析
对于添加元素,如果每次都是addLast(),则复杂度是O(1),但是我们肯定不能说数组的添加元素的时间复杂度是O(1),因为这是最为理想的情况。当我们在考虑算法的时间复杂度的时候,我们一般都是考虑最坏的情况,而不会考虑最优的情形。反之,也不能因为每次都调用addFirst(),我们就说时间复杂度是O(n)。那么,添加元素时,数组的复杂度到底是O(1)还是O(n)呢?该怎么分析它的复杂度呢?
假定数组初始化容量为10,执行了10次addLast()操作,每次操作的复杂度是O(1),最后一次导致数组扩容,resize()扩容的复杂度为O(n),那么就相当于执行了20 次操作,一共执行了11次,平均每次的复杂度是O(2),记为O(1)。同样,对于removeLast()操作,导致数组缩容的情况下,其时间复杂度也是O(1)。
这是采用了均摊复杂度分析的思想。即对于第n+1次操作导致的resize(),将此次操作的成本平均的分摊给前n次操作。
采用均摊复杂度分析的情况下,addLast(),removeLast()的复杂度都是O(1)。
五 复杂度震荡
对于addLast()和removeLast(),假定数组满的话,就触发扩容2倍的操作,数组实际大小=总容量的/2的时候,就执行缩容操作。
那么,假定当前数组容量是10,size=10,执行一次addLast()则触发扩容:size=11,capacity=20;
此时,执行了removeLast(),则size=10,capacity=10,触发缩容操作,size=10,capacity=10;
接下来执行了addLast(),导致扩容;
removeLast(),导致缩容。
此时,每一次扩容操作的时间复杂度都将达到O(n),性能变得很差。
因此,如何避免这种情况出现呢?我们考虑对于缩容操作的时候,缩容的时候,当实际空间size=capacity/4的时候,我们才执行缩容操作,这样就可以避免复杂度震荡。
public E remove(int index){ if(index<0||index>=size){ throw new IllegalArgumentException("remove failed.index is invalid"); } E temp=data[index]; for(int i=index;i+1<size;i++){ data[i]=data[i+1]; } size--; //如果实际元素个数是总容量的一般,且总容量的二分之一大小不等于1的时候,执行缩容,所谓原来的1/2大小 if(size==data.length/4 && data.length/2 ! = 1){ resize(data.length/2); } }
六 小结
至此,我们自己动手实现了一个基本的数据结构,数组。涵盖了常见的方法:
带初始化容量大小的构造方法;
默认构造方法;
返回数组实际长度;
返回数据包含元素个数;
判断数组是否为空;
向数组末尾添加元素;
向数组首位添加元素;
向数组任意位置添加元素;
删除数组末尾元素;
删除数组首位元素;
删除数组任意位置元素;
判断数组是否包含某个元素;
返回数组中任意位置处元素;
返回数组中第一个元素;
返回数组中最后一个元素;
查找某个元素在数组中存放的位置;
修改某个位置处的元素;
修改数组中某个元素的值;
修改数组中所有特定元素的值。
我们还将数组改造成了支持任意类型的泛型数组。
最后,还将它改造成了支持动态扩容的数组。接下来,我们开始研究一下栈和队列这两种基本数据结构。
一条评论
Pingback: