Skip to content

链表学习

约 3491 字大约 12 分钟

数据结构

2025-03-04

链表
链表

什么是链表

链表是通过指针串联在一起的一种线性结构,每一个节点有两部分组成,第一部分是数据域(存放对应的数据),第二部分是指针域(指向下一个节点的指针),最后一个节点的指针指向空指针。

如图所示:

image-20240112103131410
image-20240112103131410

链表的特点

我们需要明确一点,那就是链表里面不是连续分布的,链表是通过指针来连接内存中的各个节点,那么就意味着不能和数组一样通过指定的下标来访问对应元素,只能通过指针遍历访问。但是链表不需要像数组一样需要预先申请空间,但是因为指针的存在,所以会占据部分空间。

链表的类型

有单链表,双链表,循环链表。

单链表

image-20240112103131410
image-20240112103131410

如上图所示,每一个节点都有一个数据域和指针域,指针指向下一个节点,通过指针的移动可以指向下一个节点。查询元素只能从前到后依次查询。

双链表

image-20240112111421094
image-20240112111421094

如上图所示,双链表里面有两个指针域,一个指针指向前一个节点,另一个指针指向后一个节点。双链表可以从前查询也可以从后查询,功能相对于单链表增加了。

循环链表

顾名思义,就是将链表的首尾相连接起来。

包含单循环链表和双循环链表。

下图展示单循环链表。

image-20240112112043641
image-20240112112043641

链表的创建(Java)

单链表

从以上介绍我们可以知道链表需要一个存储的值value,和移动的指针next。

public class Node {
    // 数据值
    int value;
    // 指针
    Node next;
	// 构造函数
    public Node(int value, Node next) {
        this.value = value;
        this.next = next;
    }
	// 无参构造
    public Node() {
    }
	
    public Node(int value) {
        this.value = value;
    }

    public Node(Node next) {
        this.next = next;
    }
}

双链表

双链表相对于单链表,多了一个指向前面节点的指针prev。

public class Node {
    // 数据值
    int value;
    // 后指针
    Node next;
	// 前指针
    Node prev;

    public Node(int value, Node next,Node prev) {
        this.value = value;
        this.next = next;
        this.prev = prev;
    }

    public Node() {
    }

    public Node(int value) {
        this.value = value;
    }

    public Node(Node next) {
        this.next = next;
    }

}

遍历链表

对于单链表,遍历只能从头节点开始一直到尾节点获取到对应的长度,双向链表也是一样。

public static int getListLength(Node head) {
    int len = 0;
    Node node = head;
    while (node != null) {
        len++;
        node = node.next;
    }
    return len;
}

上面代码是定义了一个临时节点,让这个临时节点指向头节点,然后向后遍历,这样做的主要是为了不影响到头节点的位置,一旦头节点的位置改变了,那么之后无法获取到正确的头节点的位置。

插入元素

以下链表都默认为元素是递增的情况,否则部分情况不是太好操作的。

单链表

首先我们需要明确插入元素的位置,有首部,中部,尾部。

首部

插入在头部看起来简单,但是容易出错,总共有两步,第一步是先插入元素头头节点,第二步是插入元素之后,头节点就需要改成新的节点,不再是之前的节点了。

假设现在有链表 2>3->4需要插入1到头节点。

如下图所示

image-20240112120204636
image-20240112120204636

代码

newNode.next = head
head = newNode

中部

假设现在我们有链表 1->2->4->null,现在需要插入元素3到第三个节点的位置,(如下图所示),首先我们需要遍历找到插入元素的位置,但是当我们找到第三个节点位置的时候,我们已经无法获取之前的节点元素,所以我们只能获取目标节点的前一个节点。

image-20240112121414583
image-20240112121414583

步骤如下

  1. 先找到2节点所在元素的位置
image-20240112121430846
image-20240112121430846
  1. 将newNode的下一个指针指向2节点的下一个节点的位置
image-20240112130947290
image-20240112130947290
  1. 将2节点的指针指向newNode节点。
image-20240112121617991
image-20240112121617991

伪代码如下

Node cur = head;
int count =1;
找到前一个节点的元素位置
while(count < len -1){
    count++;
    cur=cur.next;
}
newNode.next = cur;
cur.next = newNode;

注意第二步和第三步不能反过来,如果反过来了,那么cur先指向了newNode,它会自动断开与后续节点的关联,那么就丢失了后续的节点,无法连接。

尾部

尾部就相对简单了,只需要遍历到最后一个节点,将节点的下一个指针指向插入的元素就可以了。

总结

考虑到以上的三种情况,这边我们可以编写一个通用的方法来处理这些问题,而不是单独处理。

需要传递参数:头节点head,新的节点newNode,插入元素的位置position

