代码随想录 链表

目录

在Java中自定义一个节点类(ListNode)

203.移除链表元素

707.设计链表

206.反转链表

双指针法

递归法

24.两两交换链表中的节点

19.删除链表的倒数第N个节点

面试题02.07.链表相交

142.环形链表II

如果有环,如何找到这个环的入口


Java中并没有定义ListNode这个节点类,包括单向链表,双向链表,循环链表,都需要去学习它的写法,自定义

在处理链表的题目时,要注意通过画图的方法来摸清楚各个节点的应用和处理模式

在Java中自定义一个节点类(ListNode)

public class ListNode {
    int val;
    ListNode next;

    ListNode() {}

    ListNode(int val) { 
        this.val = val; 
    }

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

203.移除链表元素

203. 移除链表元素

简单

给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == 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
输出:[]

提示:

  • 列表中的节点数目在范围 [0, 104] 内
  • 1 <= Node.val <= 50
  • 0 <= val <= 50

方法一:不添加哨兵节点,使用原链表

对头结点和头结点之后的节点的处理方式不同

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode removeElements(ListNode head, int val) {
        if (head == null) {
            return null;
        }
        // 处理头节点值等于val的情况,直到找到一个节点值不为val的节点当做头结点
        //因为要调用head.next,必须保证head不为空
        while (head != null && head.val == val) {
            head = head.next;
        }

        // 遍历链表,移除节点
        //因为要调用cur.next和cur.next.next,必须保证cur和cur.next不为空
        ListNode cur = head;
        while (cur != null && cur.next != null) {
            if (cur.next.val == val) {
                cur.next = cur.next.next; // 跳过当前节点的下一个节点
            } else {
                cur = cur.next; // 继续遍历下一个节点
            }
            //上述代码的目的是更新cur.next的值
        }

        return head; // 返回移除节点后的链表头节点
    }
}

方法二:添加哨兵节点作为头结点

在处理头节点时,引入了一个虚拟节点(dummy),它的值设置为Integer.MIN_VALUE,并且将原来的头节点作为虚拟节点的下一个节点。这样可以避免对头节点的特殊处理

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode removeElements(ListNode head, int val) {
        // 处理空链表的情况
        if (head == null) {
            return null;
        }

        // 使用虚拟节点作为头节点,简化对头节点的操作
        ListNode dummy = new ListNode(Integer.MIN_VALUE, head);
        ListNode cur = dummy;

        // 遍历链表,移除节点
        while (cur != null && cur.next != null) {
            if (cur.next.val == val) {
                cur.next = cur.next.next; // 跳过当前节点的下一个节点
            } else {
                cur = cur.next; // 继续遍历下一个节点
            }
        }

        return dummy.next; // 返回移除节点后的链表头节点
    }
}

707.设计链表

707. 设计链表

中等

你可以选择使用单链表或者双链表,设计并实现自己的链表。

单链表中的节点应该具备两个属性:val 和 next 。val 是当前节点的值,next 是指向下一个节点的指针/引用。

如果是双向链表,则还需要属性 prev 以指示链表中的上一个节点。假设链表中的所有节点下标从 0 开始。

实现 MyLinkedList 类:

  • MyLinkedList() 初始化 MyLinkedList 对象。
  • int get(int index) 获取链表中下标为 index 的节点的值。如果下标无效,则返回 -1 。
  • void addAtHead(int val) 将一个值为 val 的节点插入到链表中第一个元素之前。在插入完成后,新节点会成为链表的第一个节点。
  • void addAtTail(int val) 将一个值为 val 的节点追加到链表中作为链表的最后一个元素。
  • void addAtIndex(int index, int val) 将一个值为 val 的节点插入到链表中下标为 index 的节点之前。如果 index 等于链表的长度,那么该节点会被追加到链表的末尾。如果 index 比长度更大,该节点将 不会插入 到链表中。
  • void deleteAtIndex(int index) 如果下标有效,则删除链表中下标为 index 的节点。

示例:

输入
["MyLinkedList", "addAtHead", "addAtTail", "addAtIndex", "get", "deleteAtIndex", "get"]
[[], [1], [3], [1, 2], [1], [1], [1]]
输出
[null, null, null, null, 2, null, 3]

解释
MyLinkedList myLinkedList = new MyLinkedList();
myLinkedList.addAtHead(1);
myLinkedList.addAtTail(3);
myLinkedList.addAtIndex(1, 2);    // 链表变为 1->2->3
myLinkedList.get(1);              // 返回 2
myLinkedList.deleteAtIndex(1);    // 现在,链表变为 1->3
myLinkedList.get(1);              // 返回 3

提示:

  • 0 <= index, val <= 1000
  • 请不要使用内置的 LinkedList 库。
  • 调用 getaddAtHeadaddAtTailaddAtIndex 和 deleteAtIndex 的次数不超过 2000 。

单链表

//单链表
class ListNode {
    int val;
    ListNode next;
    ListNode(){}
    ListNode(int val) {
        this.val=val;
    }
}
class MyLinkedList {
    //size存储链表元素的个数
    int size;
    //虚拟头结点
    ListNode head;

