Java集合中的链表与结构详解

2025-08-13 22:50
文章标签 java 链表 详解 结构 集合

本文主要是介绍Java集合中的链表与结构详解,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

《Java集合中的链表与结构详解》链表是一种物理存储结构上非连续的存储结构,数据元素的逻辑顺序的通过链表中的引用链接次序实现,文章对比ArrayList与LinkedList的结构差异,详细讲解了链表...

前情提要:在学习 ArrayList 时,认识到由于其底层是一段连续空间,当在 ArrayList 任意位置插入或者删除元素时,就需要将后序元素整体往前或者往后搬移,时间复杂度为O(n),效率比较低,因此 ArrayList 不适合做任意位置插入和删除比较多的场景。因此:Java 集合中又引入了LinkedList,即链表结构。

一、链表概念与结构

        链表是一种物理存储结构上非连续的存储结构,数据元素的逻辑顺序的通过链表中的引用链接次序实现的。

其结构总共有8种:

        带头链表中的第一个节点相当于一个标记,如果采用头插法新增节点,只能在当前头的下一个节点插入;而如果是不带头的链表,头插法是在第一个节点前面新增节点。

Java集合中的链表与结构详解

二、当向单链表的实现

整个实现将围绕下图展开:

        定义一个 cur 对象,表示当前节点。用法:遍历链表。

Java集合中的链表与结构详解

注意

1、cur 为空和 cur.next 为空的区别。     

2、单向链表不能回退。

2.1 准备工作

        1、先定义一个类用来表示整个链表,名为 SingleLinkedList。

        2、而链表中包含节点,每个节点带有两个域,一个是数据域存储整型数据;另一个是指针域,指针域中存放的是下一个 节点 的引用,因此其类型也就是节点;且注意两个属性都用 public 修饰,在测试时大有用途。将每个节点封装成链表的内部类

        3、定义一个头部节点,用于查找或者输出链表数据,但这里并不代表链表带头。

        4、将对链表的操作封装成一个接口,其功能包括打印链表数据 display、计算链表长度 size、查找数据 contains、头插法插入数据 addFirst、尾插法插入数据 addLast、任意位置插入数据 addIndex、删除第一次出现 key 的节点 remove、删除所有值为 key 的节点 removeAllKey、清空。

SingleLinkedList 链表:

package test;
// 不带头 单向 链表
public class SingleLinkedList implements ILink{
    static class ListNode{   // 每一个 节点 都是对象,具有共同属性,因此可以抽象成内部类
        public int val;
        public ListNode next;  // 节点的地址,类型也应该是节点的类型
        public ListNode(int val){
            this.val = val;
        }
    }
    public ListNode head;  // 头节点
    @Override
    // ......
}

ILink 接口:

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

        在实现具体的操作之前,还需要考虑链表是否为空,即 head == null ?

2.2 初始化链表

        在具体实现各项功能之前,需要有一个原始的链表,利于展示操作效果。其定义方法如下

package test;
public class SingleLinkedList implements ILink{
    // ......
    public void creatList(){  // 创建一个原始链表
        ListNode node1 = new ListNode(1);
        ListNode node2 = new ListNode(2);
        ListNode node3 = new ListNode(3);
        ListNode node4 = new ListNode(4);
        this.head = node1;
        node1.next = node2;
        node2.next = node3;
        node3.next = node4;
    }
    // ......
}

2.3 打印数据、链表长度、查找数据

        这三个功能都需要将链表从头到尾遍历一遍。而遍历的条件只是当前节点不为空。

