数组
不要小瞧数组,数组有很多东西值得我们挖掘。
把数据弄成一排进行存放
在java中我们的格式是要求固定的。当然在一些语言中在一个数组中可以存储不同类型。
索引是一个很重要的概念,索引从0开始。最后一个元素是N-1
如果我们拥有索引,我们就可以直接进行访问。
数组拥有局限性,我们在开辟空间的时候必须指定长度
二次封装创建属于我们自己的类
索引可以有语义,也可以没有语义
数组最大的优点:快速查询
数组最好应用于索引有语义的情况
但并非所有有语义的索引都适用于数组
比如身份证号:11100544846515156
这个空间是在太大了
数组也可以处理索引没有语义的情况
在这一章中,我们主要处理索引没有语义的情况数组的使用
-
索引没有语义,如何表示没有元素
-
如何添加元素?如何删除元素?
-
基于java的数组,二次封装属于我们自己的数组类
数组 有capacity,容量。 数组中实际有多少元素用sieze表示
public class Array{
private int[] data;
private int size;
//构造函数,传入数组的容量capacity构造Array
public Array(int capacity){
data = new int[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;
}
}
向数组中添加元素
向数组末添加元素
添加了一个元素66,记得维护size
public void addLast(int e){
//判断是否超过数组的容量
if(size == data.length)
throw new IlleaglArgumentException("addlast failed, array is full");
data[size]=e;
size++;
//data[size++]=e
//add(size,e);//可以直接调用add方法
}
在index个位置插入一个新元素e
public void add(int index,int e){
//判断是否超过数组的容量
if(size == data.length)
throw new IlleaglArgumentException("add failed, array is full");
if(index < 0|| index > size)
throw new IlleaglArgumentException("add failed, require size>=0 && <capacity");
for(int i = size - 1; i >= index; i--)
data[i+1]=data[i];
data[index]=e;
size++;
}
//复用机制
public void addFirst(int e){
add(0,e);
}
数组查询元素和修改元素
@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(", ");
res.append(']');
return res.toString();
}
//获取index索引位置的元素
int get(int index){
if(index<0 ||index >= size)
throw new IllegalArgumentException("Get failed,Index is illegal");
return data[index];
}
//修改index索引位置的元素为e
void set(int index,int e){
if(index<0 ||index >= size)
throw new IllegalArgumentException("Get failed,Index is illegal");
data[index] = e;
}
数组中的包含、搜索和删除元素
public boolean contains(int e){
for(int i = 0; i< size;i++){
if(data[i]==e)return true;
return false;
}
}
//查找数组中元素e所在的索引,如果不存在元素e,则返回-
public int find(int e){
for(int i = 0; i< size;i++){
if(data[i]==e)return i;
return -1;
}
}
public int remove(int index){
if(index<0 ||index >= size)
throw new IllegalArgumentException("Remove failed,Index is illegal");
int ret = data[index];
for(int i =index+1;i<size;i++){
data[i-1]=data[i];
size--;
}
return ret;
}
//从数组中删除第一个元素,返回删除的元素
public int removeFirst(){
return remove(0);
}
//从数组中删除最后一个,返回删除的元素
public int removeLast(){
return remove(size-1);
}
//从数组中删除元素,这是删除第一个,但是可以设计删除所有的元素
public void removeElement(int e){
int index = find(e);
if(index!=-1)
remove(index);
}
泛型类
使用泛型,应该能够存储任意类型,还应该包括用户自定义的类型。
这和之前的线性查找法和排序查找法是一样的。数据结构,使用泛型的话都需要创建泛型类
改造如下
public class Array<E>{
private E[] data;
private int size;
//构造函数,传入数组的容量capacity构造Array
public Array(int capacity){
data =(E[])new Obeject[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;
}
}
//获取index索引位置的元素
E get(int index){
if(index<0 ||index >= size)
throw new IllegalArgumentException("Get failed,Index is illegal");
return data[index];
}
//修改index索引位置的元素为e
void set(int index,E e){
if(index<0 ||index >= size)
throw new IllegalArgumentException("Get failed,Index is illegal");
data[index] = e;
}
在index个位置插入一个新元素e
public void add(int index,E e){
//判断是否超过数组的容量
if(size == data.length)
throw new IlleaglArgumentException("add failed, array is full");
if(index < 0|| index > size)
throw new IlleaglArgumentException("add failed, require size>=0 && <capacity");
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 boolean contains(E e){
for(int i = 0; i< size;i++){
if(data[i].equlas(e))return true;
return false;
}
}
//查找数组中元素e所在的索引,如果不存在元素e,则返回-
public E find(E e){
for(int i = 0; i< size;i++){
if(data[i].equals(e))return i;
return -1;
}
}
public E remove(int index){
if(index<0 ||index >= size)
throw new IllegalArgumentException("Remove failed,Index is illegal");
int ret = data[index];
for(int i =index+1;i<size;i++){
data[i-1]=data[i];
}
size--;
data[size]=null;//java的自动回收机制,但这句话也不是必须的,loitering objects闲的物品!=memory leak不等于内存泄漏
return ret;
}
//从数组中删除第一个元素,返回删除的元素
public E removeFirst(){
return remove(0);
}
//从数组中删除最后一个,返回删除的元素
public E removeLast(){
return remove(size-1);
}
//从数组中删除元素,这是删除第一个,但是可以设计删除所有的元素
public void removeElement(E e){
int index = find(e);
if(index!=-1)
remove(index);
}
动态数组
动态数组扩容,先复制一个二倍的空间,然后将原data里的数据通过循环便利复制到newdata数组中,将data的引用指向new data,这个时候原数组会被java的垃圾回收机制回收。
通过改造后的add函数
在index个位置插入一个新元素e
public void add(int index,E e){
if(index < 0|| index > size)
throw new IlleaglArgumentException("add failed, require size>=0 && <capacity");
//判断是否超过数组的容量,我们进行扩容
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++;
}
这里扩容2倍是和当前元素有多少是有关的,是有数量级关系的。如果不是同一个数量级,比如10000 和10,加上常数的话,性能不是很高。
当然,这里的2倍也可以换成1.5倍等等,参考java的colleciton类
private void resize(int new Capacity){
E[] newData = (E[])new Object[newCapacity];//java不支持直接泛型类,需要转类型
for(int i = 0 ; i < size; i++)
newData[i] = data[i];
data = newData;
}
public int remove(int index){
if(index<0 ||index >= size)
throw new IllegalArgumentException("Remove failed,Index is illegal");
int ret = data[index];
for(int i =index+1;i<size;i++){
data[i-1]=data[i];
}
size--;
if(size == data.length/2)
resize(data.length/2);//节省数组空间
return ret;
}
简单的复杂度分析
- 添加操作
- addLast(e) O(1)的时间复杂度
- addFirst(e) O(n)的复杂度
- add(index, e) O(n)
- 严格计算需要一些概率论知识
综合来看是一个O(n)级别的算法
时间复杂度不能按照最好的情况来解决,而应该按照最坏的情况。
-
删除操作
- removeLast(e) O(1)
- removeFirst(e) O(n)
- remove(index, e) O(n)
O(n)级别的
-
修改操作
- set(index,e) O(1)
-
查找操作
-
get(index) o(1)
-
contains(e) O(n)
-
find(e) o(n)
综合来看:
-
均摊复杂度和防止复杂度的震荡
我们不可能每一次添加元素都触发这个resize操作,比如添加10个元素,需要到10才会触发resize,我们不可能每次添加元素都触发resize,这里使用最坏情况是有点不合理的
假设当前capacity = 8,并且每一次添加操作都使用addLast
平均,每次addLast操作进行2次基本操作
假设capacity=n,n+1次addLast操作,触发resize,总共进行2n+1次基本操作
平均,每次addLast操作,进行2次基本操作
这样的均摊计算,时间复杂度是O(1)的!
这个例子里,这样均摊计算是更有意义的
amortized time complexity
相对比较耗时的操作是可以分摊的
同理,我们看removeLast,均摊复杂度也为O(1)
但是,当我们同时看addLast和removeLast操作
复杂度震荡出现问题的原因:removeLast时resize过于着急(Eager)
解决方案:lazy
当size==capacity/4,才将capacity减半
public int remove(int index){
if(index<0 ||index >= size)
throw new IllegalArgumentException("Remove failed,Index is illegal");
int ret = data[index];
for(int i =index+1;i<size;i++){
data[i-1]=data[i];
}
size--;
if(size == data.length/4&&data.length/2!=0)//防止data.length=1,这样会导致长度为0
resize(data.length/2);//节省数组空间
return ret;
}