    //初始化链表
    public MyLinkedList() {
        size = 0;
        head = new ListNode(0);
    }

    //获取第index个节点的数值
    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;
    }

    //在链表最前面插入一个节点
    public void addAtHead(int val) {
        addAtIndex(0, val);
    }

    //在链表的最后插入一个节点
    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.next;
        pred.next = toAdd;
    }

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

双链表

//双链表类
class MyLinkedList {
    //内部节点类
    class ListNode {
        int val; //节点值
        ListNode next,prev; //指向前后节点的指针
        ListNode(int x) {val = x;} //构造函数,初始化节点值
    }

    int size; //链表长度
    ListNode head,tail; //头尾哨兵节点

    //构造函数,初始化链表
    public MyLinkedList() {
        size = 0;
        head = new ListNode(0); //头节点值为0
        tail = new ListNode(0); //尾节点值为0
        head.next = tail; //头节点指向尾节点
        tail.prev = head; //尾节点指向头节点
    }
    
    //获取指定索引位置的值
    public int get(int index) {
        if(index < 0 || index >= size){return -1;} //如果索引越界,返回-1
        ListNode cur = head; //从头节点开始遍历

        // 通过判断 index < (size - 1) / 2 来决定是从头结点还是尾节点遍历,提高效率
        if(index < (size - 1) / 2){
            for(int i = 0; i <= index; i++){
                cur = cur.next; //向前遍历
            }            
        }else{
            cur = tail; //从尾节点开始遍历
            for(int i = 0; i <= size - index - 1; i++){
                cur = cur.prev; //向后遍历
            }
        }
        return cur.val; //返回当前节点的值
    }
    
    //在头部添加元素
    public void addAtHead(int val) {
        ListNode cur = head; //从头节点开始遍历
        ListNode newNode = new ListNode(val); //创建新节点
        newNode.next = cur.next; //新节点指向原头节点的下一个节点
        cur.next.prev = newNode; //原头节点的下一个节点指向新节点
        cur.next = newNode; //头节点指向新节点
        newNode.prev = cur; //新节点指向头节点
        size++; //链表长度加1
    }
    
    //在尾部添加元素
    public void addAtTail(int val) {
        ListNode cur = tail; //从尾节点开始遍历
        ListNode newNode = new ListNode(val); //创建新节点
        newNode.next = tail; //新节点指向尾节点
        newNode.prev = cur.prev; //新节点的前一个节点指向原尾节点的前一个节点
        cur.prev.next = newNode; //原尾节点的前一个节点指向新节点
        cur.prev = newNode; //尾节点指向新节点
        size++; //链表长度加1
    }
    
    //在指定索引位置插入元素
    public void addAtIndex(int index, int val) {
        if(index > size){return;} //如果索引大于链表长度,直接返回
        if(index < 0){index = 0;} //如果索引小于0,将索引设置为0
        ListNode cur = head; //从头节点开始遍历
        for(int i = 0; i < index; i++){
            cur = cur.next; //向前遍历
        }
        ListNode newNode = new ListNode(val); //创建新节点
        newNode.next = cur.next; //新节点指向原索引位置的节点的下一个节点
        cur.next.prev = newNode; //原索引位置的节点的下一个节点指向新节点
        cur.next = newNode; //原索引位置的节点指向新节点
        newNode.prev = cur; //新节点指向原索引位置的节点
        size++; //链表长度加1
    }
    
    //删除指定索引位置的元素
    public void deleteAtIndex(int index) {
        if(index >= size || index < 0){return;} //如果索引越界,直接返回
        ListNode cur = head; //从头节点开始遍历
        for(int i = 0; i < index; i++){
            cur = cur.next; //向前遍历
        }
        cur.next.next.prev = cur; //删除节点后,更新前后节点的指针关系
        cur.next = cur.next.next; //删除节点后,更新当前节点的下一个节点指针
        size--; //链表长度减1
    }
}

206.反转链表

206. 反转链表

已解答

简单

相关标签

相关企业

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

示例 1:

输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]

示例 2:

输入:head = [1,2]
输出:[2,1]

示例 3:

输入:head = []
输出:[]

提示:

  • 链表中节点的数目范围是 [0, 5000]
  • -5000 <= Node.val <= 5000

进阶:链表可以选用迭代或递归方式完成反转。你能否用两种方法解决这道题?

双指针法
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    /**
     * 反转单链表的方法
     * @param head 给定链表的头节点
     * @return 反转后的链表头节点
     */
    public ListNode reverseList(ListNode head) {
        ListNode cur = head;   // 当前节点
        ListNode pre = null;   // 前一个节点

        // 遍历链表
        while (cur != null) {
            ListNode temp = cur.next;  // 暂存下一个节点的引用
            cur.next = pre;            // 当前节点的下一个节点指向前一个节点,实现反转
            pre = cur;                 // 更新前一个节点为当前节点
            cur = temp;                // 更新当前节点为下一个节点
        }

        return pre;  // 反转后的链表头节点
    }
}
递归法

