[Java/数据结构]线性表之链表

画师:竹取工坊

大佬们好!我是Mem0rin!现在正在准备自学转码。

如果我的文章对你有帮助的话,欢迎关注我的主页Mem0rin,欢迎互三,一起进步!


文章目录


主要内容是无头的单向和双向的链表的实现和使用。依旧,我们只针对data为整数的情况进行模拟实现,如果需要了解泛型的具体实现请参考官方文档。

图片来自知乎一口气搞懂「链表」,就靠这20+张图了

一、单向链表

1.链表的概念和结构

结构如图:

链表上的每一个节点都包含一个 data 数据和一个 next 引用数据,用来指向下一个节点,节点与节点的跳转就是通过 next 进行的。

链表的优势在于添加和删除节点的时间复杂度为O(1),只需要改变前后节点的引用即可 。

2.链表的功能及实现

链表的主体

链表的主体是用一个内部类 ListNode 实现的,包括储存值的 val 和储存下一个节点位置的引用类型 ListNode next ,我们用 head 来指定链表的入口,也就是链表的第一个节点,如果 head 为null,说明链表为空。并在最后一个节点在next 上用null表示链表的尽头,定义如下:

java 复制代码
    public static class ListNode {
        public int val;
        public ListNode next;

        public ListNode(int val) {
            this.val = val;
        }
    }
    public ListNode head;
链表的遍历

这样我们就可以实现简单的链表遍历和相关的方法了:

java 复制代码
    public int size() {
        int size = 0;
        ListNode cur = head;
        while (cur != null) {
            cur = cur.next;
            size++;
        }
        return size;
    }
    public void display() {
        ListNode cur = head;
        while (cur != null) {
            System.out.print(cur.val + " ");
            cur = cur.next;
        }
    }
    public boolean contains(int key) {

        ListNode cur = head;
        while (cur != null) {
            if (cur.val == key) {
                return true;
            }
            cur = cur.next;
        }
        return false;
    }
    public void clear() {
        head = null;
    }
添加节点的方法add()

我们分为三种情况,在头部添加节点,在尾部添加节点,以及进一步地,在任意索引处添加节点。

java 复制代码
    //头插法
    public void addFirst(int data) {
        ListNode node = new ListNode(data);
        if (head == null) {
            head = node;
        } else {
            node.next = head;
            head = node;
        }
    }
    //尾插法
    public void addLast(int data) {
        if (head == null) {
            addFirst(data);
        } else {
            ListNode node = new ListNode(data);
            ListNode cur = head;
            while (cur.next != null) {
                cur = cur.next;
            }
            cur.next = node;
        }

    }

    //任意位置插入,第一个数据节点为0号下标
    public boolean addIndex(int index,int data) {
        int len = size();
        if (index < 0 || index > len) {
            return false;
        } else if (index == 0) {
            addFirst(data);
        } else if (index == len) {
            addLast(data);
        } else {
            ListNode node = new ListNode(data);
            ListNode cur = head;
            ListNode pre = null;
            for (int i = 0; i < index; i++) {
                pre = cur;
                cur = cur.next;
            }
            pre.next = node;
            node.next = cur;
        }
        return true;
    }

因为 addIndex 方法存在索引超出范围的情况,因此我们给方法的输出类型为 boolean 而非 void

我们也会发现在该方法的内部我们用pre来存储前一个节点,从而进行前后节点的修改,这样的备份在链表的操作中是常出现的。

删除节点remove()

如图所示,删除节点相当于是把上一个节点的next指向了当前节点的下一个节点,也就是用 pre.next = cur.next

