🎇🎇🎇作者:
@小鱼不会骑车
🎆🎆🎆专栏:
《java练级之旅》
🎓🎓🎓个人简介:
一名专科大一在读的小比特,努力学习编程是我唯一的出路😎😎😎
顺序表
- 介绍线性表
- 顺序表
- 定义一个顺序表类(Arraylist)
- 查找顺序表元素(indexOf)
- 遍历顺序表(display)
- 定义一个异常(SubscriptException)
- 增加元素(Add)
- 删除元素(Delete)
- 总结
介绍线性表
在认识顺序表前我们先认识一下线性表:
小鱼曾在一本书中看到这么一个例子他是这样介绍线性表的:
在一个幼儿园中,每次放学时老师带领着小朋友们一个接着一个从教室出来,并且他们之间的次序都是一样的,例如小明排在第4位,则每次放学时他都是排在第四位,前面同样是那个的小女孩,后面也一直是那个男孩。
老师是这么解释的:为了保障小朋友的安全,避免漏掉小朋友,所以在出门前就安排好了他们出门的次序,谁谁在前,谁谁在后,当这样养成习惯时,如果是有哪个小朋友没有到位,则他前面的和后面的小朋友就会发现并且报告给老师,谁谁不在,老师也可以很快地清点人数,万一有人走丢,也能在最快时间知道,及时去寻找。
通过上面的例子我们就可以看出:线性表是一个序列,也可以理解为他们之间的元素是有顺序的,并且第一个元素无前驱,最后一个元素无后继,其余元素有且仅有一个前驱和后继,并且如果一个小朋友的衣服被两个或多个小朋友拉扯,这其实是在打架!不是有序排队。
总结:
线性表(linear list) 是n个具有相同特性的数据元素的有限序列。
线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列…
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
顺序表
在简单介绍完线性表之后,我们可以将顺序表理解为用数组的形式来实现线性表。
那一个顺序表都会有什么操作呢?举个例子:
如果有小朋友排队的时候突然想去厕所,那么当他去厕所之后,是不是他后面的同学都向前移动一下,先将这个空位补上?
对的! 大概就是如下图的过程
过了一会上厕所的小朋友回来了,是不是刚刚移动的小朋友需要集体后移来给这个小朋友腾出地方啊?
就像下图:
我们可以将上述的两个操作理解为增加和删除元素,那我们还是不是需要有查找和修改的功能啊?
那么大体的思路我们有了,接下来就是实现顺序表了!
定义一个顺序表类(Arraylist)
我们这个顺序表用到的是数组,所以我们就可以在这个顺序表中定义一个数组的成员变量,由于顺序表中的元素没有指定是什么类型,那么我们就创建一个泛型类的顺序表,以及一个泛型数组!并且我们还需要一个值来记录数组内元素的个数,那么为了方便,我们还可以利用构造方法初始化我们的数组大小。
代码如下:
public class Arraylist<E> {
public E[] elem;//创建这个成员变量
public int usedSize;//0/。记录长度
//默认容量
private static final int DEFAULT_SIZE = 10;
public Arraylist() {//初始化数组
this.elem = (E[]) new Object[DEFAULT_SIZE];
}
public Arraylist(int p) {
this.elem = (E[]) new Object[p];
}
}
我们对构造方法进行了重载,如果需要指定数组大小则可以在实例化顺序表类时输入指定的数组大小,如果没有指定数组的大小,我们就将数组大小默认设置为10个元素的大小!
那么成员属性和构造方法我们都实现了,接下来就是顺序表中的增删查改等方法了!
查找顺序表元素(indexOf)
我们查找元素一般时有两个需求。
第一:查找到元素返回元素的下标
第二:判断数组中有没有这个元素,如果有返回true否则返回false。
所以根据着两个需求我们可以写两个方法
第一个:当该元素存在时返回该元素的下标
// 查找某个元素对应的位置
//toFind是我们要查找的元素
public int indexOf(E toFind) {
for (int i = 0; i < usedSize; i++) {
if (elem[i].equals(toFind)) {
return i;
}
}
return -1;
}
第二个:当该元素存在时返回true否则返回false
// 判定是否包含某个元素
public boolean contains(E toFind) {
for (int i = 0; i <usedSize ; i++) {
if (elem[i].equals(toFind)) {
return true;
}
}
return false;
}
两者的区别其实就是返回值的不同,其实实现起来是很简单的。
遍历顺序表(display)
那我们也可以接着这个思路再写一个打印方法,打印出数组所有元素的值
遍历数组有五种方法,虽然有些不会经常用到,但是小鱼还是一一给大家列举出来!
①、使用 for 循环打印
最简单的方法,但是需要自己去实现
/**
* 打印顺序表:
* 根据usedSize判断即可
*/
public void display() {
for (int i = 0; i < usedSize; i++) {
System.out.print(elem[i]+" ");
}
System.out.println();
}
②、使用 Arrays.toString() 或 Arrays.deepToString()
对于一维数组,可以使用Arrays.toString()方法,它支持将任意类型的数组转换为字符串
一维数组用 Arrays.toString() 方法,多维数组用 Arrays.deepToString() 方法
public void display() {
//自己顺序表中不推荐使用
System.out.println(Arrays.toString(elem));
}
注:在自己实现的顺序表中不推荐使用Arrays.toString() 方法以及后面介绍到的遍历数组的方法
,因为顺序表每次都是扩容2倍或者1.5倍的内存,这就说明我们的数组其实是有很多元素是没有被赋值默认为null或0的,又因为Arrays.toString() 方法
会将数组中所以元素打印出来,那么就会有下面的情景
③、使用 Arrays.asList()
该方法是将数组转化为
list
以下几点需要注意:
(1)该方法不适用于基本数据类型
(byte,short,int,long,float,double,boolean),但可以用基本数据类型的包装类。比如int的包装类Integer.(Object 数组也是有效)
(2)该方法将数组与列表链接起来,当更新其中之一时,另一个自动更新
(3)不支持add和remove方法
public void display() {
//运行结果和上面情况一样!
System.out.println(Arrays.asList(elem));
}
运行结果
④、使用for each
for-each 是 for 循环的另外一种使用方式. 能够更方便的完成对数组的遍历. 可以避免循环条件和更新语句写错.
public void display() {
//依旧是打印全部数组
for (E x:elem) {
System.out.print(x+" ");
}
System.out.println();
}
运行结果
⑤、使用迭代器
迭代器是设计模式的一种,后序容器接触多了再给大家铺垫(由于自己实现的顺序表没有迭代器,于是用的是java自带的)
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(666);
list.add(888);
Iterator<Integer> it = list.listIterator();
while(it.hasNext()){
System.out.print(it.next() + " ");
}
System.out.println();
}
运行结果
注意:
- ArrayList最长使用的遍历方式是:for循环+下标 以及 foreach
- 迭代器是设计模式的一种,后序容器接触多了再给大家铺垫
定义一个异常(SubscriptException)
由于我们的增,删,改以及获取下标对应得元素的值,都需要传下标这个参数,又因为我们输入的下标可能会越界,所以我们自己实现一个异常,一旦输入的下标越界,则抛出这个异常
注:
我们继承的是一个编译时异常也可以称之为受查异常的
Exception
,所以我们如果用到这个异常就需要声明这个异常!
class SubscriptException extends Exception {
public SubscriptException(String e) {
super(e);
}
}
接下来我们就可以去实现后面经常会用到的判断下标是否会越界的方法了.
在这个方法中,当我们输入的下标的值如果比我们的数组的长度长或者说等于我们的数组的长度那么就抛出这个异常!
public void transBoundary(int pos) throws SubscriptException {
if (pos>=usedSize) {
throw new SubscriptException("输入下标异常");
}
}
那么有人抛出异常就一定有人去接收这个异常,在下面实现
//检查要输入的位置是否合法
private boolean checkPosInAdd(int pos) throws SubscriptException {
try {
transboundary(pos);
}catch (SubscriptException e) {
System.out.println("捕捉到了下标异常");
e.printStackTrace();
return false;
}
return true;//合法
}
当我们捕捉到这个异常的时候返回false,否则返回true,注意我们在方法的参数后面都声明了这个异常!!!
增加元素(Add)
我们将铺垫整理好了之后就可以进行增,删,改等操作了!
我们增加元素有两种方式:
第一种是在数组的最后一个位置插入,也可以称为尾插!
第二种是指定位置插入。
我们先来讨论一下,插入元素需要注意什么?
1.注意下标是否越界
2.注意数组容量是否够用
3.在插入元素之后数组元素加一
第一种方式的实现(比较简单):
代码如下:
// 新增元素,默认在数组最后新增
public void add(E data) {
if (isFull()) {
elem= Capacity(elem);
}
elem[usedSize]=data;
usedSize++;
}
根据上面提到需要注意的第二点,我们就可以知道,该方法中的isFull
方法就是判断数组容量是否够用,方法的实现如下
/**
* 判断当前的顺序表是不是满的!
*
* @return true:满 false代表空
*/
public boolean isFull() {
//当我的数组长度和我的元素个数一样多时,就返回true
//意思就是需要扩容
return usedSize==elem.length;
}
如果数组容量不够,就去调用Capacity()
方法,当然这个扩容数组的方法也是需要咱们自己实现的,方法具体内容如下
//扩容数组
public E[] Capacity(E[]array) {
return Arrays.copyOf(array,array.length*2);
}
我们的数组需要扩容时,一次扩容一倍,如果是扩容的容量太小,那么就需要扩容很多次,这就造成了时间上的浪费!虽然这样扩容可能会存在空间上的浪费,但是当我们学到线性表中的链表时就可以进一步的解决这个问题。
数组扩容之后,将数组进行尾插,最后数组元素个数加一!
第二种(指定位置插入):
这里需要考虑的就是,我们该如何插入,以及需要注意的事项.
小鱼先将总的代码拿出来,下面进行分析:
public void add(int pos, E data) throws SubscriptException {
//判断要插入的元素是否是最后一个位置
if (pos==usedSize) {//如果是则调用尾插的方法
add(data);
return;
}
if (!checkPosInAdd(pos)) {
//判断输入的下标是否越界,
//如果越界则直接返回
return;
}
//检查容量
if (isFull()) {
elem=Capacity(elem);
}
for (int i = usedSize-1; i >=pos; i--) {
//向后覆盖
elem[i+1]=elem[i];
}
set(pos, data);
usedSize++;
System.out.println("添加成功");
}
这一部分理解起来有一些困难,不过小鱼会画图一步一步剖析,大家如果看完之后还有疑问,或者改进的方法请大胆私信小鱼!!!
画图一步一步分析
第一步:我们先不管第一个方法,直接看第二个方法(蓝色框框)
但是其实是存在坑的,假如我要插入元素的位置是数组的最后一个位置,那么由于transBoundary()
中的判定条件,当我输入的下标是数组的最后一个位置,又因为相等所以他就会抛出数组越界的异常,我们为了解决这个问题,我在执行这个方法前先进行了第一步的判断,也就是上图中绿色的框框里面,当我输入的下标就是数组的最后一个位置时,则直接调用尾插方法,避免了抛出异常。(解决方法很多,这只是小鱼认为好一些的)
第二步 :
接下来就是检查容量了,这个在上面讲到过这里就不再重复,直接跳到红框框里面的第四个方法。
前面讲到的一个小朋友上厕所的例子大家还记得吧,当在队伍中的小朋友去上厕所回来时,就需要各位小朋友将他之前的位置腾出来,那么他原来位置后的小朋友就需要集体后移,如图
所以我们可以根据该图的思路,自己实现一个插入方法!
for (int i = usedSize-1; i >=pos; i--) {
//向后覆盖
elem[i+1]=elem[i];
}
set(pos,data);
usedSize++;
System.out.println("添加成功");
让我们的数组中所有在pos位置及pos位置后面的元素后移一位,移动之后将调用set()
方法。
方法实现如下:
// 给 pos 位置的元素设为【更新为】 value
public void set(int pos, E value) throws SubscriptException {
//修改元素
if (!checkPosInAdd(pos)) {
return;
}
elem[pos]=value;
}
将pos位置的元素修改为要插入的元素,并且在使用这个方法时,也需要判断下标是否越界!
最后数组元素个数加一,打印添加成功提示程序员。
删除元素(Delete)
经过上述的增加元素,我们也可以发现,删除元素其实也是有两个方法:
方法一:删除最后一个元素
方法二:删除指定元素
方法一(尾删):
public void delete() {
//删除最后一个元素
elem[usedSize-1]=null;
usedSize--;
}
由于我们的usedSize是数组元素的个数,又因为我们的数组是从0下标开始的,所以我们需要将usedSize-1
,将该下标置为null
之后再对数组元素个数进行减一。
方法二(指定删除):
这个问题就相当于前面讲到过的小朋友去厕所问题,当这个小朋友去厕所后,那么他后面的小朋友就需要向前走将他的位置补上,也就是都像前移,具体如何实现呢?
所以我们的大概思路就是,将要删除的元素的后面的元素都进行前移的操作,最后将最后一个元素置为null,最后数组元素个数减一.
注意:
需要判断下标是否越界
public void delete(int pos) throws SubscriptException {
//判断是否越界
if (!checkPosInAdd(pos)) {
return;
}
for (int i = pos; i < usedSize-1; i++) {
elem[i]=elem[i+1];
}
//将最后一个元素置为null
elem[usedSize-1]=null;
//元素个数减一
usedSize--;
}
删除下标为2的元素,运行结果
还有一些小鱼就不进行讲解了,大家可以自己实现:
清空顺序表
获取顺序表长度
获取下标对于位置的元素
判断顺序表是否为空
删除第一次出现的关键字
总结
顺序表的缺点:
- ArrayList底层使用连续的空间,任意位置插入或删除元素时,需要将该位置后序元素整体往前或者往后搬移,故时间复杂度为O(N)
- 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
- 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。
那么我们如何解决呢?
这些问题将会在链表中给大家讲解!!!