原理和双指针法相同

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    /**
     * 反转单链表的方法(入口方法)
     * @param head 给定链表的头节点
     * @return 反转后的链表头节点
     */
    public ListNode reverseList(ListNode head) {
        // 调用递归函数,初始时前一个节点为null
        return reverse(head, null);
    }
    
    /**
     * 递归函数:反转单链表
     * @param cur 当前节点
     * @param pre 前一个节点
     * @return 反转后的链表头节点
     */
    public ListNode reverse(ListNode cur, ListNode pre) {
        // 递归终止条件:当前节点为空,返回前一个节点作为新的头节点
        if (cur == null) {
            return pre;
        }
        // 暂存当前节点的下一个节点的引用
        ListNode temp = cur.next;
        // 反转当前节点的next指针,指向前一个节点
        cur.next = pre;
        // 更新前一个节点为当前节点
        pre = cur;
        // 更新当前节点为下一个节点
        cur = temp;
        // 递归调用,继续反转下一个节点
        return reverse(cur, pre);
    }
}

92.反转链表II 

92. 反转链表 II

中等

给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。

示例 1:

输入:head = [1,2,3,4,5], left = 2, right = 4
输出:[1,4,3,2,5]

示例 2:

输入:head = [5], left = 1, right = 1
输出:[5]

提示:

  • 链表中节点数目为 n
  • 1 <= n <= 500
  • -500 <= Node.val <= 500
  • 1 <= left <= right <= n

进阶: 你可以使用一趟扫描完成反转吗?

自己的思路,看的时候可以画图来理解 

class Solution {
    public ListNode reverseBetween(ListNode head, int left, int right) {
        ListNode dummyHead = new ListNode(0, head); // 创建一个虚拟头结点
        ListNode previous = dummyHead; // 前一个结点的指针
        ListNode current = head; // 当前结点的指针
        int position = 1; // 当前结点在链表中的位置

        // 创建指针用于记录待翻转部分的起始位置和结束位置
        ListNode reverseStartPrev = dummyHead; // 待翻转部分的起始位置的前一个节点
        ListNode reverseEnd = head; // 待翻转部分的结束位置,第一个翻转的节点就是翻转后的结束位置

        // 遍历链表
        while (current != null) {
            // 如果当前结点在待翻转部分之前,更新待翻转部分的起始位置和结束位置
            if (position < left) {
                reverseStartPrev = reverseStartPrev.next;
                previous = previous.next;
                current = current.next;
                reverseEnd = reverseEnd.next;
            }
            // 如果当前结点在待翻转部分之内,则进行翻转操作
            if (position >= left && position <= right) {
                ListNode temp = current.next; // 临时保存当前结点的下一个结点
                current.next = previous; // 将当前结点的 next 指针指向前一个结点
                previous = current; // 更新前一个结点的指针为当前结点
                current = temp; // 更新当前结点为临时保存的下一个结点
            }
            position++; // 位置加一
            // 如果遍历到待翻转部分的结束位置,进行部分链表的重连接并结束循环
            if (position == right + 1) {
                reverseStartPrev.next = previous; // 将待翻转部分的起始位置的前一个结点的 next 指针指向翻转后的起始位置
                reverseEnd.next = current; // 将待翻转部分的结束位置的 next 指针指向翻转后的下一个结点
                break;
            }
        }
        // 返回虚拟头结点的下一个结点,即为翻转后的链表的头结点
        return dummyHead.next;
    }
}

官方题解,思路更清晰 

class Solution {
    /**
     * 反转链表中从第 left 个节点到第 right 个节点的部分。
     * 
     * @param head  给定链表的头节点
     * @param left  起始位置
     * @param right 结束位置
     * @return 反转后的链表头节点
     */
    public ListNode reverseBetween(ListNode head, int left, int right) {
        // 创建虚拟头节点,避免头节点变化带来的分类讨论
        ListNode dummyNode = new ListNode(-1);
        dummyNode.next = head;

        ListNode pre = dummyNode;
        // 从虚拟头节点出发,移动 left - 1 步,到达 left 节点的前一个节点
        for (int i = 0; i < left - 1; i++) {
            pre = pre.next;
        }

        // 移动 right - left + 1 步,到达 right 节点
        ListNode rightNode = pre;
        for (int i = 0; i < right - left + 1; i++) {
            rightNode = rightNode.next;
        }

        // 截取从 left 到 right 节点的子链表
        ListNode leftNode = pre.next;
        ListNode curr = rightNode.next;

        // 切断链接
        pre.next = null;
        rightNode.next = null;

        // 反转子链表
        reverseLinkedList(leftNode);

        // 将反转后的子链表重新接回原链表中
        pre.next = rightNode;
        leftNode.next = curr;
        
        // 返回虚拟头节点的下一个节点,即为反转后的链表头节点
        return dummyNode.next;
    }

