【没事儿看两道Leetcode系列】100热题之链表

链表

很久没做了,再上手发现链表部分还是很熟的,居然所有的都做出来了,也可能是简单吧。

链表部分我觉得的一些重点:

  1. 注意添加一个 dummyNode 的做法。有的时候头节点可能不太方便,需要额外考虑,这个时候可以创建一个 dummyNode 指向头节点,作为一个空头节点。这样写代码的时候不用特殊处理头节点情况会比较方便。不过最后返回链表的时候,不要忘记返回 dummyNode.next
  2. 链表中删除元素的方法要比较熟悉。保存 prev curr next 三个节点,如果要删除 curr 的话,让 prev.next = next。不保存前后节点,单链表实现就比较麻烦。
  3. 快慢指针在找链表中的一些结构的时候会非常有用,比如环,链表相交,通过快慢指针的不同起点或者速度让它们在我们想要的位置相遇。
  4. 链表反转也要掌握,也是保存 prev curr next 三个节点.

160. 相交链表

判断两链表是否相交。

方便的做法自然是弄一个 HashSet 存储所有节点,然后同时遍历两个链表,如果发现有一个节点无法存入 HashSet 已经存在,这个节点就是相交的起始点。不过这个方法时空复杂度都不是特别好。

第二种做法就是双指针,感觉链表很多题都可以双指针解决。第一次我们遍历两个链表,并记下两个链表到达结尾的总长度,比如上图中一个是 5 一个是 6.那么我们可以判断出,B 链表长于 A 链表的部分肯定不会是相交的起始点出现的地方。因为两个链表相交之后的部分长度相等。所以我们可以再次遍历两个链表,B 链表从 b2 开始遍历,A 链表从 a1 开始遍历,两个指针每次同时往后挪动一个单位后判断是否相等。也就是说把长链表过长的那部分去掉了,变成一个和短链表等长的链表,然后两个链表比对到底是在什么地方最先出现的相交点。

时间复杂度 O(n) 空间复杂度 O(1)。

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        int lenA = 0, lenB = 0;
        ListNode tempA = headA, tempB = headB;
        while(tempA != null){
            lenA++;
            tempA = tempA.next;
        }
        while(tempB != null){
            lenB++;
            tempB = tempB.next;
        }

        ListNode longerListNode = lenA > lenB? headA : headB;
        ListNode shorterListNode = lenA > lenB? headB : headA;

        for(int i =0;i<Math.abs(lenA-lenB);i++)longerListNode = longerListNode.next;
        while(longerListNode != null){
            if(longerListNode == shorterListNode)return longerListNode;
            longerListNode = longerListNode.next;
            shorterListNode = shorterListNode.next;
        }
        return null;
    }
}

206. 反转链表

不限制空间复杂度的前提下,这道题还是非常好做的。我们一边读取原链表,一边创造一个新链表,不过新链表不是从头到尾逐渐添加元素的,而是每次在头的位置添加一个新的头元素,从尾到头添加。时空复杂度均为 O(N)。

如果要求空间复杂度 O(1) 就是在原来的链表基础上反转了,从头遍历地柜实现。主要难点就是需要 prev curr next 三个指针,每次遍历的时候 curr->next = prev, curr = next, prev = curr。不用额外变量保存前后节点就会更复杂。

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

234. 回文链表

判断一个链表是否回文。

不限制空间复杂度的话也很简单,可以创建个集合记录一下顺序,然后再次遍历看集合出栈顺序和原链表遍历顺序是否一致。都是 O(N)。

限制空间复杂度常数的话,可以用快慢指针实现。慢指针移动速度是快指针的二分之一,这样快指针走完了慢指针才刚走到链表中间。然后再创建一个慢指针2号从头遍历,慢指针1号反转后半部分链表,反转完成后慢指针1号和2号同步遍历看前后两部分链表是否构成回文。

用到了之前反转链表的思想。

