首先,我是一个phper,可是毕竟php是一个脚本语言,若是使用脚本语言去理解数据结构具备必定的局限性。由于脚本语言是不须要编译的,若是你的语法写的不错,可能执行起来会要比用一个更好的数据结构来的更快、更高效(在数据量不大的状况下)。并且数据结构是脱离任何一门语言存在的。因此,下面会选用java去更深刻的理解数据结构。
注:这里不会去过多的解释java的语法。php
int[] arr = new int[10];
int[] arr = new int[] {10, 20, 30};
arr[2]
。
学习任何一个数据结构,
CRUD
必不可少。下面,让咱们来一块儿一步步完善属于咱们本身的数组的增、删、改、查
public class Array { // 数组的实际大小 private int size; // 数组 private int[] data; // 构造函数,根据传入的容纳量定义一个int类型的数组 public Array(int capacity) { data = new int[capacity]; size = 0; } // 重载,没有传入容纳量,定义一个长度为10的int类型数组 public Array() { this(10); } // 数组的实际大小 public int getSize() { return size; } // 数组的容纳量 public int getCapacity() { return data.length; } // 数组是否为空 public boolean isEmpty() { return size == 0; } }
//往数组的任意位置插入 public void add(int index, int ele) { // 数组已满 if (size == data.length) { throw new IllegalArgumentException("add failed. arr is full"); } // 插入的索引位不合法 if (index < 0 || index >= size) { throw new IllegalArgumentException("add failed. index < 0 or index >= size"); } // 从index向后的全部元素均向后赋值 for (int i = size - 1; i >= index; i--) { data[i + 1] = data[i]; } data[index] = ele; size++; } // 第一个位置插入 public void addFirst(int ele) { add(0, ele); } // 最后一个位置插入 public void addLast(int ele) { add(size, ele); }
// 查询index索引位置的元素 public int get(int index) { if (index < 0 || index >= size) { throw new IllegalArgumentException("get failed. index is illegal"); } return data[index]; } // 查询ele元素的索引,不存在返回-1 public int find(int ele) { for (int i = 0; i < size; i++) { if (data[i] == ele) { return i; } } return -1; } // 更新Index的元素 public void set(int index, int ele) { if (index < 0 || index >= size) { throw new IllegalArgumentException("get failed. index is illegal"); } data[index] = ele; }
// 根据索引删除数组中的第一个ele,返回ele public int remove(int index) { if (index < 0 || index >= size) { throw new IllegalArgumentException("remove failed. index is illegal"); } for (int i = index + 1; i < size; i++) { data[i - 1] = data[i]; } size--; return data[index]; } // 删除第一个元素 public int removeFirst() { return remove(0); } // 删除最后一个 public int removeLast() { return remove(size - 1); } // 删除指定元素 public void removeElement(int ele) { int index = find(ele); if (index != -1) { remove(index); } }
Override public String toString() { StringBuffer res = new StringBuffer(); 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(); } // 查询数组中是否包含元素ele public boolean contain(int ele) { for (int i = 0; i < size; i++) { if (data[i] == ele) { return true; } } return false; }
注:经过以上方法咱们已经建立了一个最最最最最基本的数组类(见下图)。固然,你也能够去添加一些本身须要的方法,例如:removeAll
、findAll
之类的。java
可是,咱们如今的数组只支持int类型,太过局限。接下来,咱们去给咱们的数组升华一哈~
首先,为何我要在 任意这两个字加上引号,由于java的泛型不支持基本数据类型,只能是类的对象。
可是,这并不表明若是咱们使用了泛型,就不可使用基本数据类型了,由于每个基本数据类型都有一个对应的 包装类。
使用泛型的时候,咱们只须要传入对应的包装类便可。
基本数据类型 | 包装类 |
---|---|
boolean | Boolean |
byte | Byte |
char | Char |
short | Short |
int | Int |
long | Long |
float | Float |
double | Double |
public class ArrayNew<E> { // 数组的实际大小 private int size; // 数组 private E[] data; // 构造函数,根据传入的容纳量定义一个 E 类型的数组 public ArrayNew(int capacity) { // 强转 data = (E[]) new Object[capacity]; size = 0; } // 重载,没有传入容纳量,定义一个长度为10的int类型数组 public ArrayNew() { this(10); } // 数组的实际大小 public int getSize() { return size; } // 数组的容纳量 public int getCapacity() { return data.length; } // 数组是否为空 public boolean isEmpty() { return size == 0; } // 往数组的任意位置插入 public void add(int index, E ele) { // 数组已满 if (size == data.length) { throw new IllegalArgumentException("add failed. arr is full"); } // 插入的索引位不合法 if (index < 0 || index > size) { throw new IllegalArgumentException("add failed. index < 0 or index > size"); } // 从index向后的全部元素均向后赋值 for (int i = size - 1; i >= index; i--) { data[i + 1] = data[i]; } data[index] = ele; size++; } // 第一个位置插入 public void addFirst(E ele) { add(0, ele); } // 最后一个位置插入 public void addLast(E ele) { add(size, ele); } // 查询index索引位置的元素 public E get(int index) { if (index < 0 || index >= size) { throw new IllegalArgumentException("get failed. index is illegal"); } return data[index]; } // 查询ele元素的索引,不存在返回-1 public int find(E ele) { for (int i = 0; i < size; i++) { if (data[i].equals(ele)) { return i; } } return -1; } // 更新Index的元素 public void set(int index, E ele) { if (index < 0 || index >= size) { throw new IllegalArgumentException("get failed. index is illegal"); } data[index] = ele; } // 根据索引删除数组中的第一个ele,返回ele public E remove(int index) { if (index < 0 || index >= size) { throw new IllegalArgumentException("remove failed. index is illegal"); } E result = data[index]; for (int i = index + 1; i < size; i++) { data[i - 1] = (data[i]); } // 空间释放,垃圾回收会自动回收 data[--size] = null; return result; } // 删除第一个元素 public E removeFirst() { return remove(0); } // 删除最后一个 public E removeLast() { return remove(size - 1); } // 删除指定元素 public void removeElement(E ele) { int index = find(ele); if (index != -1) { remove(index); } } // 查询数组中是否包含元素ele public boolean contain(E ele) { for (int i = 0; i < size; i++) { if (data[i].equals(ele)) { return true; } } return false; } @Override public String toString() { StringBuffer res = new StringBuffer(); 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(); } }
注:建立数组时,只需ArrayNew<Student> arr = new ArrayNew<>(20);
便可。数组
原理:其实,动态数组的原理很是简单,若是咱们但愿咱们的数组具备可伸缩性,只须要咱们在添加或者删除元素时判断size
是否到达临界。而后去建立一个新capacity
的数组,而后把旧数组的引用指向新数组便可。
因此,咱们上述代码的改变极小,只须要改变add
、remove
便可。而后添加一个resize
方法。
// 往数组的任意位置插入 public void add(int index, E ele) { // 插入的索引位不合法 if (index < 0 || index > size) { throw new IllegalArgumentException("add failed. index < 0 or index > size"); } // 若是size == data.length,数组长度已满 if (size == data.length) { resize(data.length * 2); } // 从index向后的全部元素均向后赋值 for (int i = size - 1; i >= index; i--) { data[i + 1] = data[i]; } data[index] = ele; size++; } // 根据索引删除数组中的第一个ele,返回ele public E remove(int index) { if (index < 0 || index >= size) { throw new IllegalArgumentException("remove failed. index is illegal"); } E result = data[index]; for (int i = index + 1; i < size; i++) { data[i - 1] = (data[i]); } // 空间释放,垃圾回收会自动回收 data[--size] = null; // 减少数组长度,不要浪费空间 if (size == data.length / 2 && size != 0) { resize(size); } return result; } // 自动伸缩数组 private void resize(int newCapacity) { E[] newData = (E[])new Object[newCapacity]; for (int i = 0; i < size; i++) { newData[i] = data[i]; } data = newData; }
经过上面的分析和代码实现,咱们封装了一个本身的数组,而且实现了一些数组 最基本的功能,包括支持增、删、改、查、支持任意数据类型以及动态数组。那么咱们就来分析一下咱们本身封装数组的复杂度。
操做 | 复杂度 |
---|---|
增 | O(n) |
删 | O(n) |
改 | 已知索引O(1);未知索引O(n) |
查 | 已知索引O(1);未知索引O(n) |
可是:在咱们的数组中,增和删咱们都调用了resize
方法,若是size < data.length
,其实咱们执行addLast
复杂度只是O(1)
而已(removeLast
同理)。因此,咱们应该怎么去分析resize
方法所带来的复杂度呢?数据结构
让咱们拿 增 来举例
方法 | 复杂度 |
---|---|
addLast(ele) | O(1) |
addFirst(ele) | O(n) |
add(index, ele) | O(n/2) = O(n) |
resize(newCapacity) | O(n) |
其实,在执行addLast
的时候,咱们并非每次都会触发resize
方法,更多的时候,复杂度只是O(1)
而已。
比方说:
当前的capacity = 8
,而且每一次添加操做都使用addLast
,第9次addLast
操做,触发resize
,总共17次基本操做(resize
方法会进行8次操做,addLast
方法进行9次操做)。平均,每次addLast
操做,进行2次基本操做(17 / 9 ≈ 2)。
假设:capacity = n
, n + 1
次addLast
,触发resize
,总共进行了2n + 1
次操做,平均每次addLast
操做,进行了2次基本操做。app
这样均摊计算,时间复杂度是O(1)!ide
让咱们来假设这样一种状况:
当size == data.length
时,咱们执行了addLast
方法添加一个元素,这个时候咱们须要去执行resize
方法,此时,addLast
的复杂度为O(n)
。
而后,我去removeLast
,此时的removeLast
复杂度也是O(n)
。
再而后,我再去执行addLast
。
.
.
.
有没有发现,在这样一种极端状况下,addLast
和removeLast
的复杂度变成了O(n)
,其实,这个就是复杂度的震荡。函数
为何咱们会产生这种震荡?学习
add
状况下,咱们去扩容数组无可厚非。可是remove
状况下,咱们马上去缩容数组就有点不合适了。怎么去解决这种状况?this
Eager
Lazy
的方式:当size == data.length / 2
,咱们不要马上缩容,当size == data.length / 4
时,咱们才去缩容,就能够很好的解决这种震荡。
具体代码以下,其实只是对
remove
进行了极小的改变
public E remove(int index) { if (index < 0 || index >= size) { throw new IllegalArgumentException("remove failed. index is illegal"); } E result = data[index]; for (int i = index + 1; i < size; i++) { data[i - 1] = data[i]; } // 空间释放,垃圾回收会自动回收 data[--size] = null; // 减少数组长度,不要浪费空间,防止震荡 if (size == data.length / 4 && data.length / 2 != 0) { resize(data.length / 2); } return result; }