    /**
     * 反转链表。
     * 
     * @param head 给定链表的头节点
     */
    private void reverseLinkedList(ListNode head) {
        ListNode pre = null;
        ListNode cur = head;

        while (cur != null) {
            ListNode next = cur.next;
            cur.next = pre;
            pre = cur;
            cur = next;
        }
    }
}

25.K个一组翻转链表

25. K 个一组翻转链表

困难

给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。

k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

示例 1:

输入:head = [1,2,3,4,5], k = 2
输出:[2,1,4,3,5]

示例 2:

输入:head = [1,2,3,4,5], k = 3
输出:[3,2,1,4,5]

提示:

  • 链表中的节点数目为 n
  • 1 <= k <= n <= 5000
  • 0 <= Node.val <= 1000

进阶:你可以设计一个只用 O(1) 额外内存空间的算法解决此问题吗?

class Solution {
    // 反转从 head 到 tail 的链表,并返回新的头尾节点
    public ListNode[] reverse(ListNode head, ListNode tail) {
        ListNode prev = tail.next;
        ListNode curr = head;
        while (prev != tail) {
            ListNode nextTemp = curr.next;
            curr.next = prev;
            prev = curr;
            curr = nextTemp;
        }
        return new ListNode[]{tail, head};
    }
    
    public ListNode reverseKGroup(ListNode head, int k) {
        // 创建虚拟头结点,方便处理头部反转的情况
        ListNode dummy = new ListNode(0);
        dummy.next = head;
        ListNode pre = dummy;

        while (head != null) {
            ListNode tail = pre;
            // 查看剩余部分长度是否大于等于 k
            for (int i = 0; i < k; ++i) {
                tail = tail.next;
                if (tail == null) {
                    return dummy.next;
                }
            }
            ListNode nex = tail.next;
            // 反转从 head 到 tail 的链表
            ListNode[] reversed = reverse(head, tail);
            head = reversed[0];
            tail = reversed[1];
            // 将子链表重新接回原链表
            pre.next = head;
            tail.next = nex;
            pre = tail;
            head = tail.next;
        }

        return dummy.next;
    }
}

24.两两交换链表中的节点

24. 两两交换链表中的节点

中等

给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。

示例 1:

输入:head = [1,2,3,4]
输出:[2,1,4,3]

示例 2:

输入:head = []
输出:[]

示例 3:

输入:head = [1]
输出:[1]

提示:

  • 链表中节点的数目在范围 [0, 100] 内
  • 0 <= Node.val <= 100
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    /**
     * 交换相邻节点的方法
     * @param head 给定链表的头节点
     * @return 交换相邻节点后的链表头节点
     */
    public ListNode swapPairs(ListNode head) {
        // 创建一个虚拟节点作为头节点的前一个节点
        ListNode dummyNode = new ListNode(0, head);
        // 当前节点指向虚拟节点
        ListNode cur = dummyNode;

        // 遍历链表,交换相邻节点
        while (cur.next != null && cur.next.next != null) {
            // 暂存当前相邻节点的前一个节点和后一个节点
            ListNode temp1 = cur.next;
            ListNode temp2 = cur.next.next.next;
            
            // 交换相邻节点
            cur.next = cur.next.next;
            cur.next.next = temp1;
            cur.next.next.next = temp2;
            
            // 移动到下一组相邻节点的前一个节点
            cur = cur.next.next;
        }

        return dummyNode.next; // 返回交换相邻节点后的链表头节点
    }
}

19.删除链表的倒数第N个节点

19. 删除链表的倒数第 N 个结点

中等

提示

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

示例 1:

输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]

示例 2:

输入:head = [1], n = 1
输出:[]

示例 3:

输入:head = [1,2], n = 1
输出:[1]

提示:

  • 链表中结点的数目为 sz
  • 1 <= sz <= 30
  • 0 <= Node.val <= 100
  • 1 <= n <= sz

进阶:你能尝试使用一趟扫描实现吗?

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode dummyHead = new ListNode(0,head);
        ListNode slow = dummyHead;  //快慢指针均指向虚拟头节点
        ListNode fast = dummyHead;
        while(n >= 0){
            n--;
            fast = fast.next;       //将快慢指针之间的间距变为n+1
        }
        while(fast != null){        //使快指针指向最后一个节点的next(null)
            fast = fast.next;       //此刻慢指针指向要删除的节点的前一个节点
            slow = slow.next;
        }
        slow.next = slow.next.next; //执行删除操作
        return dummyHead.next;      //返回头结点
    }
}

这道题的思路大致如注释,这里需要关注的是快慢节点之间的间距,以及快指针最后指向的位置,此处用作图的方式能更好的理解

  • 定义fast指针和slow指针,初始值为虚拟头结点,如图:

  • fast首先走n + 1步 ,为什么是n+1呢,因为只有这样同时移动的时候slow才能指向删除节点的上一个节点(方便做删除操作),如图: 

  • fast和slow同时移动,直到fast指向末尾,如题: 

  • 删除slow指向的下一个节点,如图: 

有点纠结在是fast为空还是fast.next为空的终止条件,看了图解才真正理解 

82.删除排序链表中的重复元素II 

