刷题小记7:链表

160.相交链表 esay

相交链表

思路

双指针

方法一:

遍历得到两链表的的长度

指针从长度相等的地方开始同时移动

指针相等时就是相交节点

方法二:

 

若两链表 有 公共尾部 (即 c>0 ) :指针 A , B 同时指向「第一个公共节点」node 。
若两链表 无 公共尾部 (即 c=0 ) :指针 A , B 同时指向 null 。

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        ListNode A = headA, B = headB;
        while (A != B) {
            A = A != null ? A.next : headB;
            B = B != null ? B.next : headA;
        }
        return A;
    }
}

作者:Krahets
链接:https://leetcode.cn/problems/intersection-of-two-linked-lists/solutions/12624/intersection-of-two-linked-lists-shuang-zhi-zhen-l/
来源:力扣(LeetCode)

 206.反转链表 easy

 反转链表

方法一:迭代(双指针)

 

class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode cur = head, pre = null;
        while(cur != null) {
            ListNode tmp = cur.next; // 暂存后继节点 cur.next
            cur.next = pre;          // 修改 next 引用指向
            pre = cur;               // pre 暂存 cur
            cur = tmp;               // cur 访问下一节点
        }
        return pre;
    }
}

复杂度分析

  • 时间复杂度 O(N) : 遍历链表使用线性大小时间。
  • 空间复杂度 O(1) : 变量 pre 和 cur 使用常数大小额外空间。

方法二:递归法

public ListNode reverseList(ListNode head) {
    // 从头节点开始调用递归方法,前驱节点初始化为 null
    return dfs(null, head);
}

// 定义递归方法 dfs
public ListNode dfs(ListNode pre, ListNode cur) {
    // 基本情况:如果当前节点 cur 为 null,说明已经到达链表的末尾
    if (cur == null) {
        // 返回前驱节点 pre,作为新的头节点
        return pre;
    }
    
    // 递归调用 dfs,当前节点 cur 作为前驱节点,cur.next 作为当前节点
    ListNode res = dfs(cur, cur.next);
    
    // 将当前节点 cur 的 next 指向前驱节点 pre,反转链表的一部分
    cur.next = pre;

    // 返回反转后的链表头
    return res;
}

解析

  1. 反转链表的方法 (reverseList):

    • 方法接收链表的头节点 head 作为参数。
    • 调用辅助的递归函数 dfs,并传入 null 作为前驱节点(因为反转时,头节点的 next 会指向 null),以及传入当前节点 head
  2. 辅助递归方法 (dfs):

    • 参数:
      • pre:反转后的链表的前驱节点。
      • cur:当前正在处理的节点。
    • 基本情况:
      • 当 cur 为 null 时,表示递归到链表末尾,此时返回 pre,即新的链表头。
    • 递归调用:
      • 递归调用 dfs 方法,将当前节点 cur 作为新的前驱节点,将 cur.next 作为新的当前节点。
    • 反转操作:
      • 在返回时,将当前节点 cur 的 next 指向前驱节点 pre,实现链表的反转。
    • 返回值:
      • 返回反转后的链表头 res

递归过程

通过递归的方式,该函数会遍历到链表的末尾,然后在返回时逐步将每个节点的 next 指向前一个节点,从而实现链表的反转。

时间复杂度为 O(n),空间复杂度为 O(n)(主要是由于递归调用栈的使用)。

关键点

  • 尾递归:由于是递归处理,每个节点的 next 在函数返回时都被反转。
  • 链表反转:通过反向指向实现链表的反转,改变了节点的连接关系。

234.回文链表 easy

回文链表

 快慢指针,找到链表的中间,反转后半部分,然后对比

141.环形链表 easy

环形链表

快慢指针,相遇说明有环

142.环形列表Ⅱ mid

力扣(LeetCode)

快慢指针+数学证明

或者哈希

21.合并两个有序列表 easy

合并两个有序链表

递归

  • 终止条件:当两个链表都为空时,表示我们对链表已合并完成。
  • 如何递归:我们判断 l1 和 l2 头结点哪个更小,然后较小结点的 next 指针指向其余结点的合并结果。(调用递归)
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        if (list1 == null) return list2;
        if (list2 == null) return list1;

        if (list1.val < list2.val) {
            list1.next = mergeTwoLists(list1.next, list2);
            return list1;
        } else {
            list2.next = mergeTwoLists(list1, list2.next);
            return list2;
        }
    }

2. 两数相加 mid

两数相加

  1. 结果链表初始化

    • 创建一个新的 ListNode 对象 res,用于存储结果链表的头节点。cur 指针用于遍历和构建结果链表。
  2. 进位初始化

    • 初始化 carry 为 0,用于处理两个数字相加时可能产生的进位。
  3. 主循环

    • 当 l1 或 l2 不为 null 时,持续进行循环。循环内部:
      • 获取当前节点的值,如果节点为空,则值为 0。
      • 计算当前位的和,并考虑进位。
      • 更新进位 carry 和当前节点的值 cur.val
      • 更新指针 l1 和 l2 指向下一个节点(如果它们不为空)。
      • 如果 l1 或 l2 还有剩余节点,则创建一个新的 ListNode 作为 cur.next,并移动 cur 指针。
  4. 处理剩余进位

    • 在循环结束后,如果 carry 仍为 1,则在结果链表末尾添加一个新节点。
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
    // 创建一个新的 ListNode 作为结果链表的头节点
    ListNode res = new ListNode();
    // cur 是一个指针,初始化为结果链表的头节点
    ListNode cur = res;
    // carry 用于保存进位,初始为 0
    int carry = 0;

    // 遍历 l1 和 l2,直到两个链表都遍历完
    while (l1 != null || l2 != null) {
        // 如果 l1 当前节点不为空,则取其值;否则取 0
        int n1 = l1 != null ? l1.val : 0;
        // 如果 l2 当前节点不为空,则取其值;否则取 0
        int n2 = l2 != null ? l2.val : 0;

        // 计算当前位的和,加上进位
        cur.val = n1 + n2 + carry;
        // 更新进位
        carry = cur.val / 10;
        // 更新当前节点的值为和的个位数
        cur.val %= 10;

        // 移动到下一个节点,如果 l1 当前节点不为空
        if (l1 != null) {
            l1 = l1.next;
        }
        // 移动到下一个节点,如果 l2 当前节点不为空
        if (l2 != null) {
            l2 = l2.next;
        }

        // 如果还有剩余节点(l1 或 l2 不为空),则创建新的节点并移动 cur 指针
        if (l1 != null || l2 != null) {
            cur.next = new ListNode(); // 创建一个新的节点
            cur = cur.next; // 移动 cur 指针到新创建的节点
        }
    }

    // 如果最后还有进位,则在结果链表末尾添加一个新节点
    if (carry == 1) {
        cur.next = new ListNode(1);
    } 

    // 返回结果链表的头节点(跳过初始的空节点)
    return res;
}

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

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

