数据结构和算法之【链表】

目录

认识链表

链表的定义

链表的分类

性能

特殊节点

设计链表(LeetCode-707题)

实现方式一:单向链表

题解

测试结果

实现方式二:双向链表

题解

测试结果

Java中提供的链表

底层存储方式

添加元素

顺序添加元素

头部添加元素

随机添加元素

可以用作栈

可以用作队列


认识链表

链表的定义

计算机科学中,链表是数据元素的线性集合,每个元素都指向下一个元素,元素存储上并不连续

链表的分类

  • 单向链表:链表中每个元素只指向下一个元素
  • 双向链表:链表中每个元素既会指向下一个元素,也会指向上一个元素
  • 环形链表:链表中最后一个元素的next指针不指向null,而是指向链表中某个已有元素,从而形成一个闭环结构

性能

  • 随机查找元素:从头节点或尾节点依次查找,时间复杂度为O(n)
  • 插入或删除元素:增删元素本身的操作只需要改变指针指向,时间复杂度为O(1),但是定位到从何处增删元素这个过程的时间复杂度为O(n)

特殊节点

链表内还可以有一种特殊的节点,叫dummy节点,它不存储数据,通常用作头尾,用来简化边界判断

设计链表(LeetCode-707题)

实现方式一:单向链表

题解

java 复制代码
class MyLinkedList {
    private final Node dummy = new Node();
    private int size;

    public MyLinkedList() {

    }

    public int get(int index) {
        Node node = getNodeByIndex(index);
        // 题目要求:下标无效返回-1
        if (node == null) {
            return -1;
        }
        return node.val;
    }

    /**
     * 获取链表中指定下标处的节点
     */
    private Node getNodeByIndex(int index) {
        if (index < 0 || index >= size) {
            return null;
        }
        Node p = dummy;
        for (int i = 0; i <= index; i++) {
            p = p.next;
        }
        return p;
    }

    public void addAtHead(int val) {
        dummy.next = new Node(val, dummy.next);
        size++;
    }

    public void addAtTail(int val) {
        // 拿到尾节点
        Node tailNode;
        if (size == 0) {
            tailNode = dummy;
        } else {
            tailNode = getNodeByIndex(size - 1);
        }
        // 新节点追加到尾节点
        tailNode.next = new Node(val, null);
        size++;
    }

    public void addAtIndex(int index, int val) {
        // 下标不合法
        if (index < 0 || index > size) {
            return;
        }
        // addHead
        if (index == 0) {
            addAtHead(val);
            return;
        }
        // addTail
        if (index == size) {
            addAtTail(val);
            return;
        }
        // other
        Node prev = getNodeByIndex(index - 1);
        Node next = prev.next;
        prev.next = new Node(val, next);
        size++;
    }

    public void deleteAtIndex(int index) {
        // 下标不合法
        if (index < 0 || index >= size) {
            return;
        }
        Node node = getNodeByIndex(index);
        if (index == 0) {
            dummy.next = node.next;
        } else {
            Node prev = getNodeByIndex(index - 1);
            prev.next = node.next;
        }
        size--;
    }

    private static class Node {
        int val;
        Node next;

        public Node() {
        }

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

测试结果

实现方式二:双向链表

题解

java 复制代码
class MyLinkedList {
    private final Node dummyHead;
    private final Node dummyTail;
    private int size;

    public MyLinkedList() {
        // 初始状态
        dummyHead = new Node();
        dummyTail = new Node();
        dummyHead.next = dummyTail;
        dummyTail.prev = dummyHead;
    }

    public int get(int index) {
        // 为提高查找性能
        // 查找索引在中间位置之前的元素,从头节点开始查找
        // 查找索引在中间位置之后的元素,从尾节点开始查找
        Node node;
        if (index <= (size >> 1)) {
            node = getNodeByHead(index);
        } else {
            node = getNodeByTail(size - index - 1);
        }
        if (node == null) {
            return -1;
        }
        return node.val;
    }