步骤如下:

  1. 先判断头节点是否为空,为空只需要返回新的节点,这就是新的链表
  2. 头节点不为空,遍历获取链表长度,判断插入元素的位置是否再链表长度范围内
  3. 不在范围内,就意味着位置不合理
  4. 在范围内,判断是否是头节点的位置,是头节点的位置,就是头插法
  5. 不是头节点的位置,就是中部和尾部的插入方法,但是之前已经控制了范围,所以不需要判断尾部插入了,直接合并
  6. 返回新的头节点的位置
/**
 * 插入指定位置的节点
 * @param head 头节点
 * @param newNode 插入节点
 * @param position 插入位置
 * @return 头节点
 */
public static Node insertNode(Node head, Node newNode, int position) {
    // 1. 链表无元素
    if (head == null) {
        return newNode;
    }
    // 2. 获取链表长度
    int length = getListLength(head);
    // 3. 判断插入位置
    if (position < 1 || position > length + 1) {
        System.out.println("元素插入位置不合理");
        return head;
    }
    //4.头插法
    if (position == 1) {
        newNode.next = head;
        head = newNode;
        return head;
    }

    //5. 其余插入方式
    Node temp = head;
    int count = 1;
    // 当temp指向position-1位置
    while (count < position - 1) {
        count++;
        temp = temp.next;
    }
    newNode.next = temp.next;
    temp.next = newNode;
    return head;
}

双链表

算法的话,单链表是比较常见的,双链表处理方法也是类似,Java里面的LinkedList就是典型的双链表的应用,后续将会结合使用。

image-20240112132202135
image-20240112132202135

头插法

现在有2,3,4,5元素的双链表,希望使用头插法实现

image-20240112140610608
image-20240112140610608

步骤:

  1. 先记录头节点,设置新的节点的prev指针指向null
  2. 然后将新的节点的next指针指向头节点
  3. 然后将头节点的prev指针指向新的节点
  4. 确定新的头节点
image-20240112140658427
image-20240112140658427
image-20240112140720549
image-20240112140720549
image-20240112140731744
image-20240112140731744
    public static Node insertHead(Node head, Node newNode) {
        newNode.prev = null;
        newNode.next = head;
        head.prev = newNode;
        head = newNode;
        return head;
    }

中部

如下图所示

image-20240112140836347
image-20240112140836347

步骤如下

  1. 先要找到要插入元素的位置的前一个节点的位置,记为cur
image-20240112142651181
image-20240112142651181
  1. 然后将newNode的next指针指向cur的next,就记录到了后面节点。
image-20240112142920767
image-20240112142920767
  1. 此时需要断开cur的下一个节点的prev指针,指向newNode,这样就将newNode和之后的节点绑定
image-20240112144621680
image-20240112144621680
  1. newNode的prev指针就需要指向cur,开始和cur建立联系
image-20240112144833666
image-20240112144833666
  1. cur的next指向newNode,完成插入
image-20240112144857030
image-20240112144857030

这里和单链表一样的问题,首先需要先将目标节点之后的这些节点和新节点建立联系,然后才是和目标节点建立联系,不能反过来,反过来了就丢失了后续的节点。

public static Node insertMiddle(Node head, Node newNode, int position) {
    Node cur = head;
    int count = 1;
    while (count < position - 1) {
        count++;
        cur = cur.next;
    }
    // 新节点的next指向cur的next的节点
    newNode.next = cur.next;
    // cur的下一个节点的prev指针指向newNode
    cur.next.prev = newNode;
    //  新节点的prev指向cur
    newNode.prev = cur;
    // cur的next指向新节点
    cur.next = newNode;
    return head;
}

尾部

相对就简单了,只需要遍历到最后一个不为null的节点,将newNode节点接入最后一个节点就可以了

public static Node insertTail(Node head, Node newNode) {
    Node temp = head;
    while (temp.next != null) {
        temp = temp.next;
    }
    temp.next = newNode;
    newNode.prev = temp;

    newNode.next = null;
    return head;
}

总结

步骤:

  1. 判断原链表是否为空,为空直接返回新链表
  2. 判断位置是否在范围内,不在就返回
  3. 如果位置是1,也就是头插法
  4. 其他是中部和尾部方法,找到对应目标的前一个节点,然后就是中部插入的方法
public static Node insertNode(Node head, Node newNode, int position) {
    // 1. 判断是否为空
    if (head == null) {
        return newNode;
    }
	// 计算长度
    int length = getLength(head);
	//2.位置是否合理
    if(position<1 || position>length+1){
        System.out.println("位置不合理");
        return head;
    }
	//3. 头插法
    if (position == 1) {
        newNode.next = head;
        head.prev = newNode;
        head = newNode;
        return head;
    }
	// 4. 找到前一个节点
    int count = 1;
    Node cur = head;
    while (count < position - 1) {
        count++;
        cur = cur.next;
    }
	// 5. 开始插入
    newNode.next = cur.next;
    if (cur.next != null) {
        cur.next.prev = newNode;
    }

    newNode.prev = cur;
    cur.next = newNode;
    return head;
}

删除元素

单链表

头删

image-20240112151229707
image-20240112151229707