class Solution {
    public boolean isPalindrome(ListNode head) {
        ListNode fastPtr = head, slowPtr = head, prev =new ListNode(-1, head);
        while (fastPtr != null && fastPtr.next != null) {
            fastPtr = fastPtr.next.next;
            slowPtr = slowPtr.next;
            prev = prev.next;
        }

        ListNode cur = slowPtr, tmp = slowPtr.next;
        // 截断前半部分和后半部分数组。不然的话就是要从 head 比较到 tail 了,我这样截断之后就从 head 比较到 mid 就行。
        // 不过这道题最大长度也就是9,没感觉出来太多区别。
        prev.next = null;
        prev = null;

        while (cur != null) {
            tmp = cur.next;
            cur.next = prev;
            prev = cur;
            cur = tmp;
        }
        while (head != null && prev != null) {
            if (head.val != prev.val)
                return false;
            head = head.next;
            prev = prev.next;
        }
        return true;
    }
}

慢指针1号在快指针到达结尾的时候刚好会到达后半部分回文子串的开始。

141. 环形链表

判断链表中是否有环。

非常经典的题了,快慢指针,快指针走得比慢指针快一倍,看会不会相遇。如果会,说明有环。

public class Solution {
    public boolean hasCycle(ListNode head) {
        ListNode slowPtr = head, fastPtr = head;
        while (fastPtr != null && fastPtr.next != null) {
            fastPtr = fastPtr.next.next;
            slowPtr = slowPtr.next;
            if (fastPtr == slowPtr) {
                return true;
            }
        }
        return false;
    }
}

142. 环形链表 II

简单说就是不仅要判断是否为环形链表,还要找到入环点。

不考虑空间复杂度的话,还是弄个 Set 存点,第一个存不进的重复元素就是入环点了呗。

考虑空间复杂度的话,这道题会有一个比较有意思的解法。

官方题解图示如下:

首先我们还是快慢指针遍历。两者相遇的时候,慢指针走了 a+b,快指针走了 a+b+n(b+c) (多走了 n 圈)。

我们要求 a:
2x=a+b+n(b+c)x=a+ba=(n−1)b+nc 2x = a+b+n(b+c)\\ x = a+b\\ a = (n-1)b+nc 2x=a+b+n(b+c)x=a+ba=(n1)b+nc
也就是说 a 部分的路程相当于 n 圈的路程 -c。

那么我们就让一个指针从紫色的快慢指针相遇点开始转圈,一个指针从原点出发。那么两者就一定会在入环点相遇。原点出发的点走了距离 a 到达了入环点,绕圈的点走了 n 圈+c,不过绕圈点本身起点就是距离入环点 c 距离的位置,所以这个时候也会在入环点。

这样我们两次遍历就找到入环点了,时间复杂度 O(N)。

public class Solution {
    public ListNode detectCycle(ListNode head) {
        // a: 环前的元素
        // loop: 环长度
        // x: slowPtr 走过的距离
        // b: slowPtr 在环中走过的距离
        // x = a + b
        // 2*x = a + n*(b+c) + b
        // loop = b+c
        // x = n*(b+c) 
        // a = (n-1)*b + n*c = (n-1) * (b+c) + c = (n-1)*b+n*c
        ListNode slowPtr = head, fastPtr = head;
        while(fastPtr != null && fastPtr.next != null){
            fastPtr = fastPtr.next.next;
            slowPtr = slowPtr.next;
            if(slowPtr == fastPtr)break;
        }
        if(fastPtr == null || fastPtr.next == null)return null;
        ListNode slowPtr1 = head;
        while(slowPtr1 != slowPtr){slowPtr = slowPtr.next; slowPtr1 = slowPtr1.next;}
        return slowPtr1;
    }
}

21. 合并两个有序链表

没啥难度,就双指针遍历嘛,创建一个新链表作为输出结果,或者直接修改原来链表元素。

这个题主要是给后面归并排序做铺垫呢。

class Solution {
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        if (list1 == null && list2 == null)
            return null;
        ListNode newList = new ListNode();
        ListNode pre = newList;
        while (list1 != null || list2 != null) {
            if (list1 == null || (list2 != null && list2.val <= list1.val)) {
                newList.next = list2;
                list2 = list2.next;
            } else {
                newList.next = list1;
                list1 = list1.next;
            }
            newList = newList.next;
            newList.next = null;
        }
        return pre.next;
    }
}

2. 两数相加

就是类似一个进位器机制,有一定计组基础的同学应该很容易做,就是用一个 carry 变量存储之前两次链表求和的进位。最后遍历完两个链表了别忘了再检查一下 carry 是否为空。

空间复杂度上呢,要么就自己新建个链表,要么就麻烦点挑那个更长的链表在其基础之上修改。

