数据结构基础01:动态数组
数据结构的分类
线性结构
动态数组、栈、队列、链表、哈希表等
树结构
二叉树、二分搜索树、AVL、红黑树、堆、B类树、哈夫曼树等
图结构
邻接矩阵、领接表
静态数组
数组最大的优点是可以根据索引快速查询,索引最好有实际的语意,否则最好使用别的数据结构
Java自带数组的索引没有语意,且长度不可变,称为静态数组
因此,可以基于静态数组拓展一些功能,一般为增、删、改、查
public class Algorithm {
public static void main(String[] args) {
Array<String> arr = new Array<>(20);
for (int i = 0; i < 10; i++) {
arr.addLast(String.format("第%d个元素", i));
}
System.out.println(arr);
Array<Student> stu = new Array<>(20);
stu.addLast(new Student("xiaoming", 100));
stu.addLast(new Student("xiaohong", 80));
stu.addLast(new Student("xiaoming", 100));
System.out.println(stu);
}
}
/**
* 基于静态数组,拓展增删改查功能
*/
class Array<E> {
/**
* 底层采用Java的数据类型
* 加上泛型,也可用于自定义类
*/
private E[] data;
private int size;
public Array(int capacity){
/**
* 泛型具有擦除机制,在运行时Java只知道存储的对象是一个Object
* 而对于Java的数组来说,必须先知道它持有的对象的具体类型,而泛型的这种运行时擦除机制违反了数组安全检查的原则
* 因此必须使用泛型声明一个数组,但不能直接用泛型来建立数组的实例,如data = new E[capacity]是错的
* 先实例化一个Object类型的数组,然后强制转换为E[]
*/
data = (E[]) new Object[capacity];
size = 0;
}
/**
* 不指定数组容量时,默认为10
* this()指代的就是构造函数
*/
public Array(){
this(10);
}
/**
* 增
*/
public void add(int index, E element){
if (size == data.length){
throw new IllegalArgumentException("数组已满,无法添加元素");
}
if (index < 0 || index > size){
throw new IllegalArgumentException("索引值非法");
}
for (int i = size - 1; i >= index; i--) {
data[i + 1] = data[i];
}
data[index] = element;
size++;
}
public void addFirst(E element){
add(0, element);
}
public void addLast(E element){
add(size, element);
}
/**
* 删
*/
public void remove(int index){
if (index < 0 || index >= size) {
throw new IllegalArgumentException("索引值非法");
}
for (int i = index; i < size - 1; i++) {
data[i] = data[i + 1];
}
size--;
/**
* 泛型存储的是引用类型,删除只是断开了指针,但不会被回收,会占用存储空间,称为loitering objects
* 因此最好手动将其赋值为null,让JVM进行回收
*/
data[size] = null;
}
public void removeFirst(){
remove(0);
}
public void removeLast(){
remove(size - 1);
}
/**
* 改
*/
public void set(int index, E element){
if (index < 0 || index > size){
throw new IllegalArgumentException("索引值非法");
}
data[index] = element;
}
/**
* 查
*/
public int find(E element){
for (int i = 0; i < size; i++) {
if (data[i].equals(element)){
return i;
}
}
return -1;
}
public E get(int index){
if (index < 0 || index > size) {
throw new IllegalArgumentException("索引值非法");
}
return data[index];
}
public boolean contains(E element){
for (int i = 0; i < size; i++) {
if (data[i].equals(element)) {
return true;
}
}
return false;
}
/**
* 获取数组的元素个数
*/
public int getSize(){
return size;
}
/**
* 获取数组的容量
* 容量是固定的,而元素个数是可变的
*/
public int getCapacity(){
return data.length;
}
/**
* 判断数组是否为空
*/
public boolean isEmpty(){
return size == 0;
}
/**
* 重写toString()方法,打印数组的元素
*/
@Override
public String toString() {
StringBuilder str = new StringBuilder();
str.append(String.format("数组的容量为%d,元素个数为%d\n", data.length, size));
str.append("[");
for (int i = 0; i < size; i++) {
str.append(data[i]);
if (i != size - 1){
str.append(", ");
}
}
str.append("]");
return str.toString();
}
}
/**
* 对自定义类不仅要比较是否相同,还需要进行排序,则需要实现Comparable接口,重写compareTo()方法
*/
class Student implements Comparable{
private String name;
private int score;
public Student(String name, int score){
this.name = name;
this.score = score;
}
@Override
public int compareTo(Object obj) {
Student newObj = (Student) obj;
return this.score - newObj.score;
}
@Override
public String toString() {
return String.format("%s,成绩是%d", name, score);
}
}
动态数组
Java自带的数组属于静态数组,最大的缺点是不能实现扩缩容,并且必须先指定容量
而动态数组就是从底层实现扩缩容,但本节实现的动态数组本质还是使用了静态的数据结构
public class Algorithm {
public static void main(String[] args) {
Array<String> arr = new Array<>(5);
for (int i = 0; i < 10; i++) {
arr.addLast(String.format("第%d个元素", i));
}
/**
* 添加元素个数超过了容量,会进行扩容
*/
System.out.println(String.format("数组的容量为%d", arr.getCapacity()));
System.out.println(arr);
for (int i = 0; i < 5; i++) {
arr.remove(0);
}
/**
* 删除元素导致个数少于容量的一半,会进行缩容
*/
System.out.println(String.format("数组的容量为%d", arr.getCapacity()));
System.out.println(arr);
}
}
class Array<E> {
private E[] data;
private int size;
public Array(int capacity){
data = (E[]) new Object[capacity];
size = 0;
}
public Array(){
this(10);
}
public void add(int index, E element){
if (index < 0 || index > size){
throw new IllegalArgumentException("索引值非法");
}
/**
* 当数组满了以后,容量翻倍
*/
if (size == data.length){
resize(2 * data.length);
}
for (int i = size - 1; i >= index; i--) {
data[i + 1] = data[i];
}
data[index] = element;
size++;
}
public void addLast(E element){
add(size, element);
}
public void remove(int index){
if (index < 0 || index >= size) {
throw new IllegalArgumentException("索引值非法");
}
for (int i = index; i < size - 1; i++) {
data[i] = data[i + 1];
}
size--;
data[size] = null;
/**
* 当数组元素个数少于容量的一半时,容量缩小一半,节省空间
*/
if (size == data.length / 2 && data.length / 2 != 0){
resize(data.length / 2);
}
}
public int getCapacity(){
return data.length;
}
/**
* 定义扩缩容方法
*/
private void resize(int newCapacity){
E[] newData = (E[]) new Object[newCapacity];
for (int i = 0; i < size; i++) {
newData[i] = data[i];
}
data = newData;
}
@Override
public String toString() {
StringBuilder str = new StringBuilder();
str.append(String.format("数组的容量为%d,元素个数为%d\n", data.length, size));
str.append("[");
for (int i = 0; i < size; i++) {
str.append(data[i]);
if (i != size - 1){
str.append(", ");
}
}
str.append("]");
return str.toString();
}
}
动态数组的时间复杂度
增删操作,需要遍历整个数组,时间复杂度为O(n)
改查操作,如果是根据索引来,复杂度为O(1),否则也是O(n)
均摊复杂度
addLast()方法中调用了resize()方法,因此时间复杂度为O(n)
但事实上只有当元素个数超出容量时才会扩容,平均下来相当于每次操作的时间复杂度为O(2)
因此addLast()方法的均摊复杂度为O(1),这比总是考虑最坏的情况更有实际意义
防止复杂度震荡
上例中,均摊复杂度虽然为O(1),但有一种最坏的情况,那就是增加一个元素进行扩容以后,马上又删除该元素进行缩容,如此反复循环的话,复杂度仍会变成O(n),这称为复杂度震荡
为了防止这种情况,可以改为元素个数少于容量的1/4时再减半缩容,这样不会短时间反复出现扩缩容
public void remove(int index){
if (index < 0 || index >= size) {
throw new IllegalArgumentException("索引值非法");
}
for (int i = index; i < size - 1; i++) {
data[i] = data[i + 1];
}
size--;
data[size] = null;
if (size == data.length / 4 && data.length / 2 != 0){
resize(data.length / 2);
}
}