目录
(一) JAVA中的数组
1.定义
数组属于引用数据类型, 用来存储固定大小的同类型元素的容器, 数据类型可以是8种基本的数据类型, 也可以是引用数据类型…
2.两种初始化方式
- 动态初始化: 声明并开辟指定长度的数组, 会为数组之中的每个元素附上该数据类型的默认值
(byte,short,int,long: 0; float,double: 0.0; boolean: false; char: ‘\u0000’; 引用类型: null)
数据类型[] 数组名称 = new 数据类型[数组长度];
int[] arr = new int[3];
- 静态初始化: 声明数组同时指定其内容元素.
数据类型[] 数组名称 = new 数据类型[]{...元素};
String[] students = new String[] {"张三", "李四"};
数据类型[] 数组名称 = {...元素};
int[] grades = {1, 2, 3, 4};
3.遍历
存储到数组中的每个元素,都有属于自己的自动编号(索引), 因此通过索引来访问数组中的元素(快速查询).
for (int i = 0; i < grades.length; i++) {
System.out.println(grades[i]);
}
数组的优点:
1. 按照索引查询元素速度快
2. 能存储大量数据
3. 按照索引遍历数组方便
数组中存在的问题:
1. 数组的大小一经确定不能改变
2. 数组只能存储一种类型的数据
3. 如何添加元素, 删除元素? 当添加一个元素时, 数组大小已满等等问题…
因此对数组进行了二次封装
(二) 二次封装数组
1. 新建二次封装数组实体类Array
/**
* 二次封装数组实体
*
* @author Administrator
*
*/
public class Array {
/**
* 存储数据的源数组
*/
private int[] data;
/**
* 描述data源数组有效元素的个数
*/
private int size;
/**
* 构造函数, 传入数组的容量capacity构造Array
*
* @param capacity
*/
public Array(int capacity) {
data = new int[capacity];
size = 0;
}
/**
* 无参数的构造函数, 默认数组的容量capacity=10
*/
public Array() {
this(10);
}
/**
* 获取数组中的有效元素个数
*
* @return
*/
public int getSize() {
return size;
}
/**
* 获取数组的容量
*
* @return
*/
public int getCapacity() {
return data.length;
}
/**
* 返回数组中有效元素是否为空
*
* @return
*/
public boolean isEmpty() {
return size == 0;
}
}
2. 二次封装数组的增加操作
- 向所有元素最后添加一个元素e
public void addLast(int e) {
// 二次封装数组的有效元素的个数等于源数组的长度时, 抛出非法参数异常(待优化)
if(size == data.length) {
throw new IllegalArgumentException("addList() failed. Array is full.");
}
data[size] = e;
size++;
}
- 在第index个位置插入一个新元素e
public void add(int index, int e) {
// 二次封装数组的有效元素的个数等于源数组的长度时, 数组容量已满, 抛出非法参数异常
if (size == data.length) {
throw new IllegalArgumentException("add() failed. Array is full.");
}
/*
* index索引必须大于0, 且小于或等于数组有效元素的个数, 为了保证数组数据在一组连续的内存空间紧密排列 当索引等于有效元素的个数时,
* 当index索引等于size有效元素的个数, 相当于向数组最后添加一个元素
*/
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];
}
printData();
data[index] = e;
size++;
}
- 优化addLast()方法: 在add()方法中, 当index索引等于size有效元素的个数, 相当于向数组最后添加一个元素
public void addLast(int e) {
// 当index索引等于size有效元素的个数, 相当于向数组最后添加一个元素
add(size, e);
}
- 同理, 在所有元素前添加一个新元素
public void addFirst(int e) {
add(0, e);
}
3. 二次封装数组的查询操作
- 覆盖重写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(", ");
}
}
res.append("]");
return res.toString();
}
测试:
public static void main(String[] args) {
Array array = new Array(20);
for (int i = 0; i < 10; i++) {
array.addLast(i);
}
System.out.println(array);
array.add(1, 100);
System.out.println(array);
array.addFirst(-1);
System.out.println(array);
}
- 获取index索引位置的元素
public int get(int index) {
// 判断索引的合法性, 且保证无法查询未使用空间的元素
if(index < 0 || index >= size) {
throw new IllegalArgumentException("get() failed. Index is illegal.");
}
return data[index];
}
- 获取第一个元素
public E getFirst() {
return get(0);
}
- 获取最后一个元素
public E getFirst() {
return get(size - 1);
}
4. 二次封装数组的修改操作
- 修改index索引位置的元素
public void set(int index, int e) {
// 判断索引的合法性, 且保证无法查询未使用空间的元素
if(index < 0 || index >= size) {
throw new IllegalArgumentException("get() failed. Index is illegal.");
}
data[index] = e;
}
5. 二次封装数组的删除操作
- 从数组中删除index位置的元素, 返回删除的元素
public int remove(int index) {
if(index < 0 || index >= size) {
throw new IllegalArgumentException("get() failed. Index is illegal.");
}
int res = data[index];
/*
* 把 index+1索引处 至 最后一个元素 向前移一个位置, 且数组的有效个数size减一: 此操作会导致data[size]存在值且data[size]与data[size-1]的值相同
* 1. 由于getget(int index)的方法, 在获取指定索引处元素时会判断索引的合法性, 且保证无法查询未使用空间的元素
* 2. 居于数组的特性: 数组之中的每个元素附上该数据类型的默认值, 因此无论是默认值还是data[size-1]的值都可以
*
* 所以这一点无关大雅...
*/
for (int i = index + 1; i < size; i++) {
data[i - 1] = data[i];
}
size--;
return res;
}
- 从数组中删除第一个元素, 返回删除的元素
public int removeFirst() {
return remove(0);
}
- 从数组中删除最后一个元素, 返回删除的元素
public int removeLast() {
return remove(size-1);
}
- 从数组中删除元素e
public void removeElement(int e) {
int index = find(e);
if(index != -1) {
remove(index);
}
}
- 扩展: 采用递归的方式, 从数组中删除所有元素e
public void removeAllElement(int e) {
int index = find(e);
if(index != -1) {
remove(index);
removeAllElement(e);
} else {
return;
}
}
6. 二次封装数组的包含、搜索操作
- 查找数组中是否存在元素e
public boolean contain(int e) {
for (int i = 0; i < size; i++) {
if(data[i] == e) {
return true;
}
}
return false;
}
- 查找数组中元素e所在的索引, 如果不存在元素e, 则返回-1
public int find(int e) {
for (int i = 0; i < size; i++) {
if(data[i] == e) {
return i;
}
}
return -1;
}
二次封装数组中存在的问题值之一: 数组作为存储数据的容器, 不能只存储一种类型的变量
因此对二次封装数组引入泛型
(三) 二次封装数组引入泛型
引入泛型的好处和注意事项
好处: 让二次封装数组(数据结构)可以放置任何的引用类型数据
注意:
- 在有参构造函数中动态初始化泛型数组, 不存在语法:
data = new E[capaticy];
需要借助 所有类型的父类Object, 然后进行强制转换(向下转换)
data = (E[]) new Object[capacity];- 引入泛型后, 存储的都是引用类型. 引用类型的比较使用 equals()方法, 且需要重写hashCode() 和 equals() 方法.
public class Array<E> {
/**
* 存储数据的源数组
*/
private E[] data;
/**
* 描述data源数组有效元素的个数
*/
private int size;
/**
* 构造函数, 传入数组的容量capacity构造Array
*
* @param capacity
*/
@SuppressWarnings("unchecked")
public Array(int capacity) {
data = (E[]) new Object[capacity];
size = 0;
}
/**
* 无参数的构造函数, 默认数组的容量capacity=10
*/
public Array() {
this(10);
}
/**
* 获取数组中的有效元素个数
*
* @return
*/
public int getSize() {
return size;
}
/**
* 获取数组的容量
*
* @return
*/
public int getCapacity() {
return data.length;
}
/**
* 返回数组中有效元素是否为空
*
* @return
*/
public boolean isEmpty() {
return size == 0;
}
/**
* 向所有元素最后添加一个元素e
*
* @param e
*/
public void addLast(E e) {
// // 二次封装数组的有效元素的个数等于源数组的长度时, 数组容量已满, 抛出非法参数异常(待优化)
// if (size == data.length) {
// throw new IllegalArgumentException("addList() failed. Array is full.");
// }
// data[size] = e;
// size++;
// 当index索引等于size有效元素的个数, 相当于向数组最后添加一个元素
add(size, e);
}
/**
* 在第index个位置插入一个新元素e: 把 最后一个元素 至 index索引处的元素 向后移一个位置. 从最后一个元素开始依次移动, 防止元素发生覆盖
*
* @param index
* @param e
*/
public void add(int index, E e) {
// 二次封装数组的有效元素的个数等于源数组的长度时, 数组容量已满, 抛出非法参数异常
if (size == data.length) {
throw new IllegalArgumentException("add() failed. Array is full.");
}
/*
* index索引必须大于0, 且小于或等于数组有效元素的个数, 为了保证数组数据在一组连续的内存空间紧密排列 当索引等于有效元素的个数时,
* 当index索引等于size有效元素的个数, 相当于向数组最后添加一个元素
*/
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++;
}
/**
* 在所有元素前添加一个新元素
*
* @param e
*/
public void addFirst(E e) {
add(0, e);
}
/**
* 获取index索引位置的元素
*
* @param index
* @return
*/
public E get(int index) {
// 判断索引的合法性, 且保证无法查询未使用空间的元素
if(index < 0 || index >= size) {
throw new IllegalArgumentException("get() failed. Index is illegal.");
}
return data[index];
}
/**
* 修改index索引位置的元素
*
* @param index
* @param e
*/
public void set(int index, E e) {
// 判断索引的合法性, 且保证无法查询未使用空间的元素
if(index < 0 || index >= size) {
throw new IllegalArgumentException("get() failed. Index is illegal.");
}
data[index] = e;
}
/**
* 查找数组中是否存在元素e
*
* @param e
* @return
*/
public boolean contain(E e) {
for (int i = 0; i < size; i++) {
if(data[i].equals(e)) {
return true;
}
}
return false;
}
/**
* 查找数组中元素e所在的索引, 如果不存在元素e, 则返回-1
*
* @param e
* @return
*/
public int find(E e) {
for (int i = 0; i < size; i++) {
if(data[i] == e) {
return i;
}
}
return -1;
}
// 从数组中删除index位置的元素, 返回删除的元素
public E remove(int index) {
if(index < 0 || index >= size) {
throw new IllegalArgumentException("get() failed. Index is illegal.");
}
E res = data[index];
/*
* 把 index+1索引处 至 最后一个元素 向前移一个位置, 且数组的有效个数size减一: 此操作会导致data[size]存在值且data[size]与data[size-1]的值相同
* 1. 由于getget(int index)的方法, 在获取指定索引处元素时会判断索引的合法性, 且保证无法查询未使用空间的元素
* 2. 居于数组的特性: 数组之中的每个元素附上该数据类型的默认值, 因此无论是默认值还是data[size-1]的值都可以
*
* 所以这一点无关大雅...
*/
for (int i = index + 1; i < size; i++) {
data[i - 1] = data[i];
}
size--;
return res;
}
/**
* 从数组中删除第一个元素, 返回删除的元素
*
* @return
*/
public E removeFirst() {
return remove(0);
}
/**
* 从数组中删除最后一个元素, 返回删除的元素
*
* @return
*/
public E removeLast() {
return remove(size-1);
}
/**
* 从数组中删除元素e
*
* @param e
*/
public void removeElement(E e) {
int index = find(e);
if(index != -1) {
remove(index);
}
}
/**
* 从数组中删除所有元素e
*
* @param e
*/
public void removeAllElement(E e) {
int index = find(e);
if(index != -1) {
remove(index);
removeAllElement(e);
} else {
return;
}
}
/**
* 覆盖重写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(", ");
}
}
res.append("]");
return res.toString();
}
}
二次封装数组中存在的问题值之二: 数组作为存储数据的容器, 无法确定容器的空间大小
因此对数组封装动态数组
(四) 动态数组
1.优化add()方法
当数组容量已满时, 让二次封装数组类拥有容量扩容的能力
public void add(int index, E e) {
if (index < 0 || index > size) {
throw new IllegalArgumentException("add() failed. Require index >= 0 and index <= size");
}
// 二次封装数组的有效元素的个数等于源数组的长度时, 数组容量已满, 扩容元素组容量的2倍
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++;
}
/**
* 数组扩容, 新建一个容量为newCapacity的数组赋值给data
* @param capacity
*/
private void resize(int newCapacity) {
@SuppressWarnings("unchecked")
E[] newData = (E[]) new Object[newCapacity];
for(int i = 0; i < size; i++) {
newData[i] = data[i];
}
data = newData;
}
测试
public static void main(String[] args) {
// 空的构造方法默认数组初始容量为 10
Array<Integer> array = new Array<>();
// for循环为数组赋值 0-9
for (int i = 0; i < 10; i++) {
array.addLast(i);
}
System.out.println(array);
// 数组最后添加一个元素, 数组容量已满, 扩容
array.addLast(10);
System.out.println(array);
}
数组的容量capacity由原来的10变成20
2.优化remove()方法
当数组有效元素的个数较少时, 让二次封装数组类拥有容量缩容的能力
public E remove(int index) {
if(index < 0 || index >= size) {
throw new IllegalArgumentException("get() failed. Index is illegal.");
}
E res = 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 res;
}
测试
public static void main(String[] args) {
Array<Integer> array = new Array<>(20);
// for循环为数组赋值 0-10, 数组有效元素个数为 11
for (int i = 0; i < 11; i++) {
array.addLast(i);
}
System.out.println(array);
// 数组删除最后一个元素, 数组有效元素个数为 10 等于数组容量的一半, 缩容至原容量的二分之一
array.removeLast();
System.out.println(array);
}
数组的容量capacity由原来的20变成10
(五) 时间复杂度分析
1.概念
- 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中的元素个数, 函数的运行时间取决于nums中的元素个数, 算法和n呈线性关系. 为什么叫O(n)呢? 是因为呈线性关系的表达式为: T = c1*n + c2, 在此忽略了常数(常数受运行环境, 编码习惯, 硬件等影响无法得出具体的值而忽略).
c1 = 循环取出num的时间 + 求和时间; c2 = 初始化变量sum的时间 + 函数返回结果的时间
a: T = 2*n + 2 O(n)
b: T = 2000*n + 10000 O(n)
c: T = 1nn + 0 O(n^2)
在上面3个算法中, c算法的性能会比a和b算法的性能差, 应为c算法是平方级别的算法.但这有同学会问这取决于n的大小, 这就引出了O的定义: 渐进时间复杂度
O的定义: 渐进时间复杂度, 描述n趋近于无穷的情况下时间的复杂度
2.分析动态数组的时间复杂度
- 添加操作: 整体时间复杂度为 O(n)
函数 | 时间复杂度 | 分析 |
---|---|---|
addLast(e) | O(1) | 直接往size索引处赋值, O(1): 代表此操作消耗的时间与数据规模无关系的, 在常数时间内完成 |
addFirst(e) | O(n) | 数组头部添加, 所有元素向后移一个单位, 与数据规模呈线性关系 |
add(index, e) | O(n/2) = O(n) | 消耗的时间与index的取值有关,范围是 O(1)–O(n), 涉及到概率论知识.平均取值n/2, 2是常数省略 |
总结: | O(n) | 添加操作整体时间复杂度为 O(n), 分析时间复杂度一般考虑最坏的情况,同时还要考虑数组扩容resize() -> O(n) |
- 删除操作: 整体时间复杂度为 O(n)
函数 | 时间复杂度 | 分析 |
---|---|---|
removeLast(e) | O(1) | 直接size–, O(1): 代表此操作消耗的时间与数据规模无关系的, 在常数时间内完成 |
removeFirst(e) | O(n) | 删除数组头部, 所有元素向前移一个单位, 与数据规模呈线性关系 |
remove(index, e) | O(n/2) = O(n) | 消耗的时间与index的取值有关,范围是 O(1)–O(n), 涉及到概率论知识.平均取值n/2, 2是常数省略 |
总结: | O(n) | 删除操作整体时间复杂度为 O(n),同时还要考虑数组缩容resize() -> O(n) |
- 查询操作: 已知索引 O(1); 未知索引 O(n)
函数 | 时间复杂度 | 分析 |
---|---|---|
get(index) | O(1) | 直接通过索引访问, O(1): 代表此操作消耗的时间与数据规模无关系的, 在常数时间内完成 |
contain(e) | O(n) | 需要遍历素组, 查找元素 |
find(e) | O(n) | 需要遍历素组, 查找元素 |
总结: | O(n) | 已知索引 O(1); 未知索引 O(n) |
- 修改操作: 时间复杂度为 O(1)
函数 | 时间复杂度 | 分析 |
---|---|---|
set(index, e) | O(1) | 直接通过索引修改. O(1): 代表此操作消耗的时间与数据规模无关系的, 在常数时间内完成 |
总结: | O(1) | 修改操作整体时间复杂度为 O(1) |
3.防止复杂度震荡
在动态数组中的addLast和removerLast操作中存在这样的问题: 当有个capacity =n容量已满的数组, 进行addLast()操作, 势必会发生扩容resize操作(O(n)), 容量为2n. 添加操作完成后, 立马执行removerLast操作, 删除该元素后, 此时, 元素个数是容量的二分之一, 势必也会发生缩容resize操作(O(n)), 如果两次操作循环执行, 每次都会触发resize操作, 从而发生了复杂度震荡.
出现问题的原因: removerLast 时 resize 过于着急(Eager)
解决方案: 采用Lazy懒惰策略, 当数组有效元素size 等于 容量capacity的四分之一时, 才缩容二分之一, 此时, 再有添加操作时, 也无需扩容.
优化remover()方法
public E remove(int index) {
if(index < 0 || index >= size) {
throw new IllegalArgumentException("get() failed. Index is illegal.");
}
E res = 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) {
resize(data.length / 2);
}
return res;
}