java 复制代码
//删除第一次出现关键字为key的节点
    public void remove(int key) {
        if (contains(key)) {
            if (head.val ==key) {
                head = head.next;
                return;
            }
            ListNode pre = head;
            ListNode cur = head.next;
            while (cur != null) {
                if (cur.val == key) {
                    pre.next = cur.next;
                    return;
                }
                pre = cur;
                cur = cur.next;
            }
        }
    }
    //删除所有值为key的节点
    public void removeAllKey(int key) {
        if (head != null) {
            while (head != null && head.val == key) {
                head = head.next;
            }
            ListNode pre = null;
            ListNode cur = head;
            while (cur != null) {
                if (cur.val == key) {
                    pre.next = cur.next;
                }
                pre = cur;
                cur = cur.next;
            }
        }
    }

二、双向链表

1.双向链表的结构

双向链表是解决了单向链表只能从前往后遍历的缺点,内部有两个指针 prevnext,分别指向链表的前驱节点和后继节点。

其中链表的头的前驱和尾的后继均指向null。

2.双向链表的实现

节点设计

对于每个节点,都有储存数据的 data,前驱节点 prev, 后继节点 next。我们用 head 表示链表的头节点,并用 last 表示链表的尾节点。

实现如下:

java 复制代码
    static class ListNode {
        public int val;
        public ListNode prev;
        public ListNode next;

        public ListNode(int val) {
            this.val = val;
        }
    }
    public ListNode head;
    public ListNode last;
链表的遍历
java 复制代码
    //查找是否包含关键字key是否在单链表当中
    public boolean contains(int key) {
        ListNode cur = head;
        while (cur != null) {
            if (cur.val == key) {
                return true;
            }
            cur = cur.next;
        }
        return false;
    }
    public int size() {
        int size = 0;
        ListNode cur = head;
        while (cur != null) {
            cur = cur.next;
            size++;
        }
        return size;
    }
    public void display() {
        ListNode cur = head;
        while (cur != null) {
            System.out.print(cur.val + " ");
            cur = cur.next;
        }
    }
    public void clear() {
        head = null;
        last = null;
    }
添加节点

双向链表的添加节点如图所示,可以看到,我们需要改变的值有上一个节点的 next, 下一个节点的 prev,和插入节点的 prevnext

而头和尾的节点删除只需要从 headlast 处进行操作即可。

具体实现如下:

java 复制代码
    public void addFirst(int data) {
        ListNode node = new ListNode(data);
        if (head == null) {
            head = node;
            last = node;
        } else {
            head.prev = node;
            node.next = head;
            head = node;
        }
    }
    //尾插法
    public void addLast(int data) {
        if (head == null) {
            addFirst(data);
        } else {
            ListNode node = new ListNode(data);
            last.next = node;
            node.prev = last;
            last = node;
        }

    }

    //任意位置插入,第一个数据节点为0号下标
    public boolean addIndex(int index,int data) {
        int len = size();
        if (index < 0 || index > len) {
            return false;
        } else if (index == 0) {
            addFirst(data);
        } else if (index == len) {
            addLast(data);
        } else {
            ListNode node = new ListNode(data);
            ListNode cur = head;
            for (int i = 0; i < index; i++) {
                cur = cur.next;
            }
            node.prev = cur.prev;
            node.next = cur;
            cur.prev.next = node;
            cur.prev = node;
        }
        return true;
    }
删除节点

删除 cur 节点需要让前驱节点指向后驱节点,然后让后驱节点执行前驱节点,用代码表示就是 cur.prev.next = cur.next; cur.next.prev = cur.prev

具体实现如下:

java 复制代码
    public void remove(int key) {
        if (!contains(key)) {
            return;
        }
        if (head.val == key) {
            head = head.next;
            if (head != null) {
                head.prev = null;
            }
        } else {
            ListNode cur = head;
            while (cur.next != null) {
                if (cur.val == key) {
                    cur.prev.next = cur.next;
                    cur.next.prev = cur.prev;
                    return;
                }
                cur = cur.next;
            }
            if (cur.val == key) {
                last = cur.prev;
                cur.prev.next = null;
            }
        }

    }
    //删除所有值为key的节点
    public void removeAllKey(int key) {
        if (!contains(key)) {
            return;
        }
        while (head.val == key) {
            head = head.next;
            if (head == null) {
                return;
            }
            head.prev = null;
        }
        ListNode cur = head;
        while (cur.next != null) {
            if (cur.val == key) {
                cur.prev.next = cur.next;
                cur.next.prev = cur.prev;
            }
            cur = cur.next;
        }
        if (cur.val == key) {
            last = cur.prev;
            cur.prev.next = null;
        }

    }

