单链表和双链表是链表的两种分类,Day2来实现这两种链表。

首先说一说单链表。

单链表是线性表的升级版,至于线性表的内容,戳一戳这里就懂了(书接上回线性表)。单链表中数据存储的基本单元叫做节点,一个节点又包括数据data和指针next。节点里的数据存储的是该节点的数据,而指针存储的是下一个节点的地址。由于节点中本身就存储了下一个节点的地址,因此单链表的存储不同于线性表的数组结构,单链表的各节点不必通过数组下标相连。不必相连既表现出单链表通过指定元素增改数据、不浪费空间的优点,但又因为不使用下标,也暴露了不容易通过指定第几号元素获取数据的缺点。

实现单链表首先要定义一个节点:

public class ListNode {
    Object data;
    ListNode next;
    public ListNode(Object data) {
        this.data = data;
    }
}

由于单链表没有数组来控制链表的长度,因此需要定义头元素和尾元素实现上述功能。

在实现链表的增加、删除(按指定元素或下标删除)、更新、判断指定元素是否存在、获取指定位数数据、获取指定数据在第几位等功能时,就可以用头元素作为指针,利用尾元素控制单链表的结束点。(方法实现要使用MyList接口,接口在这里书接上回线性表)

实现删除方法或许不容易想到,迷惑点在于:指针挪到删除的目标元素时,我们无法得知目标元素的上一个地址。这就是单链表的缺点:只能单向扫描,为了改进这一缺点,接下来会引出双链表。但是,在这里,我们仍需要想办法在单链表中解决这个问题。解法就是:在重新定义一个指针,让这个指针指向原指针的上一位,具体方法见下面的代码。这样,我们就可以在找到目标值后,用新指针指向就指针的next,实现了目标元素的上一个节点与下一个相连。实现后,立即退出循环。

可能有些初学者会迷惑:指针定义时是局部变量,改变指向后,用删除方法外的其他方法打印单链表元素,居然不会报错?!这与Java的垃圾回收机制有关,在新指针越过目标指向下一个元素是,目标 元素就不在被使用,也就会被回收。在此之后调用其他方法,这一元素也就不再能访问,也就是实现了删除。

单链表类定义:

public class SingleLinkedList implements MyList {
    private ListNode head;//头节点
    private ListNode tail;//尾节点
    @Override
    public void add(Object element) {
        if(head==null){//初始化:处理原始为空节点的情况
            head=new ListNode(element);//头节点获取data和地址
            tail=head;//此时只有一个节点,所以头尾节点同,同时初始化了尾节点
        }else {
            tail.next=new ListNode(element);//尾元素的指针指向下一个元素
            tail=tail.next;//尾元素更新为新增元素
        }
    }

    @Override
    public void delete(Object element) {
        ListNode p=head;
        ListNode pre=p;
        while(p!=null){
            if(p.data.equals(element)){
                if(head.data.equals(element)) head=head.next;
                else pre.next=p.next;
                break;
            }
            pre=p;
            p=p.next;
        }
    }

    @Override
    public void delete(int index) {
        ListNode p=head;
        for(int i=0;i<index-1;i++){
            p=p.next;
        }
        p.next=p.next.next;
    }

    @Override
    public void update(int index, Object element) {
        ListNode p=head;
        if(index==0) head.data=element;
        else {
            for(int i=0;i<index-1;i++){
                p=p.next;
            }
            p.next.data=element;
        }
    }

    @Override
    public boolean contains(Object element) {
        ListNode p=head;
        while (p!=null){
            if(p.data.equals(element)) return true;
            p=p.next;
        }
        return false;
    }

    @Override
    public int indexOf(Object element) {
        int i=0;
        ListNode p=head;
        while (p!=null){
            if(p.data.equals(element)) return i;
            i++;
            p=p.next;
        }
        return -1;
    }

