备战秋招DAY1-今日力扣-链表

1.相交链表(难度等级:简单)

2个链表,很容易联想到双指针法。那么,指针该怎么移动,移动到什么时刻能代表考虑完这个问题呢?这里,我们还需要先思考一下。

其实,当两个指针共同移动完headA长度+headB长度后,若还未出现相交节点,既可以判定不存在相交节点了。

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        ListNode tempA = headA, tempB = headB;
        while(tempA != null || tempB != null){
            if(tempA == tempB){
                return tempA;
            }
            tempA = tempA==null ? headB : tempA.next;
            tempB = tempB==null ? headA : tempB.next;
        }
        return null;
    }
}

2.回文链表(难度等级:简单)

最直观想法就是,拿一个容器装顺序遍历的结果,然后再用双指针来判断值是否相等。

这种做法也是比较好写出来的。答案放在下面啦~

class Solution {
    public boolean isPalindrome(ListNode head) {
        if(head == null || head.next == null){
            return true;
        }
        ArrayList<Integer> res = new ArrayList<>();
        ListNode temp = head;
        int m = 0;
        while(temp != null){
            res.add(temp.val);
            temp = temp.next;
            m++;
        }
        while(m>0){
            if(head.val != res.get(m-1)){
                return false;
            }
            m--;
            head = head.next;
        }
        return true;
    }
}

更进阶的,我们需要考虑能否采用O(1)的空间复杂度呢?那必然是得在链表本身进行修改了!我们可以反转链表的后半段,然后再用快慢指针去遍历链表。不过需要注意的是,在实际业务中,我们是不希望链表被修改的!!!所以严谨的做法需要再判断完成之后恢复原始数据。这个因为代码比较繁琐,所以面试时可以考虑优先写上面的解法~

class Solution {
    public boolean isPalindrome(ListNode head) {
        if (head == null) {
            return true;
        }

        // 找到前半部分链表的尾节点并反转后半部分链表
        ListNode firstHalfEnd = endOfFirstHalf(head);
        ListNode secondHalfStart = reverseList(firstHalfEnd.next);

        // 判断是否回文
        ListNode p1 = head;
        ListNode p2 = secondHalfStart;
        boolean result = true;
        while (result && p2 != null) {
            if (p1.val != p2.val) {
                result = false;
            }
            p1 = p1.next;
            p2 = p2.next;
        }        

        // 还原链表并返回结果
        firstHalfEnd.next = reverseList(secondHalfStart);
        return result;
    }

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

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

(注:上面的解法中包含了反转链表的写法,所以反转链表那道题就不再阐述了)

3.环形链表(难度等级:简单)

做这道题呢,首先需要有一个常识:若有环存在的话,一个快指针和一个慢指针会在环中相遇(类似于运动会中的“套圈”);如果没有环存在的话,快指针会移动到null。

public class Solution {
    public boolean hasCycle(ListNode head) {
        if (head == null || head.next == null) {
            return false;
        }
        ListNode slow = head;
        ListNode fast = head.next.next;
        while (slow != fast) {
            if (fast == null || fast.next == null) {
                return false;
            }
            slow = slow.next;
            fast = fast.next.next;
        }
        return true;
    }
}

4.环形链表 II(难度等级:中等)

区别于上一道题,这道题开始要求返回入环的节点了噢!

那么,我们就需要用数学去分析,如何表示入环节点了!数学推导可以得出结论,但我们只需记住:当发现 slow 与 fast 相遇时,我们再额外使用一个指针 ptr。起始,它指向链表头部;随后,它和 slow 每次向后移动一个位置。最终,它们会在入环点相遇。

public class Solution {
    public ListNode detectCycle(ListNode head) {
        if(head == null) return null;
        ListNode slow = head, fast = head;
        while(fast != null){
            slow = slow.next;
            if(fast.next != null){
                fast = fast.next.next;
            }else{
                return null;
            }
            if(slow == fast){
                fast = head;
                while(slow != fast){
                    slow = slow.next;
                    fast = fast.next;
                }
                return slow;
            }
        }
        return null;                                                                                                  
    }
}

5.合并两个有序链表(难度等级:简单)

其实这一题,就很像数组里的“合并2个数组”了,自然而然地我们就能想到用双指针法。只不过这里,我们还要额外注意一下如何操作链表。

class Solution {
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        if(list1 == null || list2 == null) return list1 == null ? list2 : list1;
        ListNode res = new ListNode();
        ListNode temp = res;
        while(list2 != null || list1 != null){
            if(list1 == null){
                temp.next = list2;
                break;
            }
            if(list2 == null){
                temp.next = list1;
                break;
            }
            if(list1.val <= list2.val){
                temp.next = list1;
                temp = temp.next;
                list1 = list1.next;
            }else{
                temp.next = list2;
                temp = temp.next;
                list2 = list2.next;
            }
        }
        return res.next;
    }
}

