数据结构(2):LinkedList和链表[1]

 下面我们来介绍一种新的数据结构,链表。

我们曾经讨论过顺序表。它的数据存储在物理和逻辑上都是有逻辑的。而我们今天要学习的链表,则在物理结构上非连续存储,逻辑上连续。

1.链表的认识

链表由一个一个的节点组成。

我们可以想象一列火车,每一节车厢都被前面一节拉着,也拉着后面一节(头尾除外)。

数据结构(2):LinkedList和链表[1]_第1张图片

我们的链表与火车近似,我们可以将每一个节点当作一节车厢,它除了存储自己的数据之外,还能带领我们找到链接在它后面的一个节点,这样“一节一节”把所有数据串联起来。

单个节点是这样的结构:

数据结构(2):LinkedList和链表[1]_第2张图片

地址指的是下一个节点的位置,这样,我们就可以像火车一样把他们串联起来:

数据结构(2):LinkedList和链表[1]_第3张图片

(水平有限,意思到了即可)

链表也分很多种类,它可以是双向/单向,不带头/带头,非循环/循环。我们先着重讨论,单项不带头非循环链表。

2.单项不带头非循环链表的实现

同样我们也要先明白链表的实现,我们来先创建一个自己的SingleLinkedList类。

然后,我们来讨论对于节点的定义,我们需要再定义一个节点Listnode类,需要注意,它要被定义在链表这个类里面,它属于一个内部类。它里面要存放值和下一个节点地址,我们可以这样操作:

public class SingleLinkedList {
    static class Listnode{
        public int val;//节点值域
        public Listnode next;//下一个节点的地址

        public Listnode(int val) {
            this.val = val;
        }
    }
    public Listnode head;//表示当前链表头节点
}

这样我们就完成了对节点的定义,下面我们就要实现链表的各种操作。包括但不限于:创建链表、遍历链表、插入数据(头插/尾插)、删除所有值为k的节点、清空链表等等。同样,我们先把每个方法大的框架定义出来,再一个个实现。

public class SingleLinkedList {
    static class Listnode{
        public int val;//节点值域
        public Listnode next;//下一个节点的地址

        public Listnode(int val) {
            this.val = val;
        }
    }
    public Listnode head;//表示当前链表头节点

    //头插法
    public void addFirst(int data){
    }
    //尾插法
    public void addLast(int data){
    }
    //任意位置插入,第一个数据节点为0号下标
    public void addIndex(int index,int data){
    }
    //查找是否包含关键字key是否在单链表当中
    public boolean contains(int key){
        return false;
    }
    //删除第一次出现关键字为key的节点
    public void remove(int key){
    }
    //删除所有值为key的节点
    public void removeAllKey(int key){
    }
    //得到单链表的长度
    public int size(){
        return -1;
    }
    //清空链表
    public void clear() {
    }
    //遍历打印
    public void display() {}
}

creatlist():

首先我们可以手动创建链表,一个节点一个节点进行定义。它并不属于链表的方法,但可以帮助我们理解。我们先创建几个节点,给他们赋值,再用.next一个一个链接,具体可以这样实现:

 public void creatlist(){
        Listnode listnode1=new Listnode(1);
        Listnode listnode2=new Listnode(2);
        Listnode listnode3=new Listnode(3);
        Listnode listnode4=new Listnode(4);
        Listnode listnode5=new Listnode(5);

        listnode1.next=listnode2;
        listnode2.next=listnode3;
        listnode3.next=listnode4;
        listnode4.next=listnode5;

        this.head=listnode1;
    }

不要忘了设置头节点,那么相当于我们已经手动创建出来了一个链表。它大概是这样的:

数据结构(2):LinkedList和链表[1]_第4张图片

那么我们的链表就是长这个样子,我们后续操作会基于这种结构进行讲解。

dispaly():

遍历链表,我们要做的就是打印当前节点的值,之后成功走到下一个节点,直到最后,我们可以定义一个新的节点对象,让它指向头节点,从头节点开始一步一步往后走,这样既不会改变头节点的指向,也不会改变链表本身的结构。

public void display() {
        Listnode cur=head;
        while(cur!=null){
            System.out.println(cur.val);
            cur=cur.next;//cur往后走
        }
    }

