数据结构与算法——7.线性表(顺序表即动态数组)

这篇文章我们来讲一下线性表

1.线性表概述

线性表是最基本最简单,也是最常用的一种数据结构。一个线性表是n个具有相同特性的数据元素的有限序列

下面介绍两个术语:

前驱元素:若A元素在B元素前面,则称A为B的前驱元素

后继元素:若B元素在A元素后面,则称B为A的后继元素

线性表的特征:

  1. 数据元素之间具有“一对一”的逻辑关系
  2. 第一个数据元素没有前驱,这个数据元素称为头结点
  3. 最后一个数据元素没有后继,这个数据元素称为尾结点
  4. 除了第一个和最后一个数据元素外,其他元素有且仅有一个前驱和后继

如果把线性表用数学语言来定义,则可以表示为(a1,a2,a3,...ai-1,ai,ai+1,...an)ai-1领先与ai,ai领先与ai+1,称ai-1是ai的前驱,ai+1是ai的后继

如下图所示:

线性表的分类:

线性表中数据存储的方式可以是顺序存储也可以是链式存储按照存储方式的不同把线性表分为顺序表链表。 

2.顺序表

顺序表是在计算机内存中以数组的形式保存的线性表线性表的顺序存储是指用一组地址连续的存储单元,依次存储线性表中的各个元素,使得线性表中在逻辑结构上相邻的数据元素存储在相邻的物理存储单元中,即通过数据元素物理存储的相邻关系来反映数据元素之间逻辑上的相邻关系

注意:

顺序表是依靠数组来实现的,但是顺序表不是数组,二者是有区别的。数组的长度是死的,一旦定义,就不能增加或删除元素,而顺序表是活的,是可以动态的增删元素的。顺序表和数组除了这点外,其余的特性完全一样,比如都只能存同一种数据,内存中物理地址都相邻等等。可以说,顺序表是动态的数组。这点一定要区分清楚!!!

顺序表的API设计:

3.顺序表的实现

下面,我们用代码来实现一下顺序表

代码如下:


public class SequenceList<T> {

    //存储元素的数组
    private T[] eles;
    //记录当前顺序表中元素的个数
    private int N;

    //构造方法
    public SequenceList(int capacity){
        //初始化数组
        this.eles = (T[]) new Object[capacity];
        //初始化长度(默认的存储元素为0,因为刚创建初始化时,它一个元素都没有存)
        this.N = 0;
    }

    //将线性表置为空表
    public void clear(){
        this.N = 0;
    }

    //判断顺序表是否为空表
    public boolean isEmpty(){
        return N==0;
    }

    //获取线性表的长度
    public int length(){
        return N;
    }

    //获取指定位置的元素
    public T get(int i){
        return eles[i];
    }

    //向线性表中插入元素
    public void insert(T t){
        eles[N++] = t;
    }

    //往指定索引i处添加元素t
    public void insert(int i,T t){
        //先把i索引处的元素及其后面的元素依次向后移动一位
        for (int index = N-1; index >i ; index--) {
            eles[index] = eles[index - 1];
        }
        //再把t元素方到i索引处
        eles[i] = t;
    }

    //删除指定位置i处的元素,并返回该元素
    public T remove(int i){
        //记录索引i处的值
        T current = eles[i];
        //索引i后面的元素依次向前移动一位
        for (int index = i; index < N-1; index++) {
            eles[index] = eles[index+1];
        }
        //元素个数减一
        N--;
        return current;
    }

    //查找元素t第一次出现的位置
    public int indexOf(T t){
        for (int i = 0; i < N; i++) {
            if (eles[i] == t)
                return i;
        }
        return -1;
    }
}

前提:

这里事先先说明一下一些情况,这样后面解释类中的方法是如何写的时候就比较容易理解。

下面,跟着我的描述来思考。