    @Override
    public Object at(int index) {
        ListNode p=head;
        int size=0;
        while(p!=null){
            size++;
            p=p.next;
        }
        if(index>=size) return "so sad, not here";
        p=head;
        for(int i=0;i<index;i++){
            p=p.next;
        }
        return p.data;
    }
    //定义一个输出方法,便于检测
    public void outPut(){
        System.out.print("[");
        ListNode p=head;
        while(p!=null){
            if(p.next!=null) System.out.print(p.data+" ");
            else System.out.print(p.data);
            p=p.next;
        }
        System.out.println("]");
    }
}

下面进行测试:

class SingleLinkedListTest {
    SingleLinkedList list=new SingleLinkedList();
    @org.junit.jupiter.api.Test
    void add() {
        list.add("a");
        list.add("b");
        list.add("c");
        list.add("d");
        list.outPut();
    }

    @org.junit.jupiter.api.Test
    void delete() {
        list.add("a");
        list.add("b");
        list.add("c");
        list.add("d");
        list.outPut();
        list.delete("c");
        list.outPut();
    }

    @org.junit.jupiter.api.Test
    void testDelete() {
        list.add("a");
        list.add("b");
        list.add("c");
        list.add("d");
        list.delete(2);
        list.outPut();
    }

    @org.junit.jupiter.api.Test
    void update() {
        list.add("a");
        list.add("b");
        list.add("c");
        list.add("d");
        list.update(1,"n");
        list.outPut();
    }

    @org.junit.jupiter.api.Test
    void contains() {
        list.add("a");
        list.add("b");
        list.add("c");
        list.add("d");
        System.out.println(list.contains("n"));
        System.out.println(list.contains("c"));
    }

    @org.junit.jupiter.api.Test
    void indexOf() {
        list.add("a");
        list.add("b");
        list.add("c");
        list.add("d");
        System.out.println(list.indexOf("b"));
        System.out.println(list.indexOf("n"));
    }

    @org.junit.jupiter.api.Test
    void at() {
        list.add("a");
        list.add("b");
        list.add("c");
        list.add("d");
        System.out.println(list.at(1));
        System.out.println(list.at(10));
    }
}

运行结果:

[a b c d]
[a b d]
[a n c d]
false
true
b
so sad, not here
[a b c d]
[a b d]
1
-1

Process finished with exit code 0

单链表到这里就结束啦。

还记得单链表只能单向扫描的问题吗?双链表就可以解决这一问题:相比单链表,双链表的节点中,多封装了pre指针,这个指针的作业就是指向前一个节点的地址。pre的存在,双链表也就实现了双向扫描的功能。

双链表节点的封装只需要多加一行:

ListNode pre;

双链表的实现也需要定义头节点和尾节点,原因与单链表大致相同。得益于双向扫描,双链表的头尾节点并不需要存储数据,我们只需要将数据塞进头尾之间即可。由于不存储数据,头尾节点又叫做“哑元”。

如法炮制定义一个双链表类:

public class DoubleLinkedList implements MyList {
    //设置头尾哑元,操作哑元中间的元素
    ListNode head=new ListNode(null);
    ListNode tail=new ListNode(null);

    public DoubleLinkedList() {
        tail.pre=head;
        head.next=tail;
    }

    @Override
    public void add(Object element) {
        ListNode newNode=new ListNode(element);
        tail.pre.next=newNode;
        newNode.next=tail;
        newNode.pre=tail.pre;
        tail.pre=newNode;
    }

    @Override
    public void delete(Object element) {
        ListNode p=head.next;
        while (p!=tail){
            if(p.data.equals(element)) {
                p.pre.next=p.next;
                p.next.pre=p.pre;
                break;
            }
            p=p.next;
        }
    }

    @Override
    public void delete(int index) {
        ListNode p=head.next;
        for(int i=0;i<index;i++){
            p=p.next;
        }
        p.pre.next=p.next;
        p.next.pre=p.pre;
    }

