(一)自定义封装数组
package cn.data;
public class ArrayDemo {
private int[] data;
private int size;
//此处可以用data.length来替代capacity,可以少维护一个参数
//private int capacity;
//构造函数,传入数组的容量capacity构造ArrayDemo
public ArrayDemo(int capacity){
data = new int[capacity];
size = 0;
}
//无参的构造函数,默认数组的容量capacity=10
public ArrayDemo(){
this(10);
}
//获取数组中元素个数
public int getSize(){
return size;
}
//获取数组的容量
public int getCapacity(){
return data.length;
}
//返回数组是否为空
public boolean isEmpty(){
return size == 0;
}
//向所有元素后添加一个新元素
public void addLast(int e){
//添加元素前 ,要判断数组中是否还有位置
if(size == data.length){
throw new IllegalArgumentException("AddLast failed,Array is full");
}
data[size] = e;
size++;
}
/**
* size表示数组中有多少个元素,也指向了第一个没有元素的位置
* public void addLast(int e){
* 复用下方的add方法
* add(size,e);
* }
*/
public void addFirst(int e){
add(0,e);
}
//在index的位置插入一个元素e
public void add(int index,int e){
//添加元素前 ,要判断数组中是否还有位置
if(size == data.length){
throw new IllegalArgumentException("Add failed,Array is full");
}
//保证用户传来的index是合法的
if(index<0||index>size){
throw new IllegalArgumentException("Add failed,Require index<0 and index>size");
}
//将index索引后面的元素往后挪
for(int i = size-1;i>=index;i--){
data[i+1] = data[i];
}
data[index] = e;
size++;
}
//获取index索引位置的元素
int get(int index){
if(index<0||index>=size){
throw new IllegalArgumentException("Get failed,Index is illegal");
}
return data[index];
}
//修改index索引位置的元素值
void set(int index,int e){
if(index<0||index>=size){
throw new IllegalArgumentException("Get failed,Index is illegal");
}
data[index]=e;
}
//查找数组中是否含有元素e
public boolean contains(int e){
for(int i=0;i<size;i++){
if(data[i]==e){
return true;
}
}
return false;
}
//查找数组中元素e所在的索引,如果不存在元素e,则返回-1(定义的,没有元素会返回-1)
public int find(int e){
for(int i=0;i<size;i++){
if(data[i]==e){
return i;
}
}
return -1;
}
//从数组中删除index位置的元素,返回删除的元素
public int remove(int index){
if(index<0||index>=size){
throw new IllegalArgumentException("Get failed,Index is illegal");
}
int ret = data[index];
//进行删除操作
for(int i=index+1;i<size;i++){
//data[i]=data[i+1];错误,会造成数组交表访问越界、
data[i-1]=data[i];
size--;//不要忘记了维护size
}
return ret;
}
//从数组中删除第一个元素,返回删除的元素
public int removeFirst(){
//如果数组为空,那么将无法对数组进行删除操作
//所以在删除前必须对数组进行合法性校验,由于
//调用了remove()方法,在remove方法中已经进行了安全性校验
//所以此时不需要对其进行校验
return remove(0);
}
//从数组中删除元素e,用户在调用时已经知道要删除哪个元素,故不需要返回
public void removeElement(int e){
int index = find(e);
if(index!=-1){
remove(index);
}
}
//从数组中删除最后一个元素,返回删除的元素
public int reoveLast(){
return remove(size-1);
}
//覆盖父类Object类中的toString()方法
//使用@Override进行标记,防止覆盖的方法不正确,比如tostring()方法
@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(",");
}
}
//return null;
res.append(']');
return res.toString();
}
}
(二)测试上面封装的数组的方法
package cn.data;
public class Test {
public static void main(String[] args) {
ArrayDemo arr = new ArrayDemo(20);
for(int i=0;i<10;i++){
arr.addLast(i);
}
System.out.println(arr);
arr.add(1, 100);
System.out.println(arr);
arr.addFirst(-1);
System.out.println(arr);
//虽然remove方法具有返回值,若没有用处,可以不接
arr.remove(2);
System.out.println(arr);
arr.removeElement(4);
System.out.println(arr);
}
}
结果:
Array:size = 10,capacity = 20
[0,1,2,3,4,5,6,7,8,9]
Array:size = 11,capacity = 20
[0,100,1,2,3,4,5,6,7,8,9]
Array:size = 12,capacity = 20
[-1,0,100,1,2,3,4,5,6,7,8,9] 问题:
Array:size = 7,capacity = 20 1.发现数组中有大量空间浪费,存储的空间不到数组长度的一半,是否可以进行优化?可以将数组变为动态数组
[-1,0,1,2,3,4,5] 2.该数组只适用于int型参数,不适用于其他类型,可采用泛型
Array:size = 6,capacity = 20
[-1,0,1,2,3,5]
(三)将数组改为泛型动态数组
package cn.data;
public class Array<E> {
private E[] data;
private int size;
//此处可以用data.length来替代capacity,可以少维护一个参数
//private int capacity;
//构造函数,传入数组的容量capacity构造ArrayDemo
public Array(int capacity){
data = (E[])new Object[capacity];
size = 0;
}
//无参的构造函数,默认数组的容量capacity=10
public Array(){
this(10);
}
//获取数组中元素个数
public int getSize(){
return size;
}
//获取数组的容量
public int getCapacity(){
return data.length;
}
//返回数组是否为空
public boolean isEmpty(){
return size == 0;
}
//向所有元素后添加一个新元素
public void addLast(E e){
//添加元素前 ,要判断数组中是否还有位置
if(size == data.length){
throw new IllegalArgumentException("AddLast failed,Array is full");
}
data[size] = e;
size++;
}
//addLast(e)方法的时间复杂度为O(1)也就是说这个操作消耗的时间和数据规模是没有关系的
/**
* size表示数组中有多少个元素,也指向了第一个没有元素的位置
* public void addLast(E e){
* 复用下方的add方法
* add(size,e);
* }
*/
public void addFirst(E e){
add(0,e);
}
//addLast(e)时间复杂度为O(n),因为增加时需要把数组中所有元素后移一位
//在index的位置插入一个元素e
public void add(int index,E e){
//添加元素前 ,要判断数组中是否还有位置
//if(size == data.length){
// throw new IllegalArgumentException("Add failed,Array is full");
// }
//保证用户传来的index是合法的
if(index<0||index>size){
throw new IllegalArgumentException("Add failed,Require index<0 and index>size");
}
//动态数组,扩容成原来的两倍
if(size == data.length){
resize(2*data.length);
}
//将index索引后面的元素往后挪
for(int i = size-1;i>=index;i--){
data[i+1] = data[i];
}
data[index] = e;
size++;
}
//add(index,e)时间复杂度与元素插入的位置有关,如果index=0.时间复杂度和addFirst()一致
//若 index=size,时间复杂度和addLast()一致,由于index可以取到0到size中的任意一个值,因此
//在具体的情况中,考虑每一个值的取值概率是相等的,此时需要引入一些概率论的知识,这样就可以求出期望值
//时间复杂度为O(n/2)=O(n),总体来看,添加操作是一个O(n)级别的算法,通常在考虑时间复杂度的时候,我们考虑的是最坏的情况
//在动态数组中,还有一个resize方法,其时间复杂度为O(n),综上,动态数组的添加操作为O(n)。
//获取index索引位置的元素
E get(int index){
if(index<0||index>=size){
throw new IllegalArgumentException("Get failed,Index is illegal");
}
return data[index];
}
//get方法时间复杂度为O(1)
//修改index索引位置的元素值
void set(int index,E e){
if(index<0||index>=size){
throw new IllegalArgumentException("Get failed,Index is illegal");
}
data[index]=e;
}
//set方法时间复杂度为O(1),即支持随机访问
//查找数组中是否含有元素e
public boolean contains(E e){
for(int i=0;i<size;i++){
//if(data[i]==e){
if(data[i].equals(e)){
return true;
}
}
return false;
}
//contains时间复杂度为O(n)
//查找数组中元素e所在的索引,如果不存在元素e,则返回-1(定义的,没有元素会返回-1)
public int find(E e){
for(int i=0;i<size;i++){
//if(data[i]==e){
//此时data[i]和e都是类对象,此处使用值比较合理
if(data[i].equals(e)){
return i;
}
}
return -1;
}
//find方法时间复杂度为O(n)
//从数组中删除index位置的元素,返回删除的元素
public E remove(int index){
if(index<0||index>=size){
throw new IllegalArgumentException("Get failed,Index is illegal");
}
E ret = data[index];
//进行删除操作
for(int i=index+1;i<size;i++){
//data[i]=data[i+1];错误,会造成数组角标访问越界、
data[i-1]=data[i];
}
size--;//不要忘记了维护size
//经过上述删除操作,data[size]还指着一个引用,为了将这个无用的内存进行释放(可选操作)
data[size] = null;//loitering objects(闲逛的对象)!=memory leak (内存泄漏)
//与数组增加时扩容一致,当数组缩小时也需要进行扩容,扩到原来的一半
if(size==data.length/2){(Eager策略)
//if(size==data.length/4&&data.length!=0){ //(Lazy策略)
resize(data.length/2);
}
return ret;
}
//remove方法的时间复杂度为O(n/2)=O(n),动态数组删除操作时间复杂度分析类似添加操作
//从数组中删除第一个元素,返回删除的元素
public E removeFirst(){
//如果数组为空,那么将无法对数组进行删除操作
//所以在删除前必须对数组进行合法性校验,由于
//调用了remove()方法,在remove方法中已经进行了安全性校验
//所以此时不需要对其进行校验
return remove(0);
}
//removeFirst时间复杂度为O(n)
//从数组中删除元素e,用户在调用时已经知道要删除哪个元素,故不需要返回
public void removeElement(E e){
int index = find(e);
if(index!=-1){
remove(index);
}
}
//从数组中删除最后一个元素,返回删除的元素
public E removeLast(){
return remove(size-1);
}
//removeLast()时间复杂度为O(1)
//覆盖父类Object类中的toString()方法
//使用@Override进行标记,防止覆盖的方法不正确,比如tostring()方法
@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(",");
}
}
//return null;
res.append(']');
return res.toString();
}
private void resize(int newCapacity){
E[] newData = (E[])new Object[newCapacity];
for(int i = 0;i<size;i++){
newData[i]=data[i];
}
data = newData;
System.out.println(newData);
}
//假设capacity=n,那么n+1次addLast,触发resize,总共进行2n+1次基本操作
//平均每次addLast操作,进行2次基本操作,这样均摊计算,时间复杂度为O(1).
//在这个例子里面,由于resize方法不会每次都调用,因此均摊计算比计算最坏情况有意义。
//均摊复杂度amortized time complexity
/**addLast的均摊复杂度为O(1)
* 同理,removeLast操作,均摊复杂度也为O(1)。
*
* 计算均摊复杂度时,addLast()和removeLast()操作时间复杂度都为O(1).
* 但在某些特殊的情况,它们的复杂度会发生改变
* 比如以下情景:对于一个数组,capacity=n,此时数组已经填满,当我们同时看addLast和removeLast操作时
* 即先执行addLast操作,此时由于长度不够,会进行扩容操作,addLast复杂度为O(1).再执行removeLast操作,数组长度
* 减小为二分之一,会进行缩容操作,removeLast操作的复杂度也为O(1).此时,addLast和removeLast操作在这种情况下就不再是O(1).
* 而变成了O(1),造成了复杂度震荡的现场。
*
*
* 出现问题的原因:removeLast时resize过于着急(Eager)
*
* 解决方案:Lazy
* 当size==capacity/4时,才将capacity减半。(而不是只到当前数组长度的一半就开始进行缩容操作)
* */
}
、
(四)改进后数组的测试类
(a)泛型测试--对象类型
package cn.data;
public class Student {
private String name;
private int score;
public Student(String studentName,int studentScore){
name = studentName;
score = studentScore;
}
@Override
public String toString(){
return String.format("Student(name:%s,score:%d)", name,score);
}
public static void main(String[] args){
Array<Student> arr = new Array<Student>();
arr.addLast(new Student("Alice",100));
arr.addLast(new Student("Bob",60));
arr.addLast(new Student("Charlie",88));
System.out.println(arr.toString());
}
}
测试结果:
Array:size = 3,capacity = 10
[Student(name:Alice,score:100),Student(name:Bob,score:60),Student(name:Charlie,score:88)]
(b)动态性能测试--数组扩容,缩容
package cn.data;
public class TestArray {
public static void main(String[] args) {
Array<Integer> arr = new Array<Integer>();
for(int i=0;i<10;i++){
arr.addLast(i);
}
System.out.println(arr);
arr.add(1, 100);
System.out.println(arr);
arr.addFirst(-1);
System.out.println(arr);
//虽然remove方法具有返回值,若没有用处,可以不接
arr.remove(2);
System.out.println(arr);
arr.removeElement(4);
System.out.println(arr);
arr.removeFirst();
System.out.println(arr);
}
}
测试结果:
Array:size = 10,capacity = 10
[0,1,2,3,4,5,6,7,8,9]
[Ljava.lang.Object;@4aa298b7
Array:size = 11,capacity = 20 数组长度超出10个,数组扩容为原来的2倍
[0,100,1,2,3,4,5,6,7,8,9]
Array:size = 12,capacity = 20 继续添加一个元素
[-1,0,100,1,2,3,4,5,6,7,8,9]
Array:size = 11,capacity = 20 删减一个元素
[-1,0,1,2,3,4,5,6,7,8,9]
[Ljava.lang.Object;@7d4991ad
Array:size = 10,capacity = 10 原来数组长度为20,减去一个后长度为10,为数组空间长度的一半,进行缩容操作
[-1,0,1,2,3,5,6,7,8,9]
Array:size = 9,capacity = 10 继续删减元素
[0,1,2,3,5,6,7,8,9]
(五)简单复杂度分析
简单的时间复杂度分析
O(1),O(n),O(lgn),O(nlogn),O(n^2)
大O描述的是算法的运行时间和输入数据之间的关系
public static int sum(int[] nums){
int sum = 0 ;
for(int num:nums)
sum+=num;
return sum;
}
该算法的时间复杂度为O(n),n是nums中的元素个数,算法和n呈线性关系
为什么要用大O,叫做O(n)? 忽略了常数,实际时间T=c1*n+c2
(1) T=2*n^2+2 O(n)
(2) T=2000*n+10000 O(n)
(3) T=1*n*n+0 O(n^2)
(4) T=2*n*n+300*n+10 O(n^2)
假设n=10
T(2)=30000 T(3)=100,此时T(2)>T(3),说明对于任意输入n,并不代表着
T(O(n))<T(O(n^2))
大O的表示的是渐进时间复杂度,渐进的意思是描述n趋近于无穷的情况
Array数组中有具体方法的复杂度分析。