我们要做什么?我们要创建一个顺序表。前面说了,这个顺序表就是一个动态数组,它可以实现元素的插入啊,删除啊,求长度啊等等等等一系列操作(这些操作数组无法完成)。那么这个顺序表的本质是什么?是数组。数组的本质是什么?数组的本质是一块连续空间,然后将这些空间分割为一系列小块,然后在这些小块中放数据。哦,那我们就知道了,顺序表的本质也是在内存中开辟一块空间,然后将大空间分割为小空间,然后在小空间里面放数据。只不过顺序表可以实现一些插入删除的操作罢了。至此,顺序表的本质我们明白了。

下面回到代码中来,我们来解释代码。

成员变量:

我们顺序表的成员变量有一个数组,一个整型数据N,数组好理解,放数据用的(即顺序表的核心),那么N是用来干什么的呢?N是用来记录顺序表中的数据个数用的!!! 这时,就有人说了“那N就是第6行数组的长度,即顺序表中数组的长度呗”,错!!!N不是顺序表中数组的长度!!!它们是两个不同的概念!!!顺序表中数组的长度在我们初始化的时候给出,N不是顺序表中数组的长度(再次强调!!!),N是顺序表中有效数据,即用户放入顺序表中数据的个数!!!

这里又涉及到对象的初始化问题了,简单的说一下吧。

当我们在测试类中,new出一个对象的时候,jvm会根据类中数组的类型对其赋予默认的初始值,比如,我们指定泛型T为int类型的,那么在我们new出对象的时候,对象内部的数组eles中的每一个元素都有其默认初始值0,这些值是无效的,而这些值的个数即为我们数组的长度。但是我们这是顺序表,我们需要用它来存数据,假设,我们对象内部数组eles的长度为5,即数组长度为5,但是我们在这个顺序表中存了3个数据,那么顺序表的有效数据就是3,这个3就是N的值,即顺序表的长度。

以上就是对N与数组长度的说明。这点一定要清楚

下面,我们再来逐个分析类中的每个方法。

3.1 构造函数

顺序表的构造方法如下:

解析:

我们需要传的仅仅只是顺序表的长度,这个顺序表的长度就是顺序表内部数组的长度,所以就有了第13行,但是,我们用的是泛型,我们不能new T[ capacity] 啊,所以我们只能new最大的类——object,类型不一致啊,没关系,前面加个强制类型转换就行。这就是第13行的逻辑

此时,我们只是初始化好了顺序表,表中没有放任何数据,那么N的值当然为0了。这就是第15行的逻辑

3.2 置空方法

代码如下:

解析:

首先,我们需要将这个顺序表当做一个整体看待!!!这个N就是记录里面存储数据的个数的,顺序表为空,那么顺序表中存储元素的个数就为0了,所以直接设置N=0就行

有人就要问了,那数组中元素怎么办呢?前面说了,将顺序表当做一个整体看待,它就是一个黑匣子,你看不到里面的内容。所以,完全可以不用管数组中的那些元素。你只需要知道,你使用时,在指定的位置存储了什么,即有限个位置的确定值就行,至于其他位置,它存的是多少,关你何事?

3.3 判断是否为空

代码如下:

解析:

顺序表为空,那就是N=0;所以直接判断N是否为0就行,这里用来隐式类型转换,返回Boolean类型的值 

3.4 获取表长度

代码如下:

解析:

顺序表的长度就是N的值,所以就直接返回N就行 

3.5 获取指定位置的值

代码如下:

解析:

元素是存在数组中的,并且在有限位中,数组的索引和顺序表的索引是一致的,所以获取顺序表指定位置的元素,就是获取数组指定位置的元素

3.6 插入元素

代码如下:

解析:

很简单啊,顺序表索引加1,然后在里面放指定的元素 

3.7 在指定位置插入元素

代码如下:

解析:

在指定位置插入指定元素,因为顺序表的本质是数组,所以就是在数组的中间插入元素。那就简单了啊,在 索引 i 处的元素及其后面的元素依次向后移动一位,一个for循环就能解决,然后在指定位置处插入指定的元素。很简单的事。

注意:这两个插入用来方法的重载,区别是参数不同。

3.8 删除并返回指定处的元素

代码如下:

解析:

这个也很简单啊。删除并返回指定位置的元素,那就先用个变量记录指定位置的元素,然后把这个元素后面的所有元素都往前移动一位,然后将那个记录元素的变量返回就行了啊,so easy! 