我们可以测试一下

public static void main(String[] args) {
        SingleLinkedList list=new SingleLinkedList();
        list.creatlist();
        list.display();
    }

数据结构(2):LinkedList和链表[1]_第5张图片

这就证明向后一步一步走这个策略是没问题的,我们后续很多操作都要遍历链表,我们要始终贯彻这一思想。

插入数据--头插:

头插就是让插入的节点在最开头。我们要贯彻一件事,不管怎么插,头节点永远是第一个节点。那么我们的思想是先将值赋给一个新的节点,让这个节点指向原来的头节点,再改变头节点的指向,这样就可以将新节点与原来的链表连接起来,并且保证了头节点的正确性。

public void addFirst(int data){
        Listnode node=new Listnode(data);
        node.next=head;
        head=node;
    }

插入数据--尾插:

这个就会简单一些,我们只需要定义一个cur,让它从头开始往后走,当走到最后一个时(此时是cur.net==null!),把它和新节点连接起来就可以了。

 //尾插法
    public void addLast(int data){
        Listnode node=new Listnode(data);
        Listnode cur=head;
        while(cur.next!=null){
            cur=cur.next;
        }
        cur.next=node;
    }

插入数据--任意位置插入

首先我们要保证pos位置的合法性(头节点位置为0),然后当我们插入时,我们要记录前后两个节点的信息,我们要同时保证前面的可以连接上新节点,并且保证新节点可以连接上后面的,不改变链表的连贯性。所以,我们要找到插入节点的前一个节点的信息,才能方便操作。顺便,当index==0/index==size(),我们可以进行头插尾插,所以我们可以把获取链表长度这个方法写出来。

public void addIndex(int index,int data){
        Listnode node=new Listnode(data);
        Listnode cur=findIndexSubOne(index);
        if(index==0){
            addFirst(data);
            return;
        }
        if(index==size()){
            addLast(data);
            return;
        }
        cur.next=node.next;
        node.next=cur;
    }
    private Listnode findIndexSubOne(int index){
        //找到想要删除/插入节点位置的前一个节点
        Listnode cur=head;
        while(index-1!=0){
            cur=cur.next;
        }
        return cur;
    }
public int size(){
        Listnode cur=head;
        int cnt=0;
        while(cur!=null){
            cnt++;
            cur=cur.next;
        }
        return cnt;
    }

我们重点体会这样两行代码:

cur.next=node.next;
node.next=cur;

这两行是插入的关键。第一行是让新插入节点的后继是后面的节点,第二行则是改变前面节点的指向,指向新插入的节点,这样就既与前面链接,也与后面连上了,且没有改变结构,我们也就实现了中间节点的插入。

contains():

查找是否包含关键字key是否在单链表当中

这个就很简单了,我们遍历一下一带而过就可以了。

public boolean contains(int key){
        Listnode cur=head;
        while(cur!=null){
            if(cur.val==key)
                return true;
            cur=cur.next;
        }
        return false;
    }

remove():

删除分为两种,一种是删除第一次出现的关键字,一种是删除链表里所有的关键字的节点。它们的思想都是一样的。我们删除的思想是:找到要删除的关键字,把它跳过,就可以了。下面我们来实现一下:

 public void remove(int key){
        Listnode cur=head.next;
        Listnode pre=head;//要删除节点的前驱
        while(cur!=null){
            if(cur.val==key){
                pre.next=cur.next;
                return;
            }else{
                pre=cur;
                cur=cur.next;
            }
        }
        //删除头节点
        if(head.val==key){
            head=head.next;
        }
    }
public void remove(int key){
        Listnode cur=head.next;
        Listnode pre=head;//要删除节点的前驱
        while(cur!=null){
            if(cur.val==key){
                pre.next=cur.next;
            }else{
                pre=cur;
                cur=cur.next;
            }
        }
        //删除头节点
        if(head.val==key){
            head=head.next;
        }
    }

我们定义一个cur,一个prev,代表当前节点和它的前驱,如果cur走到了要删除的节点,就让prev.next=cur.next,进行跳过。如果没有,就让两“人”都往前走一步,直到最后。当然,这里没有考虑头节点被删除的情况,我们单独实现了一下。

