链表 力扣hot100热门面试算法题 面试基础 核心思路 背题 LRU 合成K个升序链表 环形链表II 合成两个有序链表 两数相加 删除链表的倒数第N个节点 两两交换链表中的节点 K个一组反转链表等

链表

一定要有模版思想,特别是反转链表,直接记住。

相交链表

https://leetcode.cn/problems/intersection-of-two-linked-lists/

核心思路

  1. 设第一个公共节点为 node , headA的节点数量为 a , headB的节点数量为 b ,两链表的公共尾部的节点数量为 c ,则有:
  • 头节点 headA 到 node 前,共有 a−c 个节点;
  • 头节点 headB 到 node 前,共有 b−c 个节点;
  1. 考虑构建两个节点指针 A , B 分别指向两链表头节点 headA , headB ,做如下操作:

​ 指针 A 先遍历完链表 headA ,再开始遍历链表 headB ,当走到 node 时,共走步数为:
​ a+(b−c)
​ 指针 B 先遍历完链表 headB ,再开始遍历链表 headA ,当走到 node 时,共走步数为:
​ b+(a−c)

  1. 那么while一定会停止,因为a+(b−c)=b+(a−c)
  2. 若两链表有公共尾部 指针 A , B 同时指向「第一个公共节点」node 。
    若两链表无公共尾部 指针 A , B 同时指向 null 。

这种方法的时间复杂度是O(m + n),其中m和n分别是两个链表的长度,因为它最多遍历两个链表各一次。空间复杂度是O(1),因为它只使用了两个额外的指针。

示例代码

// 定义一个名为Solution的类
public class Solution {
    // 定义一个方法getIntersectionNode,接收两个链表的头节点headA和headB作为参数,返回它们的第一个相交节点
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        // 使用两个指针A和B分别指向链表headA和headB的头节点
        ListNode A = headA, B = headB;
        // 使用一个while循环来遍历链表,直到两个指针相等(找到相交节点或都为null)或遍历完两个链表
        while (A != B) {
            // 如果指针A不为null,则将其移动到下一个节点;否则,将其重新指向链表headB的头节点
            A = A != null ? A.next : headB;
            // 如果指针B不为null,则将其移动到下一个节点;否则,将其重新指向链表headA的头节点
            B = B != null ? B.next : headA;
        }
        // 当两个指针相等时,返回它们指向的节点(可能是相交节点,也可能是null,表示没有相交节点)
        return A;
    }
}

反转链表

https://leetcode.cn/problems/reverse-linked-list/

核心思路

核心就是要背下来

示例代码

