数组
1. 数组基础
介绍
-
有限个相同类型的变量组成的有序集合(军训,固定的人在固定的位置)
-
顺序存储结构
-
不同类型的数组每个元素所占的字节数也不同
基本操作
-
读取 O(1)
-
更新 O(1)
-
插入 O(n)
-
- 尾插(直接在数组的空元素位置插入元素)
- 中插(在数组的中间位置插入元素,插入位置之后的元素要统一往后移动一个位置)
- 超范围插(数组的位置不够,创建一个新的数组覆盖原来的数组,新数组长度最后是数组的二倍)
-
删除 O(n)
-
- 中间位置删除(删除之后,之后的元素统一向前移动一个单位)
- 末位置删除
数组的优劣
优:快速查询,高效的随机访问,可根据下表找到对应的元素。
缺:增加,删除操作的数据大量迁移,严重影响效率
使用范围
适合那些查,改操作多,增删操作较少的场景。
-
数组最大优点:快速查询。scores[2]
-
数组最好应用于“索引有语意”的情况
-
但并非所有有语意的索引都是用于数组 如身份证号:410181195112112511
-
数组的默认取值
2. 数组使用
0. 引入问题
问题:
- 索引没有语意,如何表示没有元素?
- 规定长度为8,如果有九个或者更多元素,如何添加元素?删除元素?
- 。。。。。。
解决:
-
由于java并没有给我们提供解决以上问题的方法,所以我们需要自己去解决
-
java数组属于静态数组,解决上面的问题我们需要基于java封装自己的动态数组
1. 创建数组
- 创建一个int类型的空数组
- 创建一个成员变量size(用于记录数组中元素的数量) 非数组容量
- 创建有参构造,接收数组容量capacity,默认size为0
- 定义无参构造,调用有参构造,默认数组容量为10
- 定义获取数组容量的方法
- 定义获取数组内元素个数的方法
- 定义判断数组是否为空的方法
package array_01.base;
/**
* @Classname Array
* @Description TODO
* @Date 2021/12/25 21:40
* @Created by zhq
*/
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() {
//这里调用的是上方的Array
//10为参数
this(10);
}
//获取数组中的元素个数
public int getSize() {
return size;
}
//获取数组的容量
public int getCapacity() {
return data.length;
}
//返回数组是否为空
public boolean isEmpty() {
return size == 0;
}
}
2. 添加元素
末尾添加
步骤
- 判断数组的容量是否已满
- 添加元素
- 数组元素数量+1
//数组末尾添加元素
public void addLast(int e) {
//当元素个数等于数组的长度时,是不能进行元素添加的,会出现错误。在此处现抛出异常
if (size == data.length) {
//抛出了一个传入参数有问题的异常
throw new IllegalArgumentException("AddLast fail. Array is full.");
}
data[size] = e;
size++;
}
指定位置添加
思路
- 将索引为1、2,3的元素分别向后一位平移
- 平移应该从后往前,从索引为3开始
- 添加之后,size++;
步骤
- 判断数组的容量是否已满
- 判断索引是否小于0或大于size(如果大于size,数组会排列不紧密,这样不好)
- 更改数组元素位置,添加位置至最后一个元素整体往后偏移
- 指定位置设置元素
- 数组元素数量+1
// 在第index位置插入元素e
// 根据插入位置不同,时间复杂度不同,如果从中间插入,时间复杂度为O(n/2)=O(n)
public void add(int index, int e) {
if (size == data.length) {
//抛出了一个传入参数有问题的异常
throw new IllegalArgumentException("Add fail. Array is full.");
}
//如果index > size 说明索引的插入位置大于数组中已存在的元素的个数,说明数据不是紧密排列的,所以要杜绝这种情况的发生
if (index < 0 || index > size) {
throw new IllegalArgumentException("Add fail. Require index>=0 and index<=size.");
}
//指定位置插入元素
for (int i = size - 1; i >= index; i--) {
data[i + 1] = data[i];
}
data[index] = e;
size++;
}
头部位置添加
//向数组中的首位添加元素
//时间复杂度O(n),其他位置元素都往后挪
//数组头部添加元素
public void addFirst(int e) {
//直接调用add()方法即可实现
this.add(0, e);
}
3. 查询和修改元素
查询和修改
//获取index索引位置的元素
//O(1)
public int get(int index) {
if (index < 0 || index >= size)
throw new IllegalArgumentException("Get fail. Require index>=0 and index<size.");
return data[index];
}
//修改index索引位置的元素为e
public void set(int index, int e) {
if (index < 0 || index > size)
throw new IllegalArgumentException("Set fail. Require index>=0 and index<=size.");
data[index] = e;
}
查询数组中是否包含某个元素
查找数组中是否包含某个元素e
//查找数组中是否包含某个元素e
//O(n)
public boolean contains(int e) {
for (int i = 0; i < data.length; i++) {
if (data[i] == e) return true;
}
//如果循环结束,还没有找到,则说明不包含,return false即可
return false;
}
如果包含某个元素,则查看这个元素的索引
//查找数组中是否包含某个元素e
//O(n)
public int find(int e) {
for (int i = 0; i < data.length; i++) {
if (data[i] == e) return i;
}
//如果此数组中没有元素e,则返回-1 无效索引
return -1;
}
4. 删除数组中元素
思路
- 和往数组中某个位置添加元素思路一样
- 删除是让删除位置的元素的值等于其后面元素的元素,依次往前,直至最后一个元素
- 问题:到最后一个元素时,会出现下列问题:4号和3号内容一样
- 解决:其实这个问题不需要管,因为从正常操作来看,**size即指已有的元素的数量,又可以表示下个元素要插入的索引。**比如size=4,表示有四个元素,下一个元素插入的位置正好是四号位,插入新元素时,会直接把4号为原有的内容覆盖掉。所以不用在意4号位和三号为的内容一致。
删除指定位置的元素 并返回删除元素
//删除某个位置的元素 并返回对应的值 1、删除某个索引位置的元素 2、删除内容为***的元素
//O(2/n)即O(n) 因为要考虑删除的是否是第一个或最后一个位置的元素
public int remove(int index) {
if (index < 0 || index >= size)
throw new IllegalArgumentException("Remove failed. Index is illegal." + "Require index>=0 and index<size.");
int ret = data[index];
//注意临界值是<size-1,size-1对应索引是4,我们删除一个元素之后,这个位置上的元素就没有意义了,因此不用<=size-1;
for (int i = index; i < size - 1; i++) {
data[i] = data[i + 1];
}
size--;
return ret;
}
//删除第一个元素
//O(1)
public int removeFirst() {
return remove(0);
}
//删除最后一个元素
//O(n)
public int removeLast() {
//注意临界值
return remove(size - 1);
}
删除元素内容为*的元素(只删除一个)
public void removeElement(int e) {
//方法一:
int index = -1;
for (int i = 0; i < size; i++) {
if (data[i] == e) index = i;
break;
}
if (index > -1) remove(index);
//方法二:
// int index = find(e); //find(e)返回的为e的索引
// if (index != -1) remove(index);
}
删除多个指定位置元素
注意:同时删除多个索引时,要先从大的索引开始删除
//移除多个索引
public void removeSelIndex(int... selIndex) {
//排序:正序
Arrays.sort(selIndex);
//排序:反序
for (int i = 0; i < selIndex.length / 2; i++) {
int temp = selIndex[i];
selIndex[i] = selIndex[selIndex.length - 1 - i];
selIndex[selIndex.length - 1 - i] = temp;
}
//移除指定位置元素
for (int i = 0; i < selIndex.length; i++) {
//只删除在元素索引范围内的元素,超出范围的不删除
if (selIndex[i] >= 0 && selIndex[i] < size) {
for (int j = selIndex[i]; j < size - 1; j++) {
data[j] = data[j + 1];
}
size--;
}
}
}
删除多个元素
//删除多个元素
public void removeElements(int... elements) {
for (int i = 0; i < elements.length; i++) {
for (int j = 0; j < size; j++) {
if (elements[i] == data[j]) {
remove(j);
j--;
}
}
}
}
5. 重写toString()方法
重写
@Override
//添加@Override的原因是:因为toString属于Object类的方法,此时要覆盖object类的同toString方法,
//如果toString拼写错误时,Override会检索不到Object中的方法,就会提示写法有错误
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();
}
Java源码
//重写toString()方法
@Override
public String toString() {
if (data == null)
return "null";
int iMax = size - 1;
if (iMax == -1)
return "[]";
StringBuilder b = new StringBuilder();
b.append('[');
for (int i = 0; ; i++) {
b.append(data[i]);
if (i == iMax)
return b.append(']').toString();
b.append(", ");
}
}
6. 数组代码汇总
import java.util.Arrays;
/**
* @Classname Array
* @Description TODO
* @Date 2021/12/25 21:40
* @Created by zhq
*/
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() {
//这里调用的是上方的Array
//10为参数
this(10);
}
//获取数组中的元素个数
public int getSize() {
return size;
}
//获取数组的容量
public int getCapacity() {
return data.length;
}
//返回数组是否为空
public boolean isEmpty() {
return size == 0;
}
//向数组中的首位添加元素
//时间复杂度O(n),其他位置元素都往后挪
//数组头部添加元素
public void addFirst(int e) {
//直接调用add()方法即可实现
this.add(0, e);
}
//数组末尾添加元素
public void addLast(int e) {
//当元素个数等于数组的长度时,是不能进行元素添加的,会出现错误。在此处现抛出异常
if (size == data.length) {
//抛出了一个传入参数有问题的异常
throw new IllegalArgumentException("AddLast fail. Array is full.");
}
data[size] = e;
size++;
}
// 在第index位置插入元素e
// 根据插入位置不同,时间复杂度不同,如果从中间插入,时间复杂度为O(n/2)=O(n)
public void add(int index, int e) {
if (size == data.length) {
//抛出了一个传入参数有问题的异常
throw new IllegalArgumentException("Add fail. Array is full.");
}
//如果index > size 说明索引的插入位置大于数组中已存在的元素的个数,说明数据不是紧密排列的,所以要杜绝这种情况的发生
if (index < 0 || index > size) {
throw new IllegalArgumentException("Add fail. Require index>=0 and index<=size.");
}
//指定位置插入元素
for (int i = size - 1; i >= index; i--) {
data[i + 1] = data[i];
}
data[index] = e;
size++;
}
//获取index索引位置的元素
//O(1)
public int get(int index) {
if (index < 0 || index >= size)
throw new IllegalArgumentException("Get fail. Require index>=0 and index<size.");
return data[index];
}
//修改index索引位置的元素为e
public void set(int index, int e) {
if (index < 0 || index > size)
throw new IllegalArgumentException("Set fail. Require index>=0 and index<=size.");
data[index] = e;
}
//查找数组中是否包含某个元素e
//O(n)
public boolean contains(int e) {
for (int i = 0; i < data.length; i++) {
if (data[i] == e) return true;
}
//如果循环结束,还没有找到,则说明不包含,return false即可
return false;
}
//查找数组中是否包含某个元素e
//O(n)
public int find(int e) {
for (int i = 0; i < data.length; i++) {
if (data[i] == e) return i;
}
//如果此数组中没有元素e,则返回-1 无效索引
return -1;
}
//删除某个位置的元素 并返回index 1、删除某个索引位置的元素 2、删除内容为***的元素
//O(2/n)即O(n) 因为要考虑删除的是否是第一个或最后一个位置的元素
public int remove(int index) {
if (index < 0 || index >= size)
throw new IllegalArgumentException("Remove failed. Index is illegal." + "Require index>=0 and index<size.");
int ret = data[index];
//注意临界值是<size-1,size-1对应索引是4,我们删除一个元素之后,这个位置上的元素就没有意义了,因此不用<=size-1;
for (int i = index; i < size - 1; i++) {
data[i] = data[i + 1];
}
size--;
return ret;
}
//移除多个索引
public void removeSelIndex(int... selIndex) {
//排序:正序
Arrays.sort(selIndex);
//排序:反序
for (int i = 0; i < selIndex.length / 2; i++) {
int temp = selIndex[i];
selIndex[i] = selIndex[selIndex.length - 1 - i];
selIndex[selIndex.length - 1 - i] = temp;
}
//移除指定位置元素
for (int i = 0; i < selIndex.length; i++) {
//只删除在元素索引范围内的元素,超出范围的不删除
if (selIndex[i] >= 0 && selIndex[i] < size) {
for (int j = selIndex[i]; j < size - 1; j++) {
data[j] = data[j + 1];
}
size--;
}
}
}
//删除第一个元素
//O(1)
public int removeFirst() {
return remove(0);
}
//删除最后一个元素
//O(n)
public int removeLast() {
return remove(size - 1);
}
//删除指定元素
public void removeElement(int e) {
//方法一:
int index = -1;
for (int i = 0; i < size; i++) {
if (data[i] == e) index = i;
break;
}
if (index > -1) remove(index);
//方法二:
// int index = find(e); //find(e)返回的为e的索引
// if (index != -1) remove(index);
}
//删除多个元素
public void removeElements(int... elements) {
for (int i = 0; i < elements.length; i++) {
for (int j = 0; j < size; j++) {
if (elements[i] == data[j]) {
remove(j);
j--;
}
}
}
}
//重写toString()方法
@Override
public String toString() {
if (data == null)
return "null";
int iMax = size - 1;
if (iMax == -1)
return "[]";
StringBuilder b = new StringBuilder();
b.append('[');
for (int i = 0; ; i++) {
b.append(data[i]);
if (i == iMax)
return b.append(']').toString();
b.append(", ");
}
}
}
3. 泛型使用
0. 泛型目的
-
让我们的数据结构可以放置“任何”数据类型
-
泛型限制:不可以是基本数据类型,只能是类和对象。
-
限制解决:java中每个基本数据类型都有对应的包装类(首字母大写就是其包装类)
-
Boolean、Byte、Char、Short、Int、Long、Float、Double
1. 定义泛型类
public class Array<E> {
// 定义数组
private E[] data;
// 定义数组中的元素个数
// size在运用中既充当了数组中元素的个数(非长度),又可以间接充当元素的索引
private int size;
// 构造函数,传入数组的容量capacity构造Array
public Array(int capacity) {
//因为 new一个 E[]的数组会报错,所以这里new了一个Object,然后进行强制转换
data = (E[])new Object[capacity];
size = 0;
}
// 无参构造函数,默认容器的容量
public Array() {
this(10);
}
}
测试
public class Main {
public static void main(String[] args) {
// <Integer>表示我们存放的是Integer类型的数据
Array<Integer> arr = new Array<>(20);
// 在数组中添加0-9这10个元素
for (int i = 0; i < 10; i++) {
arr.addLast(i);
}
}
2. 泛型数组代码汇总
public class Array<E> {
// 定义数组
private E[] data;
// 定义数组中的元素个数
// size在运用中既充当了数组中元素的个数(非长度),又可以间接充当元素的索引
private int size;
// 构造函数,传入数组的容量capacity构造Array
public Array(int capacity) {
//因为 new一个 E[]的数组会报错,所以这里new了一个Object,然后进行强制转换
data = (E[]) new Object[capacity];
size = 0;
}
// 无参构造函数,默认容器的容量
public Array() {
this(10);
}
//获取数组中的元素个数
public int getSize() {
return size;
}
//获取数组的容量
public int getCapacity() {
return data.length;
}
//返回数组是否为空
public boolean isEmpty() {
return size == 0;
}
//向数组中的首位添加元素
//时间复杂度O(n),其他位置元素都往后挪
//数组头部添加元素
public void addFirst(E e) {
//直接调用add()方法即可实现
this.add(0, e);
}
//数组末尾添加元素
public void addLast(E e) {
this.add(size,e);
}
// 在第index位置插入元素e
// 根据插入位置不同,时间复杂度不同,如果从中间插入,时间复杂度为O(n/2)=O(n)
public void add(int index, E e) {
//如果index > size 说明索引的插入位置大于数组中已存在的元素的个数,说明数据不是紧密排列的,所以要杜绝这种情况的发生
if (index < 0 || index > size) {
throw new IllegalArgumentException("Add fail. Require index>=0 and index<=size.");
}
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++;
}
//数组重置方法 O(n)
public void resize(int newCapacity) {
//新建一个数组对象
E[] newData = (E[]) new Object[newCapacity];
for (int i = 0; i < s; i++) {
//将之前的数组遍历到新数组中
newData[i] = data[i];
}
//更换为类数组
data = newData;
}
//获取index索引位置的元素
//O(1)
public E get(int index) {
if (index < 0 || index >= size)
throw new IllegalArgumentException("Get fail. Require index>=0 and index<size.");
return data[index];
}
//修改index索引位置的元素为e
public void set(int index, E e) {
if (index < 0 || index > size)
throw new IllegalArgumentException("Set fail. Require index>=0 and index<=size.");
data[index] = e;
}
//查找数组中是否包含某个元素e
//O(n)
public boolean contains(E e) {
for (int i = 0; i < data.length; i++) {
if (data[i] == e) return true;
}
//如果循环结束,还没有找到,则说明不包含,return false即可
return false;
}
//查找数组中是否包含某个元素e
//O(n)
public int find(E e) {
for (int i = 0; i < data.length; i++) {
if (data[i] == e) return i;
}
//如果此数组中没有元素e,则返回-1 无效索引
return -1;
}
//删除某个位置的元素 并返回对应的值 1、删除某个索引位置的元素 2、删除内容为***的元素
//O(2/n)即O(n) 因为要考虑删除的是否是第一个或最后一个位置的元素
public E remove(int index) {
if (index < 0 || index >= size)
throw new IllegalArgumentException("Remove failed. Index is illegal." + "Require index>=0 and index<size.");
E ret = data[index];
//注意临界值是<size-1,size-1对应索引是4,我们删除一个元素之后,这个位置上的元素就没有意义了,因此不用<=size-1;
for (int i = index; i < size - 1; i++) {
data[i] = data[i + 1];
}
size--;
//当元素的个数小于数组长度的1/2时,则数组长度也/2
if(size<data.length/2) {
resize(data.length/2);
}
return ret;
}
//移除多个索引
public void removeSelIndex(int... selIndex) {
//排序:正序
Arrays.sort(selIndex);
//排序:反序
for (int i = 0; i < selIndex.length / 2; i++) {
int temp = selIndex[i];
selIndex[i] = selIndex[selIndex.length - 1 - i];
selIndex[selIndex.length - 1 - i] = temp;
}
//移除指定位置元素
for (int i = 0; i < selIndex.length; i++) {
//只删除在元素索引范围内的元素,超出范围的不删除
if (selIndex[i] >= 0 && selIndex[i] < size) {
for (int j = selIndex[i]; j < size - 1; j++) {
data[j] = data[j + 1];
}
size--;
}
}
//当元素的个数小于数组长度的1/2时,则数组长度也/2
if(size<data.length/2) {
resize(data.length/2);
}
}
//删除第一个元素
//O(1)
public E removeFirst() {
return remove(0);
}
//删除最后一个元素
//O(n)
public E removeLast() {
return remove(size - 1);
}
//删除指定元素
public void removeElement(E e) {
//方法一:
int index = -1;
for (int i = 0; i < size; i++) {
if (data[i] == e) index = i;
break;
}
if (index > -1) remove(index);
}
//删除多个元素
public void removeElements(E... elements) {
for (int i = 0; i < elements.length; i++) {
for (int j = 0; j < size; j++) {
if (elements[i] == data[j]) {
remove(j);
j--;
}
}
}
}
@Override
//添加@Override的原因是:因为toString属于Object类的方法,此时要覆盖object类的同toString方法,
//如果toString拼写错误时,Override会检索不到Object中的方法,就会提示写法有错误
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();
}
}
4. 动态数组
0. 引入问题
**问题:**之前的数组,内部使用的还是java的静态数组。数组的数量是有限的,数组空间是自己定义的,如果开辟过大,则资源浪费,如果过小,则不够用。
**解决:**新建一个数组,并把原来数组中的元素转移到新数组中,执行完之后,newData这个变量在函数执行完之后就失效了,因为data是类变量,所以只要类存在,data就存在。而之前的空数组,在java的自动垃圾回收机制就删除了。
1. 扩容数组
指定位置添加元素
// 在第index位置插入元素e
// 根据插入位置不同,时间复杂度不同,如果从中间插入,时间复杂度为O(n/2)=O(n)
public void add(int index, E e) {
//如果index > size 说明索引的插入位置大于数组中已存在的元素的个数,说明数据不是紧密排列的,所以要杜绝这种情况的发生
if (index < 0 || index > size) {
throw new IllegalArgumentException("Add fail. Require index>=0 and index<=size.");
}
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++;
}
末尾添加元素
//数组末尾添加元素
public void addLast(E e) {
this.add(size,e);
}
2. 数组重置方法
//数组重置方法 O(n)
public void resize(int newCapacity) {
//新建一个数组对象
E[] newData = (E[]) new Object[newCapacity];
for (int i = 0; i < size; i++) {
//将之前的数组遍历到新数组中
newData[i] = data[i];
}
//更换为类数组
data = newData;
}
3. 缩容数组
根据指定索引删除元素
//删除某个位置的元素 并返回对应的值
//O(2/n)即O(n) 因为要考虑删除的是否是第一个或最后一个位置的元素
public E remove(int index) {
if (index < 0 || index >= size)
throw new IllegalArgumentException("Remove failed. Index is illegal." + "Require index>=0 and index<size.");
E ret = data[index];
//注意临界值是<size-1,size-1对应索引是4,我们删除一个元素之后,这个位置上的元素就没有意义了,因此不用<=size-1;
for (int i = index; i < size - 1; i++) {
data[i] = data[i + 1];
}
size--;
//当元素的个数小于数组长度的1/2时,则数组长度也/2
if(size<data.length/2) {
resize(data.length/2);
}
return ret;
}
批量删除索引元素
//移除多个索引
public void removeSelIndex(int... selIndex) {
//排序:正序
Arrays.sort(selIndex);
//排序:反序
for (int i = 0; i < selIndex.length / 2; i++) {
int temp = selIndex[i];
selIndex[i] = selIndex[selIndex.length - 1 - i];
selIndex[selIndex.length - 1 - i] = temp;
}
//移除指定位置元素
for (int i = 0; i < selIndex.length; i++) {
//只删除在元素索引范围内的元素,超出范围的不删除
if (selIndex[i] >= 0 && selIndex[i] < size) {
for (int j = selIndex[i]; j < size - 1; j++) {
data[j] = data[j + 1];
}
size--;
}
}
//当元素的个数小于数组长度的1/2时,则数组长度也/2
if(size<data.length/2) {
resize(data.length/2);
}
}
5 .时间复杂度分析
0. 数组复杂度分析
-
n是数组nums元素的个数
-
O(n)表示的是时间复杂度和n是线性相关
-
O 是渐进复杂度,描述n趋近于无穷的情况
-
线性方程:忽略常数,实际时间T=c1*n+c2。c1表示一个元素执行的时间,c2表示除去数组元素外,其他数据的执行时间
- T=2*n+2 O(n )
- T=2000*n+1000 O(n)
- T=1 * n *n+0 O(n^2)
- 当n=10时,比较第二和第三,执行时间n^2<n,但是当趋于无穷大时,就不成立了
- T=2* n * n+300n+100 O(n^2)
-
数组操作的时间复杂度
- 增加: O(n)
- 删除: O(n)
- 修改: 已知索引(O1),未知索引 O(n)
- 查询:已知索引(O1),未知索引 O(n)
- 増和删的操作时,如果只对最后一个数操作时,时间复杂度为O(1)。但是我们说他是O(n)级别的操作,是因为有resize,resize需要把整个元素全都复制一遍。
- resize(扩容),假如每次进行添加操作时,我们都进行扩容,那么他们复杂度就是O(n)
- 但是resize的使用频率很小,比如我向容器为10的数组中添加元素,添加十次之后,才进行一次扩容,再添加10此,在进行一次扩容。所有遇到这种情况,我们往最坏的方面考虑是不合适的。考虑到这个问题,我们就可以想到使用均摊复杂度
1. 均摊复杂度
-
一个相对比较耗时的操作,如果我们能保证他不会每次都触发的话,这个相对复杂的操作的操作时间是可以分配到整个操作中来的。
- 假如一个数组容量为8,我进行9次addLast()操作时,才会调用一次resize()扩容方法,每调用一次resize()扩容方法,那么操作数+8。
- 我们一共进行了17次基本操作。平均每次addLast(),执行了两次基本操作。 17/8约等于2.
- 通过这样均摊计算,时间复杂度是O(1)的,和数组中有多少个元素无关。
-
针对这个例子,我们采用均摊计算,比计算最坏情况更有意义。
-
同理,removeLast()的时间复杂度也是O(1)级别的。
2. 复杂度震荡
问题:
假如一个数组的容量为8,此时数组已满,我再调用addLast()方法,此时会调用一次resize()的扩容方法,此次操作复杂度为O(n),下一步操作,我调用removeLast()操作。此时会调用一次resize()的缩容方法,此次操作复杂度又是O(n),如此循环。
问题原因:
removeLast()时,resize()的缩容操作过急(Eager)
解决:
lazy方式
扩容操作后,我们的数组容量为16。我们修改缩容的resize()方法,当数组内元素的数量占数组容量的1/4时,我们再将数组的容量缩小至原来的1/2.这样就避免了复杂度震荡这种情况的发生.
//当元素的个数小于数组长度的1/2时,则数组长度也1/2
if(size<data.length/2) {
resize(data.length/2);
}
//修改
if(size<data.length/4&&data.lentgth/4!=0) {
resize(data.length/2);
}
算法
1. 力扣26:删除排序数组中的重复项
26. 删除有序数组中的重复项 - 力扣(LeetCode) (leetcode-cn.com)
我的答案
public class RemoveRepeat_26 {
public static void main(String[] args) {
int[] nums = {1,1,1,2};
int length = removeDuplicates(nums);
for (int i = 0; i < length; i++) {
System.out.println(nums[i]);
}
}
public static int removeDuplicates(int[] nums) {
int pointer = 0;
int var = 0;
int length = 1;
int repeat = 0;
for (int i = pointer + 1; i < nums.length-repeat; i++) {
//判断元素是否重复
if (nums[pointer] == nums[i]) {
//重复元素的个数
var++;
if (var == nums.length - 1)
//全是一个数
return 1;
} else {
length++;
for (int j = pointer; j < nums.length - var; j++) {
nums[j] = nums[j + var];
}
pointer++;
repeat+=var;
i = pointer;
var = 0;
}
}
return length;
}
}
2. 力扣189:旋转数组
189. 轮转数组 - 力扣(LeetCode) (leetcode-cn.com)
我的答案
public class RotateArray {
public static void main(String[] args) {
int[] nums = {1, 2, 3, 4, 5, 6, 7};
rotateTwo(nums, 3);
for (int i = 0; i < nums.length; i++) {
System.out.println(nums[i]);
}
}
//方法一:一个一个移动,超时
public static void rotateOne(int[] nums, int k) {
if (k >= nums.length) k = k % nums.length;
while (k > 0) {
int val = nums[nums.length - 1];
for (int i = nums.length - 1; i > 0; i--) {
nums[i] = nums[i - 1];
}
nums[0] = val;
k--;
}
}
//方法二:旋转后,找对应规律。成功。
public static void rotateTwo(int[] nums, int k) {
if (k >= nums.length) k = k % nums.length;
int[] brr = new int[nums.length];
for (int i = 0; i < nums.length; i++) {
brr[i] = nums[i];
}
for (int i = 0; i < nums.length; i++) {
nums[i] = brr[(i + nums.length - k) % nums.length];
}
}
}
3. 力扣217:存在重复元素
217. 存在重复元素 - 力扣(LeetCode) (leetcode-cn.com)
我的答案
public class JudgeRepeat_217 {
public static void main(String[] args) {
int[] nums = {1, 2, 3, 4};
System.out.println(containsDuplicate(nums));
}
public static boolean containsDuplicate(int[] nums) {
if (nums.length==0){
return false;
}
Arrays.sort(nums);
for (int i = 0; i < nums.length - 1; i++) {
if (nums[i] == nums[i + 1]) {
return true;
}
}
return false;
}
}
stream流
public static boolean containsDuplicateTwo(int[] nums) {
return Arrays.asList(nums).stream().distinct().count() < nums.length;
}
使用set
public boolean containsDuplicate(int[] nums) {
Set<Integer> set = new HashSet<Integer>();
for (int x : nums) {
if (!set.add(x)) {
return true;
}
}
return false;
}
4. 力扣136:只出现一次的数字
只出现一次的数字 - 只出现一次的数字 - 力扣(LeetCode) (leetcode-cn.com)
我的答案
public class OnlyAppearOneNum_136 {
public static void main(String[] args) {
int[] nums = {4, 1, 2, 1, 2};
singleNumber(nums);
}
public static int singleNumber(int[] nums) {
Arrays.sort(nums);
for (int i = 0; i < nums.length - 1; i += 2) {
if (nums[i] != nums[i + 1]) {
return nums[i];
}
}
if (nums.length % 2 != 0) {
return nums[nums.length - 1];
}
return -1;
}
}
异或运算
异或运算有以下三个性质。
- 任何数和 0做异或运算,结果仍然是原来的数,即 a ⊕ 0=a。
- 任何数和其自身做异或运算,结果是 0,即 a⊕a=0。
- 异或运算满足交换律和结合律,即 a⊕b⊕a=b⊕a⊕a=b⊕(a⊕a)=b⊕0=b。
4⊕1⊕2⊕1⊕2
=100⊕001⊕010⊕001⊕010
=101⊕010⊕001⊕010
=101⊕001⊕010
=100⊕010
=100
=4
public static int singleNumberTwo(int[] nums) {
int single = 0;
for (int num : nums) {
single ^= num;
}
return single;
}