clear():

最后是清空链表,很简单,直接让头节点为空就可以了,这样链表就不存在了。

public void clear() {
        this.head=null;
    }

那么我们就完成了,下面是完整的代码实现:

public class SingleLinkedList {
    static class Listnode{
        public int val;//节点值域
        public Listnode next;//下一个节点的地址

        public Listnode(int val) {
            this.val = val;
        }
    }
    public Listnode head;//表示当前链表头节点
    public void creatlist(){
        Listnode listnode1=new Listnode(1);
        Listnode listnode2=new Listnode(2);
        Listnode listnode3=new Listnode(3);
        Listnode listnode4=new Listnode(4);
        Listnode listnode5=new Listnode(5);

        listnode1.next=listnode2;
        listnode2.next=listnode3;
        listnode3.next=listnode4;
        listnode4.next=listnode5;

        this.head=listnode1;
    }
    //头插法
    public void addFirst(int data){
        Listnode node=new Listnode(data);
        node.next=head;
        head=node;
    }
    //尾插法
    public void addLast(int data){
        Listnode node=new Listnode(data);
        Listnode cur=head;
        while(cur.next!=null){
            cur=cur.next;
        }
        cur.next=node;
    }
    //任意位置插入,第一个数据节点为0号下标
    public void addIndex(int index,int data){
        Listnode node=new Listnode(data);
        Listnode cur=findIndexSubOne(index);
        if(index==0){
            addFirst(data);
            return;
        }
        if(index==size()){
            addLast(data);
            return;
        }
        cur.next=node.next;
        node.next=cur;
    }
    private Listnode findIndexSubOne(int index){
        //找到想要删除/插入节点位置的前一个节点
        Listnode cur=head;
        while(index-1!=0){
            cur=cur.next;
        }
        return cur;
    }
    //查找是否包含关键字key是否在单链表当中
    public boolean contains(int key){
        Listnode cur=head;
        while(cur!=null){
            if(cur.val==key)
                return true;
            cur=cur.next;
        }
        return false;
    }
    //删除第一次出现关键字为key的节点
    public void remove(int key){
        Listnode cur=head.next;
        Listnode pre=head;//要删除节点的前驱
        while(cur!=null){
            if(cur.val==key){
                pre.next=cur.next;
                return;
            }else{
                pre=cur;
                cur=cur.next;
            }
        }
        //删除头节点
        if(head.val==key){
            head=head.next;
        }
    }
    //删除所有值为key的节点
    public void removeAllKey(int key){
        Listnode cur=head.next;
        Listnode pre=head;//要删除节点的前驱
        while(cur!=null){
            if(cur.val==key){
                pre.next=cur.next;
            }else{
                pre=cur;
                cur=cur.next;
            }
        }
        //删除头节点
        if(head.val==key){
            head=head.next;
        }
    }
    //得到单链表的长度
    public int size(){
        Listnode cur=head;
        int cnt=0;
        while(cur!=null){
            cnt++;
            cur=cur.next;
        }
        return cnt;
    }
    //清空链表
    public void clear() {
        this.head=null;
    }
    //遍历打印
    public void display() {
        Listnode cur=head;
        while(cur!=null){
            System.out.println(cur.val);
            cur=cur.next;//cur往后走
        }
    }
}

和顺序表一样,我们使用链表也不需要自己重新写,java已经帮我们写好了,我们只需要直接使用就可以了,下面我们来学习链表的使用。

3.单链表的使用

 public static void main(String[] args) {
        Listlist=new LinkedList<>();
        list.add(1);//头插
        list.add(2);
        list.add(3);
        for( int x:list){
            System.out.println(x);
        }
    }

我们可以像这样对单链表对象进行构造,并使用其中的方法,方法大部分都与我们实现的没有太大区别。

4.小结

这篇文章我们主要讨论的是单链表的实现,其中定义cur节点进行遍历这个思想十分重要。单链表十分重要,我们下篇文章会找一些常见典型的单链表的题,通过对题目的的分析加深大家对单链表的印象。

你可能感兴趣的:(数据结构:用java描述,数据结构,链表)