/**
 * 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 reverseList(ListNode head) {
        ListNode pre = null;
        ListNode cur = head;
        while (cur != null) {
            ListNode nxt = cur.next;
            cur.next = pre;
            pre = cur;
            cur = nxt;
        }
        return pre;
    }
}

回文链表

https://leetcode.cn/problems/palindrome-linked-list/

核心思路

找到中点,反转后半段,与前半段进行比较

示例代码

class Solution {
    // 判断链表是否是回文链表
    public boolean isPalindrome(ListNode head) {
        // 找到链表的中点
        ListNode mid = mid(head);
        // 反转链表从中点开始的后半部分
        ListNode rev = revList(mid);
        // 同时遍历原始链表的前半部分和反转后的后半部分,比较对应节点的值
        while(rev != null){
            if(head.val != rev.val) return false; // 如果对应节点的值不相等,则链表不是回文链表
            rev = rev.next; // 移动反转链表中的指针
            head = head.next; // 移动原始链表中的指针
        }
        return true; // 如果所有对应节点的值都相等,则链表是回文链表
    }

    // 使用快慢指针法找到链表的中点
    ListNode mid(ListNode head){
        ListNode slow = head; // 慢指针
        ListNode fast = head; // 快指针
        while(fast != null && fast.next != null){ // 快指针到达链表末尾或其前一个节点时停止
            slow = slow.next; // 慢指针每次移动一步
            fast = fast.next.next; // 快指针每次移动两步
        }
        return slow; // 当快指针到达链表末尾或其前一个节点时,慢指针指向链表的中点
    }

    // 反转链表
    ListNode revList(ListNode head){
        ListNode pre = null; // 前一个节点,初始化为null
        ListNode cur = head; // 当前节点,从链表的头开始
        while(cur != null){ // 遍历链表直到末尾
            ListNode nxt = cur.next; // 保存当前节点的下一个节点
            cur.next = pre; // 将当前节点的next指针指向前一个节点,实现反转
            pre = cur; // 前一个节点更新为当前节点
            cur = nxt; // 当前节点更新为下一个节点
        }
        return pre; // 当原链表的末尾节点成为反转链表的头节点时,返回该头节点
    }
}

环形链表

https://leetcode.cn/problems/linked-list-cycle/

核心思路

快慢指针

示例代码

public class Solution {
    public boolean hasCycle(ListNode head) {
        ListNode slow = head, fast = head; // 乌龟和兔子同时从起点出发
        while (fast != null && fast.next != null) {
            slow = slow.next; // 乌龟走一步
            fast = fast.next.next; // 兔子走两步
            if (fast == slow) // 兔子追上乌龟(套圈),说明有环
                return true;
        }
        return false; // 访问到了链表末尾,无环
    }
}

环形链表II

https://leetcode.cn/problems/linked-list-cycle-ii/

核心思路

图片来源:零茶山艾府

image-20250316144928154

示例代码

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 cur = head;
                //再head与slow指针走,找相遇点为所求
                while (cur != slow) {
                    cur = cur.next;
                    slow = slow.next;
                }
                return cur;
            }
        }
        return null;

    }
}

合成两个有序链表

https://leetcode.cn/problems/merge-two-sorted-lists/

核心思路

双指针排序

示例代码

class Solution {
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        // 创建一个哑节点(dummy node),用于简化边界情况的处理
        ListNode ans = new ListNode();
        // cur用于遍历新链表,初始指向哑节点
        ListNode cur = ans;
        // 当两个链表都不为空时,进行合并
        while(list1!=null && list2!=null){
            // 如果list1的当前节点值小于list2的当前节点值
            if(list1.val<list2.val){
                // 将cur的next指向list1的当前节点
                cur.next = list1;
                // list1前进到下一个节点
                list1 = list1.next;
            }else{
                // 否则,将cur的next指向list2的当前节点
                cur.next = list2;
                // list2前进到下一个节点
                list2 = list2.next;
            }
            // cur前进到下一个节点,为下一次比较做准备
            cur = cur.next;
        }
        // 当其中一个链表为空时,直接将另一个链表的剩余部分连接到结果链表的末尾
        cur.next = list1 != null ? list1 : list2;

        // 返回合并后的链表(跳过哑节点)
        return ans.next;
    }
}

两数相加

https://leetcode.cn/problems/add-two-numbers/

核心思路

easy,处理一下余数就行

示例代码

/**
 * 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 addTwoNumbers(ListNode l1, ListNode l2) {
        ListNode ans = new ListNode();
        ListNode cur = ans;
        int ex = 0;

        while(l1!=null||l2!=null){
            int v = 0;
            
            if(l1 != null) v+=l1.val;
            if(l2 != null) v+=l2.val;
            v+=ex;
            ex = v/10;
            v%=10;
            cur.next = new ListNode(v);
            cur = cur.next;
            if(l1 != null)
                l1 = l1.next;
            if(l2 != null)
                l2 = l2.next;
        }

        if(ex == 1) {
            cur.next = new ListNode(ex);
        }
        return ans.next;
    }
}

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

https://leetcode.cn/problems/remove-nth-node-from-end-of-list/

核心思路

关键在于怎么找到第N个节点,先让A从头节点走n步,然后B从头结点与A同时走,A走到尾部,B刚好到倒数第n个节点

示例代码

/**
 * 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 dummy = new ListNode(0,head);
        ListNode ll =dummy;
        ListNode rr = dummy;
        while(n-->0){
            rr = rr.next;
        }
        while(rr.next!=null){
            ll = ll.next;
            rr = rr.next;
        }
        ll.next = ll.next.next;

        return dummy.next;
    }
}

两两交换链表中的节点

https://leetcode.cn/problems/swap-nodes-in-pairs/

核心思路

基本操作,链表节点的添加与删除,因为要操作头结点,增加哑节点

示例代码

/**
 * 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 swapPairs(ListNode head) {
        ListNode ans = new ListNode(0, head);
        ListNode h0 = ans;
        ListNode h1 = ans.next;
        while (h1!=null && h1.next!= null) {
            ListNode h2 = h1.next;
            ListNode h3 = h2.next;

            h0.next = h2;
            h2.next = h1;
            h1.next = h3;

            h0 = h1;
            h1 = h3;
        }
        return ans.next;
    }
}

K个一组反转链表

https://leetcode.cn/problems/reverse-nodes-in-k-group/

核心思路

用head计数,ans做答案,反转后保存在pre,再放到ans。

就是一个反转链表,背下模版即可。

示例代码

/**
 * 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 reverseKGroup(ListNode head, int k) {
        ListNode ans = new ListNode(0,head);
        ListNode p0 = ans;

        ListNode pre = null;
        ListNode cur = p0.next;
        int cnt = 0;
        while(head!=null){
            head = head.next;
            cnt++;
            if(cnt % k == 0){
                for(int i = 0;i<k;i++){
                    ListNode nxt= cur.next;
                    cur.next = pre;
                    pre = cur;
                    cur = nxt;
                }
                p0.next.next = cur;
                p0.next = pre;
                for(int i = 0;i<k;i++) p0= p0.next;
            }
        }

        return ans.next;
    }
}

随机链表的复制

https://leetcode.cn/problems/copy-list-with-random-pointer/

核心思路

就是深拷贝复制链表。没什么算法,就是个窍门,看注释能看懂。

示例代码

/*
// Definition for a Node.
class Node {
    int val;
    Node next;
    Node random;

    public Node(int val) {
        this.val = val;
        this.next = null;
        this.random = null;
    }
}
*/

