Java链表常见解决方法LeetCode题

代码随想录的Java算法学习笔记

单链表

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

链表的入口节点称为链表的头结点也就是head。

双链表

单链表中的指针域只能指向节点的下一个节点。

双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。

双链表 既可以向前查询也可以向后查询。

循环链表

循环链表,顾名思义,就是链表首尾相连。

循环链表可以用来解决约瑟夫环问题。

链表的虚拟头结点为方便操作,返回时需要省略其虚拟头结点

链表节点定义
public class ListNode {
    //结点的值
    int val;
    //下一个结点
    ListNode next;
    //节点的构造函数(无参)
    public ListNode() {
    }
    //节点的构造函数(有一个参数)
    public ListNode(int val) {
        this.val = val;
    }
    //节点的构造函数(有两个参数)
    public ListNode(int val, ListNode next) {
        this.val = val;
        this.next = next;
    }
}
1.移除链表元素

时间复杂度 O(n)
空间复杂度 O(1)

import java.util.*;
//测试
public class test01 {
    public static void main(String[] args) {
    /*题意:删除链表中等于给定值 val 的所有节点。
    示例 1:输入:head = [1,2,6,3,4,5,6], val = 6 输出:[1,2,3,4,5]
    示例 2:输入:head = [], val = 1 输出:[]
    示例 3:输入:head = [7,7,7,7], val = 7 输出:[]*/
        ListNode head = new ListNode();
        ListNode cur = head;
        List<Integer> list =Arrays.asList(1,2,3,4,5,6,7,7,7);
        //循环添加
        for (int i = 0; i < list.size(); i++) {
            cur.next = new ListNode(list.get(i));
            cur = cur.next;
        }
        cur=head.next;
        ListNode result = removeElements(cur, 7);
        //遍历输出
        while (result != null) {
            System.out.print(result.val + " ");
            result = result.next;
        }
    }


    public static class ListNode {
        // 结点的值
        int val;
        // 下一个结点
        ListNode next;
        // 节点的构造函数(无参)
        public ListNode() {
        }
        // 节点的构造函数(有一个参数)
        public ListNode(int val) {
            this.val = val;
        }
        // 节点的构造函数(有两个参数)
        public ListNode(int val, ListNode next) {
            this.val = val;
            this.next = next;
        }
    }


    /**
     * 添加虚节点方式
     * 时间复杂度 O(n)
     * 空间复杂度 O(1)
     *
     * @param head
     * @param val
     * @return
     */
    public static ListNode removeElements(ListNode head, int val) {
        if (head == null) {
            return head;
        }
        // 因为删除可能涉及到头节点,所以设置dummy节点,统一操作
        ListNode dummy = new ListNode(-1, head);
        ListNode pre = dummy;
        ListNode cur = head;
        while (cur != null) {
            if (cur.val == val) {
                pre.next = cur.next;
            } else {
                pre = cur;
            }
            cur = cur.next;
        }
        return dummy.next;
    }
}
/*
输出结果:
1 2 3 4 5 6
 */

2.设计链表
//测试
public class test02 {
    public static void main(String[] args) {
    /*题意:
    在链表类中实现这些功能:
    1.get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。

    2.addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。
    插入后,新节点将成为链表的第一个节点。

    3.addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。

    4.addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val  的节点。
    如果 index 等于链表的长度,则该节点将附加到链表的末尾。
    如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。

    5.deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。
    */
        List<Integer> list = Arrays.asList(1, 2, 34, 5, 6, 7);
        MyLinkedList linkedList1 = new MyLinkedList();
        MyLinkedList linkedList2 = new MyLinkedList();

        //在链表最前面插入一个节点
        for (Integer value : list) {
            linkedList1.addAtTail(value);
        }
        System.out.println(linkedList1.get(2));//34
        //在链表最后面插入一个节点
        for (Integer integer : list) {
            linkedList2.addAtHead(integer);
        }
        System.out.println(linkedList2.get(2));//5

        //在第 index 个节点之前插入一个新节点
        linkedList1.addAtIndex(-1, 520);
        System.out.println(linkedList1.get(0));//520

        linkedList1.addAtIndex(linkedList1.size, 520);
        System.out.println(linkedList1.get(linkedList1.size - 2));//7
        System.out.println(linkedList1.get(linkedList1.size - 1));//520

        //删除第index个节点
        linkedList1.deleteAtIndex(linkedList1.size - 1);
        System.out.println(linkedList1.get(linkedList1.size - 1));//7


    }

