高频面试算法-链表

1. 相交链表

当一个链表到头后,交换另一个链表,继续,直到2个节点相等

    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        ListNode pa = headA;
        ListNode pb = headB;
        while (pa != pb) {
            if (pa != null) {
                pa = pa.next;
            } else {
                pa = headB;
            }

            if (pb != null) {
                pb = pb.next;
            } else {
                pb = headA;
            }
        }
        return pa;
    }

2.反转链表

1.迭代法

    public ListNode reverseList(ListNode head) {
         ListNode prev = null;
         ListNode curr = head;
         while (curr != null) {
            ListNode next = curr.next;
            curr.next = prev;
            prev = curr;
            curr = next;
         }
         return prev;
    }

2.递归 

    public ListNode reverseList(ListNode head) {
        if ((head == null) || (head.next == null)) {
            return head;
        }

        ListNode newHead = reverseList(head.next);
        head.next.next = head;
        head.next = null;
        return newHead;
    }  

3.回文链表

找到中间节点,然后反转中间节点的后续节点,然后再2部分比较

注意:

1. 找到中间结点,其实对应2种方式,这2种方式找到的结点,略微有点差异。如果不注意会出现空指针异常。主要是因为 反转链表时修改了结点的next指针,中间结点指向null了。

2. 反转链表时会修改结点的next指向,导致链表从中点断开,两边链表长度不一致时同时遍历容易出现空指针问题。

    private void testMidReversList(int[] arr) {
        //arr=[1->2->3->4]
        ListNode head = createLinkListInsertTail(arr);
        traverse(head, "create head"); //create head=[1->2->3->4]

        ListNode mid = findMidNodeFromHeadNext(head);
        traverse(mid, "mid"); //mid=[2->3->4]
        ListNode reverse = reverseList(mid); //反转时会从中点断开链表
        traverse(reverse, "reverse"); //reverse=[4->3->2]


        ListNode left = head;
        ListNode right = reverse;
        traverse(left, "left");    //left=[1->2] 已经断开了,head next指针被修改
        traverse(right, "right");  //right=[4->3->2]
    }

第一种  fast =head

    public ListNode findMidNodeFromHead(ListNode head) {
        //初始化是slow和fast都指向 head
        ListNode slow = head;
        ListNode fast = head;
        while ((fast != null) && (fast.next != null)) {
            slow = slow.next;
            fast = fast.next.next;
        }
        //这种找到的中间结点,在偶数时会有差异,由于fast只比slow快一步,所以中点找到的是偶数中间2个点的最后一个
        //2.不管那种方式当链表长度为奇数时,找到的中点都是一个
        return slow;
    }

 由于找到的中点位置的不同,所以在遍历时也会有略微的不同

public boolean isPalindrome(ListNode head) {
        if ((head == null) || (head.next == null)) {
            return true;
        }

        ListNode slow = head;
        ListNode fast = head;
        while ((fast != null) && (fast.next != null)) {
            slow = slow.next;
            fast = fast.next.next;
        }

        ListNode reverse = reverse(slow);

        ListNode left = head;
        ListNode right = reverse;

        //由于这种方式找的mid靠后,所以right会相对较少,使用right遍历
        while (right != null) {
            if (left.val != right.val) {
                return false;
            }
            left = left.next;
            right = right.next;
        }

        return true;
    }

    public ListNode reverse(ListNode node) {
        ListNode prev = null;
        ListNode curr = node;
        while (curr != null) {
            ListNode next = curr.next;
            curr.next = prev;
            prev = curr;
            curr = next;
        }
        return prev;
    }

第二种  fast = head.next; 

    public ListNode findMidNodeFromHeadNext(ListNode head) {
        //初始化是slow和fast时,fast已经比slow 快一步了
        ListNode slow = head;
        ListNode fast = head.next;
        while ((fast != null) && (fast.next != null)) {
            slow = slow.next;
            fast = fast.next.next;
        }
        //1.这种找到的中间结点,在偶数时会有差异,由于fast只比slow快2步,所以中点找到的是偶数中间2个点的最前面一个
        //2.不管那种方式当链表长度为奇数时,找到的中点都是一个
        return slow;
    }

 