class Solution {
    public Node copyRandomList(Node head) {
        // 如果输入的链表为空,直接返回 null
        if(head == null) return null;

        // 第一步:在原链表的每个节点后面插入一个新节点
        // 新节点的值与当前节点相同,新节点的 next 指向当前节点的 next
        for(Node cur = head; cur != null; cur = cur.next.next) {
            cur.next = new Node(cur.val, cur.next);
        }

        // 第二步:设置新节点的 random 指针
        // 遍历链表,将原节点 random 指针指向的节点的 next(即对应的新节点)赋值给新节点的 random
        for(Node cur = head; cur != null; cur = cur.next.next) {
            if(cur.random != null) {
                cur.next.random = cur.random.next;
            }
        }

        // 第三步:拆分链表
        // 将原链表和新链表分离
        // `ans` 是新链表的头节点,`cur` 用于遍历原链表,`idx` 用于遍历新链表
        Node ans = head.next;
        Node cur = head;
        Node idx = ans;

        // 遍历链表,将原链表和新链表拆分
        // 同时重新连接原链表中的 next 指针和新链表中的 next 指针
        while(cur.next.next != null) {
            // 恢复原链表的 next 指针
            cur.next = cur.next.next;
            // 设置新链表的 next 指针
            idx.next = cur.next.next;
            // 移动到下一个节点
            cur = cur.next;
            idx = idx.next;
        }

        // 最后一个节点的 next 指针需要手动设置为 null,避免与原链表产生连接
        cur.next = null;

        // 返回新链表的头节点
        return ans;
    }
}

排序链表

https://leetcode.cn/problems/sort-list/

核心思路

就是归并排序的思路一毛一样

示例代码