package test;
public class SingleLinkedList implements ILink{
    // ......
    @Override
    public void display() {
        ListNode cur = head;
        while(cur != null){
            System.out.print(cur.val + " ");
            cur = cur.next;
        }
        System.out.println();   // 换行
    }
    @Override
    public int size() {
        ListNode cur = head;
        int count = 0;
        while (cur != null){
            count++;
            cur = cur.next;
        }
        return count;
    }
    @Override
    public boolean contains(int key) {
        ListNode cur = head;
        while (cur != null){
            if(cur.val == key){
                return true;
            }
            cur = cur.next;
        }
        return false;
    }
}
package test;
public class TestSingle {
    public static void main(String[] args) {
        // ILink iLink = new SingleLinkedList(); // 通过接口无法调用 creatList()
        SingleLinkedList linkedList = new SingleLinkedList();
        linkedList.creatList();  // 还没实现新增节点功能前调用的原始链表
        linkedList.display();
        System.out.println(linkedList.size());
    }
}

2.4 头插法

Java集合中的链表与结构详解

@Override
public void addFirst(int data) {
    ListNode newNode = new ListNode(data);
    newNode.next = head;
    this.head = newNode;
}

        即使原先的链表是空的,代码一样能实现头插法插入节点。

2.5 尾插法

Java集合中的链表与结构详解

@Override
public void addLast(int data) {
    ListNode newNode = new ListNode(data);
    if (head == null){  // 原来链表中一个节点都没有的情况下
        head = newNode;
        return;
    }
    ListNode cur = head;
    while (cur.next != null){
        cur = cur.next;
    }  // 循环走完说明 cur.next == null 了
    cur.next = newNode;
}
package test_8_7;
public class TestSingle {
    public static void main(String[] args) {
        SingleLinkedList linkedList = new SingleLinkedList();
        // 实现新增节点功能之后,不需要调用 creatList(),也可以创建链表
        // 头插法
        linkedList.addFirst(11);
        linkedList.addFirst(22);
        linkedList.addFirst(33);
        linkedList.addFirst(44);
        // 尾插法
        linkedList.addLast(55);
        linkedList.display();
        System.out.println(linkedList.size());
    }
}

2.6 任意位置插入节点

注意

1、指定位置的合法性;

2、指定下标为 0 和 指定位置等于链表长度 的情况分开处理;

3、单向链表不能回退,因此注意 cur 的位置;

4、所有的插入,优先绑定后面的节点。

Java集合中的链表与结构详解

    @Override
    public void addIndex(int index, int data) throws IllegalIndexException{
        int len = this.size();  // 存储当前长度,好处:提高效率
        if (index < 0 || index > len){
            throw new IllegalIndexException("指定位置不合法!!!");
        }else {
            if (index == 0){
                this.addFirst(data);
                return;
            }
            if (index == len){
                this.addLast(data);
                return;
            }
            ListNode newNode = new ListNode(data);
            ListNode cur = head;
            while (index - 1 != 0){
                cur = cur.next;
                index--;
            }
            newNode.next = cur.next;
            cur.next = newNode;
        }
    }
package test;
// 处理指定位置的不合法异常
public class IllegalIndexException extends RuntimeException{
    public IllegalIndexException(){
    }
    public IllegalIndexException(String msg){
        super(msg);
    }
}

2.7 删除第一次出现 key 的节点

Java集合中的链表与结构详解

    @Override
    public void remove(int key) {
        if (head == null){
            return;
        }
        if(head.val == key){
            head = null;
        }
        ListNode cur = findNodeOfKey(key);
        if (cur == null){
            return;
        }
        ListNode del = cur.next;
        cur.next = del.next;
    }
    public ListNode findNodeOfKey(int key){  // 找到存储的数据是key的对象,并返回
        ListNode cur = head;
        while (cur.next != null){
            if (cur.next.val == key){
                return cur;
            }
            cur = cur.next;
        }
        return null;
    }

提示

1、在链表中查找 key 可封装成一个方法

2、查找 key 的条件 cur.next 不为空的前提下,已经判断了 cur.next 存储的数据是否等于 key 了

2.8 删除链表中所有数据为 key 的节点

Java集合中的链表与结构详解

Java集合中的链表与结构详解

Java集合中的链表与结构详解

    @Override
    public void removeAllKey(int key) {
        if (head == null){
            return;
        }
        ListNode prev = head;
        ListNode cur = head.next;
        while (cur != null){
            if (cur.val == key){
                prev.next = cur.next;
                cur = cur.next;
            }else{
                prev = cur;
                cur = cur.next;
            }
        }
        if (head.val == key){
            head = head.next;
        }
    }

