数组是一种最基本的数据结构,它采用一组连续的内存空间按顺序存储对象或基本数据类型。那么我们在Java程序中,声明一个数组会要求先指明其容量大小,那么这个机制就带来了一些问题。
问题1,数组的扩容问题
例如我们创建了一个容量为10的数组,后期需要将其扩容为容量12的数组,这样我们又必须手动去修改它的容量。若只需要修改一两次还好,如果需要修改1000次,10000次呢?
为了解决这个问题,就诞生了Collection集合中的ArrayList,ArrayList是底层基于Object[ ]数组实现的一个动态数组,初始长度为10,其中存在一个扩容机制,关于ArrayList的具体实现可以参考源码,这里就不具体细说了。
问题2,数组的空间浪费问题
例如我们创建了一个容量为10的数组,预期往里面存储10条数据,但是由于某些原因我们只存储了1条数据,那么数组剩余的部分就造成了一定的资源浪费,因此数组的空间效率不是很好,经常会有空闲的区域没有被使用。
对于这个问题,我们不能解决只能优化,毕竟谁也不能预见创建一个数组后是否会需要扩容的情况。于是该优化机制只能通过ArrayList的扩容机制进行完成。具体实现参考ArrayList源码。
需要注意的是:ArrayList每一次进行扩容,都需要进行额外的开销,这对时间性能存在负面影响,因此即便是使用自动扩容的动态数组,我们也需要尽可能的减少改变数组容量大小的操作。
那么总结以下数组的缺点之后,我们再来谈一谈数组的优点。
优点1,查询效率高
因为数组中每个元素都对应着一个独一无二的下标,那么可想而知,数组的查询效率是非常高的。我们可以根据下表在O(1)时间读/写任何元素,因此数组的时间效率是很高的。
根据数组其特点,抛出一个问题
如何用数组实现一个HashMap?
我们都知道HashMap采用key-value的形式进行数据存储,底层采用数组+链表(JDK1.8的红黑树)实现,那么HashMap的查询的特点是什么呢?就是根据key查找对应的value对吧。是不是有点像数组根据下标查找对应的元素。
那么我们就可以通过把数组的下标设置为key,把数组的元素设置为value,这样数组中每个元素都形成一个key-value键值对,也就形成了一个简单的HashMap了。
概念需要通过实践理解,那么抛出问题。
找出数组中重复的数字
在一个长度为n的数组中,所有数字都在0~n-1的范围内,数组中有些数字是重复的,但并不知道几个数字重复了,也不知道重复的数字重复了几次。请找出数组中任意一个重复的数字,例如一个长度为7的数组{2,3,1,0,2,5,3},对应输出2或3。
在解决问题之前我们先理清思路,首先我们需要将数组进行排序(快速排序,时间复杂度O(nlogn)),然后遍历,找出两两重复的数字就好。
public static void duplicatedNum01(int array[]){
if(array == null|| array.length< 0){
System.out.println("error");
return;
}
//1 快速排序
Arrays.sort(array);
for(int i = 0;i< array.length-1;i++){
if(array[i] == array [i+1]){
System.out.println(array[i]);
}
}
}
以上是一个非常简单的实现,我们还可以通过HashMap进行实现:从头到尾扫描这个数组的每个元素,每扫描一个元素,都可以用O(1)的时间复杂度进行判断Hash表中是否已经存在该元素。若没有则添加,若有则输出。这个算法的时间复杂度为O(n)。
public static void duplicatedNum02(int array[]){
if(array == null|| array.length< 0){
System.out.println("error");
return;
}
HashMap map = new HashMap();
for(int i = 0;i<=array.length-1;i++){
if(map.containsValue(array[i])){
System.out.println(array[i]);
}else {
map.put(i, array[i]);
}
}
}
第三个方法思路很清奇…属性我这种菜鸡想破头都不可能想到的算法…从头到尾依次扫描这个数组,当扫描的下标为 i 时,首先比较这个数字(用m表示)是否和下标 i 相等(充分地利用了数组下表有序的特性)
,如果是则扫描下一个数字,如果不是则将这个数字与下标为它本身数值 m 的数字比较,若相等则找到一个重复的数字(当排序到最后,前面的元素基本已经有序)
,若不相等二者交换位置。接下来继续判断数字本身的值是否等于其下标,若相等则扫描下一个数字,若不相等则重复之前的步骤。
太过抽象举个例子吧:{2,3,1,0,4,3},首先下标为0的2 它是否与其下标0相等,不相等则将下标为2的1比较,不相等则交换位置,此时数组为{1,3,2,0,4,3}。还是下标为0的1 它是否与下标0相等,不相等则与下标为1的3比较,也不相等,交换位置,此时数组为{3,1,2,0,4,3},重复步骤…,{0,1,2,3,4,3},这时我们突然发现,前面的数据不知不觉中竟然有序了!此时直接跳到了下标为5的3,它不与下标5相等,与下标为3的3比较,相等,找到重复。
public static void duplicatedNum03(int array[]){
if(array == null|| array.length< 0){
System.out.println("error");
return;
}
for(int i = 0;i<=array.length-1;i++){
while (i != array[i]){
if(array[i] == array[array[i]]){
System.out.println(array[i]);
break;
}
int temp = array[i];
array[i] = array[temp];
array[temp] = temp;
}
}
}
本质也是一个排序算法,尽管有两重循环,但每个数字只需要交换2次,就能够找到属于它的位置,因此总的时间复杂度为O(n),此外,所有操作步骤都在数组中进行,不需要分配额外内存,空间复杂度为O(1)。
毫不夸张地说,第三个算法我是这辈子都想不出来。