/**
 * 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 sortList(ListNode head) {
        // 如果链表为空或只有一个节点,则无需排序,直接返回
        if (head == null || head.next == null) {
            return head;
        }

        // 使用快慢指针找到链表的中点,并将链表从中点断开,得到两个子链表
        ListNode head2 = mid(head);

        // 递归地对两个子链表进行排序
        head = sortList(head);
        head2 = sortList(head2);

        // 将排序后的两个子链表合并成一个有序链表
        return merge(head, head2);
    }

    // 将两个链表从中间断开的辅助函数
    // 使用快慢指针技巧找到链表的中点,并断开链表,返回后半部分链表的头节点
    ListNode mid(ListNode head) {
        ListNode slow = head;    // 慢指针,每次移动一步
        ListNode fast = head;    // 快指针,每次移动两步
        ListNode pre = head;     // 用于记录慢指针的前一个节点,以便最后断开链表
        while (fast != null && fast.next != null) { // 当快指针未到达链表尾部时继续循环
            pre = slow;         // 更新pre为当前慢指针的位置
            slow = slow.next;   // 慢指针前进一步
            fast = fast.next.next; // 快指针前进两步
        }
        pre.next = null;        // 断开链表,将前半部分和后半部分分开
        return slow;            // 返回后半部分链表的头节点
    }
    
    // 合并两个有序链表的辅助函数
    ListNode merge(ListNode list1, ListNode list2) {
        ListNode dummy = new ListNode(); // 创建一个哨兵节点,用于简化链表操作,dummy.next将指向合并后的新链表的头节点
        ListNode cur = dummy;            // cur用于遍历新链表,初始指向哨兵节点
        while (list1 != null && list2 != null) { // 当两个链表都未遍历完时继续循环
            if (list1.val < list2.val) { // 比较两个链表当前节点的值,选择较小的节点加入新链表
                cur.next = list1;
                list1 = list1.next;
            } else {
                cur.next = list2;
                list2 = list2.next;
            }
            cur = cur.next; // cur移动到新链表的下一个位置
        }
        cur.next = list1 != null ? list1 : list2; // 将剩余未遍历完的链表直接接到新链表的末尾
        return dummy.next; // 返回合并后的新链表的头节点(跳过哨兵节点)
    }
}

合成K个升序链表

https://leetcode.cn/problems/merge-k-sorted-lists/

核心思路

归并,与上一题思路相同

示例代码

/**
 * 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 mergeKLists(ListNode[] lists) {
        return mergeKLists(lists, 0, lists.length);
    }

    private ListNode mergeKLists(ListNode[] lists, int i, int j) {
        int m = j - i;
        if (m == 0) {
            return null; // 注意输入的 lists 可能是空的
        }
        if (m == 1) {
            return lists[i]; // 无需合并,直接返回
        }
        ListNode left = mergeKLists(lists, i, i + m / 2); // 合并左半部分
        ListNode right = mergeKLists(lists, i + m / 2, j); // 合并右半部分
        return mergeTwoLists(left, right); // 最后把左半和右半合并
    }

    // 21. 合并两个有序链表
    private ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        ListNode dummy = new ListNode(); // 用哨兵节点简化代码逻辑
        ListNode cur = dummy; // cur 指向新链表的末尾
        while (list1 != null && list2 != null) {
            if (list1.val < list2.val) {
                cur.next = list1; // 把 list1 加到新链表中
                list1 = list1.next;
            } else { // 注:相等的情况加哪个节点都是可以的
                cur.next = list2; // 把 list2 加到新链表中
                list2 = list2.next;
            }
            cur = cur.next;
        }
        cur.next = list1 != null ? list1 : list2; // 拼接剩余链表
        return dummy.next;
    }

}

LRU缓存

核心思路

数据结构:
1.双向链表,链表头部有固定哑结点,pre指向链表尾部;
2.Map:存储key和key对应的Node;

示例代码

class LRUCache {
    private static class Node{
        int key,value;
        Node prev,next;
        Node(int k,int v){
            key = k;
            value = v;
        }
    }

    private final int capacity;
    private final Node dummy = new Node(0, 0); // 哨兵节点
    Map<Integer,Node> keyToNode = new HashMap<>();
    
    public LRUCache(int capacity) {
        this.capacity = capacity;
        dummy.prev = dummy;
        dummy.next = dummy;    
    }
    
    public int get(int key) {
        Node node = getNode(key);
        return node!=null ? node.value : -1;
    }
    
    public void put(int key, int value) {
        Node node = getNode(key);
        if (node != null) { // 有这本书
            node.value = value; // 更新 value
            return;
        }
        node = new Node(key,value);
        keyToNode.put(key,node);
        pushFront(node);
        if(keyToNode.size()>capacity){
            Node backNode = dummy.prev;
            keyToNode.remove(backNode.key);
            remove(backNode); // 去掉最后一本书 
        }
    }

    // 获取 key 对应的节点,同时把该节点移到链表头部
    private Node getNode(int key) {
        if (!keyToNode.containsKey(key)) { // 没有这本书
            return null;
        }
        Node node = keyToNode.get(key); // 有这本书
        remove(node); // 把这本书抽出来
        pushFront(node); // 放在最上面
        return node;
    }
    // 删除一个节点(抽出一本书)
    private void remove(Node x) {
        x.prev.next = x.next;
        x.next.prev = x.prev;
    }

    // 在链表头添加一个节点(把一本书放在最上面)
    private void pushFront(Node x) {
        x.prev = dummy;
        x.next = dummy.next;
        x.prev.next = x;
        x.next.prev = x;
    }
}

/**
 * Your LRUCache object will be instantiated and called as such:
 * LRUCache obj = new LRUCache(capacity);
 * int param_1 = obj.get(key);
 * obj.put(key,value);
 */

你可能感兴趣的:(算法,链表,leetcode)