public boolean isPalindrome(ListNode head) {
        //没有结点或者只有1个结点的时候,也是 回文链表
        if ((head == null) || (head.next == null)) {
            return true;
        }        

        ListNode mid = findMidNodeFromeHeadNext(head);
        ListNode reverse = reverseFromRecursion(mid);

        ListNode left = head;
        ListNode right = reverse;

        while (left != null) { //由于使用的时head next找中点,所以使用left
            if (left.val != right.val) {
                return false;
            }
            left = left.next;
            right = right.next;
        }
        return true;
    }

    // 使用 fast = head; 找中点
    private ListNode findMidNodeFromHead(ListNode head) {
        ListNode slow = head;
        ListNode fast = head;
        while ((fast != null) && (fast.next != null)) {
            slow = slow.next;
            fast = fast.next.next;
        }
        return slow;
    }

    // 使用 fast = head.next; 找中点
    private ListNode findMidNodeFromeHeadNext(ListNode head) {
        ListNode slow = head;
        ListNode fast = head.next;
        while ((fast != null) && (fast.next != null)) {
            slow = slow.next;
            fast = fast.next.next;
        }
        return slow;
    }

    private ListNode reverseFromIterator(ListNode node) {
        ListNode prev = null;
        ListNode curr = node;
        while (curr != null) {
            ListNode next = curr.next;
            curr.next = prev;
            prev = curr;
            curr = next;
        }
        return prev;
    }

    private ListNode reverseFromRecursion(ListNode node) {
        if ((node == null) || (node.next == null)) {
            return node;
        }

        ListNode newHead = reverseFromRecursion(node.next);
        node.next.next = node;
        node.next = null;

        return newHead;
    }

4.环形链表

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

当2个节点相等时,需要退出循环

思路:使用“快慢指针”法来判断链表是否有环

使用两个指针,一个快指针(每次移动两步)和一个慢指针(每次移动一步)。如果链表中存在环,快指针和慢指针最终会相遇;如果没有环,快指针会到达 null

    public boolean hasCycle(ListNode head) {
        if ((head == null) || (head.next == null)) {
            return false;
        }

        ListNode slow = head;
        ListNode fast = head;
        while ((fast != null) && (fast.next != null)) {
            slow = slow.next;
            fast = fast.next.next;

            if (slow == fast) {  // 环形链表,慢指针和快指针相遇
                return true;
            }
        }
        return false;
    }

5.环形链表II

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

核心思想: 

1. 先找到2个节点相遇的时候

2. 相遇时slow节点移回到head节点

3. slow和fast都已一步的前进,再次相遇就是环的入口

    public ListNode detectCycle(ListNode head) {
        if ((head == null) || (head.next == null)) {
            return null;
        }

        ListNode slow = head;
        ListNode fast = head;
        // 1. 判断链表是否有环
        while ((fast != null) && (fast.next != null)) {
            slow = slow.next;
            fast = fast.next.next;
            // 2. slow 和 fast 相遇说明有环
            if (slow == fast) {
                slow = head;  // 慢指针回到链表头
                while (slow != fast) {
                    slow = slow.next;
                    fast = fast.next;
                }
                return slow; // slow 和 fast 相遇的地方就是环的入口节点
            }
        }
        // 如果没有环
        return null;
    }

延伸扩展  环的大小

  • 判断链表是否有环:首先用快慢指针找出链表中的环,确认链表有环。
  • 计算环的大小:如果找到相遇点,将 current 指针固定在该相遇点,沿环前进并计数,直到回到起始点。每前进一步计数加一,最终得到环的长度。
    public int getCycleLength(ListNode head) {
        if ((head == null) || (head.next == null)) {
            return 0;
        }

        ListNode slow = head;
        ListNode fast = head;

        // 1. 判断链表是否有环并找到相遇点
        while ((fast != null) && (fast.next != null)) {
            slow = slow.next;
            fast = fast.next.next;

            if (slow == fast) {  // 找到环
                // 2. 计算环的大小
                int length = 1;
                ListNode curr = slow;
                while (curr.next != slow) {
                    length++;
                    curr = curr.next;
                }
                return length;
            }
        }
        // 没有环
        return 0;
    }

环形链表的几个问题本质上都是需要先确定是否存在环,存在并且找到环的相遇点,然后再此基础上进一步的操作 

6.合并2个有序链表

记得链表剩余时,直接添加到末尾

方法一:迭代法

    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        ListNode dummy = new ListNode(0);
        ListNode curr = dummy;

        while ((list1 != null) && (list2 != null)) {
            if (list1.val <= list2.val) {
                curr.next = list1;
                list1 = list1.next;
            } else {
                curr.next = list2;
                list2 = list2.next;
            }
            curr = curr.next;
        }

        // 接上剩余节点
        curr.next = (list1 != null) ? list1 : list2;

        return dummy.next;
    }

方法二:递归法

  • 比较 l1l2 的当前节点值。
  • 如果 l1 的值较小,将 l1.next 指向合并 l1.nextl2 的结果。
  • 如果 l2 的值较小,将 l2.next 指向合并 l1l2.next 的结果。
  • 返回较小的节点作为新的头节点。
