前言
在我们具体学习数据结构这门课程之前,大一学习C语言的时候,我们就学习了数组,而当时对数组的学习都是基于定长(N)数组的,空间开多了浪费,开少了不够用。而如何在现有定长的数组基础上去动态的开辟一定的空间来供我们使用需要进一步来学习。
一、什么是顺序表?
顺序表是用一段**物理地址连续**的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
顺序表一般可以分为:
静态顺序表:使用定长数组存储。
动态顺序表:使用动态开辟的数组存储。
适用场景:
静态顺序表适用于确定知道需要存多少数据的场景.
静态顺序表的定长数组导致N定大了,空间开多了浪费,开少了不够用
相比之下动态顺序表更灵活, 根据需要动态的分配空间大小.
接下来,我们重点介绍动态顺序表的相关操作。
二、动态顺序表的相关操作
说明:在public class MyArray{}类中定义如下:
//存储元素仍然还在数组中存储
private int[] data;
//当前动态数组中实际存储的元素个数
private int size;
1.增加(插入)数据
- 尾插法:
(在数组的尾部进行插入数据)
public void addLast(int value){
//先判断当前数组是否已满
if(size==data.length){
//数组已满,需要扩容
grow();
}
data[size]=value;//将数据值赋给数组有效长度的末尾
size++;//数组长度增加
}
-
头插法:
(在数组的头部进行插入数据)思路:先将原数组从最后一个位置开始向后移动一个单位,将第一个位置空出来,然后对它赋值
public void addFirst(int value){
if(size==data.length)//判断满
{
grow();//扩容
}
for (int i = size-1; i >=0; i--) {//注意边界条件
data[i+1]=data[i];//不要写反
}
data[0]=value;
size++;
}
- 任意位置插入
当然这个任意指的是在数组内部,并且是要连续的,如果说数组长度为6,有效数据个数为3,那么最后一个元素的下标为a[2],此时你要想在a[4]这个位置插入数据,显然是不合法的,因为它违反了数组要连续存储数据元素这个本质。
public void addIndex(int index,int value){
//判断index位置的合法性
if(index<0||index>size){
System.err.println("add index illegal!");
return;
}
//判断数组满
if(size==data.length){
grow();
}
if(index==0) {//在头部插入
addFirst(value);
return;
}
if(index==size) {//在尾部插入
addLast(value);
}
else{//在中间合理位置插入
for (int i =size-1; i >=index;i--) {//注意边界
data[i+1]=data[i];
}
data[index]=value;
size++;
}
}
进一步思考,其实我么可以对上面的三种在不同位置增加数据的做法进行改进精简。将头部插入和尾部插入也可以看作是在任意合法位置插入。
改进代码如下:
public void addFirst(int value){
addIndex(0,value);
}
public void addLast(int value){
addIndex(size,value);
}
public void addIndex(int index,int value){
//判断数组满
if(size==data.length){
grow();
}
//判断index位置的合法性
if(index<0||index>size){
System.err.println("add index illegal!");
return;
}
else{//将index位置空出来
for (int i =size-1; i >=index;i--) {
data[i+1]=data[i];
}
data[index]=value;
size++;
}
}
注意:不要要先判断数组是否满,在判断插入位置是否合理,不要弄反。
扩容操作:
private void grow() {
int[] newData=Arrays.copyOf(data,this.data.length<<1);
this.data=newData;//将数组扩大为原来的一倍(指向扩容后的新数组)
}
2.查找数据
- 查看当前数组中是否存在某个值
public boolean contains(int value){
int index=getByValue(value);
if(index==-1){
return false;
}
return true;
}
根据索引查询元素
public int get(int index){
if(index<0||index>=size){//判断合法性(size下标:当前有效元素的下一个位置)
System.err.println("get index illegal!");
return -1;
}
return data[index];
}
在数组中查找value值对应的索引下标
public int getByValue(int value){
for (int i = 0; i <size ; i++) {//遍历数组
if(data[i]==value){
return i;
}
}
//此时循环走完还没找到元素,value不存在
return -1;
}
3.更新(改)数据
将指定索引位置元素修改为newValue,返回修改前的元素值
public int set(int index,int newValue){
if(index<0||index>=size){//判断合法
System.err.println("set index illegal!");
return -1;
}
int oldValue=data[index];
data[index]=newValue;
return oldValue;
}
4.删除数据
根据索引Index删除元素(将index之后的元素向前移动一个单位即可,)
由上图我们就会发现一个小问题,虽然size-1之后,数组的有效元素个数会减少,但下标为size的值还是原来的值,所以需要我们将其置为0;
- 删除指定索引位置的元素
public void removeInex(int index){
if(index<0||index>=size){
System.err.println("remove index illegal!");
}
for (int i = index; i <size-1 ; i++) {//当size==data.length时,要防止越界
data[i]=data[i+1];
}
size--;
data[size]=0;//删除原先数组的最后一个位置元素
}
public void removeFirst(int index){//删除第一个元素
removeInex(0);
}
public void removeLast(int index){//删除最后一个元素
removeInex(size-1);
}
- 删除第一个值为value的节点
public void removeValueOnce(int value){
for (int i = 0; i < size; i++) {
if(data[i]==value){//此时i对应的索引是第一个值为value的节点
removeInex(i);
return;
}
}
}
- 删除数组中所有值为value的元素
public void removeValueAll(int value){
for (int i = 0; i < size; i++) {
while(i!=size&&data[i]==value){//考虑到出现连续的重复元素
removeInex(i);
}
}
}
如果将while循环改成if(data[i]==value),会出错,因为没考虑到数字连续出现的情况,就会漏掉几个,因为此时移除data[i],所有数据向前移动一个单位,那么紧跟在后面重复的数字移到上一个位置,而此时i++,就刚好会漏判,所以要使用while。
栗子:
一个数组:[33,1,1,1,1,1,1,44]
要删除数字1;
[33,1,1,1,44](使用if判断输出后的结果)
[33,44](使用while输出后的结果)
可以画图试一试。
总结
- 顺序表中间/头部的插入删除,时间复杂度为O(N)
- 增容需要申请新空间,拷贝数据,释放旧空间,会有不小的消耗(耗时)。
- 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。(空间浪费)
所以,为了解决上面问题,我们还会学习链表。
4 动态顺序表优点:根据索引查找目标元素很方便