    //定义节点
    public static class ListNode {
        //节点的值
        int val;
        //下一个节点
        ListNode next;
        //节点的构造函数(无参)
        public ListNode() {
        }
        //节点的构造函数(一个参数)
        public ListNode(int val) {
            this.val = val;
        }
        //节点的构造函数(两个参数)
        public ListNode(int val, ListNode next) {
            this.val = val;
            this.next = next;
        }
    }
	//设计链表
    public static class MyLinkedList {
        //size存储链表元素的个数
        int size;
        //虚拟头结点
        ListNode head;
        //初始化链表
        public MyLinkedList() {
            size = 0;
            head = new ListNode(0);
        }

        //获取第index个节点的数值,注意index是从0开始的,第0个节点就是头结点
        public int get(int index) {
            //如果index非法,返回-1
            if (index < 0 || index >= size) {
                return -1;
            }
            ListNode currentNode = head;
            //包含一个虚拟头节点,所以查找第 index+1 个节点
            for (int i = 0; i <= index; i++) {
                currentNode = currentNode.next;
            }
            return currentNode.val;
        }

        //在链表最前面插入一个节点,等价于在第0个元素前添加
        public void addAtHead(int val) {
            addAtIndex(0, val);
        }

        //在链表的最后插入一个节点,等价于在(末尾+1)个元素前添加
        public void addAtTail(int val) {
            addAtIndex(size, val);
        }

        // 在第 index 个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。
        // 如果 index 等于链表的长度,则说明是新插入的节点为链表的尾结点
        // 如果 index 大于链表的长度,则返回空
        public void addAtIndex(int index, int val) {
            if (index > size) {
                return;
            }
            if (index < 0) {
                index = 0;
            }
            size++;
            //找到要插入节点的前驱
            ListNode pred = head;
            for (int i = 0; i < index; i++) {
                pred = pred.next;
            }
            ListNode toAdd = new ListNode(val);
            //完成插入
            //加入的节点(toAdd)的next等于pred的下一个节点的存储位置
            toAdd.next = pred.next;
            //pred的next等于加入的节点(toAdd)的存储位置
            pred.next = toAdd;
        }

        //删除第index个节点
        public void deleteAtIndex(int index) {
            if (index < 0 || index >= size) {
                return;
            }
            size--;
            if (index == 0) {
                head = head.next;
                return;
            }
            ListNode pred = head;
            for (int i = 0; i < index; i++) {
                pred = pred.next;
            }
            pred.next = pred.next.next;
        }
    }
}
3.反转链表

1.双指针

  • 时间复杂度: O(n)
  • 空间复杂度: O(1)
package com.mytest.mk5.ListNode;

import java.util.Arrays;
import java.util.List;
//测试
public class test03 {
    public static void main(String[] args) {
        //定义列表添加数据
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
        //定义节点
        ListNode head = new ListNode();
        //定义节点用来添加数据
        ListNode cur = head;
        //增强for添加数据
        for (Integer integer : list) {
            cur.next = new ListNode(integer);
            cur=cur.next;
        }
        //将cur指针返回到添加的第一个数据
        cur=head.next;
        //实现反转方法
        ListNode result = reverseList(cur);
        //输出结果
        while (result != null) {
            System.out.print(result.val+" ");
            result = result.next;
        }
    }
    //定义节点
    public static class ListNode {
        int val;
        ListNode next;
        public ListNode() {
        }
        public ListNode(int val) {
            this.val = val;
        }
        public ListNode(int val, ListNode next) {
            this.val = val;
            this.next = next;
        }
    }

    public static ListNode reverseList(ListNode head) {
        ListNode prev = null; // 前一个节点
        ListNode cur = head; // 当前节点
        ListNode temp; // 临时节点用于保存下一个节点
        while (cur != null) {
            temp = cur.next; // 保存下一个节点
            cur.next = prev; // 将当前节点的next指向前一个节点,实现反转
            prev = cur; // 更新前一个节点为当前节点
            cur = temp; // 更新当前节点为下一个节点
        }
        return prev; // 返回反转后的头节点
    }
}
/*输出结果:
5 4 3 2 1
*/

2.递归反转

  • 时间复杂度: O(n), 要递归处理链表的每个节点
  • 空间复杂度: O(n), 递归调用了 n 层栈空间
