【数据结构基础】之链表介绍,生动形象,通俗易懂,算法入门必看

47 阅读13分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情

前言

本文为 数据结构基础【链表】 相关知识介绍,具体将对链表概念单链表双链表循环链表Java中链表的使用等进行详尽介绍~

👉Java全栈学习路线可参考:【Java全栈学习路线】最全的Java学习路线及知识清单,Java自学方向指引,内含最全Java全栈学习技术清单~ 👉算法刷题路线可参考:算法刷题路线总结与相关资料分享,内含最详尽的算法刷题路线指南及相关资料分享~


一、链表介绍

上节我们介绍了数组,是一种线性结构,而链表也是一种线性结构,它们都可以称作线性表。不同的是数组在内存空间中是顺序存放的,而链表不需要连续存放。下面就首先来看看链表的定义与相关介绍。 1.png

  • 在内存空间中,数组和链表都是基本的数据结构,都是【表】,或者叫【线性表】。
  • 线性表是一个线性结构,它是一个含有n≥0个结点的有限序列,对于其中的结点,有且仅有一个开始结点没有前驱但有一个后继结点,有且仅有一个终端结点没有后继但有一个前驱结点,其它的结点都有且仅有一个前驱和一个后继结点,说人话,就是有头有尾一条线。

2.png 还不明白就再来一张图:

3.png

  • 链表是一种动态数据结构,他的特点是用一组任意的存储单元(可以是连续的,也可以是不连续的)存放 数据元素。
  • 链表中每一个元素成为 “结点” , 每一个结点都是由数据域和指针域组成的, 每个结点中的指针域指向下一 个结点。
  • Head 是 “头指针” , 表示链表的开始, 用来指向第一个结点, 而最后一个指针的指针域为 NULL( 空地址 ) ,表示链表的结束。
  • 可以看出链表结构必须利用指针才能实现,即一个结点中必须包含一个指针变量,用来存放下一个结点的 地址。
  • 实际上,链表中的每个结点可以用若干个数据和若干个指针。

由以上介绍我们可以知道,链表它与数组一样是一种线性结构,区别在于它在内存空间中不是顺序存放的,也就是说物理结构不是线性的,而逻辑结构是线性的,要想保持这种逻辑上的线性结构,就需要将它们以某种方式能够连接起来形成线性结构,链表呢就是通过指针来实现的,每一个结点中除了数据元素外 还有一个用于指向 下一个结点的指针,通过这些指针,结点之间进行连接最终成一个链,逻辑上是线性的。

二、单链表

由以上介绍相信已经对于链表是什么已经有了一些认识,那么链表具有一些特殊的特点我们需要了解一下。

单链表特点:

  • 长度可变,扩展性好
  • 内存利用高(可以不连续)
  • 时间性能:查找O(n)、插入和删除O(1)
  • 空间性能:不需要分配存储空间,只要有就可以分配,元素个数不受限制

单链表的逻辑结构:

4.png 单向链表的节点的抽象:

其节点由两部分构成:

  • data域:数据域,用来存储元素数据
  • next域:用于指向下一节点

以下是链表的定义代码示例:

class Node<E> {
    E item;
    Node<E> next;

    //构造函数
    Node(E element) {
       this.item = element;
       this.next = null;
   }
}

由以上对于链表的特点的介绍我们可以知道,链表具有长度可变,可扩展性好的特点,而数组我们知道它是在内存空间中是顺序存放的,我们在定义数组的时候就已经定义好了数组的大小,相当于在内存中已经给数组分配了固定大小的内存空间,在后续的使用过程中数组的大小是不可以改变的,相比于此,链表的大小可变的特性就方便我们对于线性表的使用。

三、双链表

我们上边介绍的链表 其实是一种单链表,单链表就是说每一个结点只含有一个指针,这个指针指向它的下一个结点,所以在对链表进行遍历查找的时候就只能从头往后进行查找。所以本节我们将分析单链表存在的 缺点,并针对单链表的这些缺点引入双链表的介绍。