    /**
     * 从头节点开始查找元素
     */
    private Node getNodeByHead(int index) {
        if (index < 0 || index >= size) {
            return null;
        }
        Node p = dummyHead;
        for (int i = 0; i <= index; i++) {
            p = p.next;
        }
        return p;
    }

    /**
     * 从尾节点开始查找元素
     */
    private Node getNodeByTail(int index) {
        if (index < 0 || index >= size) {
            return null;
        }
        Node p = dummyTail;
        for (int i = 0; i <= index; i++) {
            p = p.prev;
        }
        return p;
    }

    public void addAtHead(int val) {
        // 链表头部添加元素
        // 创建新节点
        // 改变原本的头节点的指针指向
        // 设置新节点的指针指向
        Node curr = new Node(val, null, null);
        Node next = dummyHead.next;
        dummyHead.next = curr;
        curr.prev = dummyHead;
        curr.next = next;
        next.prev = curr;
        size++;
    }

    public void addAtTail(int val) {
        Node curr = new Node(val, null, null);
        Node prev = dummyTail.prev;
        dummyTail.prev = curr;
        curr.next = dummyTail;
        curr.prev = prev;
        prev.next = curr;
        size++;
    }

    public void addAtIndex(int index, int val) {
        // 下标不合法
        if (index < 0 || index > size) {
            return;
        }
        // 头部添加元素
        if (index == 0) {
            addAtHead(val);
            return;
        }
        // 尾部添加元素
        if (index == size) {
            addAtTail(val);
            return;
        }
        // 其他位置添加元素
        Node node;
        if (index <= (size >> 1)) {
            node = getNodeByHead(index - 1);
        } else {
            node = getNodeByTail(size - index);
        }
        Node curr = new Node(val, null, null);
        Node next = node.next;
        node.next = curr;
        curr.next = next;
        next.prev = curr;
        curr.prev = node;
        size++;
    }

    public void deleteAtIndex(int index) {
        if (index < 0 || index >= size) {
            return;
        }
        Node node;
        if (index <= (size >> 1)) {
            node = getNodeByHead(index - 1);
            if (node == null)
                node = dummyHead;
        } else {
            node = getNodeByTail(size - index);
        }
        Node next = node.next.next;
        node.next = next;
        next.prev = node;
        size--;
    }

    private static class Node {
        int val;
        Node prev;
        Node next;

        public Node() {
        }

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

测试结果

Java中提供的链表

在Java中,提供了一个类LinkedList,它的底层是用双向链表实现的

底层存储方式

java 复制代码
package java.util;

// ...

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
    // 记录元素个数
    transient int size = 0;
    // 链表中第一个节点
    transient Node<E> first;
    // 链表中最后一个节点
    transient Node<E> last;

    public LinkedList() {
    }

    // 数据节点:双向链表
    private static class Node<E> {
        E item;
        Node<E> next;
        Node<E> prev;

        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }
    
    // ...
}

添加元素

顺序添加元素

即向尾部添加元素,通过last节点链接新元素,时间复杂度为O(1)

java 复制代码
package java.util;

// ...

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
    // ...

    // 该方法就是向尾部添加元素
    public boolean add(E e) {
        linkLast(e);
        return true;
    }

    // 同样是向尾部添加元素,和add一样,区别是没有返回值
    public void addLast(E e) {
        linkLast(e);
    }

    void linkLast(E e) {
        final Node<E> l = last;
        final Node<E> newNode = new Node<>(l, e, null);
        last = newNode;
        if (l == null)
            first = newNode;
        else
            l.next = newNode;
        size++;
        modCount++;
    }

    // ...
}

头部添加元素

向头部添加元素,通过first节点链接元素,时间复杂度为O(1)

java 复制代码
package java.util;

// ...

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
    // ...

    // 向头部添加元素
    public void addFirst(E e) {
        linkFirst(e);
    }