// 递归 
class Solution {
    public ListNode reverseList(ListNode head) {
        return reverse(null, head);
    }
    private ListNode reverse(ListNode prev, ListNode cur) {
        if (cur == null) {
            return prev;
        }
        ListNode temp = null;
        // 先保存下一个节点
        temp = cur.next;
        // 反转
        cur.next = prev;
        //通过递归更新prev、cur位置(传递参数)
        //prev = cur; cur = temp;
        return reverse(cur, temp);
    }
}
/*
这段代码是使用递归来反转单链表的实现。
首先,定义了一个 reverseList 方法,该方法接受一个头结点 head 作为参数,并且返回反转后的链表的头结点。
在 reverseList 方法中,调用了一个私有方法 reverse,该方法用于实际执行链表反转操作。初始时,传入的 prev 参数为 null,表示反转后的链表的头结点的前一个节点为空。
在 reverse 方法中,首先进行终止条件判断,即如果当前节点 cur 为 null,表示已经遍历到链表的末尾,此时返回 prev,即反转后的链表的头结点。
接下来,定义一个临时节点 temp,用于保存当前节点 cur 的下一个节点。然后将当前节点 cur 的 next 指针指向前一个节点 prev,实现反转操作。
最后,通过递归调用 reverse(cur, temp),传入当前节点 cur 和下一个节点 temp,继续反转剩余的链表部分。
整个递归过程会一直执行到链表末尾,然后逐层返回,最终返回反转后的链表的头结点。
需要注意的是,在每次递归调用时,需要更新 prev 和 cur 的值,将 prev 更新为当前节点 cur,将 cur 更新为临时节点 temp,以便下一次递归调用时使用正确的参数。
这样,通过递归的方式,可以实现链表的反转操作。
*/
// 从后向前递归
class Solution {
    ListNode reverseList(ListNode head) {
        // 边缘条件判断
        if(head == null) return null;
        if (head.next == null) return head;
        
        // 递归调用,翻转第二个节点开始往后的链表
        ListNode last = reverseList(head.next);
        // 翻转头节点与第二个节点的指向
        head.next.next = head;
        // 此时的 head 节点为尾节点,next 需要指向 NULL
        head.next = null;
        return last;
    } 
}
/*
这段代码是使用递归从后向前翻转单链表的实现。
首先,在 reverseList 方法中进行边缘条件判断。如果头结点 head 为 null,表示链表为空,直接返回 null。如果头结点 head 的下一个节点为 null,表示链表只有一个节点,无需翻转,直接返回头结点。
接下来,进行递归调用,传入头结点的下一个节点 head.next,即翻转从第二个节点开始往后的链表部分。将返回的结果保存在变量 last 中。
然后,将头结点 head 的下一个节点的 next 指针指向头结点 head,实现翻转头结点与第二个节点的指向关系。
接着,将头结点 head 的 next 指针指向 null,将头结点变为尾节点,确保翻转后的链表尾节点的 next 指向为 null。
最后,返回变量 last,即翻转后的链表的头结点。
通过这种从后向前的递归方式,可以实现链表的翻转操作。每次递归调用都会翻转当前节点与下一个节点的指向关系,直至遍历到链表的末尾,然后逐层返回,最终返回翻转后的链表的头结点。

在这段代码中,递归调用是通过 reverseList 方法本身来实现的。具体来说,递归调用发生在以下代码行:
ListNode last = reverseList(head.next);
在这行代码中,reverseList 方法被传入了当前头结点的下一个节点 head.next 作为参数进行调用。这样就实现了从后向前递归调用,翻转从第二个节点开始往后的链表部分。
通过递归调用,会一直向链表的末尾递归下去,直到遇到边缘条件的基准情况,即链表为空或只有一个节点的情况。然后逐层返回,将翻转后的链表的头结点逐个连接起来,最终返回整个翻转后的链表的头结点。
需要注意的是,每次递归调用中的 head 参数都是当前处理的节点,而 head.next 参数则是下一个节点。通过不断传递下去,递归调用会依次处理链表中的每个节点,实现链表的翻转操作。
*/
4.两两交换链表中的节点

1.递归

时间复杂度:O(n),其中 n 是链表的长度。由于我们需要遍历整个链表,递归地交换每一对节点,所以时间复杂度是线性的。

空间复杂度:O(n),其中 n 是链表的长度。空间复杂度取决于递归调用的栈空间。在最坏情况下,递归调用可能达到 n 个,代表 n 个节点的交换。因此,空间复杂度是线性的。

package com.mytest.mk5.ListNode;

import java.util.Arrays;
import java.util.List;