单向链表的缺点分析:

  • 单向链表,在查找的时候查找的方向只能是一个方向,而双向链表可以向前或者向后查找。
  • 对于单向链表的某一个结点不能自我删除,需要靠辅助节点,而双向链表的某一个结点则可以自我删除,所以前面我们单链表删除时节点,总是找到temp,temp是待删除节点的前一个节点。

为了弥补单链表的缺点,使得链表能够实现双向查找的和自我删除的功能,引出了双向链表的概念。

双向链表的逻辑结构:

5.png 双向链表的节点的抽象:

双链表的节点由三部分构成:

  • data域:数据域,用来存储元素数据
  • next域:用于指向后一节点
  • front域:用于指向前一节点

以下是双链表的定义代码示例:

class Node<E> {

    E item;
    Node<E> next;
    Node<E> prev;

    //构造函数
    Node(E element) {
       this.item = element;
       this.next = null;
       this.prev = null;
   }
}

四、循环链表

还有一种比较特殊的链表结构叫做循环链表:链表的最后一个节点指向第一个节点,整体构成一个链环。

将单链表中的终端结点的指针端由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表(circular linked list),循环链表不一定需要头结点。

下图是一个循环链表的结构示意图: 6.png

由上图我们可以看到循环链表头尾相连在逻辑上形成一个环,这种特殊的数据结构会在一些特殊的地方有一些妙用。

五、Java中链表的使用

在最后一节将为大家介绍在Java中对于链表的一些使用,在Java中的ArrayList和LinkedList底层都是实现了链表。两者区别在于:

  • ArrayList和LinkedList两者的区别在于与 ArrayList 相比,LinkedList的增加和删除的操作效率更高,而查找和修改的操作效率较低。
  • 链表用LinkedList较多。

1️⃣Java中LinkedList的操作

首先我们来看看Java中对于LinkedList的一些操作与使用。

LinkedList类 是位于java.util 包中的,我们要想使用它,使用前需要引入这个包,引入的语法格式如下:

// 引入 LinkedList 类
import java.util.LinkedList; 

LinkedList<E> list = new LinkedList<E>();   // 普通创建方法
// 或者
LinkedList<E> list = new LinkedList(Collection<? extends E> c); // 使用集合创建链表

创建一个简单的链表实例:

import java.util.LinkedList;

public class RunoobTest {
    public static void main(String[] args) {
        LinkedList<String> sites = new LinkedList<String>();
        sites.add("Baidu");
        sites.add("CSDN");
        sites.add("Juejin");
        sites.add("Yahoo");
        System.out.println(sites);
    }
}

以上链表实例的执行输出结果为:

// 打印结果:
[Baidu, CSDN, Juejin, Yahoo]

以下表格列出了LinkedList的其他操作方法:

方法描述
addFirst()在表头增加一个元素
addLast()在表尾增加一个元素
removeFirst()从表头移除一个元素
removeLast()从表尾移除一个元素
getFirst()获得表头的元素
getLast()获得表尾的元素

2️⃣Java实现链表

java中ListNode链表就是用Java自定义实现的链表结构。

基本结构:

class ListNode {        //类名 :Java类就是一种自定义的数据结构
    int val;            //数据 :节点数据 
    ListNode next;      //对象 :引用下一个节点对象。在Java中没有指针的概念,Java中的引用和C语言的指针类似
}

添加构造方法方便初始化:

class ListNode {        //类名 :Java类就是一种自定义的数据结构
    int val;            //数据 :节点数据 
    ListNode next;      //对象 :引用下一个节点对象。在Java中没有指针的概念,Java中的引用和C语言的指针类似
    
    ListNode(int val){  //构造方法 :构造方法和类名相同   
        this.val=val;     //把接收的参数赋值给当前类的val变量
    }
}

范型写法,使用范型可以兼容不同的数据类型:

class ListNode<E>{                //类名 :Java类就是一种自定义的数据结构
    E val;                        //数据 :节点数据 
    ListNode<E> next;             //对象 :引用下一个节点对象。在Java中没有指针的概念,Java中的引用和C语言的指针类似
    
    ListNode(E val){              //构造方法 :构造方法和类名相同   
        this.val=val;             //把接收的参数赋值给当前类的val变量
    }
}