相对简单,只需要执行head = head.next,然后jvm会来执行对应的垃圾回收

指定位置删除

比如我现在想删除第三个节点也就是3

步骤:

  1. 先找到第二个节点的位置
  2. 然后将第二个节点的next指向第三个节点的next,也就是第四个节点
image-20240112151650681
image-20240112151650681
cur.next = cur.next.next

尾部删除

image-20240112151815891
image-20240112151815891

也是同理,不过只需要获取最后一个节点的前一个节点,然后这个节点指向null就可以了

总结

步骤:

  1. 判断是否为空,为空删除失败
  2. 不为空,判断删除位置是否合理
  3. 删除位置为1,表示头节点,直接删
  4. 删除其他位置,遍历到指定位置的前一个节点位置操作。

    /**
     * 删除指定位置的节点
     * @param head 头节点
     * @param position 从1开始
     * @return
     */
    public static Node deleteNode(Node head, int position) {
        // 空链表,无法删除
        if (head == null) {
            System.out.println("链表为空,无法删除");
            return null;
        }
        int length = getListLength(head);
        // 删除位置不合法
        if (position < 1 || position > length ) {
            System.out.println("删除位置不合法");
            return head;
        }
        // 头删除
        if (position == 1) {
            head = head.next;
            return head;
        }
        
        // 删除指定位置
        int count = 1;
        Node cur = head;
        while (count < position - 1) {
            count++;
            cur = cur.next;
        }
        cur.next = cur.next.next;
        return head;
    }

position > length 而不是`position > length +1,当输入的位置是最后一个节点的后面,也就是null,这时候如果加上了1,就认为这个null是一个节点,但是很显然,这个已经超出了这个长度,不合理。

双链表

image-20240112154730524
image-20240112154730524

删除头节点

只需要将head移动到下面一个节点,然后让这个节点的prev指向null就可以

    public static Node deleteHead(Node head) {
        head = head.next;
        head.prev = null;
        return head;
    }

这边省去了判断head后面是否为空的步骤

删除中间指定位置

image-20240112155522845
image-20240112155522845

比如删除指定节点3

步骤:

  1. 找到删除指定位置的前一个节点cur
  2. 然后cur的next指向删除节点的下一个节点
  3. 然后这个节点再指向cur
public static Node deleteMiddle(Node head, int position) {
    int count = 1;
    Node cur = head;
    while (count < position-1){
        count++;
        cur =  cur.next;
    }
    cur.next = cur.next.next;
    cur.next.prev = cur;
    return head;
}

其中设置postion-1的目的是找到前一个节点的位置,如果是position,当经过while条件的时候,此时cur是前一个节点,但是此时还是会找下一个next,那么此时就变成了指定节点的位置,而不是前一个节点了。

尾部删除

需要找到倒数第二个节点的位置,然后这个节点的next指向null

public static Node deleteTail(Node head) {
    Node cur = head;
    while (cur.next.next != null) {
        cur = cur.next;
    }
    cur.next = null;
    return head;
}

这边省去了cur的next节点为空的情况

总结

步骤:

  1. 判断是否存在链表
  2. 判断删除位置是否合法
  3. 如果位置是1,头删法
  4. 其他方式需要取到删除节点的前一个节点的位置,需要判断两个节点是否为空
public static Node deleteNode(Node head, int position) {
    if (head == null) {
        System.out.println("删除失败,不存在节点");
        return null;
    }
    int length = getLength(head);
    // 位置不合法
    if (position < 1 || position > length) {
        System.out.println("删除失败,位置不合法");
        return null;
    }
    // 删除头节点
    if (position == 1) {
        head = head.next;
        if (head != null) {
            head.prev = null;
        }
        return head;
    }

    // 删除其他节点
    int count = 1;
    Node cur = head;
    while (count < position - 1 && cur.next != null) {
        count++;
        cur = cur.next;
    }
	// 判断下一个节点是否为空
    if (cur.next != null && cur.next.next != null) {
        cur.next = cur.next.next;
        cur.next.prev = cur;
    }
    cur.next = null;
    return head;
}

这个里面删除的时候需要判断一些情况

  1. head的下一个节点为空
  2. 删除指定节点的时候,这个节点是空
  3. 删除指定节点的时候,这个节点的前一个节点也是空

时间复杂度

访问 O(n)

查找 O(n)

插入 O(1):插入指定位置节点的这个过程是O(n),只是这个插入操作时O(1)

删除 O(1):删除指定位置节点的这个过程是O(n),只是这个删除操作是O(1)

LinkedList源码分析

常见面试题

  1. 描述链表的数据结构?
  2. Java里面的LinkedList使用的是什么链表?
  3. 链表删除,插入,获取元素的时间复杂度是多少?
  4. 什么场景下使用链表比较合适?

leetcode算法

详情访问算法链表章节

贡献者

  • flycodeuflycodeu

公告板

2025-03-04正式迁移知识库到此项目