一、基础的线性数据结构:数组
1. 静态数组
1.1 随机读写性能很棒
- 数组是线性数据结构,使用数组存储的数据会排成像一条线一样
- 用一组
连续的内存空间,来存储一组具有相同类型的数据
选址公式:
int[] data = new int[5];
data[2]元素内存地址=1000+2*4=1008
所以data[2]=0
随机访问数组中任一元素的时间复杂度为O(1)
将index=3的元素的值设置为4
-
- 先找到index=3的元素所在的内存地址=1000+3*4=1012
- 将内存地址为1012指向的内存值设置为4
随机读写数组中任一元素的时间复杂度为O(1)
结论:数组的随机读写性能非常的好
1.2 随机新增元素
public static int[] insertElement(int[] src,int index,int element){
int length=src.length;
int[] dest = new int[length+1];
for (int i = 0; i < index; i++) {
dest[i]=src[i];
}
dest[index]=element;
for (int i = index; i < length; i++) {
dest[i+1]=src[i];
}
return dest;
}
时间复杂度为O(n),空间复杂度为O(n)
1.3 随机删除元素
public static int[] removeElement(int[] src,int index){
int[] dest = new int[src.length-1];
for (int i = 0; i < index; i++) {
dest[i]=src[i];
}
for (int i = index; i < src.length-1; i++) {
dest[i]=src[i+1];
}
return dest;
}
空间复杂度为O(n),时间复杂度为O(n)
结论:相对于数组的随机读写,数组的随机删除和新增的性能不是很好
2. 二次封装静态数组
Java内置数组的缺点:
需要写一个ArrayUtils来实现以下功能:
- 没有新增元素的方法
- 没有删除元素的方法
- 没有判空方法
- 没有获取真实存储元素个数的方法
- 没有判断是否存在指定目标值的方法
- 例如:
/**
* 判断是否为空
* @param arr
* @return
*/
public static boolean isEmpty(int[] arr){
for(int num:arr){
//此处假设等于0表示没有元素
//这个是不严谨的,因为元素的值可能是0
if(num!=0)
return false;
}
return true;
}
因此需要二次封装静态数组ArrayList
成员变量包括:一个内置数组,capacity记录数组容量,size记录数组元素个数
2.1 新增操作
例如:把21插入到索引为1的位置
将index指向1,并将后面的元素都向后移一位(从最后一个元素开始往后移),最后size加1
public void add(int index,int e){
if(size == data.length){
throw new IllegalArgumentException("add failed,Array is full");
}
if(index<0 || index>size){
throw new IllegalArgumentException("add failed,require index>=0 && index<=size");
}
//计算最差时间复杂度,循环代码运行最大次数
//size=data.length && index=0
//时间复杂度O(n)
for(int i=size-1;i>=index;i--){
data[i+1]=data[i];
}
data[index]=e;
size++;
}
时间复杂度为O(n)
2.2 查询操作
- 获取index索引位置的元素
public int get(int index){
if(index<0||index>=size){
throw new IllegalArgumentException("get failed,require index>=0 && index<=size");
}
return data[index];
}
时间复杂度为O(1)
- 查找元素e所在的索引,如果不存在元素e,则返回-1
public int find(int e){
for(int i=0;i<size;i++){
if(data[i]==e){
return i;
}
}
return -1;
}
时间复杂度为O(n)
- 是否包含元素e
public boolean contains(int target){
for (int num : data) {
if(num==target)
return true;
}
return false;
}
时间复杂度为O(n)
- 重写toString()
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(String.format("Array:size = %d,capacity = %d\n",size,data.length));
sb.append("[");
for (int i = 0; i < size; i++) {
sb.append(data[i]);
if(i!=size-1){
sb.append(",");
}
}
sb.append("]");
return sb.toString();
}
2.3 修改操作
public void set(int index,int e){
if(index<0||index>=size){
throw new IllegalArgumentException("set failed,require index>=0 && index<=size");
}
data[index]=e;
}
时间复杂度为O(1)
2.4 删除操作
例如:删除索引为1的元素
将后面的元素覆盖掉前面的元素,size减1,并将当前位置的元素对象释放掉
public int remove(int index){
if(index<0||index>=size){
throw new IllegalArgumentException("remove failed,require index>=0 && index<=size");
}
//记录要删除的元素
int res = data[index];
//最差:index=0 && size=data.length
//时间复杂度为O(n)
for (int i = index+1; i < size; i++) {
data[i-1]=data[i];
}
size--;
//GC 清除不用的对象
data[size]=null;
return res;
}
//删除指定元素
public void removeElement(int e){
//时间复杂度为O(n)
int index = find(e);
if(index!=-1){
remove(index);
}
}
2.5 使用泛型
public class ArrayList<E> {
private E[] data;
private int capacity;
private int size;
public ArrayList(int capacity){
this.data = (E[]) new Object[capacity];
this.capacity=capacity;
this.size=0;
}
public ArrayList(){
this(15);
}
public boolean isEmpty(){
return size==0;
}
public int getSize(){
return size;
}
public int getCapacity(){
return capacity;
}
/**
* 新增操作:在指定位置插入一个元素
* @param index
* @param e
*/
public void add(int index,E e){
if(size == data.length){
throw new IllegalArgumentException("add failed,Array is full");
}
if(index<0 || index>size){
throw new IllegalArgumentException("add failed,require index>=0 && index<=size");
}
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);
}
/**
* 查询操作:获取index索引位置的元素
* @param index
* @return
*/
public E get(int index){
if(index<0||index>=size){
throw new IllegalArgumentException("get failed,require index>=0 && index<=size");
}
return data[index];
}
/**
* 查找元素e所在的索引,如果不存在元素e,则返回-1
* @param e
* @return
*/
public int find(E e){
for(int i=0;i<size;i++){
if(data[i].equals(e)){
return i;
}
}
return -1;
}
/**
* 修改操作:将index索引位置的元素修改为新元素e
* @param index
* @param e
*/
public void set(int index,E e){
if(index<0||index>=size){
throw new IllegalArgumentException("set failed,require index>=0 && index<=size");
}
data[index]=e;
}
/**
* 删除操作:删除指定索引位置的元素
* @param index
*/
public E remove(int index){
if(index<0||index>=size){
throw new IllegalArgumentException("remove failed,require index>=0 && index<=size");
}
E res = data[index];
for (int i = index+1; i < size; i++) {
data[i-1]=data[i];
}
size--;
//GC 清除不用的对象
data[size]=null;
return res;
}
public E removeFirst(){
return remove(0);
}
public E removeLast(){
return remove(size-1);
}
/**
* 删除指定元素
* @param e
*/
public void removeElement(E e){
int index = find(e);
if(index!=-1){
remove(index);
}
}
public boolean contains(E target){
for (E num : data) {
if(num.equals(target))
return true;
}
return false;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(String.format("Array:size = %d,capacity = %d\n",size,data.length));
sb.append("[");
for (int i = 0; i < size; i++) {
sb.append(data[i]);
if(i!=size-1){
sb.append(",");
}
}
sb.append("]");
return sb.toString();
}
}
3. 动态数组
3.1 扩容
扩容缩容三步法:
- 创建一个容量为newCapacity的临时数组
- 将原来数组中的元素拷贝到新数组中
- 将新数组覆盖老数组
//改进前
public void add(int index,E e){
if(index<0 || index>size){
throw new IllegalArgumentException("add failed,require index>=0 && index<=size");
}
//扩容
if(size == data.length){
//1.创建一个容量为2倍于之前容量的临时数组
int newCapacity = capacity<<1;
E[] newData = (E[])new Object[newCapacity];
//2.将原来数组中的元素拷贝到新数组中
for (int i = 0; i < data.length; i++) {
newData[i] = data[i];
}
//3.将新数组覆盖老数组
data = newData;
// bug 修复:将容量设置位新容量值
capacity = newCapacity;
}
for(int i=size-1;i>=index;i--){
data[i+1]=data[i];
}
data[index]=e;
size++;
}
//改进后,扩容只需调用resize方法,将新容量传入即可
public void add(int index,E e){
if(index<0 || index>size){
throw new IllegalArgumentException("add failed,require index>=0 && index<=size");
}
//扩容
if(size == data.length){
resize(capacity<<1);
}
for(int i=size-1;i>=index;i--){
data[i+1]=data[i];
}
data[index]=e;
size++;
}
3.2 缩容
将扩容和缩容的公共部分提取成resize方法,如需改变容量,调用resize方法即可,将新的数组容量通过参数传入
//扩容和缩容操作
public void resize(int newCapacity){
//1.创建一个容量为newCapacity的临时数组
E[] newData = (E[])new Object[newCapacity];
//2.将原来数组中的元素拷贝到新数组中
//此处缩容会有数组下标越界异常,需注意!
for (int i = 0; i < data.length; i++) {
newData[i] = data[i];
}
//3.将新数组覆盖老数组
data = newData;
// bug 修复:将容量设置位新容量值
capacity = newCapacity;
}
//删除操作
public E remove(int index){
if(index<0||index>=size){
throw new IllegalArgumentException("remove failed,require index>=0 && index<=size");
}
E res = data[index];
for (int i = index+1; i < size; i++) {
data[i-1]=data[i];
}
size--;
//GC 清除不用的对象
data[size]=null;
//如果size等于总容量的一半的话,则进行缩容
//此处可能会出现时间复杂度震荡,需注意!
if(size==data.length/2){
resize(data.length>>1);
}
return res;
}
3.3 缩容的bug修复
缩容的情况下如果使用data.length会报错,不能使用data.length
//扩容和缩容操作
public void resize(int newCapacity){
//1.创建一个容量为newCapacity的临时数组
E[] newData = (E[])new Object[newCapacity]; //7
//2.将原来数组中的元素拷贝到新数组中
//此处不需要将原先整个data数组复制到新数组中,只需要将里面的元素拷贝到新数组即可,元素个数size(此时size为总容量的一半),此时传入的newCapacity为data的一半,新数组的容量为newCapacity,如果将原先数组data.length都复制到新数组中时,会出现下标越界异常
for (int i = 0; i < size; i++) { //data.length:14
newData[i] = data[i];
}
//3.将新数组覆盖老数组
data = newData;
}
3.4 均摊时间复杂度
addLast均摊时间复杂度:
在往数组最后添加元素时,如果数组满了,需要扩容,不会影响时间复杂度
正常情况下,addLast时间复杂度为O(1)
操作:第一次:56;第二次:12;第三次:33;第四次:24;第五次:45
第六次:复制…
第十一次:31
第十二次:data = newData;
addLast均摊时间复杂度:O(1)
removeLast均摊时间复杂度:O(1)
3.5 时间复杂度震荡
在边界时,不断插入元素或者删除元素,在缩容和扩容之间徘徊,会导致时间复杂度震荡
addLast时间复杂度O(n)
removeLast时间复杂度O(n) (扩容或者缩容都需要遍历数组)
解决办法:在缩容的时候,并非在size为容量一半的时候缩容,而是选择在1/4的时候缩容,尽可能减少时间复杂度震荡;另外需要注意原先数组的长度有可能不断的减少,有可能会小于 2,此时假如是1的话,1/2为0,传入的新容量就为0了,所以需要判断不等于0
public E remove(int index){
if(index<0||index>=size){
throw new IllegalArgumentException("remove failed,require index>=0 && index<=size");
}
E res = data[index];
for (int i = index+1; i < size; i++) {
data[i-1]=data[i];
}
size--;
//GC 清除不用的对象
data[size]=null;
//如果size等于总容量的四分之一的话,则进行缩容
//此处可能会有时间复杂度震荡,为了避免,选择在1/4的时候进行缩容
//注意:因为data.length有可能不断的减少,所以有可能小于2了,所以需要判断下
if(size==data.length/4 && data.length/2!=0){
resize(data.length>>1);
}
return res;
}
4. 静态数组vs动态数组
- 动态数组使用相对方便
- 动态数组支持自动扩容缩容
- 动态数组申请内存耗费时间,没有静态数组性能好
- 动态数组不能存储基本类型的元素
5. 最终代码
public class ArrayList<E> {
private E[] data;
private int capacity;
private int size;
public ArrayList(int capacity){
this.data = (E[]) new Object[capacity];
this.capacity=capacity;
this.size=0;
}
public ArrayList(){
this(15);
}
public ArrayList(E[] arr) {
this.data = (E[])new Object[arr.length];
for (int i = 0; i < arr.length; i++) {
data[i] = arr[i];
}
size = arr.length;
// bug 修复:加上 capacity 的初始化
this.capacity = arr.length;
}
//判断是否为空
public boolean isEmpty(){
return size==0;
}
//获取数组元素个数
public int getSize(){
return size;
}
//获取容量
public int getCapacity(){
return capacity;
}
/**
* 新增操作:在指定位置插入一个元素
* 时间复杂度:O(n)
* @param index
* @param e
*/
public void add(int index,E e){
if(index<0 || index>size){
throw new IllegalArgumentException("add failed,require index>=0 && index<=size");
}
//扩容
if(size == data.length){
resize(capacity<<1);
}
//计算最差时间复杂度,循环代码运行最大次数
//size=data.length && index=0
//时间复杂度O(n)
for(int i=size-1;i>=index;i--){
data[i+1]=data[i];
}
data[index]=e;
size++;
}
//时间复杂度O(n)
public void addFirst(E e){
add(0,e);
}
//时间复杂度O(1),如果添加最后时数组满了,需要扩容,此时均摊时间复杂度
public void addLast(E e){
add(size,e);
}
//扩容和缩容操作
public void resize(int newCapacity){
//1.创建一个容量为newCapacity的临时数组
E[] newData = (E[])new Object[newCapacity];
//2.将原来数组中的元素拷贝到新数组中
//缩容bug修复
for (int i = 0; i < size; i++) {
newData[i] = data[i];
}
//3.将新数组覆盖老数组
data = newData;
// bug 修复:将容量设置位新容量值
capacity = newCapacity;
}
/**
* 删除操作:删除指定索引位置的元素
* @param index
*/
public E remove(int index){
if(index<0||index>=size){
throw new IllegalArgumentException("remove failed,require index>=0 && index<=size");
}
//暂存要删除索引位置的元素
E res = data[index];
for (int i = index+1; i < size; i++) {
data[i-1]=data[i];
}
size--;
//GC 清除不用的对象
data[size]=null;
//如果size等于总容量的四分之一的话,则进行缩容
//此处可能会有时间复杂度震荡,为了避免,选择在1/4的时候进行缩容
//注意:因为data.length有可能不断的减少,所以有可能小于2了,所以需要判断下
if(size==data.length/4 && data.length/2!=0){
resize(data.length>>1);
}
return res;
}
//时间复杂度为O(n)
public E removeFirst(){
return remove(0);
}
//时间复杂度为O(1)
public E removeLast(){
return remove(size-1);
}
/**
* 删除指定元素
* 时间复杂度为O(n)
* @param e
*/
public void removeElement(E e){
int index = find(e);
if(index!=-1){
remove(index);
}
}
/**
* 修改操作:将index索引位置的元素修改为新元素e
* @param index
* @param e
*/
public void set(int index,E e){
if(index<0||index>=size){
throw new IllegalArgumentException("set failed,require index>=0 && index<=size");
}
data[index]=e;
}
/**
* 查询操作:获取index索引位置的元素
* 时间复杂度为O(1)
* @param index
* @return
*/
public E get(int index){
if(index<0||index>=size){
throw new IllegalArgumentException("get failed,require index>=0 && index<=size");
}
return data[index];
}
/**
* 查找元素e所在的索引,如果不存在元素e,则返回-1
* 时间复杂度为O(n)
* @param e
* @return
*/
public int find(E e){
for(int i=0;i<size;i++){
if(data[i].equals(e)){
return i;
}
}
return -1;
}
/**
* 是否包含某一元素
* 时间复杂度为O(n)
* @param target
* @return
*/
public boolean contains(E target){
for (E num : data) {
if(num.equals(target))
return true;
}
return false;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(String.format("Array:size = %d,capacity = %d\n",size,data.length));
sb.append("[");
for (int i = 0; i < size; i++) {
sb.append(data[i]);
if(i!=size-1){
sb.append(",");
}
}
sb.append("]");
return sb.toString();
}
}
6.数据结构与算法学习
https://tmdus.xetlk.com/s/eUMyr