3.9 找到元素第一次出现的位置

代码如下:

解析:

一个for循环加一个 if 判断的事,找到了返回指定位置的索引,找不到,返回-1 

3.10 测试

下面,我们写个测试类来测试一下:



public class SequenceTest {
    public static void main(String[] args) {
        //创建顺序表对象
        SequenceList<String> s1 = new SequenceList<>(10);
        //测试插入
        s1.insert("a");
        s1.insert("b");
        s1.insert("c");
        s1.insert(1,"aaaaa");

        //测试获取
        String getResult = s1.get(1);
        System.out.println("1处的元素为:"+getResult);

        //测试删除
        String removeResult = s1.remove(0);
        System.out.println("删除的元素是:"+removeResult);

        //测试清空
        s1.clear();
        System.out.println("清空后的顺序表中的元素个数为:"+s1.length());
    }
}

测试结果如下图:

 完美! 

4.顺序表的遍历

一般作为容器存储数据的数据结构,都需要向外部提供遍历的方式。所以,下面,我们也来看一下顺序遍历方式

在java中,遍历集合一般都是用for-each循环,如果想让我们的SequenceList也能支持for-each循环,则需要如下操作:

  1. 让SequenceList实现 Iterable 接口,重写iterator方法;
  2. 在SequenceList内部提供一个内部类SIterable,实现iterator接口,重写hasNext方法和next方法

代码如下:



import java.util.Iterator;

public class SequenceList<T> implements Iterable<T>{

    //存储元素的数组
    private T[] eles;
    //记录当前顺序表中元素的个数
    private int N;

    //构造方法
    public SequenceList(int capacity){
        //初始化数组
        this.eles = (T[]) new Object[capacity];
        //初始化长度(默认的存储元素为0,因为刚创建初始化时,它一个元素都没有存)
        this.N = 0;
    }

    //将线性表置为空表
    public void clear(){
        this.N = 0;
    }

    //判断顺序表是否为空表
    public boolean isEmpty(){
        return N==0;
    }

    //获取线性表的长度
    public int length(){
        return N;
    }

    //获取指定位置的元素
    public T get(int i){
        return eles[i];
    }

    //向线性表中插入元素
    public void insert(T t){
        eles[N++] = t;
    }

    //往指定索引i处添加元素t
    public void insert(int i,T t){
        //先把i索引处的元素及其后面的元素依次向后移动一位
        for (int index = N; index >i ; index--) {
            eles[index] = eles[index - 1];
        }
        //再把t元素方到i索引处
        eles[i] = t;
        //元素个数加1
        N++;
    }

    //删除指定位置i处的元素,并返回该元素
    public T remove(int i){
        //记录索引i处的值
        T current = eles[i];
        //索引i后面的元素依次向前移动一位
        for (int index = i; index < N-1; index++) {
            eles[index] = eles[index+1];
        }
        //元素个数减一
        N--;
        return current;
    }

    //查找元素t第一次出现的位置
    public int indexOf(T t){
        for (int i = 0; i < N; i++) {
            if (eles[i] == t)
                return i;
        }
        return -1;
    }

    //遍历
    @Override
    public Iterator<T> iterator() {

        return new SIterator();
    }

    private class SIterator implements Iterator{
        private int cusor;
        public SIterator(){
            this.cusor = 0;
        }
        //判断当前容器中还有没有下一个元素
        @Override
        public boolean hasNext() {
            return cusor < N;
        }
        //获取当前容器中的下一个元素的
        @Override
        public Object next() {
            return eles[cusor++];
        }
    }

}

遍历的原理上面讲了,这里就不过多赘述了。

注意,在指定位置添加元素的方法做出了一点小修改,最终方法如下图:

画个图,自己看一下就明白了。

5.顺序表的容量可变

我们上面实现的顺序表,真的是一个完整是顺序表吗?Of Course Not !!Why??Because 它的容量不可变啊。就比如说,我定义它的长度为5,但是我偏要存10个数据,行吗?Of Course Not Too !!你看java中的List列表是这样的?当然不是了。所以,它还缺一个容量可变的功能。下面,让我们来实现它!

