【DSaAA】线性表

线性表

什么是线性表

通俗地讲,线性表表可以定义为:一组按照一定索引顺序排好的数据集合,集合中的元素可以通过自身找到下一个元素的位置;元素的前一个元素称为该元素的前驱,后一个元素称为该元素的后继;每个元素最多只有一个直接前驱或直接后继。

ADT(Abstract Data Type)

抽象数据类型是指带有一组操作的对象集合,比如集合ADT就可以定义为add remove contains get indexOf等等,类似于Java中的Collection接口定义+ArrayList的实现:接口定义了操作,实现包含了对象集合。

顺序表

顺序表可以简单地理解为数组,因为数组元素的物理地址和逻辑地址完全一致,也就是顺序表中的所有元素的物理地址是连续的。为了能够通过某个元素的地址找到下一个元素的地址,必须先定义好元素的类型;达到通过起始地址算出后继的地址,假设每个元素占用c个存储单元,那么:

l o c i + 1 = l o c i + c
l o c i = l o c 0 + c ( i 1 )

如果是基础数据类型(int long double等)还好说,很容易就得到每个元素占用的内存单元,但是针对对象的列表呢?比如String[] List<Object>,这种列表里的元素没法判断到底有多大,怎么计算下一个元素的地址呢?其实对象列表中并不存储独享本身,而是存储对象的引用。对象创建时放在JVM堆中,集合里只存储了这个对象的引用(物理地址、指针),物理地址占用的存储单元是一定的,那么对象集合的问题就解决了。

Java中顺序表的实现是ArrayList,这里我们自己先定义一个简单的Collection接口:

public interface Collection<E> extends Iterable<E> {

    boolean add(E element);
    E remove(E element);
    int size();
    boolean isEmpty();
    boolean contains(E element);
    void clear();

}

再定义一个针对表的List接口:

public interface List<E> extends Collection<E> {

    int indexOf(E element);
    E get(int index);
    boolean insert(int index,E element);
    E replace(int index,E newElement);
}

顺序表ArrayList的具体实现参考:ArrayList.java

可以看出ArrayList的对象集合就是一个一维数组,导致它的一个致命的缺点就是:增删元素可能导致太多的数组复制操作,这些操作对数据本身是没有任何意义的,还会浪费大量的时间和资源。当一个数据集合中有大量数据且增删频繁的时候,使用顺序表并不是特别好的选择,但由于顺序表在遍历时没有短板,并且纯粹的对象集合(纯的数组),对内存的开销也是相对较少,这就决定了它的使用场景:数据量较小且数据固定,初始化之后几乎没有增删操作,仅仅是用来保存数据、用作遍历或通过下标来访问元素时,ArrayList是首选。

链表

由于顺序表增删效率低下的问题,在数据操作频繁的时候,我们急需一种能够适应频繁增删的表,链表可以解决这个问题。

单向链表

单向链表的数据集合不再是一个数组,而是一个带有下一个元素地址的节点,节点指向的下一个元素中又包含下下个元素的地址,一直到指向下一个元素的地址为空,表示数组到了尽头,同时还需维护一个size成员。

所以单向链表其实只要一个头节点head即可,同时使用了一个尾节点tail可以提高在末尾添加元素操作的效率。
单向链表的实现:SingleLinkedList.java

获取特定下标的元素可能更慢,因为需要一个地址一个地址地遍历并手动计数,导致每次查找都要重新遍历一次,不像顺序表那样一击即中。

插入、删除和增加元素不必考虑数组复制,可能会更快,因为在特定下标插入元素和获取特定下表元素一样,需要先搜索一次。

所以SingleLinkedList的使用场景是数据变化大,增删操作频繁的情况。单向链表可以解决顺序表增删效率低、占用资源多的问题,但同时也降低了查询效率,因此不适合通过下标来获取元素。

单向循环链表

循环链表拥有所有单向链表的特性,同时可以很方便地循环计数(约瑟夫环),因为循环链表的尾节点指向头结点,可以无穷无尽地遍历下去。

这种链表的使用场景一般是解决类似约瑟夫环之类的问题,不过这类问题直接可以使用单向链表实现,而且循环链表实现起来需要维护的地方较多,容易出错,我觉得很难用到实际情况中:

public static void main(String[] args){
    //约瑟夫环
    //已知n个人(以编号1,2,3...n分别表示)围坐在一张圆桌周围。
    //从编号为k的人开始报数,数到m的那个人出列;
    //他的下一个人又从1开始报数,数到m的那个人又出列;
    //依此规律重复下去,直到圆桌周围的人全部出列。
    //打印游戏过程。
    joseph(new CircularLinkedList<>("a","b","c","d","e"),3,5);
}

private static void joseph(CircularLinkedList<String> users,Integer startNo,Integer countNo){
    if(startNo > users.size()){
        throw new IndexOutOfBoundsException(startNo + " > " + users.size());
    }

    if(countNo <= 0){
        throw new IllegalArgumentException("m <= 0");
    }

    System.out.println("游戏开始>>>>>>>>>>>:" + users.toString());
    System.out.println();

    Integer currentNo = 0;
    Boolean start = false;
    Iterator<String> iterator = users.iterator();

    while(iterator.hasNext()){
        currentNo ++;
        String user = iterator.next();
        if(!start && currentNo.equals(startNo)){
            start = true;
            currentNo = 1;
        }
        if(currentNo.equals(countNo)){
            iterator.remove();
            currentNo = 0;

            System.out.println(user + ">>出列>>");
            System.out.println("剩余:" + users.toString());
            System.out.println();
        }
    }

    System.out.println("游戏结束>>>>>>>>>>>");
}
游戏开始>>>>>>>>>>>:[ a,b,c,d,e ]

e>>出列>>
剩余:[ a,b,c,d ]

a>>出列>>
剩余:[ b,c,d ]

b>>出列>>
剩余:[ c,d ]

c>>出列>>
剩余:[ d ]

d>>出列>>
剩余:[  ]

游戏结束>>>>>>>>>>>

单向循环链表:CircularLinkedList.java

双向链表和双向循环链表

双向链表和单向链表的区别在于:双向链表的每个节点存有前驱的地址,即一个元素既可访问到后继又能访问到前驱,这样可以实现双向遍历,并且删除元素也变得简单。

实际场景中像搜索、fork join工作窃取等等。

Java中的线性表

Java中的线性表有ArrayListLinkedList,前者是顺序表,后者是双向链表。

Java中还有一个Vector,这个也是一个顺序表,和ArrayList很像,不过Vector在原子操作上都加了synchronized,是一种线程安全的顺序表。

猜你喜欢

转载自blog.csdn.net/cl_yd/article/details/80088038