6.两数相加(难度等级:中)

这道题的思路还是很好想的,跟上一道题很类似。但是我在动手写代码的时候,就发现不是这么回事了,譬如 创造了多余节点(即创造新节点的时机不对)、以为两个链表都走到最末就结束了(其实还要考虑最后一位节点相加是否产生进位,如果有还要再多创一个节点),以及写代码时考虑不周的问题!(括号有没有加,有没有考虑null值情况)

我想,这道题难度设置为中,也有上面因素的影响吧(hhh

话不多说,先附上我的垃圾解法:

class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        ListNode res = null;
        ListNode temp = res;
        int add = 0, sum = 0;
        while(l1 != null || l2 != null){
            //如果上一个节点有进位,则需要加上进位
            if(l1 == null || l2 == null) {
                sum = (l1 == null ? l2.val : l1.val) + add;
            }else{
                sum = l1.val + l2.val + add;
            }
            //当前节点的加法和
            int num = sum % 10;
            if(temp == null){
                res = temp = new ListNode(num);
            }else{
                temp.next = new ListNode(num);
                temp = temp.next;
            }
            //获得当前加法对下一节点的进位
            add = sum / 10;                  
            if (l1 != null) {
                l1 = l1.next;
            }
            if (l2 != null) {
                l2 = l2.next;
            }
        }
        //最后还要判断有没有上一个节点的进位来构成新的节点
        if(add > 0) temp.next = new ListNode(add);
        return res;
    }
}

再看leetcode官方题解,你会发现逻辑一样,但它比我的优雅多了…

class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        ListNode head = null, tail = null;
        int carry = 0;
        while (l1 != null || l2 != null) {
            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;
    }
}

7. 删除链表的倒数第 N 个结点(难度等级:中)

这道题...本人今天貌似在xhs上面经刚见过。这种处理链表 倒数第n个节点 的问题呀,其实用快慢指针就可以解决——让快指针先走n步,随后快慢指针同时向前移动,当快指针移动到最后时,慢指针就移动到倒数第n个节点啦!

嘿!我心想这不是很简单嘛~说来就来,结果在敲代码过程中就发现考虑还是不周全了...

首先,需要删除倒数第n个节点,那么对于链表的操作来说,我们需要慢指针指向倒数第n+1个节点,才能进行删除。其次,如果需要删除的是头节点该怎么办呢?

这时候,我死去的回忆开始攻击我了:需要考虑删除头节点的情况???那造一个虚拟头节点不就完事了!于是,我的代码最终如下:

class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        if(head == null || head.next == null) return null;
        ListNode fast = new ListNode();
        fast.next = head;
        ListNode slow = fast;
        //需要创造一个虚拟头节点,以防止原头节点被删除!
        ListNode pre = fast;
        while(n>0){
            fast = fast.next;
            n--;
        }
        //获取需要删除的第n个节点的前一个节点
        while(fast != null && fast.next != null){
            fast = fast.next;
            slow = slow.next;
        }
        if(slow.next != null){
            slow.next = slow.next.next;
        }else{
            slow.next = null;
        }
        
        return pre.next;
    }
}

8.两两交换链表中的节点(难度等级:中)

这道题我原始思路是用迭代法做的,但做着做着发现不对劲:这好像不是单纯的2个2个节点单独交换了,还要考虑前后2个节点整体的连接关系。

然后我就突然脑洞大发——咦?操作都是一样的,那岂不是可以用递归啊!

于是我先尝试了递归,事实证明,递归的代码真的很短(但是空间复杂度会高于迭代法)

class Solution {
    public ListNode swapPairs(ListNode head) {
        //终止条件
        if(head == null || head.next == null) {
            return head;
        }
        //获取当前节点的下一个节点
        ListNode nextHead = head.next;
        //交换操作
        head.next = swapPairs(nextHead.next);
        nextHead.next = head;
        
        return nextHead;
    }
}

下面是leetcode官方的迭代法答案:

class Solution {
    public ListNode swapPairs(ListNode head) {
        ListNode dummyHead = new ListNode(0);
        dummyHead.next = head;
        ListNode temp = dummyHead;
        while (temp.next != null && temp.next.next != null) {
            ListNode node1 = temp.next;
            ListNode node2 = temp.next.next;
            temp.next = node2;
            node1.next = node2.next;
            node2.next = node1;
            temp = node1;
        }
        return dummyHead.next;
    }
}