82. 删除排序链表中的重复元素 II

中等

给定一个已排序的链表的头 head , 删除原始链表中所有重复数字的节点,只留下不同的数字 。返回 已排序的链表 。

示例 1:

输入:head = [1,2,3,3,4,4,5]
输出:[1,2,5]

示例 2:

输入:head = [1,1,1,2,3]
输出:[2,3]

提示:

  • 链表中节点数目在范围 [0, 300] 内
  • -100 <= Node.val <= 100
  • 题目数据保证链表已经按升序 排列
class Solution {
    public ListNode deleteDuplicates(ListNode head) {
        // 如果头结点为空,则无需删除重复节点,直接返回头结点
        if (head == null) {
            return head;
        }
        
        // 创建一个虚拟节点来简化处理边界情况
        ListNode dummy = new ListNode(0, head);

        // 初始化一个指针'cur'指向虚拟节点
        ListNode cur = dummy;
        
        // 循环直到'cur'后面至少还有两个节点
        while (cur.next != null && cur.next.next != null) {
            // 检查下两个节点是否具有相同的值
            if (cur.next.val == cur.next.next.val) {
                // 如果是,将下一个节点的值存储在'x'中
                int x = cur.next.val;
                
                // 循环直到下一个节点的值不等于'x'
                while (cur.next != null && cur.next.val == x) {
                    // 跳过值为'x'的节点
                    cur.next = cur.next.next;
                }
            } else {
                // 如果下两个节点的值不相等,移动'cur'到下一个节点
                cur = cur.next;
            }
        }

        // 返回虚拟节点后面的下一个节点,即修改后链表的头结点
        return dummy.next;
    }
}

面试题02.07.链表相交

面试题 02.07. 链表相交

简单

提示

给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。

图示两个链表在节点 c1 开始相交

题目数据 保证 整个链式结构中不存在环。

注意,函数返回结果后,链表必须 保持其原始结构 。

示例 1:

输入: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 个节点。

示例 2:

输入:intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Intersected at '2'
解释:相交节点的值为 2 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [0,9,1,2,4],链表 B 为 [3,2,4]。
在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。

示例 3:

输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。
由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
这两个链表不相交,因此返回 null 。

提示:

  • listA 中节点数目为 m
  • listB 中节点数目为 n
  • 0 <= m, n <= 3 * 104
  • 1 <= Node.val <= 105
  • 0 <= skipA <= m
  • 0 <= skipB <= n
  • 如果 listA 和 listB 没有交点,intersectVal 为 0
  • 如果 listA 和 listB 有交点,intersectVal == listA[skipA + 1] == listB[skipB + 1]

进阶:你能否设计一个时间复杂度 O(n) 、仅用 O(1) 内存的解决方案?

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    // 获取两个链表的交点
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        // 创建两个指针,分别指向两个链表的头节点
        ListNode curA = headA;
        ListNode curB = headB;
        // 计算两个链表的长度
        int lengthA = 0;
        int lengthB = 0;
        while(curA != null){
            lengthA++;
            curA = curA.next;
        }
        while(curB != null){
            lengthB++;
            curB = curB.next;
        }
        // 创建两个指针,分别指向两个链表的头节点
        ListNode longer = headA;
        ListNode shorter = headB;
        // 计算两个链表的长度差值
        int distance = lengthA - lengthB;
        // 如果链表B比链表A长,则将longer指向链表B的头节点,shorter指向链表A的头节点,distance为长度差值
        if(lengthB > lengthA){
            longer = headB;
            shorter = headA;
            distance = lengthB - lengthA;
        }
        // 将longer指针向前移动distance步
        while (distance > 0){
            longer = longer.next;
            distance--;
        }
        // 同时遍历两个链表,直到找到交点或者遍历结束
        while(longer != null && shorter != null){
            // 如果找到交点,则返回交点
            if(longer == shorter){
                return longer;
            }
            // 否则,将longer和shorter指针向前移动一步
            longer = longer.next;
            shorter = shorter.next;
        }
        // 如果没有找到交点,则返回null
        return null;
    }
}

简单的思路描述:找出两个链表中长度更长的一条,由于两条链表相交后面的部分均是一样的,所以使长度较长的链表移动到和长度较短的链表的长度一样时,此时的两节点才可能相交。

142.环形链表II

142. 环形链表 II

中等

相关标签

相关企业

给定一个链表的头节点  head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

不允许修改 链表。

示例 1:

输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。

示例 2:

输入:head = [1,2], pos = 0
输出:返回索引为 0 的链表节点
解释:链表中有一个环,其尾部连接到第一个节点。

示例 3:

输入:head = [1], pos = -1
输出:返回 null
解释:链表中没有环。

提示:

  • 链表中节点的数目范围在范围 [0, 104] 内
  • -105 <= Node.val <= 105
  • pos 的值为 -1 或者链表中的一个有效索引

进阶:你是否可以使用 O(1) 空间解决此题?

自己的想法:

看到这道题的第一点,首先想到的是采用空间换时间的方式,这样思路比较简单,如下:

