导读:数组是作为最基础的物理数据结构,是很多高级数据结构实现的基石,打好这个基础才能走的更远。本文会从数组的含义到自己手写一个简易版的数组封装类,最后介绍一道 LeetCode 上有关数组面试题。
今天我们要学习的是数组,数组是数据结构中最基础的结构,是很多高级数据结构实现的基石,打好这个基础才能走的更远。数组与链表是物理内存中真实存在的物理结构,二叉树、二叉搜索树、红黑树、图、堆等其他数据结构都是属于逻辑结构,底层都是用数组和链表实现。
因为数组是最基础的数据结构,所以几乎所有的程序设计语言都把数组类型设定为固定的基础变量类型,接下来我们看一下什么是数组:
数组(Array):一种线性表数据结构,用一组连续的内存空间,来存储一组具有相同类型的数据。
通过数组的定义,我们可以看到数组是一种线性表数据结构。线性表,顾名思义,就是将存储的数据排成一条线一样的结构,存储的每个数据最多只有前后两个方向。
数组是用连续内存空间存储相同类型的元素,就是因为有这个限制条件,使得数组按照下标随机访问(随机访问:可以用同等的时间访问到一组数据中的任意一个元素)数组中数据元素时间复杂度达到 O(1) 级别。当然这样的限制也有缺点,在头部或者中间进行数据删除、插入操作时,为了保证这个连续性,需要对数据进行大量的复制迁移来保持此特性。
下面我们通过代码来看一下,数组是如何通过下标来访问数据,使得时间复杂度达到 O(1) 级别的。
// 数组初始化必须为它指定初始容量
int[] i = new int[10];
上面的代码,我们声明了一个数组 i ,i 是这个数组的引用变量,指向这个数组的首地址(计算机会给每个内存单元分配一个地址,计算机通过这个地址来访问内存中的数据)。因为数组是连续的内存空间且数据类型相同,当我们知道了数据的首地址,便可以通过下面的公式,计算出数组中每个元素的内存地址,然后让计算机直接访问,达到 O(1) 级别的时间复杂度。
// i 表示数组下标, base_address 表示数组首地址,data_type_size 表示数组中每个数据大小
a[i]_address = base_address + i * data_type_size
这里有一个注意点,我们是通过数组下标访问数据时,时间复杂度才是 O(1),当我们通过数据查找元素时,我们需要遍历数组查找对应的数据,时间复杂度是 O(n)。
在平时的工作,我们很少会直接去操作数组,数组又太常用了,所以编程语言都会为我们提供数组的包装类,在 Java 中数组对应的包装类就是 ArrayList,这里我们尝试自己写一个简易版的 MyArrayList ,来增强大家对数组的了解。
在动手实现我们自己的 MyArrayList 之前,我们应该先想一下,我们自己的 MyArrayList 应该提供哪些功能。数组是存储数据的,那么我们肯定需要提供操作数据的增删改查功能。围绕增删改查功能,我们还应该提供清空数组元素,查看是否包含指定元素,数组是否为空,数组中有多少元素,元素对应的数组下标是多少。针对于上面这些功能需求,我们制定自己数组的规范,代码如下:
public interface MyArrayList<T> {
/**
* 数组元素数量
* @return size
*/
int size();
/**
* 数组是否为空
* @return true or false
*/
boolean isEmpty();
/**
* 是否包含指定元素
* @return true or false
*/
boolean contains(T element);
/**
* 返回index对应的元素
* @param index
* @return element
*/
T get(int index);
/**
* 修改数组 index 位置的元素
*
* @param index
* @param element
* @return old element
*/
T set(int index, T element);
/**
* 数组末尾添加元素
* @param element
*/
void add(T element);
/**
* 向指定位置添加元素
* @param index
* @param element
*/
void add(int index, T element);
/**
* 删除index位置元素
* @param index
* @return remove element
*/
T remove(int index);
/**
* 删除指定元素
* @return index
*/
int remove(T element);
/**
* 查看元素位置
* @param element
* @return index
*/
int indexOf(T element);
/**
* 删除所有元素
*/
void clean();
}
制定好规范之后,我们就可以动手实现属于自己的数组 MyArrayListImpl 实现类了。
// 我们 MyArrayListImpl 可以实现随机快速访问的,所以这个可以实现 RandomAccess 标志接口
public class MyArrayListImpl<T> implements RandomAccess,MyArrayList {
//存储元素的数组
private T[] elementData;
private static final int DEFAULT_CAPACITY = 1 << 4;
//数组中元素个数
private int size;
public MyArrayList() {
this(DEFAULT_CAPACITY);
}
public MyArrayList(int initialCapacity) {
initialCapacity = initialCapacity > DEFAULT_CAPACITY ? initialCapacity : DEFAULT_CAPACITY;
this.elementData = (T[]) new Object[initialCapacity];
this.size = 0;
}
@Override
public int size() {
return size;
}
@Override
public boolean isEmpty() {
return size == 0;
}
@Override
public boolean contains(Object element) {
return indexOf(element) >= 0;
}
@Override
public T get(int index) {
rangeCheck(index);
return elementData[index];
}
@Override
public T set(int index, T element) {
rangeCheck(index);
T oldElement = elementData[index];
elementData[index] = element;
return oldElement;
}
@Override
public void add(T element) {
ensureCapacity();
elementData[size++] = element;
}
@Override
public void add(int index, T element) {
rangeCheck(index);
ensureCapacity();
// System.arraycopy 是 JDK 提供的 native 方法,比我们用循环复制迁移数据效率好很多
System.arraycopy(elementData, index, elementData, index + 1, size - index);
elementData[index] = element;
size++;
}
@Override
public T remove(int index) {
rangeCheck(index);
T oldElement = elementData[index];
System.arraycopy(elementData, index + 1, elementData, index, size - index - 1);
elementData[--size] = null;
return oldElement;
}
@Override
public int remove(Object element) {
int index = indexOf(element);
// index 大于等于 0 证明存在指定的元素
if (index >= 0) {
remove(index);
size--;
return index;
} else {
throw new IllegalArgumentException("This element does not exits");
}
}
@Override
public int indexOf(Object element) {
// 数组允许存储 null 值,所以要先判断指定元素是否为 null
if (element == null) {
for (int i = 0; i < size; i++) {
if (elementData[i] == null) {
return i;
}
}
} else {
for (int i = 0; i < size; i++) {
if (element.equals(elementData[i])) {
return i;
}
}
}
return -1;
}
@Override
public void clean() {
for (int i = 0; i < size; i++) {
// 帮助垃圾回收
elementData[i] = null;
}
size = 0;
}
private void ensureCapacity() {
if (size >= elementData.length) {
// Arrays.copyOf 底层也是调用的 System.arraycopy ,这里我们扩容为原来数组的 1.5 倍
elementData = Arrays.copyOf(elementData, elementData.length + (elementData.length >> 1));
}
}
// 检测数组越界
private void rangeCheck(int index) {
if (index >= size || index < 0) {
throw new IndexOutOfBoundsException("index must less than size and greater zero");
}
}
}
在介绍完数组概念和我们自己手写了一个简易版数组封装类后,最后我们一起来做一道 LeetCode 上有关数组的面试题,题目如下:
找出数组中重复的数字。
在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。 限制:2 <= n <= 100000
例如输入: [2, 3, 1, 0, 2, 5, 3] 输出:2 或 3力扣(LeetCode)
因为这里我们只学习了数组,所以这里我们求解这个题目,都是基于原生数组的操作,不引入其他数据结构。
按照题目,我们只需要找出数组中任意一个重复的数字,因此遍历数组,遇到重复的数字即返回。下面我们用 for 遍历来完成这个问题。
public int findRepeatNumber(int[] nums) {
for (int i = 0; i < nums.length; i++) {
for (int j = i + 1; j < nums.length; j++) {
if (nums[i] == nums[j]) {
return nums[i];
}
}
}
return -1;
}
上面的代码虽然可以实现题目要求的功能,但是时间复杂度却达到了 O(n²) ,这显然不是一个好的算法。如果这里我们采用 Hash 表这种数据结构,那么时间复杂度就可以达到 O(n) 。正是因为有这样的需求,所以才会在数组和链表的基础上,诞生了那么多高级的数据结构,所以接下来的时间,让我们一起在数据结构与算法的海洋里狗刨吧!