9.K 个一组翻转链表(难度等级:困难)

第一道困难题诞生了!看起来,它是上一道题的进阶版,延续上一题的思路试试看吧

需要注意的是,在翻转子链表的时候,我们不仅需要子链表头节点 head,还需要有 head 的上一个节点 pre,以便翻转完后把子链表再接回 pre

其实思路真的不难,但是代码会比较长,面试的时候非常容易出错!所以还是要多练练的~

class Solution {
    public ListNode reverseKGroup(ListNode head, int k) {
        ListNode dump = new ListNode();
        dump.next = head;
        ListNode pre = dump;
        while(head != null){
            ListNode temp = pre;
            //查看剩余节点是否小于长度k
            for(int i=0; i<k; i++){
                temp = temp.next;
                if(temp == null){
                    return dump.next;
                }
            }
            //不小于长度K,则开始操作
            ListNode nextHead = temp.next; //下一组k节点的头
            //操作当前k个节点
            ListNode[] result = reverse(head, temp);
            head = result[0];
            temp = result[1];
            //拼接回原链表
            pre.next = head;
            temp.next = nextHead;
            //移动指针指向下k个待翻转节点
            pre = temp;
            head = temp.next;
        }
        return dump.next;
    }
    public ListNode[] reverse(ListNode head, ListNode tail){
        ListNode prev = tail.next;
        ListNode p = head;
        while (prev != tail) {
            ListNode nex = p.next;
            p.next = prev;
            prev = p;
            p = nex;
        }
        return new ListNode[]{tail, head};
    }
}

10.随机链表的复制(难度等级:中等)

唉,到这道题就不再是普通链表了噢,而是自定义的一种新的链式结构。不过,除了多了一个指针以外,好像也没有什么区别呢?那复制这个链表与复制普通链表也应该是差不多的处理逻辑吧?

but我在动手写的过程中就发现问题了!新链表的random可不能随便指呀,如果下一个random已经存在,就不能再创建新节点再指向了

OK这道题我是真没思路,让我们看看力扣官方的思路吧(记住它):

遇到没思路的题时,首先看题解,看完自己动手实现!如果实在写不出来,再copy代码噢

class Solution {
    public Node copyRandomList(Node head) {
        if (head == null) {
            return null;
        }
        for (Node node = head; node != null; node = node.next.next) {
            Node nodeNew = new Node(node.val);
            nodeNew.next = node.next;
            node.next = nodeNew;
        }
        for (Node node = head; node != null; node = node.next.next) {
            Node nodeNew = node.next;
            nodeNew.random = (node.random != null) ? node.random.next : null;
        }
        Node headNew = head.next;
        for (Node node = head; node != null; node = node.next) {
            Node nodeNew = node.next;
            node.next = node.next.next;
            nodeNew.next = (nodeNew.next != null) ? nodeNew.next.next : null;
        }
        return headNew;
    }
}

11.排序链表(难度等级:中等)

对不起,看到这题,脑袋空空的我只会想到先遍历一遍将结果放进ArrayList,然后调用API将数组排成升序,最后构造新的链表。

但是考虑空间复杂度和时间复杂度的情况下,这肯定不是最优解啊!sort排序的复杂度就很高了...

ok又是没有思路的一题(这真的是中等难度嘛?!),不过当我看到答案的时候,好像有点get到意思了。其实,这一题属于排序问题,那自然而然就回想到回溯了呀!所以,用递归二分的方式,先“剪断”链表,直至拆分到只有<=2个节点相连。这个时候,就相当于只用处理2个节点的排序了!然后,我们再慢慢合并。这里需要注意的是!合并的时候,2组链表并不是随便连的噢,还要大小情况。最后还有一点也需要注意,不要忘记处理链表节点为单数的情况!

代码放在下面了,要好好品味呀~

class Solution {
    public ListNode sortList(ListNode head) {
        if(head == null || head.next == null){
            return null;
        }
        //二分分割
        ListNode fast = head;
        ListNode slow = head;
        while(fast != null){
            fast = fast.next.next;
            slow = slow.next;
        }
        ListNode nextHead = slow.next;
        slow.next = null;
        //递归
        ListNode left = sortList(head);
        ListNode right = sortList(nextHead);
        //创造一个虚拟头节点
        ListNode temp = new ListNode();
        ListNode res = temp;
        //合并节点
        while(left != null && right != null){
            if(left.val < right.val){
                temp.next = left;
                left = left.next;
            }else{
                temp.next = right;
                right = right.next;
            }
            temp = temp.next;
        }
        //最后还要考虑最末节点(节点数为奇数的情况)
        temp.next = left != null ? left : right;
        return res.next;
    }
}

