数据结构(一)--数组

(一)自定义封装数组
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数组中有具体方法的复杂度分析。



 

猜你喜欢

转载自blog.csdn.net/jaybillions/article/details/80807402