数据结构与算法之----数组
1 数组
1.1 数组基础
-
数组
一种线性表结构,使用一组连续的内存空间,存储一组具有相同类型的数据。 -
索引
每个元素在数组中的编号(编号从0开始)。 -
注:索引可以有语义也可以没有语义;数组最好应用于“索引有语义”的情况。但并非所有有语义的索引都适用于数组。
例如:一个小组成员的身份证号,过长的数字作为索引会造成索引使用的不方便与存储空间的浪费。
1.2 线性表与非线性表
-
线性表
数据排成像一条线一样的结构。每个线性表上的数据最多只有前和后两个方向。例如:数组,链表、队列、栈等。
-
非线性表
非线性表中,数据之间并不是简单的前后关系,往往具有多前或者多后。例如:二叉树、堆、图等。
1.3 优点与应用场景
1.3.1 优缺点分析
- 数组:
优点:使用方便 ,查询效率 比链表高,内存为一连续的区域
缺点:大小固定,不适合动态存储,不方便动态添加 - 链表:
优点:可动态添加删除 大小可变
缺点:只能通过顺次指针访问,查询效率低
1.3.2 应用场景
- Java ArrayList 无法存储基本类型,比如int、long,需要封装未Integer、Long类,而Autoboxing、Unboxing则有一定的性能消耗;所以如果特别关注性能,或者希望使用基本类型,就可以选择数据。
- 如果数据大小事先已知,并且对数据的操作非常简单,用不到ArrayList提供的大部分方法,也可以直接使用数组。
- 当表示多维数组时,用数组往往会更加直观,比如 Object[][] array;
1.4 代码实现
- 声明数组:
语法:数据类型[] 数组名 或者 数据类型 数组名[] - 分配空间:
语法: 数组名 = new 数据类型 [数组长度] - 直接创建(将数组的声明、分配空间和赋值合并完成)
例如:int[] scores =new int[]{100,99,66}
package DataStructure;
public class Main {
public static void main(String[] args) {
int [] arr=new int[20];
for(int i=0;i<arr.length;i++)
arr[i]=i;
int[] scores =new int[]{100,99,66};
//两种赋值方式
for(int score:scores){
System.out.println(score);
}//增强型for循环
for(int i=0;i<scores.length;i++){
System.out.println(scores[i]);
}//遍历索引输出
}
}
2 基本功能CRUD的实现
- 基本功能:
增删改查 - 基本参数:
数组容量(capacity)、数组长度(size)、数组名称(data)
2.1 向数组中添加元素:
2.1.1 向数组末添加元素:
- 思路:向size指向的位置添加元素,同时维护size:size+1。
//向所有元素后添加一个新元素
public void addLast(int e){
/* if(size==data.length){
throw new IllegalArgumentException("AddLast failed.Array is full")
}
data[size]=e;
size++;*/
//方法2:复用add方法,在add方法中检查了数据合法性
add(size,e);
}
2.1.2 向指定位置添加元素
- 思路:把当前索引为1和之后的元素都向后挪一个位置,同时为了避免移动元素的时候覆盖其他元素,需要从后往前挪。
//在第index个位置插入一个新元素e
public void add(int index,int e){
if(size==data.length){
throw new IllegalArgumentException("AddLast failed.Array is full");
}
if(index<0||index>size){
throw new IllegalArgumentException("AddLast failed.Require index>=0 and index<size");
}
for(int i=size-1;i>=index;i--)
data[i+1]=data[i];
data[index]=e;
size++;
}
2.2 在数组中删除元素
-
所有删除索引后的元素都向前移动一个位置进行覆盖,最后维护一下size:size-1.
-
注:不用管最后一个元素,因为size指向的元素对用户不可见
//从数组中删除index位置的元素,返回删除的元素
public int remove(int index){
if(index<0||index>=size)
throw new IllegalArgumentException("Remove failed.Index is Illegal");
int ret=data[index];
for(int i=index+1;i<size;i++){
data[i-1]=data[i];
}
size--;
return ret;
}
2.3 修改数组中的元素
void set(int index,int e){
if(index<0||index>=size)
throw new IllegalArgumentException("Get failed.Index is illegal");
data[index]=e;
}
2.4 在数组中查找元素
//查找数组中的元素e
public boolean contains(int e){
for(int i=0;i<size;i++){
if(data[i]==e)
return true;
}
return false;
}
//查找数组中元素e所在位置的索引,如果不存在返回-1
public int find(int e){
for(int i=0;i<size;i++){
if(data[i]==e)
return i;
}
return -1;
}
2.5 附代码
数组类:
package DataStructure;
public class Array {
private int[] data;
private int size;
/**
*
* @param capacity
*/
//构造函数,传入数组的容量capacity构造Array
public Array(int capacity){
data=new int[capacity];
size=0;
}
//无参构造函数,默认数组的容量是capacity=10
public Array(){
this(10);
}
//获取数组中的元素个数
public int getSize(){
return size;
}
//获取数组的容量
public int getCapacity(){
return data.length;
}
//返回数组是否为空
public boolean isEmpty(){
return size==0;
}
//向所有元素后添加一个新元素
public void addLast(int e){
/* if(size==data.length){
throw new IllegalArgumentException("AddLast failed.Array is full")
}
data[size]=e;
size++;*/
//方法2:复用add方法,在add方法中检查了数据合法性
add(size,e);
}
public void addFirst(int e){
add(0,e);
}
//在第index个位置插入一个新元素e
public void add(int index,int e){
if(size==data.length){
throw new IllegalArgumentException("AddLast failed.Array is full");
}
if(index<0||index>size){
throw new IllegalArgumentException("AddLast failed.Require index>=0 and index<size");
}
for(int i=size-1;i>=index;i--)
data[i+1]=data[i];
data[index]=e;
size++;
}
//获取index索引位置的元素
int get(int index){
if(index<0||index>=size)
throw new IllegalArgumentException("Get failed.Index is illegal");
return data[index];
}
//修改index索引位置的元素为e
void set(int index,int e){
if(index<0||index>=size)
throw new IllegalArgumentException("Get failed.Index is illegal");
data[index]=e;
}
@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();
}
}
3 使用泛型
3.1 泛型
让我们的数据结构可以放置任何数据类型
-
基本数据类型:
boolean、byte、char、short、int、long、float、double -
每个基本数据类型都有对应的包装类
Boolean、Byte、Char、Short、Int、Long、Float、Double
基本类型在需要的时候可以自动转换为对应的包装类,包装类在需要的时候也可以自动解包为对应的基本数据类型。
public class Array<E>
3.2 泛型使用案例
package DataStructure;
public class Student {
private String name;
private int score;
public Student(String studentName,int studentScore){
name=studentName;
score=studentScore;
}
@Override
public String toString(){
return String.format("Student(name:%s,score:%d)",name,score);
}
public static void main(String[] args){
Array<Student> arr=new Array<>();
arr.addLast(new Student("Alice",100));
arr.addLast(new Student("Bob",99));
arr.addLast(new Student("Charlie",98));
System.out.println(arr);
}
}
4 动态数组
问题来源:
静态数组的容量有限,无法预估往数组中放入多少个元素,容量太大会浪费,容量太小不够用。
思路:
开创一个新的数组(大于原来的数组空间),循环将所有原来数组中的元素复制到新的数组中,此时capacity发生变化,size不变,将原来的数组引用指向新的数组的引用。
原来的数组没有索引指向,会被java的垃圾回收机制回收。
- resize方法:
private void resize(int newCapacity){
E[] newData=(E[])new Object[newCapacity];
for(int i=0;i<size;i++)
newData[i]=data[i];
data=newData;
}
- 在数组空间不够时扩容:
if(size==data.length){
resize(2*data.length);
}
-在删除逻辑时可以将数组容量缩小:
if(size==data.length/2){
resize(data.length/2);
}
使用resize实现动态数组
5 简单的时间复杂度分析
- 功能:分析性能;
- O(n)大O描述的是算法的运行时间和输入数据之间的关系
- O(n)运行时间与n呈线性关系,忽略常数,实际运行时间T=c1*n+c2;
均摊复杂度分析:
假设capacity=n,n+1次addLast,触发resize,总共进行2n+1次基本操作平均每次addLast操作进行2次基本操作。均摊计算时间复杂度,不计算最坏情况。