快慢指针

让 快指针 领先 慢指针 n + 1 个结点

当 fast 到达链表末尾时,slow 就是待删除节点的前一个节点

注意特殊情况:删除的为第一个节点

设置哨兵结点,是关键

24.两两交换链表中的结点 mid

两两交换链表中的节点

递归

25. K个一组翻转链表 hard

K 个一组翻转链表

思路

  1. 每次翻转前,要确定翻转链表的范围,这个必须通过 k 此循环来确定
  2. 需记录翻转链表前驱和后继,方便翻转完成后把已翻转部分和未翻转部分连接起来
  3. 初始需要两个变量 pre 和 endpre 代表待翻转链表的前驱,end 代表待翻转链表的末尾
  4. 经过k此循环,end 到达末尾,记录待翻转链表的后继 next = end.next

翻转链表,然后将三部分链表连接起来,然后重置 pre 和 end 指针,然后进入下一次循环
特殊情况,当翻转部分长度不足 k 时,在定位 end 完成后,end==null,已经到达末尾,说明题目已完成,直接返回即可

复杂度分析

  1. 时间复杂度为 O(n∗K) 最好的情况为 O(n) 最差的情况未 O(n2)
  2. 空间复杂度为 O(1) 除了几个必须的节点指针外,没有占用其他空间

作者:房建斌学算法
链接:https://leetcode.cn/problems/reverse-nodes-in-k-group/solutions/10416/tu-jie-kge-yi-zu-fan-zhuan-lian-biao-by-user7208t/
来源:力扣(LeetCode)

 138.随机链表的复制 mid

随机链表的复制

借助 HashMap 

第一次遍历 建立 “原节点 -> 新节点” 的 Map 映射

第二次遍历 构建新链表的 next 和 random 指向

148.排序链表 mid

排序链表

自底向上的归并排序,复杂的很

23.合并 K 个升序链表 hard

合并 K 个升序链表

方法一:分治,归并排序

public ListNode mergeKLists(ListNode[] lists) {
        int i = 0;
        int j = lists.length;

        return mergeKList(lists, i, j);// 左闭右开区间

    }

    // 分治法合并链表
    public ListNode mergeKList(ListNode[] lists, int i, int j) {
        int m = j - i; // 计算当前处理的链表数量
        if (m == 0) {
            return null; // 如果没有链表,返回 null
        }
        if (m == 1) {
            return lists[i]; // 如果只有一个链表,直接返回该链表
        }

        // 递归分治,合并左半部分链表
        ListNode left = mergeKList(lists, i, i + m / 2);
        // 递归分治,合并右半部分链表
        ListNode right = mergeKList(lists, i + m / 2, j);

        // 合并左右两部分的链表并返回结果
        return mergeTwoKLists(left, right);
    }

方法二:优先队列

详细解释

  1. 优先队列的初始化:

    PriorityQueue<ListNode> pq = new PriorityQueue<>((a, b) -> a.val - b.val);
    • 使用 PriorityQueue 来存储链表的节点,构造时通过比较节点的值(a.val - b.val)来维护小根堆的性质,以便每次都能快速获取最小的节点。
  2. 将非空节点加入优先队列:

    for (ListNode node : lists) { 
        if (node != null) pq.offer(node);
    }
    • 遍历所有输入的链表头节点,如果节点不为空,则将其加入优先队列。
  3. 使用虚拟头节点简化合并操作:

    ListNode dummy = new ListNode(); 
    ListNode cur = dummy;
    • 创建一个虚拟头节点 dummycur 指针用来构建新的合并链表,最开始指向 dummy
  4. 合并链表:

    while (!pq.isEmpty()) {
        ListNode node = pq.poll();
        cur.next = node;
        if (node.next != null) {
            pq.offer(node.next);
        }
        cur = cur.next;
    }
     
    • 当优先队列不为空时,不断从中提取出最小节点 node,并将其连接到合并链表的尾部。
    • 如果 node 还有下一个节点,则将其下一个节点加入优先队列。
  5. 返回合并后的链表

时间复杂度为 O(N log k),其中 N 是所有链表中节点的总数,而 k 是链表的数量。这是因为每个节点最多会被加入和移除优先队列一次,因此处理 N 个节点的总时间与优先队列的操作(大小为 k)成正比。使用优先队列使得在合并过程中能够始终保持节点的有序性。

146.LRU 缓存 mid

LRU 缓存

需要的数据结构

函数 get 和 put 必须以 O(1) 的平均时间复杂度运行

查找借助HashMap

删除移动操作使用双向链表

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值