数据结构(1)-数组
填补那些摸棱两可的后知后觉
这一篇幅我们将重点介绍数组这种非常重要的数据结构,并实现一个自己的数组类。
一,什么是数组
数组简单来说就是将所有的数据排成一排存放在系统分配的一个内存块上,通过使用特定元素的索引作为数组的下标,可以在常数时间内访问数组元素的这么一个结构;
二,为什么能在常数时间内访问数组元素?
为了访问一个数组元素,该元素的内存地址需要计算其距离数组基地址的偏移量。需要用一个乘法计算偏移量,再加上基地址,就可以获得某个元素的内存地址。首先计算元素数据类型的存储大小,然后将它乘以元素在数组中的索引,最后加上基地址,就可以计算出该索引位置元素的地址了;整个过程可以看到需要一次乘法和一次加法就完成了,而这两个运算的执行时间都是常数时间,所以可以认为数组访问操作能在常数时间内完成;
三、数组的优缺点
优点:
- 简单易用
- 访问元素快,(查询,更新)
缺点:
- 大小固定:数组的大小是静态的,在使用之前必须制定其大小
- 分配一个连续空间:数组初始分配空间时,有时候无法分配能存储整个数组的内存空间(当数组规模太大时);
- 基于位置的插入操作实现复杂:如果要在数组中的给定位置插入元素,那么可能就会需要移动存储在数组中的其他元素,这样才能腾出指定的位置来放插入的新元素;而如果在数组的开始位置插入元素,那么这样的移动操作开销就会很大。
思考?
我们知道,数组的明显缺点是在创建之前需要提前声明好要使用的空间,那么当我们空间满了该如何处理呢?又该如何删除元素呢?在Java中提供给我们的默认数组是不支持这些功能的,我们需要开发属于自己的数组类才行;
四、使用泛型实现一个自己的数组类
下面我们一起来实现一个属于自己的数组类,并且具备基本的增删改查功能。
1、我们需要一个成员变量来保存数据,还需要一个变量来记录我们数据的个人。
public class Array<E> { /** * 存放的数据 */ private E data; /** * 用于记录的个数 */ private int size; /**=======一些基本的增删改查方法=======**/ }
2、创建构造函数来初始话我们的数组,两种方式,一种是指定我们的容量大小,还有直接使用默认的容量大小
/** * 指定一个默认的大小 */ private static final int DEFAULT_CAPACITY = 10; /** * 指定capacity大小 * @param capacity */ public Array(int capacity){ data = (E[]) new Object[capacity]; size = 0; } /** * 默认容量 */ public Array(){ this(DEFAULT_CAPACITY); }
3、成员方法,为了实现我们自己的动态数组,在增加和删除中,我们对临界值进行了判断,动态的增加或者缩小数组的大小
/**=======一些基本的增删改查方法=======**/ // 获取数组的容量 public int getCapacity() { return data.length; } // 获取数组中的元素个数 public int getSize() { return size; } // 返回数组是否为空 public boolean isEmpty() { return size == 0; } // 在index索引的位置插入一个新元素e public void add(int index, E e) { if (index < 0 || index > size) throw new IllegalArgumentException("Add failed. 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) { add(size, e); } // 在所有元素前添加一个新元素 public void addFirst(E e) { add(0, e); } // 获取index索引位置的元素 public E get(int index) { if (index < 0 || index >= size) throw new IllegalArgumentException("Get failed. Index is illegal."); return data[index]; } // 修改index索引位置的元素为e public void set(int index, E e) { if (index < 0 || index >= size) throw new IllegalArgumentException("Set failed. Index is illegal."); data[index] = e; } // 查找数组中是否有元素e public boolean contains(E e) { for (int i = 0; i < size; i++) { if (data[i].equals(e)) return true; } return false; } // 查找数组中元素e所在的索引,如果不存在元素e,则返回-1 public int find(E e) { for (int i = 0; i < size; i++) { if (data[i].equals(e)) return i; } return -1; } // 从数组中删除index位置的元素, 返回删除的元素 public E remove(int index) { if (index < 0 || index >= size) throw new IllegalArgumentException("Remove failed. Index is illegal."); E ret = data[index]; for (int i = index + 1; i < size; i++) data[i - 1] = data[i]; size--; data[size] = null; // loitering objects != memory leak if (size == data.length / 4 && data.length / 2 != 0) resize(data.length / 2); return ret; } // 从数组中删除第一个元素, 返回删除的元素 public E removeFirst() { return remove(0); } // 从数组中删除最后一个元素, 返回删除的元素 public E removeLast() { return remove(size - 1); } // 从数组中删除元素e public void removeElement(E e) { int index = find(e); if (index != -1) remove(index); } @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(); } // 将数组空间的容量变成newCapacity大小 private void resize(int newCapacity) { E[] newData = (E[]) new Object[newCapacity]; for (int i = 0; i < size; i++) newData[i] = data[i]; data = newData; }
五、简单时间复杂度分析
添加操作
在添加操作中,我们可以明显看到,addLast()
方法是与n无关的,所以为O(1)复杂度;而addFirst()
和add()
方法都涉及到挪动数组元素,所以都是O(n)复杂度,包括resize()
方法;综合起来添加操作的复杂度就是O(n);
addLast(e) O(1)
addFirst(e) O(n)
add(index,e) O(n/2)=O(n)
删除操作
在删除操作中,与添加操作同理,综合来看删除操作的复杂度就是O(n);
removeLast(e) O(1)
removeFirst(e) O(n)
remove(index,e) O(n/2)=O(n)
修改操作
在修改操作中,如果我们知道了需要修改元素的索引,那么我们就可以在常数时间内找到元素并进行修改操作,所以很容易的知道这个操作时一个复杂度为O(1)的操作,所以修改操作的复杂度就是O(1);但另外一种情况是我们不知道元素的索引,那么我们就需要先去查询这个元素,我把这归结到查询操作中去;
set(index,e) O(1)
查询操作
在查询操作中,如果我们已知索引,那么复杂度为O(1);如果未知索引,我们需要遍历整个数组,那么复杂度为O(n)级别;
get(index) O(1)
contains(index) O(n)
find(e) O(n)
总结
以上我们简单分析了我们自己创建的数组类的复杂度:
- 增加:O(n);
- 删除:O(n);
- 修改:已知索引 O(1);未知索引 O(n);
- 查询:已知索引 O(1);未知索引 O(n);
作者:黑米面包派
同步更新: https://www.wujiwen.cn
欢迎一起交流进步