    @Override
    public void update(int index, Object element) {
        ListNode p=head.next;
        for (int i=0;i<index;i++){
            p=p.next;
        }
        p.data=element;
    }

    @Override
    public boolean contains(Object element) {
        ListNode p=head.next;
        while (p!=tail){
            if(p.data.equals(element)) return true;
            p=p.next;
        }
        return false;
    }

    @Override
    public int indexOf(Object element) {
        int i=0;
        ListNode p=head.next;
        while(p!=tail){
            if(p.data.equals(element)) return i;
            i++;
            p=p.next;
        }
        return -1;
    }

    @Override
    public Object at(int index) {
        ListNode p=head.next;
        int n=0;
        while (p!=tail){
            n++;
            p=p.next;
        }
        if(index>=n) return "so sad, not here";
        p=head.next;
        for(int i=0;i<index;i++){
            p=p.next;
        }
        return p.data;
    }
    public void outPut(){
        System.out.print("[");
        ListNode p=head.next;
        while(p!=tail){
            if(p.next.data!=null) System.out.print(p.data+" ");
            else System.out.print(p.data);
            p=p.next;
        }
        System.out.println("]");
    }
}

为了使头尾节点产生联系,要加入一个双链表构造器,让头的next指向尾,尾的pre指向头。

增删方法的定义也与单链表有所不同。先说一说增加:大致思路就是要创建一个新节点,把增加的元素传进新节点,然后把新节点塞进尾节点和上一个节点之间。至于怎么塞进去,就需要考虑一下顺序了:先把尾节点的pre的next指向新节点(搞断尾与上个节点的联系),然后让新节点的next指向尾(建立新节点和尾的联系),最后让新节点的pre指向尾的pre+尾的pre指向新节点。这样做很妙,有点擒贼先擒王的感觉了,切断最重要的,建立目标的,然后再慢慢维护其他的。这里其实也能看出双链表的缺点的,指针多了,维护起来就有点复杂。

终于,来到了测试环节:

class DoubleLinkedListTest {
    DoubleLinkedList list=new DoubleLinkedList();
    @Test
    void add() {
        list.add("a");
        list.add("b");
        list.add("c");
        list.add("d");
        list.outPut();
    }

    @Test
    void delete() {
        list.add("a");
        list.add("b");
        list.add("c");
        list.add("d");
        list.outPut();
        list.delete("b");
        list.outPut();
        list.delete("a");
        list.outPut();
    }

    @Test
    void testDelete() {
        list.add("a");
        list.add("b");
        list.add("c");
        list.add("d");
        list.outPut();
        list.delete(0);
        list.outPut();
        list.delete(2);
        list.outPut();
    }

    @Test
    void update() {
        list.add("a");
        list.add("b");
        list.add("c");
        list.add("d");
        list.outPut();
        list.update(0,"n");
        list.outPut();
        list.update(2,"k");
        list.outPut();
    }

    @Test
    void contains() {
        list.add("a");
        list.add("b");
        list.add("c");
        list.add("d");
        list.outPut();
        System.out.println(list.contains("a"));
        System.out.println(list.contains("k"));
    }

    @Test
    void indexOf() {
        list.add("a");
        list.add("b");
        list.add("c");
        list.add("d");
        list.outPut();
        System.out.println(list.indexOf("a"));
        System.out.println(list.indexOf("c"));
        System.out.println(list.indexOf("k"));
    }

    @Test
    void at() {
        list.add("a");
        list.add("b");
        list.add("c");
        list.add("d");
        list.outPut();
        System.out.println(list.at(0));
        System.out.println(list.at(2));
        System.out.println(list.at(9));
    }
}

测试结果:

[a b c d]
[a c d]
[c d]
[a b c d]
[n b c d]
[n b k d]
[a b c d]
true
false
[a b c d]
a
c
so sad, not here
[a b c d]
[a b c d]
[b c d]
[b c]
[a b c d]
0
2
-1

Process finished with exit code 0