前言:
作为java的一种容器,数组的优缺点同样明显
优点:使用简单 ,查询效率高,内存为连续的区域
缺点:大小固定,不适合动态存储,不方便动态添加
一、自定义实现数组
1、Java中定义数组的三种形式
-
// 第一种:数组格式 类型[] 数组名 = new 类型[数组长度] -
int[] arr = new int[10]; -
// 第二种:定义数组,直接赋值 -
int[] arr = new int[]{11, 22, 33, 44}; -
// 第三种:定义数组,直接赋值 -
int[] arr = {11, 22, 33, 44};
提示:第三种定义方式最常用
2、自定义数组
为了加深对数组的理解,实现自定义数组,定义数组的动态扩减容,且自动维护数组的大小size,从而节省了内存空间资源。
自定义数组的完整代码
-
package com.theone.arrays; -
/** -
* @Description TODO 自定义数组 -
* @Author Pureman -
* @Date 2018/9/1 13:08 -
**/ -
public class MyArray<E> { -
// 定义数组类型 -
private E[] data; -
// 特别提示一点,这里的数组容量默认是10,因此使用data.length不是数组真实的长度 -
// 定义数组大小,实现动态管理数组长度 -
private int size; -
// 构造函数,传入数组的容量capacity构造Array -
public MyArray(int capacity) { -
this.data = (E[]) new Object[capacity]; -
this.size = 0; -
} -
// 空参构造,默认数组容量为10 -
public MyArray() { -
this(10); -
} -
// 获取数组大小 -
public int getSize() { -
return this.size; -
} -
// 获取数组的容量 -
public int getCapacity() { -
return this.data.length; -
} -
// 判断数组是否为空 -
public Boolean isEmpty() { -
return this.size == 0; -
} -
// 定义方法,向数组的最后添加元素 -
public void addLast(E e) { -
add(size, e); -
} -
// 定义方法,向数组的0索引位置添加元素 -
public void addFirst(E e) { -
add(0, e); -
} -
// 定义方法向数组中的任意位置添加元素 -
public void add(int index, E e) { -
if (index < 0 || index > size) { -
throw new IllegalArgumentException("Add failed,index is not expected"); -
} -
if (size == data.length) { -
this.resize(2 * data.length); -
} -
// 遍历数据,将原来数据从index向后移动一位 -
for (int i = size - 1; i >= index; i--) { -
data[i + 1] = data[i]; -
} -
// 覆盖原下标的元素 -
data[index] = e; -
// 数组大小加一 -
size++; -
} -
// 获取数组中指定下标的元素 -
public E get(int index) { -
if (index == size) { -
throw new IllegalArgumentException("Get failed ,because required index is not legal"); -
} -
return data[index]; -
} -
// 更新数组中指定角标的元素 -
public void setIndex(int index, E e) { -
if (index == size) { -
throw new IllegalArgumentException("update failed ,because required index is not legal"); -
} -
data[index] = e; -
} -
// 判断数组中是否包含指定元素 -
public Boolean contains(E e) { -
for (int i = 0; i < size; i++) { -
// ==比较的是对象的引用,equeals比较内容 -
if (data[i].equals(e)) { -
return true; -
} -
} -
return false; -
} -
// 获取元素的索引 -
public int search(E e) { -
for (int i = 0; i < size; i++) { -
if (data[i].equals(e)) { -
return i; -
} -
} -
return -1; -
} -
// 指定索引,删除数组中的元素,并返回被删除的元素值 -
public E remove(int index) { -
if (0 > index || size <= index) { -
throw new IllegalArgumentException("Remove failed.Index is illegal"); -
} -
// 获取待被待删除元素 -
E ret = data[index]; -
// 不需要关注data[size-1]的元素是否还有值,数组封装对外部不暴露,因此调用时并不知道该数组下标为size-1的位置还有元素 -
for (int i = index + 1; i < size; i++) { -
data[i - 1] = data[i]; -
} -
size--; -
// loitering object(游荡的对象) != memory leak(内存泄漏) -
data[size] = null; -
// 判断数组长度是否是容量的1/4,如果是,实现动态减容 -
if (size == data.length / 4 && data.length / 2 != 0) { -
this.resize(data.length / 2); -
} -
// 返回删除的元素 -
return ret; -
} -
// 移除数组中的第一个元素 -
public E removeFirst() { -
return this.remove(0); -
} -
//移除数组中的最后一个元素 -
public E removeLast() { -
return this.remove(this.getSize() - 1); -
} -
// 移除数组中指定元素 -
public void removeElement(E e) { -
int index = this.search(e); -
if (1 != index) { -
this.remove(index); -
} -
} -
@Override -
public String toString() { -
StringBuffer sb = new StringBuffer(); -
sb.append(String.format("Array:size=%d,capacity=%d\n", size, data.length)); -
sb.append("["); -
for (int i = 0; i < size; i++) { -
sb.append(data[i]); -
if (i != size - 1) { -
sb.append(" ,"); -
} -
} -
sb.append("]"); -
return sb.toString(); -
} -
// 动态拓展数组容量 -
private void resize(int capacity) { -
E[] newArr = (E[]) new Object[capacity]; -
for (int i = 0; i < size; i++) { -
newArr[i] = data[i]; -
} -
data = newArr; -
} -
}
二、简单介绍时间复杂度
简单时间复杂度分为
O(1)、O(n)、O(lgn)、O(nlogn)、O(n^2)
注意:大O描述的是算法的运行时间和输入数据之间的关系
-
public static int sum(int[] nums){ -
int sum = 0 ; -
for(int num : nums){ -
sum += num ; -
} -
return sum ; -
}
sum(int[] nums)方法的时间复杂度为O(n),实际复杂度公式为:T = c1*n + c2
参数解释:
- n: nums中元素的个数
- c2:指sum初始化的时间,及返回值sum返回的时间
- c1:for循环内遍历num,与sum累加的时间
为什么要使用大O,叫做O(n)?
在时间复杂度分析过程中,都会假设n趋势于无穷,此时c2所消耗的时间远远小于c1*n的时间,因此分析时忽略常数,此时算法和n呈线性关系
提示:计算时间复杂度都是趋向于最坏情况
另外两个简单的时间复杂度概念:均摊复杂度分析、防止复杂度震荡
均摊复杂度分析(amortized time complexity):resize()
当size == data.length时,自动调用方法resize(2 * data.length)实现数组的动态扩容,扩容后数组的容量是原来的二倍,因此不是每次添加元素都会调用resize()方法进行扩容,所以当自定义数组中的方法addLast()执行所消耗的时间,因由数组arr中的每个元素均应分摊。例如:数组arr的大小为9时,其每个元素所执行的基本操作为15/9次操作
-
public void test02() { -
// 定义数组容量为6 -
MyArray<Integer> arr = new MyArray<Integer>(6); -
// 向数组添加8个元素 -
for (int i = 0; i < 8; i++) { -
arr.addLast(i); -
} -
System.out.println("arr = " + arr); -
// 向数组的最后碎银添加元素100,此时数组大小为9 -
arr.addFirst(100); -
System.out.println("arr = " + arr); -
}
防止复杂度震荡:在resize()扩容后,立刻删除数组中的元素,触发数组自动减容
下面代码演示了复杂度震荡,数组动态扩容后,立即删除元素,数组动态减容,这样会造成时间复杂度增加
-
public void test03() { -
// 定义数组容量为6 -
MyArray<Integer> arr = new MyArray<Integer>(5); -
for (int i = 0; i < 5; i++) { -
arr.addLast(i); -
} -
// 添加元素,此时数组需要动态扩容 -
arr.addLast(100); -
// 动态扩容后,立即删除元素,数组需要动态减容 -
arr.removeLast(); -
}
为了防止复杂度震荡,避免删除元素后,造成复杂度震荡,采用延迟动态减容,代码如下
-
public E remove(int index) { -
if (0 > index || size <= index) { -
throw new IllegalArgumentException("Remove failed.Index is illegal"); -
} -
// 不需要关注data[size-1]的元素是否还有值,数组封装对外部不暴露,因此调用时并不知道该数组下标为size-1的位置还有元素 -
for (int i = index + 1; i < size; i++) { -
data[i - 1] = data[i]; -
} -
size--; -
// loitering object(游荡的对象) != memory leak(内存泄漏) -
data[size] = null; -
// 判断数组长度是否是容量的1/4,如果是,实现动态减容 -
if (size == data.length / 4 && data.length / 2 != 0) { -
this.resize(data.length / 2); -
} -
return data[index]; - }