public class test04 {
    public static void main(String[] args) {
    /*
    给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
    你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
    */
        ListNode cur = new ListNode();
        List<Integer> list = Arrays.asList(1, 2, 3, 5);
        //添加数据
        cur = AddListNode(list, cur);
        //两两交换其中相邻的节点
        cur = swapPairs(cur);
        //输出cur的值
        while (cur != null) {
            System.out.print(cur.val + " ");
            cur = cur.next;
        }

    }
    //定义节点
    public static class ListNode {
        int val;
        ListNode next;
        public ListNode() {
        }
        public ListNode(int val) {
            this.val = val;
        }
        public ListNode(int val, ListNode next) {
            this.val = val;
            this.next = next;
        }
    }
    //定义添加列表数据进节点的方法(尾插法)
    public static ListNode AddListNode(List<Integer> list, ListNode head) {
        ListNode cur = head;
        for (Integer val : list) {
            cur.next = new ListNode(val);
            cur = cur.next;
        }
        return head.next;
    }
    //使用递归实现两两交换
    public static ListNode swapPairs(ListNode head) {
        // base case 退出提交
        if (head == null || head.next == null) return head;
        // 获取当前节点的下一个节点
        ListNode next = head.next;
        // 进行递归
        ListNode newNode = swapPairs(next.next);
        // 这里进行交换
        next.next = head;
        head.next = newNode;

        return next;
    }
}
/*输出结果
*2 1 5 3
 * */

2.迭代版本

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
 public static ListNode swapPairs(ListNode head) {
        ListNode dumyhead = new ListNode(-1); // 设置一个虚拟头结点
        dumyhead.next = head; // 将虚拟头结点指向head,这样方便后面做删除操作
        ListNode cur = dumyhead;
        ListNode temp; // 临时节点,保存两个节点后面的节点
        ListNode firstnode; // 临时节点,保存两个节点之中的第一个节点
        ListNode secondnode; // 临时节点,保存两个节点之中的第二个节点
        while (cur.next != null && cur.next.next != null) {
            temp = cur.next.next.next;
            firstnode = cur.next;
            secondnode = cur.next.next;
            cur.next = secondnode;       // 步骤一
            secondnode.next = firstnode; // 步骤二
            firstnode.next = temp;      // 步骤三
            cur = firstnode; // cur移动,准备下一轮交换
        }
        return dumyhead.next;
    }
5.删除链表的倒数第N个节点
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
//测试
public class test05 {
    public static void main(String[] args) {
    /*
    给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
    进阶:你能尝试使用一趟扫描实现吗?*/
        ListNode cur = new ListNode();
        List<Integer> list = Arrays.asList(1, 2, 3, 5);
        int target = 1;
        //添加数据
        cur = AddListNode(list, cur);

        ListNode result = removeNthFromEnd(cur, target);
        while (result != null) {
            System.out.print(result.val+" ");
            result = result.next;
        }
    }
    public static class ListNode {
        int val;
        ListNode next;
        public ListNode() {
        }
        public ListNode(int val) {
            this.val = val;
        }
        public ListNode(int val, ListNode next) {
            this.val = val;
            this.next = next;
        }
    }

    public static ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode dummyNode = new ListNode(); // 创建一个虚拟节点作为头节点
        dummyNode.next = head; // 将虚拟节点的下一个节点指向实际的头节点

        ListNode fastIndex = dummyNode; // 快指针从虚拟节点开始
        ListNode slowIndex = dummyNode; // 慢指针从虚拟节点开始

        // 快指针先移动n个节点,使快慢指针相差n个节点
        for (int i = 0; i < n; i++) {
            fastIndex = fastIndex.next;
        }

        // 同时移动快慢指针,直到快指针指向链表末尾
        // 此时慢指针的位置就是待删除元素的前一个位置
        while (fastIndex.next != null) {
            fastIndex = fastIndex.next;
            slowIndex = slowIndex.next;
        }

        // 跳过待删除元素,将慢指针的下一个节点指向下下个节点
        slowIndex.next = slowIndex.next.next;
        // 返回虚拟节点的下一个节点作为新的头节点
        return dummyNode.next;
    }

    //定义添加列表数据进节点的方法(尾插法)
    public static ListNode AddListNode(List<Integer> list, ListNode head) {
        ListNode cur = head;
        for (Integer val : list) {
            cur.next = new ListNode(val);
            cur = cur.next;
        }
        return head.next;
    }
}
/*输出结果:
1 2 3
 */
6.链表相交

不知道怎么做链表相交(不想做),先放着吧

 /*
        给你两个单链表的头节点 headA 和 headB
        请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。
		(这有啥用?)
        输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
        输出:Intersected at '8'
        解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
        从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。
        在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
        简单来说,就是求两个链表交点节点的指针。
        注意,交点不是数值相等,而是指针相等。
        * */
