leetcode高频题笔记之链表

面试笔试中链表的题目都比较死,几种类型反复做反复记忆就好了

本文整理了面试高频的十二个链表相关的算法题,覆盖了几乎所有常考的情况
刷熟了这几个题,再遇到手撕链表题就没太大压力了!

本文的每个标题都是leetcode中国站的直达链接,如果想要动手试一试就点击进去吧

本文的所有代码均为我学习了大量题解后写出的的较为优美的代码,推荐反复观看记忆

环形链表

在这里插入图片描述
本题是典型的快慢指针法应用题,比较容易想到的就是采用额外的空间进行存储,如解法一的set存储。但是最经典的解法还是采用快慢指针,可以一次遍历并且不使用额外的空间,面试时使用快慢指针会是一个大的亮点

解法一:Hash查找法


public class Solution {
    public boolean hasCycle(ListNode head) {
        Set<ListNode> set = new HashSet<ListNode>();
        ListNode node = head;
        while (node != null) {

            if (set.contains(node)) {
                return true;
            }

            set.add(node);
            node = node.next;
        }

        return false;
    }
}

解法二:快慢指针法(推荐)

public class Solution {
    public boolean hasCycle(ListNode head) {

        if (head == null || head.next == null) {
            return false;
        }

        ListNode fast = head.next;
        ListNode slow = head;

        while (fast != slow) {
            if (fast == null || fast.next == null) {
                return false;
            }
            slow = slow.next;
            fast = fast.next.next;
        }
        return true;
    }
}

环形链表II

在这里插入图片描述
本题同样是对快慢指针法的典型应用,相比第一题更为巧妙,尽量理解,如果实在不能理解可以采用记忆,记住结论就好了

解法一:Hash记录法

public class Solution {
    public ListNode detectCycle(ListNode head) {

        Set<ListNode> set = new HashSet<ListNode>();
        ListNode cur = head;
        while (cur != null) {
            if (set.contains(cur)) {
                return cur;
            }
            set.add(cur);
            cur = cur.next;
        }
        return null;
    }
}

解法二:快慢指针法(推荐)
在这里插入图片描述
思路:

  • 先和环形链表I一样判断有没有环,有才进行第二步
  • 从同一起点开始,fast一次走两步,slow一次走一步,当他们相遇之后,fast回到原点一次走一步,第二次相遇节点就是环连接点
  • 我的理解:
    • 假设第二圈发生第一次相遇,第二圈走完了就第二次相遇了
    • 第一次相遇时fast走了F+(a+b)+a
    • slow走了F+a
    • 因为fast是slow的两倍速度,所以第二次相遇时应该满足fast=2*slow
    • 设slow还需要走 x远第二次相遇,可得方程(F+2a+b+2x)=2(F+a+x),,解得F=b
    • 所以就假设fast以一倍的速度陪slow走完b,另一倍速从F出发,当fast与slow相遇时,相当于fast走过了b+F = 2b = 2倍slow的距离
    • 此时满足第二次相遇的情况
public class Solution {
    public ListNode detectCycle(ListNode head) {

        ListNode fast = head;
        ListNode slow = head;
        while (true) {
            if (fast == null || fast.next == null) return null;
            fast = fast.next.next;
            slow = slow.next;
            if (fast == slow) break;
        }
        fast = head;
        //第二阶段,找环接入点
        while (fast != slow) {
            fast = fast.next;
            slow = slow.next;
        }
        return fast;
    }
}

合并两个有序链表

在这里插入图片描述
要点:

  • 保留一个pre标记,让一个新的指针去遍历
  • 一次遍历结束后注意只有一个链表遍历完了,还应该在结尾加上没有遍历完的链表
public class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {

        ListNode pre = new ListNode(0);
        ListNode cur = pre;
        while (l1 != null && l2 != null) {
            if (l1.val < l2.val) {
                cur.next = l1;
                cur = cur.next;
                l1 = l1.next;
            } else {
                cur.next = l2;
                cur = cur.next;
                l2 = l2.next;
            }
        }
        //如果l1空了,就把l2接上去
        if (l1 == null) {
            cur.next = l2;
        } else {
            cur.next = l1;
        }
        return pre.next;
    }
}

反转链表

在这里插入图片描述
反转链表如果没有训练过可能不太好想象到,所以需要反复的多刷几次,动手多画一画
反转链表非常重要!!!! 在复杂的程序设计题中常作为一个小的部分,如后面的回文链表

原地反转比较好想到,递归就太妙了,建议就记住原地反转就好了,容易理解记忆