创建链表及遍历链表:

class ListNode {        //类名 :Java类就是一种自定义的数据结构
    int val;            //数据 :节点数据 
    ListNode next;      //对象 :引用下一个节点对象。在Java中没有指针的概念,Java中的引用和C语言的指针类似
    
    ListNode(int val){  //构造方法 :构造方法和类名相同   
        this.val=val;   //把接收的参数赋值给当前类的val变量
    }
}

class Test{
    public static void main(String[] args){
        
        ListNode nodeSta = new ListNode(0);    //创建首节点
        ListNode nextNode;                     //声明一个变量用来在移动过程中指向当前节点
        nextNode=nodeSta;                      //指向首节点

        //创建链表
        for(int i=1;i<10;i++){
            ListNode node = new ListNode(i);  //生成新的节点
            nextNode.next=node;               //把心节点连起来
            nextNode=nextNode.next;           //当前节点往后移动
        } //当for循环完成之后 nextNode指向最后一个节点,
        
        nextNode=nodeSta;                     //重新赋值让它指向首节点
        print(nextNode);                      //打印输出
      
    }
    
    //打印输出方法
    static void print(ListNode listNoed){
        //创建链表节点
        while(listNoed!=null){
            System.out.println("节点:"+listNoed.val);
            listNoed=listNoed.next;
        }
        System.out.println();
    }
   
}

插入节点:

7.png

class ListNode {        //类名 :Java类就是一种自定义的数据结构
    int val;            //数据 :节点数据 
    ListNode next;      //对象 :引用下一个节点对象。在Java中没有指针的概念,Java中的引用和C语言的指针类似
    
    ListNode(int val){  //构造方法 :构造方法和类名相同   
        this.val=val;   //把接收的参数赋值给当前类的val变量
    }
}

class Test{
    public static void main(String[] args){
        
        ListNode nodeSta = new ListNode(0);          //创建首节点
        ListNode nextNode;                           //声明一个变量用来在移动过程中指向当前节点
        nextNode=nodeSta;                            //指向首节点
        
        //创建链表
        for(int i=1;i<10;i++){
            ListNode node = new ListNode(i);         //生成新的节点
            nextNode.next=node;                      //把心节点连起来
            nextNode=nextNode.next;                  //当前节点往后移动
        } //当for循环完成之后 nextNode指向最后一个节点,
        
        nextNode=nodeSta;                            //重新赋值让它指向首节点
        print(nextNode);                             //打印输出
     
        //插入节点
        while(nextNode!=null){
            if(nextNode.val==5){
                ListNode newnode = new ListNode(99);  //生成新的节点
                ListNode node=nextNode.next;          //先保存下一个节点
                nextNode.next=newnode;                //插入新节点
                newnode.next=node;                    //新节点的下一个节点指向 之前保存的节点
            }
            nextNode=nextNode.next;
        }//循环完成之后 nextNode指向最后一个节点
         nextNode=nodeSta;                            //重新赋值让它指向首节点
         print(nextNode);                             //打印输出
      
    }
    
    static void print(ListNode listNoed){
        //创建链表节点
        while(listNoed!=null){
            System.out.println("节点:"+listNoed.val);
            listNoed=listNoed.next;
        }
        System.out.println();
    }
}

替换节点:

8.png

class ListNode {        //类名 :Java类就是一种自定义的数据结构
    int val;            //数据 :节点数据 
    ListNode next;      //对象 :引用下一个节点对象。在Java中没有指针的概念,Java中的引用和C语言的指针类似
    
    ListNode(int val){  //构造方法 :构造方法和类名相同   
        this.val=val;   //把接收的参数赋值给当前类的val变量
    }
}