2.9 清空

        像之前“清空” ArrayList 的方法在链表中是不可行的,因为如果引用一直存在,那么引用所指的对象无法被内存回收,容易造成内存泄漏,因此只有将 next 都置空了才算清空了链表数据。

Java集合中的链表与结构详解

    @Override
    public void clear() {
        ListNode cur = this.head;
        while (cur != null){
            ListNode prev = cur.next;
            cur.next = null;
            cur = prev;
        }
        head = null;
    }

三、面试题

3.1 删除链表中等于给定值 key 的所有节点。(与上面的 2.8 一个意思)

3.2反转一个单链表。

Java集合中的链表与结构详解

    public ListNode reverseList() {  // 反转
        if(head == null){
            return null;
        }
        ListNode cur = head.next;
        head.next = null;
        while(cur != null){
            ListNode curNode = cur.next;
            cur.next = head;
            head = cur;
            cur = curNode;
        }
        return head;
    }

3.3返回链表中的中间节点,如果有两个中间节点,则返回第2个节点。

提示,不能通过计算链表长度的方法进行编程

那么,采用双指针的方法,快指针走两步,慢指针走1步,最后慢指针指向的就是中间。

Java集合中的链表与结构详解

    public ListNode middleNode() {  // 找中间节点
      python  ListNode fast = head;  // 快指针,走两步
        ListNode slow = head;  // 慢指针,走一步
        while(fast != null && fast.next != null){  // &&前面必须是 fast != null 的条件
            fast = fast.next.next;
            slow = slow.next;
        }
        /*while(fast.next != null && fast != null){  // 输出 NullPointerException
            fast = fast.next.next;
            slow = slow.next;
        }*/
        return slow;
    }

3.4输出该链表中倒数第 k 个节点。

        虽然 leetcode 上已经保证了 k 的合法性,但自己实现的时候还是需要注意 k 的有效范围。

Java集合中的链表与结构详解

    public int kthToLast(int k) {  // 返回倒数第k个字节的数据
        if(head == null){
            return -1;
        }
        // 注意k的范围
        if (k <= 0){
            return -1;
        }
        /*if (k > 5){  // 这样写还得求链表的长度,不好
            return -1;
        }*/
        ListNode fast = head;
        ListNode slow = head;
        int count = 0;
        while(count != k-1){  // 为了与慢指针拉开一定的距离
            fast = fast.next;
            if(fast == null)  // 这里就是判断k是否超出链表长度的条件
                return -1;
            count++;
        }
        while(fast.next != null){
            fast = fast.next;
            slow = slow.next;
        }
        return slow.val;
    }

3.5合并两个有序链表并按升序排序成有序链表返回。

提示

1、创建一个头节点,作为标志,可存储数据,但该数据无实际意义;

2、返回的节点不是创建的头节点。

3、在自己实现的链表中实现时,需要注意,因为该方法操作的是两个链表,因此不好将它定义到 SingleLinkedList 这个类中,因此直接定义到测试类。但直接定义到测试类时因为 ListNode 是 SingleLinkedList 的内部类,需要用 SingleLinkedList. 来调用 ListNode

Java集合中的链表与结构详解

