前言:
作为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];
}