这是这段时间学习数组的一个笔记和总结。
数组概述:
声明一个数组就是在内存空间中划出一串连续的空间,数据具有以下特点:
1. 数组名代表的是连续空间的首地址
2. 通过首地址可以依次访问数组所有元素
3. 元素在数组中的排序叫做下标从零开始
4. 数组可以看成是多个相同数据类型数据的组合,对这些数据的统一管理。
5. 数组变量属引用类型,数组也可以看成是对象,数组中的每个元素相当于该对象的成员变量。
6. 数组中的元素可以是任何类型,包括基本类型和引用类型。
优点:
1. 通过下标访问元素的效率很高,指定下标为n的元素的地址:首地址*元素类型字节数
2. 数组可以保存若干个元素的值。
缺点:
1. 数组长度是固定的不能变的;
2. 数组进行元素的删除和插入操作的时候,效率比较低。需要移动大量的元素
3. 数组元素的类型只能是一种
4. 数组通过内容查找元素的效率比较低的。
5. 数组的元素是连续分配的,所以在heap内存中必须找到连续的内存空间才能容纳数组的所有数据。对内存要求高一些;
6. 数组没有提供任何的封装,所有对元素的操作,都是通过自定义的方法实现的,对数组元素的操作比较麻烦;
一维数组的声明:
1. 一维数组的声明方式:
类型 数组名称[]
类型[] 变量名称;
例如:
int arr[];
int[] arr;
2. java语言中声明数组时不能指定其长度(数组中元素的个数),例如:
int arr[5]; // 会提示编译不通过
数组对象的创建:
1. java中使用关键字new 创建数组对象,格式为:
数组名 = new 数组元素类型[数组元素个数];
数组创建:
/**
* 数组对象的创建
*/
private static void define() {
int arr[];
arr = new int[10];
for(int i = 0; i < 10; i ++){
arr[i] = i;
System.out.println("===============> arr[" + i + "]" + arr[i]);
}
}
数组初始化:
1. 静态初始化
在定义数组的同时就为数组元素分配空间并赋值,例如:
private static void init1() {
int arr[] = {1, 2, 3, 4, 5};
for(int i = 0; i < arr.length; i ++){
arr[i] = i;
System.out.println("===============> arr[" + i + "]" + arr[i]);
}
}
2. 动态初始化:
数组定义与为数组元素分配空间和赋值的操作分开进行,例如:
private static void init1() {
int[] arr = new int[5];
arr[0] = 1;
arr[1] = 2;
arr[2] = 3;
for(int i = 0; i < arr.length; i ++){
arr[i] = i;
System.out.println("===============> arr[" + i + "]: " + arr[i]);
}
}
3. 数组元素的默认初始化:
数组是引用类型,它的元素相当于类的成员变量,因此数组分配空间后,每个元素也被按照成员变量的规则被隐式初始化,例如:
/**
* 数组初始化
*/
private static void init() {
int[] arr = new int[5];
User[] user = new User[5];
System.out.println(arr[1]);
System.out.println(user[1]);
}
user:
class User {
String name;
String sex;
public User(String name, String sex){
this.name = name;
this.sex = sex;
}
}
输出结果:
0
null
可以看到每个元素也被按照成员变量的规则被隐式初始化
数组元素的引用:
1. 定义并用运算符new为之分配空间后,才可以引用数组中的每个元素,数组元素的引用方式为:
数组名[index]
index为数组元素下标,可以使整形常亮或整形表达式。如:
arr[3], arr[i];
数组元素的下标从0开始;长度为n的数组的合法下标取值范围为:0~n-1;
2. 每个数组都有一个属性lendth(注:这里length是一个属性,不是方法,没有加括号(),我们这里特别说明是为了和String的length()方法做区别)指明他的长度,例如:
a.length的值为数组a的长度(元素个数)
我们每个类中的主函数也有一个数组,名叫args,
public static void main(String args[]){
}
这个参数可以接受命令行参数,我们在输入
java类名 xx,xxx
基础类型的包装类
基础类型的包转类, 基础类型是分配在栈内存中的,包装类是分配在堆空间里面的 。
基础类型的包装类有:Boolean---boolean、Byte---byte、Character---char、Double---double 、
Float---float、Integer---int、Long---long、Short---short。
二维数组:
1、二维数组可以看成是以数组为元素的数组。例如:
int arr[][] = {{1, 2}, {3, 4}};
2、java中多维数组的声明和初始化应按从高维到低维的顺序进行,例如:
int arr[][] = new int[3][];
arr[0] = new int[2];
arr[1] = new int[4];
arr[2] = new int[3];
二维数组初始化:
1、静态初始化:
int arr[][] = {{1, 2}, {3, 4}};
2、动态初始化:
int arr[][] = new int[3][];
arr[0] = new int[2];
arr[1] = new int[4];
arr[2] = new int[3];
数组的拷贝:
1、使用java.lang.system类的静态方法
public static void arrayCopy(object src, int srcPos, object dest, int destPos, intlength){
}
2、可以用于数组src从第srcPos项元素开始的length个元素拷贝到目标数组从destPos项开始的lenght个元素。
3、如果源数据数目超过目标数组边界会抛出IndexOutOfBoundsException异常。
例子:
private static void copy() {
int[] a = {1, 2, 3};
int[] b = new int[5];
System.arraycopy(a,0, b,0, a.length);
for(int i = 0; i < b.length; i ++){
System.out.println("===============> b[" + i + "]: " + b[i]);
}
}
数组的封装:
下面是对一个数组的基本封装,提供下面一些实现
getSize() 获取数组的容量
isEmpty() 返回数组是否为空
add(int index, E e) 在index索引的位置插入一个新元素e
addLast(E e) 向所有元素后添加一个新元素
addFirst(E e) 在所有元素前添加一个新元素
get(int index) 获取index索引位置的元素
set(int index, E e) 修改index索引位置的元素为e
contains(E e) 查找数组中是否有元素e
find(E e) 查找数组中元素e所在的索引,如果不存在元素e,则返回-1
remove(int index) 从数组中删除index位置的元素, 返回删除的元素
removeFirst() 从数组中删除第一个元素, 返回删除的元素
removeLast() 从数组中删除最后一个元素, 返回删除的元素
removeElement(E e) 从数组中删除元素e
代码:
public class Array<E> {
private E[] data;
private int size;
// 构造函数,传入数组的容量capacity构造Array
public Array(int capacity){
data = (E[])new Object[capacity];
size = 0;
}
// 无参数的构造函数,默认数组的容量capacity=10
public Array(){
this(10);
}
// 获取数组的容量
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;
}
// test
public static void main(String[] args) {
Array<Integer> arr = new Array<>();
for(int i = 0 ; i < 10 ; i ++)
arr.addLast(i);
System.out.println(arr);
arr.add(1, 100);
System.out.println(arr);
arr.addFirst(-1);
System.out.println(arr);
arr.remove(2);
System.out.println(arr);
arr.removeElement(4);
System.out.println(arr);
arr.removeFirst();
System.out.println(arr);
for(int i = 0 ; i < 4 ; i ++){
arr.removeFirst();
System.out.println(arr);
}
}
}
简单复杂度分析:
我们平时经常看到的一些时间复杂度:
O(1), O(n), O(lgn), O(nlogn), O(n^2)
上面的O描述的是算法的运行时间和输入数据之间的关系,这个并不是O严格的数学定义。
看下面一个例子,n表示的是nums中的元素个数,那么O(n)表示下面的的算法运行时间的多少和nums元素的个数成线性关系的,n表示的幂是一次方,所以说是线性关系。
public static int sum(int[] nums) {
int sum;
for(int num : nums)
sum += num;
return num;
}
上面的场景忽略和很多常数的情景,实际上线性的关系严格可以表示为:
T=c1 * n + c2
上述方程由于在不同环境,不同语言等差异,很难确定c1和c2的值,所以我们在分析时间复杂度的时候往往忽略掉常数进行分析,表示算法消耗的时间和输入数据的规模的关系。O(n)描述的是渐进时间复杂度,描述的是n趋于无穷的情况,所以在数量少的时候,O(n)并不一定就快于o(n^20),还和常数相关,但是在n趋于无穷大的时候,低阶算法的性能是快于高阶算法的。当一个算法既有高阶项又有低阶项的时候,往往只考虑高阶项,低阶项将被忽略掉,因为当n趋于无穷的时候,低阶项起的作用就不大了。
上述例子的时间复杂度分析:
添加操作:
addLast(E e) O(1) 表示这个操作和数组中的数据规模是没有关系的。
addFirst(e) O(n)
add(index, e) O(n/2) = O(n)
上面的方法可以引入概率分析,因为插入的index概率是一样的,上面的添加操作综合考虑可以说是一个O(n)的算法。在时间复杂度分析上,我们通常考虑的是最坏的情况。
删除操作:
removeLast(e) O(1)
removeFirst(e) O(n)
remove(index, e) O(n/2) = O(n)
综合来看,删除操作的时间复杂度是O(n)
修改操作:
set(index, e) O(1)
查找操作
get (index) O(1)
contains(e) O(n)
find(e) O(n)
均摊复杂度分析:
在上面的例子中,我们使用addLast(e)添加元素时,有可能会调用resize方法实现数组的扩容,所以这个方法的时间复杂度是O(n)。但是我们忽略了一种情况,就是并不是每次addLast都会导致数组扩容,我们使用最坏情况分析是不合理的。实际上我们每次扩容都是基于现有容量的,假设addLast()现在导致扩容操作,那么总共操作的次数是:现有容量 * 2 + 1(当前操作),平均来说,相当于每次addLast操作,都进行2次基本操作,相当于把总共addLst的时间平摊给了每个操作。这样均摊分析的话,时间复杂度是O(1),这种算法称为均摊复杂度分析。