package test;
public class TestSingle {
    public static SingleLinkedList.ListNode mergeTwoLists
            (SingleLinkedList.ListNode headA, SingleLinkedList.ListNode headB) { // 合并两个有序链表并排序
        SingleLinkedList.ListNode newHead = new SingleLinkedList.ListNode(-1); // 随便传一个数据,因为这个数据无效
        SingleLinkedList.ListNode tmp = newHead;
        while(headA != null && headB != null){
            if (headA.val < headB.val){
                tmp.next = headA;
                headA = headA.next;
                tmp = tmp.next;
            }else{
                tmp.next = headB;
                headB = headB.next;
                tmp = tmp.next;
            }
        }
        if (headA != null){
            tmp.next = headA;
        }
        if (headB != null){
            tmp.next = headB;
        }
        return newHead.next;
    }
    public static void main(String[] args) {
        // 测试合并两个链表
        SingleLinkedList linkedList = new SingleLinkedList();
        linkedList.addFirst(4);
        linkedList.addLast(11);
        linkedList.addLast(19);
        linkedList.addLast(44);
        SingleLinkedList linkedList2 = new SingleLinkedList();
        linkedList2.addFirst(3);
        linkedList2.addLast(6);
        linkedList2.addLast(32);
        linkedList2.addLast(57);
        SingleLinkedList.ListNode newNode = mergeTwoLiwww.chinasem.cnsts(linkedList.head, linkedList2.head);
        linkedList.display(newNode);
    }
}

3.6以 x 值为基准将链表分割成两部分,所有小于 x 的节点排在大于或等于 x 的节点之后。

Java集合中的链表与结构详解

    public ListNode partition(int x) {  // 不改变原来数据的顺序下,将所有小于x的结点排在其余结点之前
        ListNode beforeStart = null;  // 小于x的头节点
        ListNode beforeEnd = null;    // 小于x的尾节点
        ListNode afterStart = null;   // 大于x的头节点
        ListNode afterEnd = null;     // 大于x的尾节点
        ListNode cur = head;
        while(cur != null){
            if(cur.val < x){
                if(beforeStart == null){  // 第一次插入
                    beforeStart = beforeEnd = cur;
                }else{
                    beforeEnd.next = cur;
                    beforeEnd = beforeEnd.next;
                }
            }else{
                if(afterStart == null){
                    afterStart = afterEnd = cur;
                }else{
                    afterEnd.next = cur;
                    afterEnd = afterEnd.next;
                }
            }
            cur = cur.next;
        }
        // 如果没有小于x的节点
        if (beforeStart == null){
            return afterStart;
        }
        // 前后两部分衔接起来
        beforeEnd.next = afterStart;
        // 第2部分的最后一个节点的引用要置空,否则编译超出范围
        if(afterStart != null){
            afterEnd.next = null;
        }
        return beforeStart;
    }

3.7判断是否为回文链表。

提示

1、找到链表的中间节点;

2、将中间节点后面的节点反转;

3、头节点的值与最后一个节点的值进行比较,然后头节点往后,最后一个节点往前。

        注意此题需要区分链表的节点数,如果是奇数比较好判断,但如果是偶数,需要考虑另一个判断条件。

奇数情况下:

Java集合中的链表与结构详解

偶数情况下:

Java集合中的链表与结构详解

判断条件是 fast.next = slow;  不能是 slow.next = fast;
且该判断条件前提是两者的数据是相等的,因此判断顺序很重要。

    public boolean chkPalindrome(){ // 判断是否是回文链表
        if(head == null){
            return true;
        }
        // 1、找到中间节点
        ListNode fast = head;
        ListNode slow = head;
        while(fast != null && fast.next != null){
            fast = fast.next.next;
            slow = slow.next;
        }
       javascript // 2、反转
        ListNode cur = slow.next;
        while(cur != null){
            ListNode curN = cur.next;
            cur.next = slow;
            slow = cur;
            cur = curN;
        }
        // 3、判断是否是回文
        while(head != slow){
            if (head.val != slow.val){
                return false;
            }
            // 如果链表是偶数的话,应该是下面的判断条件
            // 前提是两个val值是相等的
            if (head.next == slow){  // 注意是head的下一个节点是slow才对,因为slow的下一个节点回到了slow后面了。
                return true;
            }
            head = head.next;
            slandroidow = slow.next;
        }
        return true;
    }

3.8输入两个链表,找出它们的公共节点。