public static ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        ListNode curA = headA;
        ListNode curB = headB;
        int lenA = 0, lenB = 0;
        while (curA != null) { // 求链表A的长度
            lenA++;
            curA = curA.next;
        }
        while (curB != null) { // 求链表B的长度
            lenB++;
            curB = curB.next;
        }
        curA = headA;
        curB = headB;
        // 让curA为最长链表的头,lenA为其长度
        if (lenB > lenA) {
            //1. swap (lenA, lenB);
            int tmpLen = lenA;
            lenA = lenB;
            lenB = tmpLen;
            //2. swap (curA, curB);
            ListNode tmpNode = curA;
            curA = curB;
            curB = tmpNode;
        }
        // 求长度差
        int gap = lenA - lenB;
        // 让curA和curB在同一起点上(末尾位置对齐)
        while (gap-- > 0) {
            curA = curA.next;
        }
        // 遍历curA 和 curB,遇到相同则直接返回
        while (curA != null) {
            if (curA == curB) {
                return curA;
            }
            curA = curA.next;
            curB = curB.next;
        }
        return null;
    }
7.环形链表II
  • 时间复杂度: O(n)
  • 空间复杂度: O(1)
//测试
public class test07 {
    public static void main(String[] args) {
/*
    题意: 给定一个链表,返回链表开始入环的第一个节点。
    如果链表无环,则返回 null。
    为了表示给定链表中的环,
    使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。
     如果 pos 是 -1,则在该链表中没有环。
    说明:不允许修改给定的链表。*/

        ListNode cur = new ListNode();
        ListNode protocur = new ListNode();

        List<Integer> list = Arrays.asList(3, 2, 0, -4);
        //添加数据
        cur = AddListNode(list, cur);
        protocur = AddListNode(list, protocur);
        int target = 1;
        ListNode errorList = ErrorList(target, cur);

        //测试结果:-4 2 0 -4 2 0...........
       /* while (errorList != null) {
            System.out.print(errorList.val+" ");
            errorList=errorList.next;
        }*/
        ListNode result = detectCycle(errorList);
        if (result == null) {
            System.out.println("无环");
        } else {
            System.out.println("环的入口点为" + result.val);
        }

        ListNode protoresult = detectCycle(protocur);
        if (protoresult == null) {
            System.out.println("无环");
        } else {
            System.out.println("环的入口点为" + protoresult.val);
        }

    }

    public static class ListNode {
        int val;
        ListNode next;
        public ListNode() {
        }
        public ListNode(int val) {
            this.val = val;
        }
        public ListNode(int val, ListNode next) {
            this.val = val;
            this.next = next;
        }
    }


    //定义添加列表数据进节点的方法(尾插法)
    public static ListNode AddListNode(List<Integer> list, ListNode head) {
        ListNode cur = head;
        for (Integer val : list) {
            cur.next = new ListNode(val);
            cur = cur.next;
        }
        return head.next;
    }

    //尾部成环
    public static ListNode ErrorList(int target, ListNode head) {
        ListNode dummy = new ListNode();
        dummy.next = head;
        ListNode cur = dummy;
        int size = 1;
        while (cur.next != null) {
            cur = cur.next;
            size++;
        }
        if (target >= size) return null;
        for (int i = 0; i < target; i++) {
            head = head.next;
        }
        cur.next = head;
        return dummy.next;
    }
    //判断是否为环形链表
    public static ListNode detectCycle(ListNode head) {
        ListNode slow = head; // 慢指针,每次前进一步
        ListNode fast = head; // 快指针,每次前进两步
        while (fast != null && fast.next != null) { // 当快指针未到达链表尾部时循环继续
            slow = slow.next; // 慢指针前进一步
            fast = fast.next.next; // 快指针前进两步
            if (slow == fast) { // 如果两个指针相遇,则链表中存在环
                ListNode index1 = fast; // 创建指针index1指向相遇点
                ListNode index2 = head; // 创建指针index2指向链表头部
                // 两个指针分别从相遇点和链表头部开始,每次前进一步,直到它们再次相遇,此时相遇点即为环的入口点
                while (index1 != index2) {
                    index1 = index1.next;
                    index2 = index2.next;
                }
                return index1; // 返回环的入口点
            }
        }
        return null; // 如果链表中没有环,则返回null
    }
}
/*输出结果:
环的入口点为2
无环
*/
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值