    private void linkFirst(E e) {
        final Node<E> f = first;
        final Node<E> newNode = new Node<>(null, e, f);
        first = newNode;
        if (f == null)
            last = newNode;
        else
            f.prev = newNode;
        size++;
        modCount++;
    }

    // ...
}

随机添加元素

需要先找到插入位置,再进行插入操作,单就插入操作而言的时间复杂度为O(1),但是查找的过程时间复杂度为O(n),所以整体的时间复杂度为O(n)

java 复制代码
package java.util;

// ...

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
    // ...
   
    // 向随机位置添加元素
    public void add(int index, E element) {
        checkPositionIndex(index);

        if (index == size)
            linkLast(element);
        else
            linkBefore(element, node(index));
    }

    // 这里有查找元素的过程,时间复杂度为O(n)
    Node<E> node(int index) {
        // assert isElementIndex(index);

        if (index < (size >> 1)) {
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }

    // ...
}

可以用作栈

LinkedList中提供了push方法和pop方法,通过两个方法的组合,可以实现栈,先进后出的特点

  • push方法:入栈操作,从头部添加元素
java 复制代码
public void push(E e) {
    addFirst(e);
}
  • pop方法:出栈操作,从头部删除元素(如果栈中没有元素了,会抛出异常)
java 复制代码
public E pop() {
    return removeFirst();
}
  • 栈先进后出的特点
java 复制代码
package algorithm.list;

import java.util.LinkedList;

public class LinkedListJava {
    public static void main(String[] args) {
        LinkedList<String> stack = new LinkedList<>();
        // 入栈
        stack.push("aaa");
        stack.push("bbb");
        stack.push("ccc");
        stack.push("ddd");
        // 出栈
        while (!stack.isEmpty()) {
            System.out.println(stack.pop());
        }
    }
}
bash 复制代码
ddd
ccc
bbb
aaa

可以用作队列

LinkedList中提供了offer方法和poll方法,通过两个方法的组合,可以实现队列,先进先出的特点

  • offer方法:入队操作,从尾部添加元素
java 复制代码
public boolean offer(E e) {
    return add(e);
}

public boolean add(E e) {
    linkLast(e);
    return true;
}
  • poll方法:出队操作,从头部删除元素(如果队列中没有元素了,返回null,而不是抛出异常)
java 复制代码
public E poll() {
    final LinkedList.Node<E> f = first;
    return (f == null) ? null : unlinkFirst(f);
}
  • 队列先进先出的特点
java 复制代码
package algorithm.list;

import java.util.LinkedList;

public class LinkedListJava {

    public static void main(String[] args) {
        LinkedList<String> queue = new LinkedList<>();
        // 入队
        queue.offer("aaa");
        queue.offer("bbb");
        queue.offer("ccc");
        queue.offer("ddd");
        // 出队
        while (!queue.isEmpty()) {
            System.out.println(queue.poll());
        }
    }
}
bash 复制代码
aaa
bbb
ccc
ddd
相关推荐
Sun 32851 小时前
MyBatis-Plus 新版代码生成器的使用
java·spring boot·后端·spring·配置·mybatis-plus·代码生成器
一直都在5721 小时前
新Java基础(二十五):异常类
java·开发语言
礼拜天没时间.1 小时前
力扣热题100实战 | 第31期:下一个排列——数组规律的极致探索
java·算法·leetcode·字典序·原地算法·力扣热题100
ws540d1 小时前
Ranking All UsersLast Updated: 2026-03-14(Sat) 19:46算法启发式活跃用户所有用户
算法
xiaoye37082 小时前
java后端面试一般问什么?
java·面试
进击的小头2 小时前
第8篇:线性二次型调节器
python·算法·动态规划
Z9fish2 小时前
sse哈工大C语言编程练习42
c语言·开发语言·算法
badhope2 小时前
OpenClaw卸载命令全解析
java·linux·人工智能·python·sql·数据挖掘·策略模式
Hello.Reader2 小时前
Flink Task Lifecycle 一篇讲透 StreamTask 与 Operator 生命周期
java·大数据·flink