迭代法:原地反转(推荐)

在这里插入图片描述

public class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode pre = null;
        ListNode cur = head;
        while (cur != null){
            ListNode tmp = cur.next;
            cur.next  = pre;
            pre = cur;
            cur = tmp;
        }
        return pre;
    }
}

递归:
在这里插入图片描述

public class Solution {
    public ListNode reverseList(ListNode head) {
        //递归终止条件
        if (head == null || head.next == null) {
            return head;
        }
        //下探
        ListNode cur = reverseList(head.next);
        
        head.next.next = head;
        head.next = null;
        //每一次都返回最后一个节点
        return cur;
    }
}

图片引自leetcode题解

两两交换链表中的节点

在这里插入图片描述
两两交换也是很重要的一类题型,推荐熟练的写迭代的方式,并掌握原理,如果进阶可能会是交换多个结点或者以交换n个结点作为程序的一部分

解法一:递归(看别人写递归觉得自己就是傻子)

public class Solution {
    //每次看人家写递归都觉得自己像个傻子,看得懂想不到
    public ListNode swapPairs(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }

        ListNode next = head.next;
        head.next = swapPairs(next.next);
        next.next = head;
        return next;
    }
}

解法二:迭代(推荐)

class Solution {
    public ListNode swapPairs(ListNode head) {
        ListNode node = new ListNode(-1);
        node.next = head;
        ListNode pre = node;
        while (pre.next != null && pre.next.next != null) {
            ListNode l1 = pre.next, l2 = pre.next.next;

            ListNode next = l2.next;
            l1.next = next;
            l2.next = l1;
            pre.next = l2;

            pre = l1;
        }
        return node.next;
    }
}

相交链表

在这里插入图片描述
在这里插入图片描述
本题的要点就是交替遍历两条路
在这里插入图片描述
已上图为例,一条路径遍历a-c-b,另一条遍历b-c-a
当两条路径遍历相遇时,就是交点

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {

        if (headA == null || headB == null) return null;

        ListNode node1 = headA;
        ListNode node2 = headB;

        while (node1 != node2) {
            node1 = node1 == null ? headB : node1.next;
            node2 = node2 == null ? headA : node2.next;
        }
        return node1;
    }
}

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

在这里插入图片描述
迭代实现(推荐)

	public ListNode deleteDuplicates(ListNode head) {
        if (head == null || head.next == null) return head;
        ListNode node = head;
        while (node.next != null && node != null) {

            if (node.val == node.next.val){
                node.next = node.next.next;
            }else {
                node = node.next;
            }
        }
        return head;
    }

递归实现:

	public ListNode deleteDuplicates(ListNode head) {
        if (head == null || head.next == null) return head;
        head.next = deleteDuplicates(head.next);
        return head.val == head.next.val ? head = head.next : head;
    }

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

在这里插入图片描述
本题也运用到了快慢指针,说明快慢指针在链表题中的地位

本题思路:

  • 移动快指针n步,使快慢指针步差为n
  • 如果此时快指针为null,说明要删除的节点是头结点,直接返回head.next;
  • 如果快指针不是null,那么就通过快慢指针查找到倒数第k+1个节点
  • 然后删除掉倒数第k个节点

一趟扫描:快慢指针法

public class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {

        if (head == null || head.next == null) return null;

        ListNode fast = head;
        ListNode slow = head;

        //将fast和slow的步调差调成n,当fast到达末尾了,slow到达指定节点
        while (n-- > 0) {
            fast = fast.next;
        }
        //倒数的数就是头结点的情况,此时fast是null
        if (fast == null){
            return head.next;
        }

        //倒数的数不是头结点,按顺序移动fast和slow节点,直到倒数第二个,测试slow节点到达删除节点的上一个
        while (fast.next != null) {
            fast = fast.next;
            slow = slow.next;
        }
        //slow是待删除节点上一个节点,删除slow.next节点
        slow.next = slow.next.next;

        return head;

    }
}

两数相加II

在这里插入图片描述
非常非常经典高频的考题,如果没有练过真的很难想象,一定要记得大致的步骤

  • 遍历链表,将值分别压入栈中
  • 获取的栈顶元素就是同一位的元素了,如果有就直接出栈获取,如果没有就为0
  • 计算两个值与进位数的和为sum
  • 让sum对10求模得本位的数
  • 让sum对10整除得进位数
  • 用本位数创建一个节点插入到链表头(pre.next)