代码如下:


import java.util.Iterator;

public class SequenceList<T> implements Iterable<T>{

    //存储元素的数组
    private T[] eles;
    //记录当前顺序表中元素的个数
    private int N;

    //构造方法
    public SequenceList(int capacity){
        //初始化数组
        this.eles = (T[]) new Object[capacity];
        //初始化长度(默认的存储元素为0,因为刚创建初始化时,它一个元素都没有存)
        this.N = 0;
    }

    //将线性表置为空表
    public void clear(){
        this.N = 0;
    }

    //判断顺序表是否为空表
    public boolean isEmpty(){
        return N==0;
    }

    //获取线性表的长度
    public int length(){
        return N;
    }

    //获取指定位置的元素
    public T get(int i){
        return eles[i];
    }

    //向线性表中插入元素
    public void insert(T t){
        if (N==eles.length){
            reSize(2*eles.length);
        }
        eles[N++] = t;
    }

    //往指定索引i处添加元素t
    public void insert(int i,T t){

        if (N==eles.length){
            reSize(2*eles.length);
        }
        //先把i索引处的元素及其后面的元素依次向后移动一位
        for (int index = N; index >i ; index--) {
            eles[index] = eles[index - 1];
        }
        //再把t元素方到i索引处
        eles[i] = t;
        //元素个数加1
        N++;
    }

    //删除指定位置i处的元素,并返回该元素
    public T remove(int i){
        //记录索引i处的值
        T current = eles[i];
        //索引i后面的元素依次向前移动一位
        for (int index = i; index < N-1; index++) {
            eles[index] = eles[index+1];
        }
        //元素个数减一
        N--;
        if (N<eles.length/4){
            reSize(eles.length/2);
        }
        return current;
    }

    //查找元素t第一次出现的位置
    public int indexOf(T t){
        for (int i = 0; i < N; i++) {
            if (eles[i] == t)
                return i;
        }
        return -1;
    }

    //根据参数newSize来重置eles的大小
    public void reSize(int newSize){
        //定义一个临时数组,指向原数组
        T[] temp = eles;
        //创建一个新数组
        eles = (T[]) new Object[newSize];
        //把原数组中的元素拷贝到新数组中
        for (int i = 0; i < N; i++) {
            eles[i] = temp[i];
        }
    }

    //遍历
    @Override
    public Iterator<T> iterator() {
        return new SIterator();
    }

    private class SIterator implements Iterator{
        private int cusor;
        public SIterator(){
            this.cusor = 0;
        }
        //判断当前容器中还有没有下一个元素
        @Override
        public boolean hasNext() {
            return cusor < N;
        }
        //获取当前容器中的下一个元素的
        @Override
        public Object next() {
            return eles[cusor++];
        }
    }

}

解析:

什么时候需要容量改变?插入元素或删除元素时。插入好理解,删除怎么说?假设,我们定义数组长度为100w,一开始也存储了100w个数据,但是我们删啊删,删到最后只剩1个数据,你还要占用100w个数据空间吗?这不浪费空间吗?所以删除时也要改变容量。

怎么改变?简单,改变顺序表中数组的大小就行,怎么变?变为原来的2倍就行。所以思路清晰了。我们先定义临时数组,存储eles的值,然后将eles的大小变为原来的2倍,然后再将临时数组中的值传给新的eles就行,so easy!

然后就是在插入元素和删除元素时判断一下就行啦,很简单。

代码上面给了,下面测试一下:

结果如下图所示:

完美!

6.小结

 这篇文章主要讲了顺序表。会者不难,难者不会。还是那句话:容量不可变前,顺序表是低级版的动态数组;容量可变后,顺序表是完全版的动态数组。它的底层就是靠数组实现的。一个数组的长度,一个记录顺序表元素个数的变量,在一定的范围内(顺序表元素个数 <= 数组的长度),二者相同,即顺序表和数组的索引相同,所以对顺序表的操作就是对数组的操作!!!就这,没啥,so easy的事。

最后,给个图。看着图,瞅着代码,多思考思考:

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

L纸鸢

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值