class Test{
    public static void main(String[] args){
        
        ListNode nodeSta = new ListNode(0);          //创建首节点
        ListNode nextNode;                           //声明一个变量用来在移动过程中指向当前节点
        nextNode=nodeSta;                            //指向首节点
        
        //创建链表
        for(int i=1;i<10;i++){
            ListNode node = new ListNode(i);         //生成新的节点
            nextNode.next=node;                      //把心节点连起来
            nextNode=nextNode.next;                  //当前节点往后移动
        } //当for循环完成之后 nextNode指向最后一个节点,
        
        nextNode=nodeSta;                            //重新赋值让它指向首节点
        print(nextNode);                             //打印输出
     
        //替换节点
        while(nextNode!=null){
            if(nextNode.val==4){
                ListNode newnode = new ListNode(99);  //生成新的节点
                ListNode node=nextNode.next.next;     //先保存要替换节点的下一个节点
                nextNode.next.next=null;              //被替换节点 指向为空 ,等待java垃圾回收
                nextNode.next=newnode;                //插入新节点
                newnode.next=node;                    //新节点的下一个节点指向 之前保存的节点
            }
            nextNode=nextNode.next;
        }//循环完成之后 nextNode指向最后一个节点
         nextNode=nodeSta;                            //重新赋值让它指向首节点
         print(nextNode);                             //打印输出
      
    }
    
    //打印输出方法
    static void print(ListNode listNoed){
        //创建链表节点
        while(listNoed!=null){
            System.out.println("节点:"+listNoed.val);
            listNoed=listNoed.next;
        }
        System.out.println();
    }
}

删除节点:

9.png

class ListNode {        //类名 :Java类就是一种自定义的数据结构
    int val;            //数据 :节点数据 
    ListNode next;      //对象 :引用下一个节点对象。在Java中没有指针的概念,Java中的引用和C语言的指针类似
    
    ListNode(int val){  //构造方法 :构造方法和类名相同   
        this.val=val;   //把接收的参数赋值给当前类的val变量
    }
}

class Test{
    public static void main(String[] args){
        
        ListNode nodeSta = new ListNode(0);          //创建首节点
        ListNode nextNode;                           //声明一个变量用来在移动过程中指向当前节点
        nextNode=nodeSta;                            //指向首节点
        
        //创建链表
        for(int i=1;i<10;i++){
            ListNode node = new ListNode(i);         //生成新的节点
            nextNode.next=node;                      //把心节点连起来
            nextNode=nextNode.next;                  //当前节点往后移动
        } //当for循环完成之后 nextNode指向最后一个节点,
        
        nextNode=nodeSta;                            //重新赋值让它指向首节点
        print(nextNode);                             //打印输出
     
        //删除节点
        while(nextNode!=null){
            if(nextNode.val==5){
                ListNode listNode=nextNode.next.next;     //保存要删除节点的下一个节点
                nextNode.next.next=null;                  //被删除节点 指向为空 ,等待java垃圾回收
                nextNode.next=listNode;                   //指向要删除节点的下一个节点
            }
            nextNode=nextNode.next;
        }//循环完成之后 nextNode指向最后一个节点
         nextNode=nodeSta;                            //重新赋值让它指向首节点
         print(nextNode);                             //打印输出
    }
    
    //打印输出方法
    static void print(ListNode listNoed){
        //创建链表节点
        while(listNoed!=null){
            System.out.println("节点:"+listNoed.val);
            listNoed=listNoed.next;
        }
        System.out.println();
    }
}

在对节点进行替换或删除的时候,被替换或被删节点的next引用需不需要设置为null?

答案是不需要的,这是因为一个对象被回收的前提是这个对象的引用(引用计数器为0)不再被持有,也就是说它不在被引用,这个时候它将会被回收,这个时候对于它引用什么对象已经无关紧要,这是因为对于它所引用的对象来说依然是看它的引用计数器是否为0。


后记

本文对链表概念单链表双链表循环链表,包括它们的定义与特点,以及Java中链表的使用(包括链表的创建与遍历,链表结点的插入与删除等)进行了详尽的介绍,如果你想继续深入了解更多数据结构与算法知识或深入学习Java,可参考以下学习路线。

👉Java全栈学习路线可参考:【Java全栈学习路线】最全的Java学习路线及知识清单,Java自学方向指引,内含最全Java全栈学习技术清单~ 👉算法刷题路线可参考:算法刷题路线总结与相关资料分享,内含最详尽的算法刷题路线指南及相关资料分享~

看完不关注就想跑.gif