class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        // if(l1 == null && l2 == null)return null;
        ListNode preListNode = new ListNode();
        ListNode tmp = preListNode;
        int carry = 0;
        while (l1 != null || l2 != null) {
            if (l1 == null) {
                tmp.next = new ListNode((carry + l2.val) % 10);
                carry = (carry + l2.val) / 10;
                l2 = l2.next;
            } else if (l2 == null) {
                tmp.next = new ListNode((carry + l1.val) % 10);
                carry = (carry + l1.val) / 10;
                l1 = l1.next;
            } else {

                tmp.next = new ListNode((carry + l1.val + l2.val) % 10);
                carry = (carry + l1.val + l2.val) / 10;

                l1 = l1.next;

                l2 = l2.next;
            }
            tmp = tmp.next;
        }
        if (carry != 0) {
            tmp.next = new ListNode(carry);
        }
        return preListNode.next;
    }
}

19. 删除链表的第 n 个节点

这个仍然没啥难度,主要是给后面的难题做铺垫呢。主要还是要保存 prev 和 next 节点进行拼接。

class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode slowPtr = new ListNode(0, head), fastPtr = head;
        for (int i = 0; i < n; i++) {
            if (fastPtr == null)
                return head;
            fastPtr = fastPtr.next;
        }
        while (fastPtr != null) {
            slowPtr = slowPtr.next;
            fastPtr = fastPtr.next;
        }
        if (slowPtr.next == head) {
            return slowPtr.next.next;
        } else if (slowPtr.next != null)
            slowPtr.next = slowPtr.next.next;

        return head;
    }
}

我这里命名不好,其实应该 prev 和 curr 的。别在意。

24. 两两交换链表节点

这个其实也不难,我直接递归实现了,空间复杂度有点麻烦,迭代可能会好些。就是2个2个元素交换,然后处理其之前和之后的元素的指向拼接。

class Solution {
    public ListNode swapPairs(ListNode head) {
        if(head == null || head.next == null)return head;
        ListNode tmpHead = head.next;
        head.next = head.next.next;
        tmpHead.next = head;
        if(head.next != null){
            head.next = swapPairs(head.next);
        }
        return tmpHead;
    }
}

25. K 个一组翻转链表

这个是在之前那道题上的进阶。两两交换还很简单,多了就得用到反转链表的方法了。

反转链表,还有前后链表段处理这些前面都有出现过,没什么非常新奇的东西,就是写起来麻烦,不要犯错就行。

class Solution {
    public ListNode reverseKGroup(ListNode head, int k) {
        ListNode dummyNode = new ListNode(-1, head);
        ListNode prev = dummyNode;
        ListNode currNode = head;

        while (currNode != null) {
            for (int i = 0; i < k - 1 && currNode != null; i++) {
                currNode = currNode.next;
            }
            if (currNode == null)
                break;
            ListNode next = currNode.next;
            currNode.next = null;
            prev.next = reverseSubGroup(prev.next);
            while (prev.next != null)
                prev = prev.next;
            prev.next = next;
            currNode = next;
        }
        return dummyNode.next;
    }

    public ListNode reverseSubGroup(ListNode head) {
        if (head == null || head.next == null)
            return head;
        ListNode prev = null;
        ListNode currNode = head;
        while (currNode != null) {
            ListNode tmpNode = currNode.next;
            currNode.next = prev;
            prev = currNode;
            currNode = tmpNode;
        }
        return prev;
    }
}

138. 随机链表的复制

从这道题开始有点意思。

深拷贝一个链表比较简单,但是这个链表中还有 random,这个 random 我们不一定知道是指向链表中哪一个元素。

我想到的比较蠢的实现方法就是遍历链表去记录 random 指向的链表元素的索引,然后我在我自己深拷贝的数组里也找到对应索引的位置,用 random 指向这些位置。时间复杂度炸完了,O(n2) 了吧。

不过其实最简单的方法是哈希表映射。我用 key value 分别对应原链表的某一个节点和对应的新链表的节点。然后我发现有一个 B 节点 random 指向 A 节点,我去 map 中看看 A 节点对应的新链表中是什么节点, 发现是 A’ 节点。那我新链表中的 B’.random 就指向 A’ 节点即可。