public ListNode mergeTwoListsRecursive(ListNode l1, ListNode l2) {
    if (l1 == null) return l2;
    if (l2 == null) return l1;

    if (l1.val < l2.val) {
        l1.next = mergeTwoListsRecursive(l1.next, l2);
        return l1;
    } else {
        l2.next = mergeTwoListsRecursive(l1, l2.next);
        return l2;
    }
}
  • 迭代法:时间复杂度为 O(m + n),其中 mn 是两个链表的长度。空间复杂度为 O(1),因为我们只用了一些辅助指针。
  • 递归法:时间复杂度也为 O(m + n),但空间复杂度为 O(m + n),因为递归会使用额外的栈空间。

7. 两数相加

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

最主要是需要考虑进位

    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        // 创建一个哑节点(dummy),方便处理头节点的特殊情况
        ListNode dummy = new ListNode(0);
        ListNode curr = dummy;
        int carry = 0;

        // 遍历链表,直到 l1 和 l2 都为空,且没有进位
        while ((l1 != null) || (l2 != null) || (carry != 0)) {
            int sum = carry; // 从进位开始
            if (l1 != null) {
                sum = sum + l1.val;
                l1 = l1.next;
            }
            if (l2 != null) {
                sum = sum + l2.val;
                l2 = l2.next;
            }

            // 更新进位和当前节点的值
            carry = sum / 10; // 进位值
            curr.next = new ListNode(sum % 10); // 新建节点存储个位数
            curr = curr.next; // 后移当前指针
        }

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

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

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

使用dummy节点主要是为了方便操纵删除头节点的场景

删除结点的语句:curr.next = curr.next.next;

为什么需要fast先移动N+1步的原因

fast 指针先移动 n+1 步的原因是为了让 slow 指针停在要删除节点的前一个节点位置。这样我们可以直接让 slow.next 跳过要删除的节点,实现删除操作。

假设链表有 L 个节点,要删除倒数第 n 个节点:

  1. 倒数第 n 个节点的前一个节点:在删除操作中,我们需要让 slow 指针停在倒数第 n 个节点的前一个节点,才能直接修改 slow.next 指针跳过目标节点。

  2. 双指针距离控制:通过让 fast 先移动 n+1 步,可以使 fastslow 指针之间保持 n 个节点的间隔。当 fast 移动到链表末尾(即 null)时,slow 指针正好到达倒数第 n 个节点的前一个节点。

  3. 删除节点:此时,slow.next 就是我们要删除的节点。通过让 slow.next = slow.next.next,可以跳过目标节点并完成删除。

    public ListNode removeNthFromEnd(ListNode head, int n) {
        //使用 dummy 节点可以统一处理删除头节点的情况,不需要额外判断
        ListNode dummy = new ListNode(0);
        dummy.next = head;

        ListNode slow = dummy;
        ListNode fast = dummy;

        // 1. 让 fast 指针先移动 n+1 步
        for (int i = 0; i <= n; i++) {
            fast = fast.next;
        }

        // 2. 同时移动 fast 和 slow,直到 fast 到达链表末尾
        while (fast != null) {
            slow = slow.next;
            fast = fast.next;
        }

        // 3. 删除 slow 的下一个节点
        slow.next = slow.next.next;

        // 返回头节点
        return dummy.next;
    }

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

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

    public ListNode swapPairs(ListNode head) {
        if ((head == null) || (head.next == null)) {
            return head;
        }
        ListNode dummy = new ListNode(0);
        dummy.next = head;

        // 一组反转节点的前一个位置,初始化为dummy
        ListNode prevGroup = dummy;

        // 判断是否存在要反转的节点
        while ((prevGroup.next != null) && (prevGroup.next.next != null)) {
            // 取2个待反转的节点,方便后续操作
            ListNode first = prevGroup.next;
            ListNode second = prevGroup.next.next;

            //更新一组反转节点的指向,可以理解成连接反转后的节点,
            //即一组的前一个结点要指向反转后的第一个结点
            prevGroup.next = second;
            //第一个节点指向第二个节点的下一个节点,修改第一个节点的指向
            //即第一个结点要指向第二个结点的下一个结点,保证不断链
            first.next = second.next;
            //修改第二个节点的指向,实现节点的反转,
            //即第二个结点要指向第一个结点,实现反转
            second.next = first;

            //更新一组反转节点的位置,移动到反转结束后的位置,即时第一个节点的位置
            //即反转完一组后,移动到下一组反转的前面,准备下一轮反转
            prevGroup = first;
        }
        return dummy.next;
    }