Java集合中的链表与结构详解

    public SingleLinkedList.ListNode getIntersectionNode
            (SingleLinkedList.ListNode headA, SingleLinkedList.ListNode headB) {  // 两链表是否相交
        SingleLinkedList.ListNode pLong = headA;  // 假设链表A的长度比链表B的长
        SingleLinkedList.ListNode pShort = headB;
        int lenA = 0;   // 链表A的长度
        int lenB = 0;   // 链表B的长度
        while (pLong != null){
            lenA++;
            pLong = pLong.next;
        }
        while (pShort != null){
            lenB++;
            pShort = pShort.next;
        }
        // 因为在计算长度时改变了两个值,因此要从新定义
        pLong = headA;
        pShort = headB;
        // 计算两个链表长度的差值
        int len = lenA - lenB;
        if (len < 0){  // 如果len为负数,说明lenB大于lenA,规定pLong一定指向长的链表
            pLong = headB;
            pShort = headA;
            len = lenB - lenA;  // 记住要把差值换成正数
        }
        // 长的链表先走len步
        while (len != 0){
            pLong = pLong.next;
            len--;
        }
        // 两个引用同时走,知道它们相遇
        while (pLong != pShort){
            pLong = pLong.next;
            pShort = pShort.next;
        }
        if (pLong == null){
            return null;  // 说明没有相交
        }
        return pLong;
    }

测试:

    public static void creatIntersect
            (SingleLinkedList.ListNode headA, SingleLinkedList.ListNode headB){  // 自己实现相交
        headA.next.next = headB.next.next.next;
    }
    public static void main(String[] args) {
        SingleLinkedList linkedList = new SingleLinkedList();
        linkedList.addFirst(5);
        linkedList.addLast(1);
        linkedList.addLast(8);
        linkedList.addLast(4);
        linkedList.addLast(5);
        SingleLinkedList linkedList2 = new SingleLinkedList();
        linkedList2.addFirst(4);
        linkedList2.addLast(1);
        linkedList2.addLast(6);
        linkedList2.addLast(8);
        linkedList2.addLast(4);
        linkedList2.addLast(5);
        creatIntersect(linkedList.head, linkedList2.head);
        SingleLinkedList.ListNode ret = getIntersectionNode(linkedList.head, linkedList2.head);
        System.out.println(ret.val);
    }

3.9判断链表中是否有环。* 进阶:给javascript定一个链表的头节点  head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

Java集合中的链表与结构详解

Java集合中的链表与结构详解

    public boolean hasCycle(){  // 判断当前链表是否有环
        if (head == null){
            return false;
        }
        ListNode fast = head;
        ListNode slow = head;
        while (fast != null && fast.next != null){
            fast = fast.next.next;
            slow = slow.next;
            if (fast == slow){
                return true;
            }
        }
        return false;
    }
    public void creatLoop(){  // 创建环
        ListNode cur = head;
        while (cur.next != null){
            cur = cur.next;
        }
        cur.next = head.next.next.next;
    }
    public ListNode detectCycle() {   // 给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 NULL
        ListNode fast = head;
        ListNode slow = head;
        while( fast != null && fast.next != null){
            fast = fast.next.next;
            slow = slow.next;
            if(fast == slow){
                break;
            }
        }
        if(fast == null || fast.next == null){
            return null;  // 说明没有环
        }
        slow = head;
        while(fast != slow){
            fast = fast.next;
            slow = slow.next;
        }
        return slow;
    }

测试类:

public class TestSingle {
    public static void main(String[] args) {
        // 测试返回链表开始入环的第一个节点。
        SingleLinkedList linkedList = new SingleLinkedList();
        linkedList.addFirst(5);
        linkedList.addLast(1);
        linkedList.addLast(8);
        linkedList.addLast(4);
        linkedList.addLast(5);
        linkedList.creatLoop();
        System.out.println(linkedList.hasCycle());
        SingleLinkedList.ListNode ret = linkedList.detectCycle();
        System.out.println(ret.val);
    }
}

到此这篇关于Java集合中的链表与结构详解的文章就介绍到这了,更多相关java集合链表内容请搜索China编程(www.chinasem.cn)以前的文章或继续浏览下面的相关文章希望大家以后多多支持China编程(www.chinasem.cn)!