class Solution {
    HashMap<Node, Node> map = new HashMap<>();
    public Node copyRandomList(Node head) {
        if(head == null)return null;
        Node tmpPtr = head;
        Node resPtr = new Node(1);
        Node curPtr = resPtr;
        if(map.get(tmpPtr)==null){
            curPtr.next = new Node(tmpPtr.val);
            curPtr = curPtr.next;
            map.put(tmpPtr, curPtr);
            curPtr.next = copyRandomList(tmpPtr.next);
            
            curPtr.random = copyRandomList(tmpPtr.random);
            return resPtr.next;
        }
        else {
            return map.get(tmpPtr);
        }

    }
}

148. 排序链表

题目挺简单的。

实现方法有很多,比如插入排序,每次插入一个节点;Collections.sort 帮我排序;不过题目中有提到:建议尝试 O(nlogn) 的时间复杂度,常数级空间复杂度的做法。这个数字比较眼熟,很明显是要挑战链表的归并排序。

归并排序简单说就是把链表分成两段,分别排序,然后这两段再合并起来排序。比如链表总长度16,我们自底向上归并排序,首先是16个一元元素(不用排序)。然后两两合并,变为8个2元元素,每个都要遍历2次(要合并的两个元素各访问一次)也就是遍历16次。然后两两合并,4个4元数组,遍历16次。然后合并。2个8元数组,遍历16次。最后一个16元素数组不用遍历。整个过程中需要拆分合并的次数近似等于 Log2n,所以时间复杂度总的是 O(nlogn)。

具体实现……你要说有啥难点,其实也没啥难点,都是之前的知识,比如截断后两两排序,递归排序返回头结点什么的。就是小问题一堆,写起来也麻烦

class Solution {
    public ListNode sortList(ListNode head) {
        int len = 0;
        ListNode tmpHead = head;
        ListNode dummyHead = new ListNode(-1, head);
        while (tmpHead != null) {
            tmpHead = tmpHead.next;
            len++;
        }
        for (int subLength = 1; subLength < len; subLength <<= 1) {
            // prev: 之前已经排好序的链表
            // head1 head2: 本轮循环准备归并排序的两部分链表
            // next: 之后的链表

            // 找到 prev
            ListNode prev = dummyHead, cur = dummyHead.next;

            while (cur != null) {
                // 找到 head1 head2
                ListNode head1 = cur;
                for (int i = 1; i < subLength && cur.next != null; i++)
                    cur = cur.next;
                ListNode head2 = cur.next;
                cur.next = null;

                // 找到 next
                cur = head2;
                for (int i = 1; i < subLength && cur != null && cur.next != null; i++)
                    cur = cur.next;
                ListNode next = null;
                if (cur != null) {
                    next = cur.next;
                    // 并且截断归并排序的第二个链表
                    cur.next = null;
                }

                ListNode merged = merge(head1, head2);
                prev.next = merged;
                while (prev.next != null)
                    prev = prev.next;
                cur = next;
            }
        }
        return dummyHead.next;

    }

    public ListNode merge(ListNode node1, ListNode node2) {
        ListNode dummyNode = new ListNode(-1);
        ListNode currNode = dummyNode;
        while (node1 != null && node2 != null) {
            if (node1.val < node2.val) {
                currNode.next = node1;
                node1 = node1.next;
            } else {
                currNode.next = node2;
                node2 = node2.next;
            }
            currNode = currNode.next;

        }
        if (node1 != null)
            currNode.next = node1;
        else if (node2 != null)
            currNode.next = node2;
        return dummyNode.next;
    }
}

我这个主要瓶颈在于合并之后 prev 要疯狂遍历找到这部分合并链表的结尾作为新的 prev ,除此之外符合归并排序。用递归可能会好些。

23. 合并 K 个升序列表

其实这个也是考验递归。

先写一个两链表合并的方法,然后每次放两个链表进去排序。如果是结果返回值链表每次拿一个链表集合中的元素到自己这里“插入排序”,那么时间复杂度会很高,还是两两归并效率会高很多。

