LeetCode 面试经典 150 题-队列 快慢指针 定义快慢指针,若两指针最后相遇则有环,否则快指针会指向空。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public class Solution { public boolean hasCycle (ListNode head) { if (head == null || head.next == null ) { return false ; } ListNode slow = head; ListNode fast = head.next; while (slow != fast) { slow = slow.next; if (fast == null || fast.next == null ) { return false ; } fast = fast.next.next; } return true ; } }
快慢指针 找到环形入口点。假设入环点距链表头为a,相遇点距入环点为b,相遇点到最后是c,则有快指针走了a + n(b+c) + b
,慢指针走了a + b
,快指针走的是慢指针的两倍,即a + n(n+c) + b = 2(a + b)
,即a = c + (n-1)(b+c)
。即从链表头到入环点的距离等于从相遇点出发走了n-1次环的距离,即如果定义一个指针从链表头出发,一个指针从相遇点出发,每次都向后走一步,他们最终会相遇,相遇的地点就是入环点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public class Solution { public ListNode detectCycle (ListNode head) { if (head == null || head.next == null ) { return null ; } ListNode slow = head; ListNode fast = head; while (fast != null && fast.next != null ) { slow = slow.next; fast = fast.next.next; if (slow == fast) { ListNode ptr = head; while (ptr != slow) { ptr = ptr.next; slow = slow.next; } return ptr; } } return null ; } }
时间复杂度:O(n),在最初判断快慢指针是否相遇时,slow 指针走过的距离不会超过链表的总长度;随后寻找入环点时,走过的距离也不会超过链表的总长度。
空间复杂度:O(1)
递归 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 class Solution { public ListNode addTwoNumbers (ListNode l1, ListNode l2) { return addTwo(l1, l2, 0 ); } private ListNode addTwo (ListNode l1, ListNode l2, int carry) { if (l1 == null && l2 == null ) { return carry != 0 ? new ListNode (carry) : null ; } if (l1 == null ) { l1 = l2; l2 = null ; } int sum = carry + l1.val + (l2 != null ? l2.val : 0 ); l1.val = sum % 10 ; carry = sum / 10 ; l1.next = addTwo(l1.next, (l2 != null ? l2.next : null ), carry); return l1; } }
迭代 初始化答案为一个「空链表」,每次循环,向该链表末尾添加一个节点(保存一个数位)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class Solution { public ListNode addTwoNumbers (ListNode l1, ListNode l2) { ListNode dummy = new ListNode (); ListNode cur = dummy; int carry = 0 ; while (l1 != null || l2 != null || carry != 0 ) { if (l1 != null ) { carry += l1.val; l1 = l1.next; } if (l2 != null ) { carry += l2.val; l2 = l2.next; } ListNode node = new ListNode (carry % 10 ); cur.next = node; cur = cur.next; carry /= 10 ; } return dummy.next; } }
迭代 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 class Solution { public ListNode mergeTwoLists (ListNode list1, ListNode list2) { ListNode dummy = new ListNode (); ListNode cur = dummy; while (list1 != null && list2 != null ) { if (list1.val < list2.val) { cur.next = list1; list1 = list1.next; } else { cur.next = list2; list2 = list2.next; } cur = cur.next; } cur.next = list1 != null ? list1 : list2; return dummy.next; } }
时间复杂度:O(m + n)
空间复杂度:O(1)
递归 递归边界:如果其中一个链表为空,直接返回另一个链表作为合并后的结果。
如果两个链表都不为空,则比较两个链表当前节点的值,并选择较小的节点作为新链表的当前节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Solution { 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; } list2.next = mergeTwoLists(list1, list2.next); return list2; } }
时间复杂度:O(m + n)
空间复杂度:O(m + n)
回溯+哈希表 定义一个哈希表,用于存储节点的副本,如果这个节点已经存在,就直接返回,如果不存在,就复制一份,然后递归复制它的next和random。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 class Solution { Map<Node, Node> hash = new HashMap <>(); public Node copyRandomList (Node head) { if (head == null ) { return head; } if (!hash.containsKey(head)) { Node newNode = new Node (head.val); hash.put(head, newNode); newNode.next = copyRandomList(head.next); newNode.random = copyRandomList(head.random); } return hash.get(head); } }
迭代
将所有节点都复制一份,链接到原节点后面。
复制节点的random就是原节点的random的next。
分离链表。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 class Solution { public Node copyRandomList (Node head) { if (head == null ) { return head; } for (Node cur = head; cur != null ; cur = cur.next.next) { cur.next = new Node (cur.val, cur.next, null ); } for (Node cur = head; cur != null ; cur = cur.next.next) { if (cur.random != null ) { cur.next.random = cur.random.next; } } Node newHead = head.next; for (Node cur = head; cur != null ; cur = cur.next) { Node copy = cur.next; cur.next = cur.next.next; copy.next = copy.next != null ? copy.next.next : null ; } return newHead; } }
时间复杂度:O(n)
空间复杂度:O(1),返回值不计入。
一次遍历 定义指针:
cur:当前待反转节点;
next:待反转节点的下一个节点;
pre:待反转区域的前一个节点;
将cur.next指向next.next;
将next.next指向pre.next;
将pre.next指向next。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class Solution { public ListNode reverseBetween (ListNode head, int left, int right) { ListNode dummy = new ListNode (0 , head); int i = 1 ; ListNode pre= dummy; while (i < left) { pre = pre.next; i++; } ListNode cur = pre.next; while (i < right) { ListNode next = cur.next; cur.next = next.next; next.next = pre.next; pre.next = next; i++; } return dummy.next; } }
灵神做法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 class Solution { public ListNode reverseBetween (ListNode head, int left, int right) { ListNode dummy = new ListNode (0 , head); int i = 1 ; ListNode p0= dummy; while (i < left) { p0 = p0.next; i++; } ListNode pre = null ; ListNode cur = p0.next; while (i < right) { ListNode next = cur.next; cur.next = pre; pre = cur; cur = next; i++; } p0.next.next = cur; p0.next = pre; return dummy.next; } }
分析
先得到链表的长度n,即需要翻转n/k组。
p0指向待反转组的前一个节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 class Solution { public ListNode reverseKGroup (ListNode head, int k) { int n = 0 ; for (ListNode cur = head; cur != null ; cur = cur.next) { n++; } ListNode dummy = new ListNode (0 , head); ListNode p0 = dummy; ListNode pre = null ; ListNode cur = head; for (; n >= k; n -= k) { for (int i = 0 ; i < k; i++) { ListNode nxt = cur.next; cur.next = pre; pre = cur; cur = nxt; } ListNode nxt = p0.next; p0.next.next = cur; p0.next = pre; p0 = nxt; } return dummy.next; } }
双指针 快指针先走n步,然后慢指针从哑节点开始和快指针一起走到末尾,此时慢指针指向的就是第len-n个结点。但是要删除倒数第n个节点,需要让slow指向待删除结点的前一个结点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 class Solution { public ListNode removeNthFromEnd (ListNode head, int n) { ListNode dummy = new ListNode (0 , head); ListNode fast = dummy; for (int i = 0 ; i < n; i++) { fast = fast.next; } ListNode slow = dummy; while (fast.next != null ) { slow = slow.next; fast = fast.next; } slow.next = slow.next.next; return dummy.next; } }
分析
定义哑节点;
遍历,初始化cur = dummy:
cur.next.val == cur.next.next.val:开启循环,删除所有重复的节点;
否则指向下一个即可。
返回dummy.next。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class Solution { public ListNode deleteDuplicates (ListNode head) { ListNode dummy = new ListNode (0 , head); ListNode cur = dummy; while (cur.next != null && cur.next.next != null ) { if (cur.next.val == cur.next.next.val){ int val = cur.next.val; while (cur.next != null && cur.next.val == val) { cur.next = cur.next.next; } } else { cur = cur.next; } } return dummy.next; } }
双指针 可以利用求倒数第n个结点的思想,找到倒数第k个结点,断开链表,将后端链表链接到开头。
求出链表长度n,然后找倒数第k%n个结点。
断开链接;
后段链表的最后一个节点指向链表头。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 class Solution { public ListNode rotateRight (ListNode head, int k) { if (head == null ) { return head; } int n = 0 ; for (ListNode cur = head; cur != null ; cur = cur.next) { n++; } k = k % n; if (k == 0 ) { return head; } ListNode fast = head; for (int i = 0 ; i < k; i++) { fast = fast.next; } ListNode slow = head; while (fast.next != null ) { slow = slow.next; fast = fast.next; } ListNode back = slow.next; slow.next = null ; fast.next = head; return back; } }
模拟
使用两个链表small和large,并定义哑节点指向它们;
遍历链表,小于x的链接到small,大于等于x的链接到large;
small链接到largeHead.next。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 class Solution { public ListNode partition (ListNode head, int x) { ListNode smallHead = new ListNode (); ListNode largeHead = new ListNode (); ListNode small = smallHead; ListNode large = largeHead; for (ListNode cur = head; cur != null ; cur = cur.next) { if (cur.val < x) { small.next = cur; small = small.next; } else { large.next = cur; large = large.next; } } large.next = null ; small.next = largeHead.next; return smallHead.next; } }
注意最后要让large.next置空,否则会让链表出现环路。
双向循环链表 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 class LRUCache { class DLinkedNode { int key; int value; DLinkedNode prev; DLinkedNode next; public DLinkedNode () {} public DLinkedNode (int _key, int _value) { key = _key; value = _value; } } private int capacity; private DLinkedNode dummy; private Map<Integer, DLinkedNode> cache = new HashMap <>(); public LRUCache (int capacity) { dummy = new DLinkedNode (); dummy.prev = dummy; dummy.next = dummy; this .capacity = capacity; } public int get (int key) { DLinkedNode node = getNode(key); return node == null ? -1 : node.value; } public void put (int key, int value) { DLinkedNode node = getNode(key); if (node != null ) { node.value = value; } else { node = new DLinkedNode (key, value); pushFront(node); cache.put(key, node); if (cache.size() > capacity) { cache.remove(dummy.prev.key); remove(dummy.prev); } } } private DLinkedNode getNode (int key) { if (cache.containsKey(key)) { DLinkedNode node = cache.get(key); remove(node); pushFront(node); return cache.get(key); } else { return null ; } } private void remove (DLinkedNode x) { x.prev.next = x.next; x.next.prev = x.prev; } private void pushFront (DLinkedNode x) { x.prev = dummy; x.next = dummy.next; x.prev.next = x; x.next.prev = x; } }
时间复杂度:O(1)
空间复杂度:O(min(p,capacity)),其中 p 为 put 的调用次数。
双向链表 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 class LFUCache { class DLinkedNode { int key; int value; int freq = 1 ; DLinkedNode prev; DLinkedNode next; public DLinkedNode () {} public DLinkedNode (int _key, int _value) { key = _key; value = _value; } } private int capacity; private Map<Integer, DLinkedNode> keyToNode = new HashMap <>(); private Map<Integer, DLinkedNode> freqToDummy = new HashMap <>(); private int minFreq; public LFUCache (int capacity) { this .capacity = capacity; } public int get (int key) { DLinkedNode node = getNode(key); return node == null ? -1 : node.value; } public void put (int key, int value) { DLinkedNode node = getNode(key); if (node != null ) { node.value = value; return ; } if (keyToNode.size() == capacity) { DLinkedNode dummy = freqToDummy.get(minFreq); DLinkedNode back = dummy.prev; keyToNode.remove(back.key); remove(back); if (dummy.prev == dummy) { freqToDummy.remove(minFreq); } } node = new DLinkedNode (key, value); keyToNode.put(key, node); pushFront(1 , node); minFreq = 1 ; } private void remove (DLinkedNode x) { x.prev.next = x.next; x.next.prev = x.prev; } private DLinkedNode newList () { DLinkedNode dummy = new DLinkedNode (0 , 0 ); dummy.prev = dummy; dummy.next = dummy; return dummy; } private void pushFront (int freq, DLinkedNode x) { DLinkedNode dummy = freqToDummy.computeIfAbsent(freq, k -> newList()); x.prev = dummy; x.next = dummy.next; x.prev.next = x; x.next.prev = x; } private DLinkedNode getNode (int key) { if (!keyToNode.containsKey(key)) { return null ; } DLinkedNode node = keyToNode.get(key); remove(node); DLinkedNode dummy = freqToDummy.get(node.freq); if (dummy.prev == dummy) { freqToDummy.remove(node.freq); if (minFreq == node.freq) { minFreq++; } } pushFront(++node.freq, node); return node; } }
时间复杂度:O(1)
空间复杂度:O(min(p,capacity)),其中 p 为 put 的调用次数。
总结 简单链表的题一般与双指针有关,LRU、LFU缓存的设计利用了双向循环链表,需要多加复习。