/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    // 检测链表中是否存在环,并返回环的起始节点
    public ListNode detectCycle(ListNode head) {
        // 创建一个列表用于存储遍历过的节点
        List<ListNode> node = new ArrayList<>();
        // 初始化当前节点为头节点
        ListNode cur = head;
        // 遍历链表
        while(cur != null){
            // 如果当前节点已经在列表中,说明存在环,返回当前节点作为环的起始节点
            if(node.contains(cur)){
                return cur;
            }
            // 将当前节点添加到列表中
            node.add(cur);
            // 移动到下一个节点
            cur = cur.next;
        }
        // 如果遍历完链表都没有发现环,返回null
        return null;
    }
}

题解这里就照搬了 

可以使用快慢指针法,分别定义 fast 和 slow 指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。

为什么fast 走两个节点,slow走一个节点,有环的话,一定会在环内相遇呢,而不是永远的错开呢

首先第一点:fast指针一定先进入环中,如果fast指针和slow指针相遇的话,一定是在环中相遇,这是毋庸置疑的。

那么来看一下,为什么fast指针和slow指针一定会相遇呢?

可以画一个环,然后让 fast指针在任意一个节点开始追赶slow指针。

会发现最终都是这种情况, 如下图:

142环形链表1

fast和slow各自再走一步, fast和slow就相遇了

这是因为fast是走两步,slow是走一步,其实相对于slow来说,fast是一个节点一个节点的靠近slow的,所以fast一定可以和slow重合。

动画如下:

141.环形链表

如果有环,如何找到这个环的入口

此时已经可以判断链表是否有环了,那么接下来要找这个环的入口了。

假设从头结点到环形入口节点 的节点数为x。 环形入口节点到 fast指针与slow指针相遇节点 节点数为y。 从相遇节点 再到环形入口节点节点数为 z。 如图所示:

那么相遇时: slow指针走过的节点数为: x + y + m (y + z), fast指针走过的节点数:x + y + n (y + z),n为fast指针在环内走了n圈才遇到slow指针,m为慢指针走了m圈 (y+z)为 一圈内节点的个数A。

因为fast指针是一步走两个节点,slow指针一步走一个节点, 所以 fast指针走过的节点数 = slow指针走过的节点数 * 2:

(x + y + m (y + z)) * 2 = x + y + n (y + z)

两边消掉一个(x+y)和 (y + z): x + y = n (y + z)

因为要找环形的入口,那么要求的是x,因为x表示 头结点到 环形入口节点的的距离。

所以要求x ,将x单独放在左面:x = n (y + z) - y ,

再从n(y+z)中提出一个 (y+z)来,整理公式之后为如下公式:x = (n - 1) (y + z) + z 注意这里n一定是大于等于1的,因为 fast指针至少要多走一圈才能相遇slow指针。

这个公式说明什么呢?

先拿n为1的情况来举例,意味着fast指针在环形里转了一圈之后,就遇到了 slow指针了。

当 n为1的时候,公式就化解为 x = z

这就意味着,从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点

也就是在相遇节点处,定义一个指针index1,在头结点处定一个指针index2。

让index1和index2同时移动,每次移动一个节点, 那么他们相遇的地方就是 环形入口的节点。

动画如下:

142.环形链表II(求入口)

那么 n如果大于1是什么情况呢,就是fast指针在环形转n圈之后才遇到 slow指针。

其实这种情况和n为1的时候 效果是一样的,一样可以通过这个方法找到 环形的入口节点,只不过,index1 指针在环里 多转了(n-1)圈,然后再遇到index2,相遇点依然是环形的入口节点。

代码如下:

public class Solution {
    public 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;
                ListNode index2 = head;
                // 两个指针,从头结点和相遇结点,各走一步,直到相遇,相遇点即为环入口
                while (index1 != index2) {
                    index1 = index1.next;
                    index2 = index2.next;
                }
                return index1;
            }
        }
        return null;
    }
}
  • 时间复杂度: O(n),快慢指针相遇前,指针走的次数小于链表长度,快慢指针相遇后,两个index指针走的次数也小于链表长度,总体为走的次数小于 2n
  • 空间复杂度: O(1)

141.环形链表 

141. 环形链表

简单

给你一个链表的头节点 head ,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。

如果链表中存在环 ,则返回 true 。 否则,返回 false 。

示例 1:

输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。

示例 2:

输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。

示例 3:

输入:head = [1], pos = -1
输出:false
解释:链表中没有环。

提示:

  • 链表中节点的数目范围是 [0, 104]
  • -105 <= Node.val <= 105
  • pos 为 -1 或者链表中的一个 有效索引 。

进阶:你能用 O(1)(即,常量)内存解决此问题吗?

使用列表的方法: 

public class Solution {
    public boolean hasCycle(ListNode head) {
        // 初始化当前节点为头节点
        ListNode cur = head;;
   
        // 创建一个列表用于存储遍历过的节点
        List<ListNode> list = new ArrayList();
        // 遍历链表
        while(cur != null){
            // 如果当前节点已经在列表中,说明存在环,返回true
            if(list.contains(cur)){
                return true;
            }
            // 将当前节点添加到列表中
            list.add(cur);
            // 移动到下一个节点
            cur = cur.next;
        }
        // 如果遍历完链表都没有发现环,返回false
        return false;
    }
}