class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        return mergeTwoSets(lists, 0, lists.length-1);
    }

    public ListNode mergeTwoSets(ListNode[] lists, int l, int r){
        if(l == r)return lists[l];
        else if(l > r)return null;
        int mid = (l+r)>>1;
        return mergeTwoLists(mergeTwoSets(lists, l, mid),mergeTwoSets(lists, mid+1, r));
    }

    public ListNode mergeTwoLists(ListNode head1, ListNode head2){
        ListNode dummyHead = new ListNode(-1, null);
        ListNode curNode = dummyHead;
        while(head1!=null && head2 != null){
            if(head1.val<=head2.val){
                curNode.next = head1;
                head1 = head1.next;
            }else {
                curNode.next = head2;
                head2 = head2.next;
            }
            curNode = curNode.next;
        }
        if(head1!=null){
            curNode.next = head1;
        }
        else if(head2!=null){
            curNode.next = head2;
        }
        else if(curNode!=null)curNode.next = null;
        return dummyHead.next;
    }
}

LRU 缓存

看似中等,实则真正的困难!

让我们实现一个类似操作系统中 LRU(least recently used)缓存的算法。比如缓存大小设置为10,现在存入了10个缓存了,然后又要存入一个新缓存,就要把最久没用过的那个淘汰掉。

我们可以采取类似 LinkedHashMap 的做法,既保存了顺序又可以依靠哈希表快速访问。

当用户 put 新添加一个元素进缓存的时候,我们就将其存入 HashMap 和双链表,双链表用于记载其顺序位置,头是最新存入的或者最近访问的,尾是最久没用的,HashMap 用于根据这个键值对的键快速找到对应对象的引用,用双链表实现就是头尾可以快速访问,不然我们要删除尾部元素的话每次都要 O(n) 遍历到结尾。添加完元素之后判断一下当前容量是否超出最大容量,如果超出则删除尾部元素并删除其在 HashMap 中的映射。

用户 get 一个元素的时候,先去 HashMap 里查找看有没有,快速定位,如果有则把这个元素在链表中删掉,放到头节点位置并返回 value 值给用户。如果没有直接返回 -1。缓存穿透问题无法避免吧,哈哈。必要的话可以缓存空对象,不过这里做题就不用考虑了。

用户 put 一个已经存在的键值对的时候,更新该元素,然后和 get 一样,将其从链表中间删去,放到头节点。

如下未完整代码。说实话,这个也没有什么高深的算法,都是之前我们练习过的,但是重点还是两点:1. 思想,要能够想到这种用法。2. 细节,我估计真面试遇到了我得写上30分钟都不一定纠正得了所有错误。所以还是要常练。

class LRUCache {

    class DoubleLinkedNode {
        int key;
        int value;
        DoubleLinkedNode prev;
        DoubleLinkedNode next;

        public DoubleLinkedNode() {
        }

        public DoubleLinkedNode(int _key, int _value) {
            key = _key;
            value = _value;
        }
    }

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

    public LRUCache(int capacity) {
        this.size = 0;
        this.capacity = capacity;

        head = new DoubleLinkedNode();
        tail = new DoubleLinkedNode();
        head.next = tail;
        tail.prev = head;
    }

    public int get(int key) {
        DoubleLinkedNode getNode = cache.get(key);
        if(getNode == null)return -1;
        int res = getNode.value;
        deleteNodeInCache(getNode);
        updateHeadNodeInCache(getNode);
        return res;
    }

    public void put(int key, int value) {
        
        DoubleLinkedNode getNode = cache.get(key);
        if(getNode == null){
            DoubleLinkedNode newHeadNode = new DoubleLinkedNode(key, value);
            cache.put(key, newHeadNode);
            size++;
            if(size>capacity){
                int lastKey = tail.prev.key;
                deleteLastNodeInCache();
                cache.remove(lastKey);
                size--;
            }
            
            updateHeadNodeInCache(newHeadNode);
        }
        else {
            getNode.value = value;
            deleteNodeInCache(getNode);
            updateHeadNodeInCache(getNode);

        }
    }

    private void deleteNodeInCache(DoubleLinkedNode node){
        DoubleLinkedNode prev = node.prev;
        DoubleLinkedNode next = node.next;
        prev.next = next;
        next.prev = prev;
    }

    private void updateHeadNodeInCache(DoubleLinkedNode node){
        DoubleLinkedNode oldFirst = head.next;
        oldFirst.prev = node;
        node.prev = head;
        node.next = oldFirst;
        head.next = node;
    }
    private void deleteLastNodeInCache(){
        deleteNodeInCache(tail.prev);
    }
}

/**
 * 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);
 */
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

灰海宽松

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值