三、LinkedList的使用

LinkedList类是Java内置的数据结构,底层是双向链表。这使得我们不必每次都去手动去实现一个双向链表,而是直接调用现成的数据结构即可:

在集合框架下实现的接口和继承的父类如下:

  1. LinkedList实现了List接口
  2. LinkedList的底层使用了双向链表
  3. LinkedList没有实现RandomAccess接口,因此LinkedList不支持随机访问
  4. LinkedList的任意位置插入和删除元素时效率比较高,时间复杂度为O(1)
  5. LinkedList比较适合任意位置插入的场景

构造方法

方法 解释
LinkedList() 无参构造
public LinkedList(Collection<? extends E> c) 用实现了Collection的其他元素进行构造

迭代元素

一般有三种方法:for循环,for-each循环,以及用迭代器遍历

java 复制代码
// foreach遍历
	for (int e:list) {
		System.out.print(e + " ");
	}
	System.out.println();
// 使用迭代器遍历---正向遍历
	ListIterator<Integer> it = list.listIterator();
	while(it.hasNext()){
		System.out.print(it.next()+ " ");
	}
	System.out.println();
// 使用反向迭代器---反向遍历
	ListIterator<Integer> rit = list.listIterator(list.size());
	while (rit.hasPrevious()){
		System.out.print(rit.previous() +" ");
	}
	System.out.println();

常用方法

根据功能相似性,将 List 接口中的方法进行合并,提取共同点,形成以下表格:

方法 功能
boolean add(E e) void add(int index, E element) boolean addAll(Collection<? extends E> c) 添加元素或集合。 可尾插单个元素、在指定位置插入单个元素,或尾插整个集合。
E remove(int index) boolean remove(Object o) 删除元素。 可按索引删除,或删除第一个匹配的指定对象。
E get(int index) E set(int index, E element) 访问索引位置元素。 get 获取指定位置元素,set 将指定位置元素替换为新值并返回旧值。
int indexOf(Object o) int lastIndexOf(Object o) 查找元素位置。 返回第一个或最后一个匹配对象的下标,未找到时返回 -1
boolean contains(Object o) 判断是否包含指定对象。
void clear() 清空线性表中所有元素。
List<E> subList(int fromIndex, int toIndex) 截取列表的子列表(视图),包含起始索引,不包含结束索引。

明天看看能不能把队列和栈一起写了。

相关推荐
香香甜甜的辣椒炒肉2 小时前
Spring(1)基本概念+开发的基本步骤
java·后端·spring
人大博士的交易之路2 小时前
数据结构算法——python数据结构
开发语言·数据结构·python
成都渲染101云渲染66663 小时前
跳出“硬件堆砌”陷阱|渲染101如何用技术重构云渲染的专业价值?
java·前端·javascript
golang学习记3 小时前
IDEA 2026.1全新调试新特性:Spring Boot调试不再靠猜!
java·spring boot·intellij-idea
橘子编程3 小时前
OpenClaw(小龙虾)完整知识汇总
java·前端·spring boot·spring·spring cloud·html5
大阿明3 小时前
SpringBoot - Cookie & Session 用户登录及登录状态保持功能实现
java·spring boot·后端
Binary-Jeff3 小时前
Spring 创建 Bean 的关键流程
java·开发语言·前端·spring boot·后端·spring·学习方法
Kk.08023 小时前
数据结构|排序算法(二) 冒泡排序
数据结构·算法·排序算法
rOuN STAT3 小时前
MySQL:基础操作(增删查改)
java