数组的基本认知:
如果有面试官问你:为什么数组访问会那么快?我相信你把下面的式子告诉他,那么你就已经赢了一半了。
addr = base_addr + i * type_size
如果你连这个都不知道如何去回答,我觉得,你和我曾经有一段时间一样,也太可爱了。好好听着,大家都是这么一点点学的。
式中的 base_addr 是此数组首元素在内存中基本位置。外加上数组是一组大小连续的内存空间,所以每个元素的大小 type_size相同, i 就是偏移量,通过这个公式 一算 就非常容易计算出a[i] 地址, 从而实现 访问元素一个公式计算一步到位 ,
这就是为什么人们 常说:数组的访问是 O(1) 的 时间复杂度。就是这么快!
为什么索引从0开始而不是从1开始呢?
我们的公式的i 计算的是偏移量。如果从1开始,偏移量每次就要减一,那么CPU要多做一次减法运算,降低了访问速度。
另外还有一个知识点:CPU从内存中加载数据到CPU缓存时,并非只读要访问的特定地址,而是会读取一个数据块到CPU缓存中,因为这CPU缓存这地方访问效率更高,所以在下次访问该数组时,会先从CPU缓存 开始查找,实在找不到才会到内存中读取数据。
从上可知,数组的最大优点就是可以做到:首个元素的地址就是整个数组的地址,因为首个元素的偏移是0 ,数组的索引本质就是数组的偏移量。所以根据偏移量 i,也就是索引,根据公式就 可快速访问查询到特定的元素。所以,数组最好应用在索引有语意的情况,但是也不是所有有寓意的索引都适合数组,如果索引过大,意味着偏移过大,就意味着开辟的内存空间就会很大,比如:身份证账号这种情况。
理想很丰满,现实很骨感,工作中遇到的大部分的应用场景都是索引没有语意的情况。没有语意情况下:如何表示没有元素,如何添加,删除元素?等...一系列的问题,需要处理。带着这些问题,我们要基于java的数组,封装一个属于自己的可动态扩容缩容的动态数组类。
这是我最近通过视频创作的动态数组的Java版本,里面有一个比较完整的从0到1,逐步建构出一个可用的动态数组的全过程实现,支持泛型,动态的扩容缩容,及一些算法分析的知识点,我即将加入一些JDK的动态数组 ArrayList 源码分析。数据结构 Java实现 - 播单 - 优酷视频
视频的详细代码如下:
package com.company.array;
public class DynamicArray<T> {
private static final int DEFAULT_SIZE = 10;
// container:篮子:element:apple
private T[] data;
private int size;
public DynamicArray(int capacity) {
data = (T[]) new Object[capacity];
size = 0;
}
public DynamicArray() {
this(DEFAULT_SIZE);
}
//todo delete
public int getSize(){
return size;
}
// 0 ==> O(n) O(1) ==> index 发生概率 O( n/2 ) 概率正态分布 c * n O(n)
public void add(T e, int index) {
if (index < 0 || index > size) {
throw new IllegalArgumentException(" index illegal");
}
if (data.length == size) {
resize( data.length + (data.length >> 1) );
}
for (int i = size - 1; i >= index; i--) {
data[i + 1] = data[i];
}
data[index] = e;
size++;
}
private void resize(int newCapacity) {
T [] newData = (T[]) new Object[newCapacity];
for(int i = 0 ;i < size; i++){
newData[i] = data[i];
}
data = newData;
}
public void addFirst(T e){
add(e,0);
}
public void addLast(T e){
add(e,size);
// if (data.length == size) {
// throw new IllegalArgumentException(" array is full .");
// }
// data[size] = e;
// size ++ ;
}
/**
* 查询data中,index位置的元素。
* @param index 查询索引
* @return 返回查询索引得到的元素。
*/
public T get(int index){
if (index < 0 || index > size-1) {
throw new IllegalArgumentException(" index illegal");
}
return data[index]; // baseAddress + type_size * i == 访问。
}
public void set(T e,int index){
if (index < 0 || index > size-1) {
throw new IllegalArgumentException(" index illegal");
}
data[index] = e;
}
//contains,indexOf,remove
public boolean contains(T e){
for( int i = 0;i<size;i++){
if(data[i] == e){
return true;
}
}
return false;
}
public int indexOf(T e){
for(int i = 0;i<size;i++){
if(data[i] == e){
return i ;
}
}
return -1;
}
public T remove(int index){
if (index < 0 || index > size-1) {
throw new IllegalArgumentException(" index illegal");
}
// [index...size-1]
T ret = data[index];
for(int i = index;i<size-1;i++){
data[i] =data[i+1];
}
//loitering objects. GC ,内存溢出:GC内存没有被管理到的。
data[size-1] = null;
size--;
if(size <= data.length/2 && data.length/2 != 0){
resize(data.length/2);
}
return ret;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder(String.format("Dynamic Array : size/capacity = %d/%d \n [ ",size,data.length));
for(int i = 0;i<size;i++){
sb.append(data[i]);
if(i !=size-1){
sb.append(" , ");
}
}
sb.append(" ]");
return sb.toString();
}
}
每一行代码思路,已经在视频中我已经介绍过了,这里就不再用文字冗余的再来一遍。愿各位在追求知识的路上,都能有所收获。加油!Go Go !