public class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        //初始化stack
        Stack<Integer> stack1 = buildStack(l1);
        Stack<Integer> stack2 = buildStack(l2);
        //新建个前驱结点方便返回
        ListNode pre = new ListNode(-1);
        int carry = 0;//进位数
        while (!stack1.isEmpty() || !stack2.isEmpty() || carry != 0) {
            int x = stack1.isEmpty() ? 0 : stack1.pop();
            int y = stack2.isEmpty() ? 0 : stack2.pop();
            int sum = x + y + carry;
            ListNode node = new ListNode(sum % 10);//计算出本位的值
            //将node插入到头结点
            node.next = pre.next;
            pre.next = node;
            //计算是否有进位数
            carry = sum / 10;
        }
        return pre.next;
    }

    private Stack<Integer> buildStack(ListNode head) {
        ListNode node = head;
        Stack<Integer> stack = new Stack<>();
        while (node != null) {
            stack.push(node.val);
            node = node.next;
        }
        return stack;
    }
}

回文链表

在这里插入图片描述
O(n) 时间复杂度和 O(1) 空间复杂度解法:

  • 快慢指针找到中点
  • 将链表分割为两部分
  • 选择后一半进行翻转
  • 翻转后再次比较
public class Solution {
    /**
     * 快慢指针找到中点
     * 选择一半进行翻转
     * 翻转后再次比较
     */
    public boolean isPalindrome(ListNode head) {
        if (head == null || head.next == null) return true;

        ListNode slow = head;
        ListNode fast = head.next;
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }
        if (fast != null) slow = slow.next;//偶数个结点,让slow指向下一个,作为后半段的开头
      
        //将链表分为以head和slow开头的等长的两段
        cut(head, slow);
        return isEqual(head, reverse(slow));
    }

    //翻转链表
    private ListNode reverse(ListNode head) {
        ListNode newHead = null;
        while (head != null) {
            ListNode tmp = head.next;
            head.next = newHead;
            newHead = head;
            head = tmp;
        }
        return newHead;
    }

    //判定两个链表是否相等
    private boolean isEqual(ListNode head1, ListNode head2) {
        while (head1 != null && head2 != null) {
            if (head1.val != head2.val) return false;
            head1 = head1.next;
            head2 = head2.next;
        }
        return true;
    }

    //分割链表
    private void cut(ListNode head, ListNode slow) {
        while (head.next != slow) {
            head = head.next;
        }
        head.next = null;
    }
}

奇偶链表

在这里插入图片描述
思路:奇数放一个链表,偶数放一个链表,最后两个链表连接起来

public class Solution {
    public ListNode oddEvenList(ListNode head) {

        if (head == null || head.next == null) return head;

        ListNode odd = head;//奇数节点
        ListNode even = head.next;//偶数节点
        ListNode evenHead = even;//暂存一个偶数节点头,方便重组完进行连接

        while (even != null && even.next != null) {
            odd.next = even.next;
            odd = odd.next;
            even.next = odd.next;
            even = even.next;
        }
        //奇数链表后接上偶数链表
        odd.next = evenHead;
        return head;

    }
}

分割链表

在这里插入图片描述
思路:

  • 统计链表长度
  • 长度小于等于k,每个子链表放一个节点
  • 长度大于k,计算每个子链表长度,分割链表
public class Solution {
    public ListNode[] splitListToParts(ListNode root, int k) {
        if (root == null) return new ListNode[k];
        ListNode cur = root;
        int count = 0;
        //统计节点数
        while (cur != null) {
            count++;
            cur = cur.next;
        }
        ListNode[] nodes = new ListNode[k];

        cur = root;
        //每个子链表不超过1个节点
        if (count <= k) {
            for (int i = 0; i < count; i++) {
                nodes[i] = new ListNode(cur.val);
                cur = cur.next;
            }
        } else {
            //计算每个子链表的节点数
            int remain = count % k;
            int preCount = count / k;

            //记录每部分的节点个数
            int[] counts = new int[k];
            for (int i = 0; i < k; i++) {
                //前remain个链表长度为precount+1
                counts[i] = remain-- > 0 ? preCount + 1 : preCount;
            }
            //遍历链表,存储元素
            for (int i = 0; i < k; i++) {
                //初始化子链表的头结点和个数
                int num = counts[i];
                nodes[i] = cur;

                while (--num > 0) {
                    cur = cur.next;
                }

                //截断链表
                ListNode tmp = cur.next;
                cur.next = null;
                cur = tmp;

            }


        }
        return nodes;

    }
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值