12.合并 K 个升序链表(难度等级:困难)

困难题它又向我走来了啊啊啊

这里,需要用到的是分而治之的思想。其本质也是把一个复杂问题划分成多个重复操作的子问题,那么对于这道题,最小子问题应该就是2个有序链表的合并啦。这道题,我们再次用到了递归的思想,但是逻辑比较绕,要多看多练!

class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        //递归终止条件
        if(lists == null || lists.length == 0){
            return null;
        }
        return merge(lists, 0, lists.length-1);
    }
    private ListNode merge(ListNode[] lists, int left, int right){
        if(left == right) return lists[left];
        int mid = left + (right - left) / 2; //下一次要合并的链表
        //分而治之
        ListNode l1 = merge(lists, left, mid);
        ListNode l2 = merge(lists, mid+1, right);

        //直到只有2组链表需要合并,返回合并结果
        return mergeTwoLists(l1, l2);
    }
    //两个链表的合并
    private ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        if (l1 == null) return l2;
        if (l2 == null) return l1;
        if (l1.val < l2.val) {
            l1.next = mergeTwoLists(l1.next, l2);
            return l1;
        } else {
            l2.next = mergeTwoLists(l1,l2.next);
            return l2;
        }
    }
}

13.LRU 缓存(难度等级:中等)

最近看很多大厂提前批的面经,都有这道题的出现,大家要格外注意噢~

emm很多弯弯绕绕没明白,但首先要记住,这里采用的数据结构是双向链表 + HashMap噢~很多面试官可能会要求手动封装一个双向链表,所以代码里我自己写了一个。HashMap是用于维护每个节点(data)的访问频率,双向链表是用于根据访问频率来维护缓存的(将访问频率高的节点放在链表头部)

至于为什么要用双向链表而不是单向链表,看到一个回答是这样解释的:

将某个节点移动到链表头部或者将链表尾部节点删去,都要用到删除链表中某个节点这个操作。你想要删除链表中的某个节点,需要找到该节点的前驱节点和后继节点。对于寻找后继节点,单向链表和双向链表都能通过 next 指针在O(1)时间内完成;对于寻找前驱节点,单向链表需要从头开始找,也就是要O(n)时间,双向链表可以通过前向指针直接找到,需要O(1)时间。

 记住了数据结构,在实现过程中还要注意类的常量定义以及构造函数的写法(真的没有那么容易!多写写吧)

class LRUCache {
    //双向链表
    class DlistNode{
        int key;
        int value;
        DlistNode next;
        DlistNode prev;
        public DlistNode(){}
        public DlistNode(int _key, int _value) {
            key = _key; value = _value;
        }
    }

    private Map<Integer, DlistNode> freq = new HashMap<>();
    private int size;
    private int capacity;
    private DlistNode head, tail;

    public LRUCache(int capacity) {
        this.size = 0; //当前存储量
        this.capacity = capacity; //最大容量
        //使用伪头部和伪尾部节点
        head = new DlistNode();
        tail = new DlistNode();
        head.next = tail;
        tail.prev = head;
    }
    
    public int get(int key) {
        DlistNode res = freq.get(key);
        if(res == null){
            return -1;
        }else{
            moveToHead(res); //移动节点位置
            return res.value;
        }
    }
    
    public void put(int key, int value) {
        DlistNode node = freq.get(key); //判断是否已经存在
        if(node == null){ //存入链表
            DlistNode newNode = new DlistNode(key, value);
            freq.put(key, newNode);
            addToHead(newNode); //放到链表的头部
            size++;
            if(size > capacity){
                DlistNode tail = removeTail();
                freq.remove(tail.key);
                size--;
            }
        }else{
            node.value = value;
            moveToHead(node);
        }
    }

    private void addToHead(DlistNode node){
        node.prev = head; //头节点
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
    }

    private void removeNode(DlistNode node){
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }

    private void moveToHead(DlistNode node){
        removeNode(node);
        addToHead(node);
    }
    private DlistNode removeTail(){
        DlistNode res = tail.prev;
        removeNode(res);
        return res;
    }
}

好了,链表终于完结撒花啦!总的来说,首先,你要知道链表的概念,以及如何操作链表(增删改)。其次,双指针(快慢指针等)是链表中非常常用的思想!还有一些问题需要一些小巧思,例如4、10。剩下的题,就是经常会涉及到递归思想的啦~所以,要学会将困难问题拆解,这在算法思想里一直都很重要!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值