推荐网站 Hello 算法 :学习数据结构与算法基础
代码随想录 :按照顺序刷题
本文的序号是在力扣题库中的题号
注意事项
为了避免大数越界,取中值时采用 m = i + (j - i)/2 来计算中点,而不是 m = (i + j)/2 。
递归和和迭代是编程方法,往往递归更简洁,迭代更易于理解;分治、回溯、动态规划和贪心是算法。
数组
给定一个含有 n 个正整数的数组和一个正整数 target 。
找出该数组中满足其总和大于等于 target 的长度最小的 子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度。 如果不存在符合条件的子数组,返回 0 。
示例 1:
1 2 3 输入:target = 7, nums = [2,3,1,2,4,3] 输出:2 解释:子数组 [4,3] 是该条件下的长度最小的子数组。
示例 2:
1 2 输入:target = 4, nums = [1,4,4] 输出:1
示例 3:
1 2 输入:target = 11, nums = [1,1,1,1,1,1,1,1] 输出:0
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public int minSubArrayLen (int target, int [] nums) { int result = nums.length + 1 ; int sum = 0 ; int i = 0 ; for (int j = 0 ; j < nums.length; j++) { sum += nums[j]; while (sum >= target) { result = Math.min(result, j - i + 1 ); sum -= nums[i++]; } } return result == nums.length + 1 ? 0 : result; }
链表 注意
涉及到对链表节点中的的增删改操作,使用虚拟头节点,如果只是查询,则不需使用。同时,在增删改操作中,如果使用递归,则不需要虚拟头节点,如果使用迭代,则需要。
链表问题常考虑使用双指针法来解决,例如寻找距离尾部第 K 个节点、寻找环入口、寻找公共尾部入口等。
给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。
示例 1:
1 2 输入:head = [1,2,6,3,4,5,6], val = 6 输出:[1,2,3,4,5]
示例 2:
1 2 输入:head = [], val = 1 输出:[]
示例 3:
1 2 输入:head = [7,7,7,7], val = 7 输出:[]
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 ListNode removeElements (ListNode head, int val) { if (head == null ) { return head; } head.next = removeElements(head.next, val); return head.val == val ? head.next : head; } } class Solution { public ListNode removeElements (ListNode head, int val) { ListNode dummyHead = new ListNode (0 ); dummyHead.next = head; ListNode temp = dummyHead; while (temp.next != null ) { if (temp.next.val == val) { temp.next = temp.next.next; } else { temp = temp.next; } } return dummyHead.next; } } public ListNode removeElements (ListNode head, int val) { while (head != null && head.val == val) { head = head.next; } if (head == null ) return null ; ListNode temp = head; while (temp.next != null ) { if (temp.next.val == val) { temp.next = temp.next.next; } else { temp = temp.next; } } return head; }
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
示例 1:
1 2 输入:head = [1,2,3,4,5] 输出:[5,4,3,2,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 class Solution { 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; } } class Solution { 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; } }
步骤图解 递归
以反转 1->2->3 为例:
初始状态:
递归到底:
1 2 3 1 -> 2 -> 3 -> null ↑ head (3的next是null,返回3)
回溯第一步(处理节点2):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 1 -> 2 -> 3 -> null ↑ ↑ head next head.next.next = head // 3.next = 2 1 -> 2 -> 3 -> 2 (形成环) ↑ ↑ head next head.next = null // 2.next = null 1 -> 2 -> null ← 3 ↑ head 返回 newHead = 3
回溯第二步(处理节点1):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 1 -> 2 -> null ← 3 ↑ ↑ head next head.next.next = head // 2.next = 1 1 -> 2 -> 1 (形成环) ↑ ↑ head next head.next = null // 1.next = null null ← 1 ← 2 ← 3 ↑ head 返回 newHead = 3
最终结果
1 2 null ← 1 ← 2 ← 3 (反转完成)
迭代
初始状态
1 2 3 4 5 6 7 prev = null curr = 1 next = 2 [null] [1] -> [2] -> [3] -> [4] -> [5] -> null ↑ ↑ ↑ prev curr next
第一步操作
1 2 3 4 5 6 7 8 1. 保存next (2) 2. curr.next = prev (1指向null) 3. prev = curr (prev移动到1) 4. curr = next (curr移动到2) [null] <- [1] [2] -> [3] -> [4] -> [5] -> null ↑ ↑ ↑ prev curr next
第二步操作
1 2 3 [null] <- [1] <- [2] [3] -> [4] -> [5] -> null ↑ ↑ ↑ prev curr next
继续直到完成
1 2 3 [null] <- [1] <- [2] <- [3] <- [4] <- [5] ↑ prev
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
示例 1:
1 2 输入:head = [1,2,3,4] 输出:[2,1,4,3]
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 class Solution { public ListNode swapPairs (ListNode head) { if (head == null || head.next == null ) { return head; } ListNode newHead = head.next; head.next = swapPairs(newHead.next); newHead.next = head; return newHead; } } 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; } }
步骤图解 递归
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 第1层:swapPairs(1) 第2层:swapPairs(3) 第3层:swapPairs(null) // 3.next = 4,4.next = null,所以传入的是null 第3层返回:null 第2层执行: head = 3, newHead = 4 head.next = swapPairs(4.next) = swapPairs(null) = null // 3.next = null newHead.next = head // 4.next = 3 return newHead // 返回 4 -> 3 -> null 第2层返回:4 -> 3 -> null 第1层执行: head = 1, newHead = 2 head.next = swapPairs(2.next) = swapPairs(3) 得到的 4->3->null // 1.next = 4 newHead.next = head // 2.next = 1 return newHead // 返回 2 -> 1 -> 4 -> 3 -> null
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。
示例 1:
1 2 3 4 5 输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3 输出:Intersected at '8' 解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。 从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。 在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
1 2 3 4 5 6 7 8 9 10 public class Solution { public ListNode getIntersectionNode (ListNode headA, ListNode headB) { ListNode A = headA, B = headB; while (A != B) { A = A != null ? A.next : headB; B = B != null ? B.next : headA; } return A; } }
给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
示例 1:
1 2 3 输入:head = [3,2,0,-4], pos = 1 输出:返回索引为 1 的链表节点 解释:链表中有一个环,其尾部连接到第二个节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class Solution { public ListNode detectCycle (ListNode head) { ListNode fast = head, 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 (slow != fast) { slow = slow.next; fast = fast.next; } return fast; } }
哈希表
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的 字母异位词。
示例 1:
1 2 输入: s = "anagram", t = "nagaram" 输出: true
示例 2:
1 2 输入: s = "rat", t = "car" 输出: false
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution { public boolean isAnagram (String s, String t) { int len1 = s.length(), len2 = t.length(); if (len1 != len2) return false ; HashMap<Character, Integer> dic = new HashMap <>(); for (int i = 0 ; i < len1; i++) { dic.put(s.charAt(i) , dic.getOrDefault(s.charAt(i), 0 ) + 1 ); } for (int i = 0 ; i < len2; i++) { dic.put(t.charAt(i) , dic.getOrDefault(t.charAt(i), 0 ) - 1 ); } for (int val : dic.values()) { if (val != 0 ) return false ; } return true ; } }
给定两个数组 nums1 和 nums2 ,返回 它们的 交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。
示例 1:
1 2 输入:nums1 = [1,2,2,1], nums2 = [2,2] 输出:[2]
示例 2:
1 2 3 输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4] 输出:[9,4] 解释:[4,9] 也是可通过的
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 class Solution { public int [] intersection(int [] nums1, int [] nums2) { Set<Integer> resultSet = new HashSet <>(); Set<Integer> set2 = new HashSet <>(); for (int num : nums2) { set2.add(num); } for (int num : nums1) { if (set2.contains(num)) { resultSet.add(num); } } int [] result = new int [resultSet.size()]; int index = 0 ; for (int num : resultSet) { result[index++] = num; } return result; } }
编写一个算法来判断一个数 n 是不是快乐数。
「快乐数」 定义为:
对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
如果这个过程 结果为 1,那么这个数就是快乐数。
如果 n 是 快乐数 就返回 true ;不是,则返回 false 。
示例 1:
1 2 3 4 5 6 7 输入:n = 19 输出:true 解释: 12 + 92 = 82 82 + 22 = 68 62 + 82 = 100 12 + 02 + 02 = 1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Solution { public boolean isHappy (int n) { if (n == 0 ){ return false ; } Set<Integer> res = new HashSet <>(); while (n != 1 && !res.contains(n)){ res.add(n); n = sumOfSquare(n); } return n == 1 ; } private int sumOfSquare (int n) { int temp = 0 ; while (n > 0 ){ int num = n%10 ; temp += num * num; n = n/10 ; } return temp; } }
给你四个整数数组 nums1、nums2、nums3 和 nums4 ,数组长度都是 n ,请你计算有多少个元组 (i, j, k, l) 能满足:
0 <= i, j, k, l < n
nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0
示例 1:
1 2 3 4 5 6 输入:nums1 = [1 ,2 ], nums2 = [-2 ,-1 ], nums3 = [-1 ,2 ], nums4 = [0 ,2 ] 输出:2 解释: 两个元组如下: 1. (0 , 0 , 0 , 1 ) -> nums1[0 ] + nums2[0 ] + nums3[0 ] + nums4[1 ] = 1 + (-2 ) + (-1 ) + 2 = 0 2. (1 , 1 , 0 , 0 ) -> nums1[1 ] + nums2[1 ] + nums3[0 ] + nums4[0 ] = 2 + (-1 ) + (-1 ) + 0 = 0
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution { public int fourSumCount (int [] A, int [] B, int [] C, int [] D) { Map<Integer, Integer> countAB = new HashMap <Integer, Integer>(); for (int u : A) { for (int v : B) { countAB.put(u + v, countAB.getOrDefault(u + v, 0 ) + 1 ); } } int ans = 0 ; for (int u : C) { for (int v : D) { if (countAB.containsKey(-u - v)) { ans += countAB.get(-u - v); } } } return ans; } }
步骤图解
假设:
1 2 3 4 A = [1, 2] B = [-2, -1] C = [-1, 2] D = [0, 2]
第一步结果 (A+B):
1 + (-2) = -1
1 + (-1) = 0
2 + (-2) = 0
2 + (-1) = 1
countAB = {-1:1, 0:2, 1:1}
第二步查找 (C+D = -x):
(-1,0): -( -1+0 ) = 1 → countAB[1] = 1
(-1,2): -( -1+2 ) = -1 → countAB[-1] = 1
(2,0): -( 2+0 ) = -2 → 不存在
(2,2): -( 2+2 ) = -4 → 不存在 结果 ans = 2
给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。
注意: 答案中不可以包含重复的三元组。
示例 1:
1 2 3 4 5 6 7 8 输入:nums = [-1,0,1,2,-1,-4] 输出:[[-1,-1,2],[-1,0,1]] 解释: nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。 nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。 nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。 不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。 注意,输出的顺序和三元组的顺序并不重要。
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 class Solution { public static List<List<Integer>> threeSum (int [] nums) { List<List<Integer>> ans = new ArrayList (); int len = nums.length; if (nums == null || len < 3 ) return ans; Arrays.sort(nums); for (int i = 0 ; i < len ; i++) { if (nums[i] > 0 ) break ; if (i > 0 && nums[i] == nums[i-1 ]) continue ; int L = i+1 ; int R = len-1 ; while (L < R){ int sum = nums[i] + nums[L] + nums[R]; if (sum == 0 ){ ans.add(Arrays.asList(nums[i],nums[L],nums[R])); while (L<R && nums[L] == nums[L+1 ]) L++; while (L<R && nums[R] == nums[R-1 ]) R--; L++; R--; } else if (sum < 0 ) L++; else if (sum > 0 ) R--; } } return ans; } }
给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复 的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):
0 <= a, b, c, d < n
a、b、c 和 d 互不相同
nums[a] + nums[b] + nums[c] + nums[d] == target
你可以按 任意顺序 返回答案 。
示例 1:
1 2 输入:nums = [1,0,-1,0,-2,2], target = 0 输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,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 39 40 41 class Solution { public List<List<Integer>> fourSum (int [] nums, int target) { List<List<Integer>> ans = new ArrayList (); int len = nums.length; if (nums == null || len < 4 ) return ans; Arrays.sort(nums); for (int i = 0 ; i < len - 3 ; i++) { if (i > 0 && nums[i] == nums[i-1 ]) continue ; for (int j = i + 1 ; j < len - 2 ; j++) { if (j > i + 1 && nums[j] == nums[j-1 ]) continue ; int L = j + 1 ; int R = len - 1 ; while (L < R) { long sum = (long )nums[i] + (long )nums[j] + (long )nums[L] + (long )nums[R]; if (sum == target) { ans.add(Arrays.asList(nums[i], nums[j], nums[L], nums[R])); while (L < R && nums[L] == nums[L+1 ]) L++; while (L < R && nums[R] == nums[R-1 ]) R--; L++; R--; } else if (sum < target) { L++; } else { R--; } } } } return ans; } }
字符串
给定一个字符串 s 和一个整数 k,从字符串开头算起,每计数至 2k 个字符,就反转这 2k 字符中的前 k 个字符。
如果剩余字符少于 k 个,则将剩余字符全部反转。
如果剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符,其余字符保持原样。
示例 1:
1 2 输入:s = "abcdefg", k = 2 输出:"bacdfeg"
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution { public String reverseStr (String S, int k) { char [] s = S.toCharArray(); int n = s.length; for (int i = 0 ; i < n; i += k * 2 ) { reverse(s, i, Math.min(i + k, n) - 1 ); } return new String (s); } private void reverse (char [] s, int left, int right) { while (left < right) { char tmp = s[left]; s[left++] = s[right]; s[right--] = tmp; } } }
给你一个字符串 s ,请你反转字符串中 单词 的顺序。
单词 是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的 单词 分隔开。
返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。
注意: 输入字符串 s中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格。
示例 1:
1 2 输入:s = "the sky is blue" 输出:"blue is sky the"
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution { public String reverseWords (String s) { s = s.trim(); int j = s.length() - 1 , i = j; StringBuilder res = new StringBuilder (); while (i >= 0 ) { while (i >= 0 && s.charAt(i) != ' ' ) i--; res.append(s.substring(i + 1 , j + 1 ) + " " ); while (i >= 0 && s.charAt(i) == ' ' ) i--; j = i; } return res.toString().trim(); } }
步骤图解
输入:" hello world "
trim()后:"hello world"
第一次循环:
i从末尾向前移动,找到空格前:"world"
添加:"world "
第二次循环:
i继续向前,找到下一个单词:"hello"
添加:"world hello "
返回:"world hello"
28找出字符串中第一个匹配项的下标
给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1 。
示例 1:
1 2 3 4 输入:haystack = "sadbutsad", needle = "sad" 输出:0 解释:"sad" 在下标 0 和 6 处匹配。 第一个匹配项的下标是 0 ,所以返回 0 。
示例 2:
1 2 3 输入:haystack = "leetcode", needle = "leeto" 输出:-1 解释:"leeto" 没有在 "leetcode" 中出现,所以返回 -1 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution { public int strStr (String ss, String pp) { int n = ss.length(), m = pp.length(); char [] s = ss.toCharArray(), p = pp.toCharArray(); for (int i = 0 ; i <= n - m; i++) { int a = i, b = 0 ; while (b < m && s[a] == p[b]) { a++; b++; } if (b == m) return i; } return -1 ; } }
步骤图解
示例1: ss = "hello", pp = "ll"
1 2 3 i=0: h vs l ❌ i=1: e vs l ❌ i=2: l vs l ✓, l vs l ✓ → b=2 == m=2 → 返回 2
KMP方法(待补充) 栈与队列 注意
Hello 算法 中的栈和队列讲解是用链表和数组实现栈和队列的功能,以下的算法题是让栈和队列互相实现彼此。
常用方法
push-入
pop-出
peek-查找
栈
Stack stack = new Stack<>();
push()
pop()
peek()
队列
Queue queue = new LinkedList<>();
offer()
poll()
peek()
双向队列
Deque deque = new LinkedList<>();
offerLast()/offerFirst()
pollLast()/pollFirst()
peekLast()/peekFirst()
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):
实现 MyQueue 类:
void push(int x) 将元素 x 推到队列的末尾
int pop() 从队列的开头移除并返回元素
int peek() 返回队列开头的元素
boolean empty() 如果队列为空,返回 true ;否则,返回 false
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 class MyQueue { private Stack<Integer> A; private Stack<Integer> B; public MyQueue () { A = new Stack <>(); B = new Stack <>(); } public void push (int x) { A.push(x); } public int pop () { int peek = peek(); B.pop(); return peek; } public int peek () { if (!B.isEmpty()) return B.peek(); if (A.isEmpty()) return -1 ; while (!A.isEmpty()){ B.push(A.pop()); } return B.peek(); } public boolean empty () { return A.isEmpty() && B.isEmpty(); } }
225用队列实现栈
你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty)。
实现 MyStack 类:
void push(int x) 将元素 x 压入栈顶。
int pop() 移除并返回栈顶元素。
int top() 返回栈顶元素。
boolean empty() 如果栈是空的,返回 true ;否则,返回 false 。
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 class MyStack { Queue<Integer> queue1; Queue<Integer> queue2; public MyStack () { queue1 = new LinkedList <Integer>(); queue2 = new LinkedList <Integer>(); } public void push (int x) { queue2.offer(x); while (!queue1.isEmpty()) { queue2.offer(queue1.poll()); } Queue<Integer> temp = queue1; queue1 = queue2; queue2 = temp; } public int pop () { return queue1.poll(); } public int top () { return queue1.peek(); } public boolean empty () { return queue1.isEmpty(); } }
步骤图解 我们模拟执行 push(1)、push(2)、pop() 的过程:
操作
queue1 的内容 (队首 -> 队尾)
queue2 的内容 (队首 -> 队尾)
步骤解释
初始
[]
[]
两个队列都为空。
push(1)
[1]
[]
1. queue2 加入 1 → [1]。 2. queue1 为空,不转移。 3. 交换引用后,queue1 变为 [1],queue2 变为 []。
push(2)
关键步骤:
[]
[2]
1. 先将新元素 2 放入 queue2。
[]
[2, 1]
2. 将 queue1 中的 [1] 转移到 queue2,queue2 变为 [2, 1]。
[2, 1]
[]
3. 交换引用。现在 queue1 为 [2, 1] (队首 2 是栈顶),queue2 为空。
pop()
[1]
[]
直接从 queue1 的队首取出元素 2 并返回。queue1 变为 [1]。
给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
每个右括号都有一个对应的相同类型的左括号。
示例 1:
示例 2:
示例 3:
示例 4:
示例 5:
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 boolean isValid (String s) { int n = s.length(); if (n % 2 == 1 ) { return false ; } Map<Character, Character> pairs = new HashMap <Character, Character>() {{ put(')' , '(' ); put(']' , '[' ); put('}' , '{' ); }}; Deque<Character> stack = new LinkedList <Character>(); for (int i = 0 ; i < n; i++) { char ch = s.charAt(i); if (pairs.containsKey(ch)) { if (stack.isEmpty() || stack.peek() != pairs.get(ch)) { return false ; } stack.pop(); } else { stack.push(ch); } } return stack.isEmpty(); } }
给出由小写字母组成的字符串 s,重复项删除操作 会选择两个相邻且相同的字母,并删除它们。
在 s 上反复执行重复项删除操作,直到无法继续删除。
在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。
示例:
1 2 3 4 输入:"abbaca" 输出:"ca" 解释: 例如,在 "abbaca" 中,我们可以删除 "bb" 由于两字母相邻且相同,这是此时唯一可以执行删除操作的重复项。之后我们得到字符串 "aaca",其中又只有 "aa" 可以执行重复项删除操作,所以最后的字符串为 "ca"。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution { public String removeDuplicates (String s) { StringBuffer stack = new StringBuffer (); int top = -1 ; for (int i = 0 ; i < s.length(); ++i) { char ch = s.charAt(i); if (top >= 0 && stack.charAt(top) == ch) { stack.deleteCharAt(top); --top; } else { stack.append(ch); ++top; } } return stack.toString(); } }
步骤图解 s = "abbaca"
当前字符 ch
栈 stack (左侧是栈底,右侧是栈顶)
top 索引
操作与说明
初始
"" (空)
-1
‘a’
"a"
0
栈空,直接入栈。
‘b’
"ab"
1
栈顶 ‘a’ ≠ ‘b’,入栈。
‘b’
"a"
0
发现重复 :栈顶 ‘b’ == 当前 ‘b’。 删除栈顶 ‘b’,top 变为 0。
‘a’
"" (空)
-1
发现重复 :栈顶 ‘a’ == 当前 ‘a’。 删除栈顶 ‘a’,top 变为 -1,栈空。
‘c’
"c"
0
栈空,直接入栈。
‘a’
"ca"
1
栈顶 ‘c’ ≠ ‘a’,入栈。
遍历结束,栈中字符为 "ca",即为最终结果。
为什么不用 Stack 类?
Java 的 Stack 类继承自 Vector,是线程安全的,但在单线程环境下会有不必要的性能开销(如加锁)。它的 push 和 pop 操作也是基于数组的。官方题解中使用 StringBuffer 和整型指针 top 手动模拟,直接操作底层数组,避免了方法调用的开销,是更高效的做法。
150逆波兰表达式求值
给你一个字符串数组 tokens ,表示一个根据 逆波兰表示法 表示的算术表达式。
请你计算该表达式。返回一个表示表达式值的整数。
注意:
有效的算符为 '+'、'-'、'*' 和 '/' 。
每个操作数(运算对象)都可以是一个整数或者另一个表达式。
两个整数之间的除法总是 向零截断 。
表达式中不含除零运算。
输入是一个根据逆波兰表示法表示的算术表达式。
答案及所有中间计算结果可以用 32 位 整数表示。
示例 1:
1 2 3 输入:tokens = ["2","1","+","3","*"] 输出:9 解释:该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9
示例 2:
1 2 3 4 5 6 7 8 9 10 输入:tokens = ["10","6","9","3","+","-11","*","/","*","17","+","5","+"] 输出:22 解释:该算式转化为常见的中缀算术表达式为: ((10 * (6 / ((9 + 3) * -11))) + 17) + 5 = ((10 * (6 / (12 * -11))) + 17) + 5 = ((10 * (6 / -132)) + 17) + 5 = ((10 * 0) + 17) + 5 = (0 + 17) + 5 = 17 + 5 = 22
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 int evalRPN (String[] tokens) { Deque<Integer> stack = new LinkedList <Integer>(); int n = tokens.length; for (int i = 0 ; i < n; i++) { String token = tokens[i]; if (isNumber(token)) { stack.push(Integer.parseInt(token)); } else { int num2 = stack.pop(); int num1 = stack.pop(); switch (token) { case "+" : stack.push(num1 + num2); break ; case "-" : stack.push(num1 - num2); break ; case "*" : stack.push(num1 * num2); break ; case "/" : stack.push(num1 / num2); break ; default : } } } return stack.pop(); } public boolean isNumber (String token) { return !("+" .equals(token) || "-" .equals(token) || "*" .equals(token) || "/" .equals(token)); } }
步骤图解 以 tokens = ["2", "1", "+", "3", "*"] 为例(对应中缀表达式 (2 + 1) * 3):
当前 token
栈的操作
栈的内容 (栈底 -> 栈顶)
说明
"2"
push(2)
2
数字,直接入栈
"1"
push(1)
2, 1
数字,直接入栈
"+"
弹出 1 和 2,计算 2 + 1 = 3,push(3)
3
运算符,进行计算
"3"
push(3)
3, 3
数字,直接入栈
"*"
弹出 3 和 3,计算 3 * 3 = 9,push(9)
9
运算符,进行计算
最终栈顶弹出结果 9。
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。
示例 1:
1 2 3 4 5 6 7 8 9 10 11 输入:nums = [1,3,-1,-3,5,3,6,7], k = 3 输出:[3,3,5,5,6,7] 解释: 滑动窗口的位置 最大值 --------------- ----- [1 3 -1] -3 5 3 6 7 3 1 [3 -1 -3] 5 3 6 7 3 1 3 [-1 -3 5] 3 6 7 5 1 3 -1 [-3 5 3] 6 7 5 1 3 -1 -3 [5 3 6] 7 6 1 3 -1 -3 5 [3 6 7] 7
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution { public int [] maxSlidingWindow(int [] nums, int k) { if (nums.length == 0 || k == 0 ) return new int [0 ]; Deque<Integer> deque = new LinkedList <>(); int [] res = new int [nums.length - k + 1 ]; for (int j = 0 , i = 1 - k; j < nums.length; i++, j++) { if (i > 0 && deque.peekFirst() == nums[i - 1 ]) deque.pollFirst(); while (!deque.isEmpty() && deque.peekLast() < nums[j]) deque.pollLast(); deque.offerLast(nums[j]); if (i >= 0 ) res[i] = deque.peekFirst(); } return res; } }
总结
每次遍历到一个新值时,要做一个事情:删除队列中所有比这个值小的值。因为这个值入队之后,所以比这个值小的,并且在这个值之前的,都不可能是答案。
另外,不管最新遍历到的这个值,大小如何,都要加入队列。因为这个值有可能是后面的答案。
347前 K 个高频元素
给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。
示例 1:
输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]
示例 2:
输入: nums = [1], k = 1
输出: [1]
示例 3:
输入: nums = [1,2,1,2,1,2,3,1,3,2], k = 2
输出: [1,2]
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 int [] topKFrequent(int [] nums, int k) { Map<Integer, Integer> occurrences = new HashMap <Integer, Integer>(); for (int num : nums) { occurrences.put(num, occurrences.getOrDefault(num, 0 ) + 1 ); } PriorityQueue<int []> queue = new PriorityQueue <int []>(new Comparator <int []>() { public int compare (int [] m, int [] n) { return m[1 ] - n[1 ]; } }); for (Map.Entry<Integer, Integer> entry : occurrences.entrySet()) { int num = entry.getKey(), count = entry.getValue(); if (queue.size() == k) { if (queue.peek()[1 ] < count) { queue.poll(); queue.offer(new int []{num, count}); } } else { queue.offer(new int []{num, count}); } } int [] ret = new int [k]; for (int i = 0 ; i < k; ++i) { ret[i] = queue.poll()[0 ]; } return ret; } }
步骤图解
输入:nums = [1,1,1,2,2,3], k = 2
统计频率:{1:3, 2:2, 3:1}
小顶堆维护过程:
加入[1,3] → 堆:[1,3]
加入[2,2] → 堆:[2,2], [1,3]
遇到[3,1],堆已满,1<2,不加入
结果:[2,1] ,因为是小顶堆
二叉树 二叉树的遍历方式 层序遍历
广度优先
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 List<Integer> levelOrder (TreeNode root) { Queue<TreeNode> queue = new LinkedList <>(); queue.add(root); List<Integer> list = new ArrayList <>(); while (!queue.isEmpty()) { TreeNode node = queue.poll(); list.add(node.val); if (node.left != null ) queue.offer(node.left); if (node.right != null ) queue.offer(node.right); } return list; }
前/中/后序遍历
深度优先
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 void preOrder (TreeNode root) { if (root == null ) return ; list.add(root.val); preOrder(root.left); preOrder(root.right); } void inOrder (TreeNode root) { if (root == null ) return ; inOrder(root.left); list.add(root.val); inOrder(root.right); } void postOrder (TreeNode root) { if (root == null ) return ; postOrder(root.left); postOrder(root.right); list.add(root.val); }
给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。
示例 1:
1 2 输入:root = [4,2,7,1,3,6,9] 输出:[4,7,2,9,6,3,1]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Solution { public TreeNode invertTree (TreeNode root) { invert(root); return root; } public void invert (TreeNode root) { if (root == null ){ return ; } TreeNode temp = root.left; root.left = root.right; root.right = temp; invert(root.left); invert(root.right); } }
101对称二叉树
给你一个二叉树的根节点 root , 检查它是否轴对称。
示例 1:
1 2 输入:root = [1,2,2,3,4,4,3] 输出:true
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution { public boolean isSymmetric (TreeNode root) { return check(root.left, root.right); } public boolean check (TreeNode L, TreeNode R) { if (L == null && R == null ) { return true ; } if (L == null || R == null ) { return false ; } return L.val == R.val && check(L.left, R.right) && check(L.right, R.left); } }
104二叉树的最大深度
给定一个二叉树 root ,返回其最大深度。
二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。
示例 1:
1 2 输入:root = [3,9,20,null,null,15,7] 输出:3
1 2 3 4 5 6 7 8 9 10 11 class Solution { public int maxDepth (TreeNode root) { if (root == null ) { return 0 ; } else { int leftHeight = maxDepth(root.left); int rightHeight = maxDepth(root.right); return Math.max(leftHeight, rightHeight) + 1 ; } } }
给定一个二叉树,找出其最小深度。
最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
说明: 叶子节点是指没有子节点的节点。
示例 1:
1 2 输入:root = [3,9,20,null,null,15,7] 输出:2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Solution { public int minDepth (TreeNode root) { if (root == null ) { return 0 ; } if (root.left == null && root.right == null ) { return 1 ; } int min_depth = Integer.MAX_VALUE; if (root.left != null ) { min_depth = Math.min(minDepth(root.left), min_depth); } if (root.right != null ) { min_depth = Math.min(minDepth(root.right), min_depth); } return min_depth + 1 ; } }
为什么最大深度和最小深度的解法有较大不同?