快慢指针:

public class Solution {
    public boolean hasCycle(ListNode head) {
        // 初始化快慢指针,都指向头节点
        ListNode slow = head;
        ListNode fast = head;
        // 遍历链表,直到快指针或者快指针的下一个节点为空
        while (fast != null && fast.next != null) {
            // 快指针每次移动两步
            fast = fast.next.next;
            // 慢指针每次移动一步
            slow = slow.next;
            // 如果快慢指针相遇,说明链表中存在环
            if (fast == slow) {
                return true;
            }
        }
        // 如果遍历完链表都没有相遇,说明链表中不存在环
        return false;
    }
}

61.旋转链表 

61. 旋转链表

中等

给你一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置。

示例 1:

输入:head = [1,2,3,4,5], k = 2
输出:[4,5,1,2,3]

示例 2:

输入:head = [0,1,2], k = 4
输出:[2,0,1]

提示:

  • 链表中节点的数目在范围 [0, 500] 内
  • -100 <= Node.val <= 100
  • 0 <= k <= 2 * 109

 先将链表首尾闭合成环,找到旋转后的头节点的前一个节点,然后将它设为尾结点,返回旋转后的头结点

class Solution {
    public ListNode rotateRight(ListNode head, int k) {
        // 如果头结点为空,则无法旋转,直接返回null
        if(head == null){
            return null;
        }
        
        // 初始化指针cur指向头结点,length为链表长度初始值1
        ListNode cur = head;
        int length = 1;
        
        // 计算链表长度
        while(cur.next != null){
            cur = cur.next;
            length++;
        }
        
        // 将尾节点的next指向头结点,形成循环链表
        ListNode tail = cur;
        tail.next = head;
        
        // 计算实际需要旋转的步数
        int count = length - (k % length);
        
        // 将current指针重新指向头结点
        ListNode current = head;
        
        // 移动current指针到新的头结点的前一个节点
        while(--count > 0){
            current = current.next;
        }
        
        // 新的头结点为当前节点的下一个节点
        ListNode newHead = current.next;
        
        // 将当前节点的next设为null,断开循环链表
        current.next = null;
        
        // 返回新的头结点
        return newHead;
    }
}

2.两数相加 

2. 两数相加

中等

给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。

请你将两个数相加,并以相同形式返回一个表示和的链表。

你可以假设除了数字 0 之外,这两个数都不会以 0 开头。

示例 1:

输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807.

示例 2:

输入:l1 = [0], l2 = [0]
输出:[0]

示例 3:

输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]
输出:[8,9,9,9,0,0,0,1]

提示:

  • 每个链表中的节点数在范围 [1, 100] 内
  • 0 <= Node.val <= 9
  • 题目数据保证列表表示的数字不含前导零

 这道题其实是一道模拟的题目,因为给出的顺序就是反的,要求得的答案也是反的,故直接遍历两个链表,每一位相加后出去进位加到下一个结果节点的值上,两个链表均遍历完后如果还有进位就再加一个结果节点存放进位。

class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        ListNode head = null, tail = null; // 定义头节点和尾节点
        int carry = 0; // 进位

        // 循环遍历两个链表,直到两个链表都为空
        while (l1 != null || l2 != null) {
            // 获取当前节点的值,如果节点为空则值为0
            int n1 = l1 != null ? l1.val : 0;
            int n2 = l2 != null ? l2.val : 0;
            
            // 计算当前位的和,并加上进位
            int sum = n1 + n2 + carry;
            
            // 将当前位的值添加到结果链表中
            if (head == null) {
                head = tail = new ListNode(sum % 10); // 如果结果链表为空,创建新节点作为头节点
            } else {
                tail.next = new ListNode(sum % 10); // 如果结果链表不为空,创建新节点作为尾节点的下一个节点
                tail = tail.next; // 更新尾节点
            }
            
            carry = sum / 10; // 更新进位
            
            // 移动到下一个节点
            if (l1 != null) {
                l1 = l1.next;
            }
            if (l2 != null) {
                l2 = l2.next;
            }
        }
        
        // 如果最后还有进位,则在结果链表的尾部添加一个新节点
        if (carry > 0) {
            tail.next = new ListNode(carry);
        }
        
        return head; // 返回结果链表的头节点
    }
}

21.合并两个有序链表 

21. 合并两个有序链表

简单

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 

示例 1:

输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]

示例 2:

输入:l1 = [], l2 = []
输出:[]

示例 3:

输入:l1 = [], l2 = [0]
输出:[0]

提示:

  • 两个链表的节点数目范围是 [0, 50]
  • -100 <= Node.val <= 100
  • l1 和 l2 均按 非递减顺序 排列