这篇关于Java集合中的链表与结构详解的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1155716

相关文章

C++ move 的作用详解及陷阱最佳实践

《C++move的作用详解及陷阱最佳实践》文章详细介绍了C++中的`std::move`函数的作用,包括为什么需要它、它的本质、典型使用场景、以及一些常见陷阱和最佳实践,感兴趣的朋友跟随小编一起看... 目录C++ move 的作用详解一、一句话总结二、为什么需要 move?C++98/03 的痛点⚡C++

Java方法重载与重写之同名方法的双面魔法(最新整理)

《Java方法重载与重写之同名方法的双面魔法(最新整理)》文章介绍了Java中的方法重载Overloading和方法重写Overriding的区别联系,方法重载是指在同一个类中,允许存在多个方法名相同... 目录Java方法重载与重写:同名方法的双面魔法方法重载(Overloading):同门师兄弟的不同绝

MySQL中between and的基本用法、范围查询示例详解

《MySQL中betweenand的基本用法、范围查询示例详解》BETWEENAND操作符在MySQL中用于选择在两个值之间的数据,包括边界值,它支持数值和日期类型,示例展示了如何使用BETWEEN... 目录一、between and语法二、使用示例2.1、betwphpeen and数值查询2.2、be

python中的flask_sqlalchemy的使用及示例详解

《python中的flask_sqlalchemy的使用及示例详解》文章主要介绍了在使用SQLAlchemy创建模型实例时,通过元类动态创建实例的方式,并说明了如何在实例化时执行__init__方法,... 目录@orm.reconstructorSQLAlchemy的回滚关联其他模型数据库基本操作将数据添

Spring配置扩展之JavaConfig的使用小结

《Spring配置扩展之JavaConfig的使用小结》JavaConfig是Spring框架中基于纯Java代码的配置方式,用于替代传统的XML配置,通过注解(如@Bean)定义Spring容器的组... 目录JavaConfig 的概念什么是JavaConfig?为什么使用 JavaConfig?Jav

Java数组动态扩容的实现示例

《Java数组动态扩容的实现示例》本文主要介绍了Java数组动态扩容的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录1 问题2 方法3 结语1 问题实现动态的给数组添加元素效果,实现对数组扩容,原始数组使用静态分配

Java中ArrayList与顺序表示例详解

《Java中ArrayList与顺序表示例详解》顺序表是在计算机内存中以数组的形式保存的线性表,是指用一组地址连续的存储单元依次存储数据元素的线性结构,:本文主要介绍Java中ArrayList与... 目录前言一、Java集合框架核心接口与分类ArrayList二、顺序表数据结构中的顺序表三、常用代码手动

JAVA项目swing转javafx语法规则以及示例代码

《JAVA项目swing转javafx语法规则以及示例代码》:本文主要介绍JAVA项目swing转javafx语法规则以及示例代码的相关资料,文中详细讲解了主类继承、窗口创建、布局管理、控件替换、... 目录最常用的“一行换一行”速查表(直接全局替换)实际转换示例(JFramejs → JavaFX)迁移建

Spring Boot Interceptor的原理、配置、顺序控制及与Filter的关键区别对比分析

《SpringBootInterceptor的原理、配置、顺序控制及与Filter的关键区别对比分析》本文主要介绍了SpringBoot中的拦截器(Interceptor)及其与过滤器(Filt... 目录前言一、核心功能二、拦截器的实现2.1 定义自定义拦截器2.2 注册拦截器三、多拦截器的执行顺序四、过

MySQL快速复制一张表的四种核心方法(包括表结构和数据)

《MySQL快速复制一张表的四种核心方法(包括表结构和数据)》本文详细介绍了四种复制MySQL表(结构+数据)的方法,并对每种方法进行了对比分析,适用于不同场景和数据量的复制需求,特别是针对超大表(1... 目录一、mysql 复制表(结构+数据)的 4 种核心方法(面试结构化回答)方法 1:CREATE