10. K个一组反转链表

    public ListNode reverseKGroup(ListNode head, int k) {
        if ((head == null) || (head.next == null) || (k <=1)) {
            return head;
        }

        ListNode dummy = new ListNode(0);
        dummy.next = head;

        ListNode prevGroupKEnd = dummy;
        ListNode curr = head;

        while (curr != null) {
            //需要反转的一组节点的开始节点
            ListNode startGroup = curr;
            int i = 0;
            //curr是用来遍历整个链表的,也是每次取K个节点进行反转的关键,找到下一组需要反转的开始节点
            //不过需要注意的是,当节点不足K个时,需要对节点进行判空,否则容易空指针异常
            for (; (i<k) && (curr != null); i++) {
                curr = curr.next;
            }

            if (i == k) {
                //当满足一组节点时,修改一组节点的指向,连接链表,同时更新一组节点尾的位置
                prevGroupKEnd.next = reverseK(startGroup, k);
                prevGroupKEnd = startGroup;
            } else {
                //不满足时直接修改一组节点尾指向下一组节点的开始节点
                prevGroupKEnd.next = startGroup;
            }
        }
        return dummy.next;
    }

    private ListNode reverseK(ListNode node, int k) {
        ListNode prev = null;
        ListNode curr = node;
        for (int i=0; i<k; i++) {
            ListNode next = curr.next;
            curr.next = prev;
            prev = curr;
            curr = next;
        }
        return prev;
    }

11.随机链表的复制

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

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

    public Node copyRandomList(Node head) {
        if (head == null) {
            return head;
        }

        Map<Node, Node> nodes = new HashMap<>();
        Node curr = head;
        //遍历一遍节点,保存到map中,key=当前的节点,value=copy节点,
        //只有值,next和random没有复制
        while (curr != null) {
            nodes.put(curr, new Node(curr.val));
            curr = curr.next;
        }

        curr = head;
        // 从map里面分别对copy节点进行next和random结点赋值
        while (curr != null) {
            Node copyNode = nodes.get(curr);
            copyNode.next = nodes.get(curr.next);
            copyNode.random = nodes.get(curr.random);

            curr = curr.next;
        }
        return nodes.get(head);
    }

12.排序链表

给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表 。

找结点的中点,断开时容易犯的错误

        head=[4 -> 2 -> 1 -> 3]
        
        { 
            ListNode slow = head;
            ListNode fast = head;
            while ((fast != null) && (fast.next != null)) {
            slow = slow.next;
            fast = fast.next.next;
        }
分割完后
left = sortList(head):此时 head 仍然是指向链表的头节点 4,链表为 4 -> 2 -> null。
现在问题出在 left = sortList(head) 这一步:
head 指向的是 4 -> 2 -> null,但链表已经被从 2 断开,因此此时递归处理的左半部分链表没有减少长度。
当你递归处理 4 -> 2 -> null 时,慢指针会再次指向 2,然后断开,
--------------------------------------------------------------------
这样会导致递归始终无法退出,栈溢出

正确找到中点并且断开的 

ListNode slow = head;
ListNode fast = head.next;  // 快指针从 head.next 开始,确保分割准确

// 快慢指针移动,找到中点
while (fast != null && fast.next != null) {
    slow = slow.next;
    fast = fast.next.next;
}

// mid 是后半部分的开头
ListNode mid = slow.next;
slow.next = null;  // 断开链表

方法:归并排序

归并排序的步骤如下:

  1. 找到链表的中点:使用快慢指针找到链表的中点,将链表分为两半。
  2. 递归地对两半链表进行排序:递归地将每一半链表拆分并排序。
  3. 合并两个已排序的链表:用一个辅助函数来合并两个有序链表。
    public ListNode sortList(ListNode head) {
        // 基本情况:空链表或只有一个节点
        if ((head == null) || (head.next == null)) {
            return head;
        }

        // 1. 使用快慢指针找到链表的中点,使用的是靠前中点,
        // 这种方式当有偶数个结点时能较好的平分2个链表
        ListNode mid = findMidNodeFromHeadNext(head);
        ListNode rightHead = mid.next; // 右边链表的第一个结点
        mid.next = null; // 将链表分成两半

        // 2. 递归排序左右两半
        ListNode left = sortList(head);
        ListNode right = sortList(rightHead);

        // 3. 合并排序后的两半
        return mergeTwoLists(left, right);
    }

    private ListNode findMidNodeFromHeadNext(ListNode head) {
        //初始化是slow和fast时,fast已经比slow 快一步了
        ListNode slow = head;
        ListNode fast = head.next;
        while ((fast != null) && (fast.next != null)) {
            slow = slow.next;
            fast = fast.next.next;
        }
        //1.这种找到的中间结点,在偶数时会有差异,
        //由于fast只比slow快2步,所以中点找到的是偶数中间2个点的最前面一个
        //2.不管那种方式当链表长度为奇数时,找到的中点都是一个
        return slow;
    }

    private ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        ListNode dummy = new ListNode(0);
        ListNode curr = dummy;

        while ((list1 != null) && (list2 != null)) {
            if (list1.val <= list2.val) {
                curr.next = list1;
                list1 = list1.next;
            } else {
                curr.next = list2;
                list2 = list2.next;
            }
            curr = curr.next;
        }

        // 接上剩余节点
        curr.next = (list1 != null) ? list1 : list2;

        return dummy.next;
    }

13.LRU缓存

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值