class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        ListNode prehead = new ListNode(-1); // 创建哑节点作为结果链表的头节点

        ListNode prev = prehead; // prev指针用于链接结果链表
        // 循环遍历两个链表,直到其中一个链表为空
        while (l1 != null && l2 != null) {
            // 比较两个链表当前节点的值
            if (l1.val <= l2.val) {
                prev.next = l1; // 将 l1 当前节点链接到 prev 后
                l1 = l1.next; // 移动 l1 指针到下一个节点
            } else {
                prev.next = l2; // 将 l2 当前节点链接到 prev 后
                l2 = l2.next; // 移动 l2 指针到下一个节点
            }
            prev = prev.next; // 移动 prev 指针到下一个节点
        }

        // 合并后 l1 和 l2 最多只有一个还未被合并完,我们直接将链表末尾指向未合并完的链表即可
        prev.next = l1 == null ? l2 : l1;

        return prehead.next; // 返回结果链表的头节点
    }
}

138.随机链表的复制

138. 随机链表的复制

中等

提示

给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。

构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 

例如,如果原链表中有 X 和 Y 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 x 和 y ,同样有 x.random --> y 。

返回复制链表的头节点。

用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示:

  • val:一个表示 Node.val 的整数。
  • random_index:随机指针指向的节点索引(范围从 0 到 n-1);如果不指向任何节点,则为  null 。

你的代码  接受原链表的头节点 head 作为传入参数。

示例 1:

输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]

示例 2:

输入:head = [[1,1],[2,1]]
输出:[[1,1],[2,1]]

示例 3:

输入:head = [[3,null],[3,0],[3,null]]
输出:[[3,null],[3,0],[3,null]]

提示:

  • 0 <= n <= 1000
  • -104 <= Node.val <= 104
  • Node.random 为 null 或指向链表中的节点。

通过一个map集合将新旧节点一一映射,防止重复创建节点,通过回溯的方式,回溯的值是创建好的节点,用来给next或者random来赋值。 

class Solution {
    // 创建一个哈希表用于存储已经复制过的节点,避免重复复制
    Map<Node, Node> map = new HashMap<>();

    // 复制带随机指针的链表
    public Node copyRandomList(Node head) {
        // 如果原链表为空,直接返回null
        if (head == null) {
            return head;
        }
        // 如果当前节点已经被复制过,则直接返回其对应的复制节点
        if (!map.containsKey(head)) {
            // 创建当前节点的复制节点,并将其加入哈希表中
            Node headNew = new Node(head.val);
            map.put(head, headNew);
            // 递归复制下一个节点和随机指针所指向的节点
            headNew.next = copyRandomList(head.next);
            headNew.random = copyRandomList(head.random);
        }
        // 返回当前节点的复制节点
        return map.get(head);
    }
}

86.分隔链表 

86. 分隔链表

中等

给你一个链表的头节点 head 和一个特定值 x ,请你对链表进行分隔,使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。

你应当 保留 两个分区中每个节点的初始相对位置。

示例 1:

输入:head = [1,4,3,2,5,2], x = 3
输出:[1,2,2,4,3,5]

示例 2:

输入:head = [2,1], x = 2
输出:[1,2]

提示:

  • 链表中节点的数目在范围 [0, 200] 内
  • -100 <= Node.val <= 100
  • -200 <= x <= 200
class Solution {
    public ListNode partition(ListNode head, int x) {
        // 如果头结点为空,则无法进行分区,直接返回null
        if(head == null){
            return null;
        }
        
        // 创建虚拟节点,并将其next指向头结点
        ListNode dummy = new ListNode(-10000,head);
        
        // 初始化指针cur指向头结点,preLow和pre指向虚拟节点dummy
        ListNode cur = head;
        ListNode preLow = dummy;    //上一个比x小的节点
        ListNode pre = dummy;       //当前节点的前一个节点
        
        // 遍历链表
        while(cur != null){
            // 如果当前节点的值小于x
            if(cur.val < x){
                // 如果前一个节点的值大于等于x,需要进行插入操作
                if(pre.val >= x){
                    // 保存preLow节点的下一个节点
                    ListNode temp = preLow.next;
                    
                    // 将当前节点插入到preLow节点后面
                    preLow.next = cur;
                    
                    // 更新preLow指向当前节点
                    preLow = cur;
                    
                    // 将pre的next指向当前节点的下一个节点,即跳过当前节点
                    pre.next = cur.next;
                    
                    // 将当前节点的next指向temp,即将当前节点插入到preLow后面
                    cur.next = temp;
                    
                    // 更新cur为pre的next节点,即下一个需要判断的节点
                    cur = pre.next;
                } else {
                    // 如果前一个节点的值小于x,不需要插入操作,继续下一个节点的判断
                    preLow = cur;
                    cur = cur.next;
                    pre = pre.next;
                }
                // 继续下一次循环
                continue;
            }
            // 如果当前节点的值不小于x,继续遍历下一个节点
            cur = cur.next;
            pre = pre.next;
        }
        // 返回dummy的下一个节点,即分区后的头结点
        return dummy.next;
    }
}

 

  • 14
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值