LeetCode 面试经典 150 题-数组/字符串 双指针 初始化左右指针指向首尾,如果右指针指向val,就不断左移,如果左指针指向val,就和右指针交换,然后左指针右移,右指针左移,直到两者相遇。最后left所指向的位置就是第一个为val的下标,也就是非val元素的个数。
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 int removeElement (int [] nums, int val) { int n = nums.length; int left = 0 , right = n - 1 ; while (left <= right) { while (right >= left && nums[right] == val){ right--; } while (left <= right && nums[left] != val) { left++; } if (left <= right) { int temp = nums[left]; nums[left] = nums[right]; nums[right] = temp; } } return left; } }
优化:实际上我们并不在乎放到right位置的值是多少,所以可以将right的值赋值给left,然后更新right指针,而不用管right的值是多少。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution { public int removeElement (int [] nums, int val) { int n = nums.length; int left = 0 , right = n - 1 ; while (left <= right) { if (nums[left] == val) { nums[left] = nums[right]; right--; } else { left++; } } return left; } }
类似颜色分类时用到的方法 一边遍历,一边填入,k指向是为val的下标,i指向的是不为val的下标。
初始化要填入的下标 k=0。
从左到右遍历 nums,如果 nums[i]!=val,则更新 nums[k]=nums[i],然后把 k 加一。
遍历结束,返回 k。
1 2 3 4 5 6 7 8 9 10 11 12 class Solution { public int removeElement (int [] nums, int val) { int n = nums.length; int k = 0 ; for (int i = 0 ; i < n; i++) { if (nums[i] != val) { nums[k++] = nums[i]; } } return k; } }
双指针
初始化指针k指向0,i用来遍历。
如果nums[i] != nums[k],先k++,再交换两元素,然后继续遍历。
最后返回k+1。
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution { public int removeDuplicates (int [] nums) { int n = nums.length; int k = 0 ; for (int i = 0 ; i < n; i++) { if (nums[i] != nums[k]) { k++; nums[k] = nums[i]; } } return k + 1 ; } }
双指针
处理特殊情况,n<2可以直接返回。
初始化左右指针都为2,枚举右指针。
如果nums[left-2]!=nums[right],就可以更新nums[left],然后左指针左移。
最后返回left。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution { public int removeDuplicates (int [] nums) { int n = nums.length; if (n < 2 ) { return n; } int left = 2 ; for (int right = 2 ; right < n; right++) { if (nums[left-2 ] != nums[right]) { nums[left++] = nums[right]; } } return left; } }
对于上面的题目可以总结为:左指针永远指向待更新的位置,右指针枚举每一个元素,当满足条件时,就将右指针元素放在左指针位置,然后左指针右移。
反转 先将数组反转,然后反转数组的前k的数,再反转后n-k个数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution { public void rotate (int [] nums, int k) { int n = nums.length; k = k % n; reverse(nums, 0 , n - 1 ); reverse(nums, 0 , k - 1 ); reverse(nums, k, n - 1 ); } private void reverse (int [] nums, int start, int end) { while (start < end) { int temp = nums[start]; nums[start] = nums[end]; nums[end] = temp; start++; end--; } } }
排序 首先我们可以将初始的 H 指数 h 设为 0,然后将引用次数排序,并且对排序后的数组从大到小遍历。
根据 H 指数的定义,如果当前 H 指数为 h 并且在遍历过程中找到当前值 citations[i]>h,则说明我们找到了一篇被引用了至少 h+1 次的论文,所以将现有的 h 值加 1。继续遍历直到 h 无法继续增大。最后返回 h 作为最终答案。
1 2 3 4 5 6 7 8 9 10 11 class Solution { public int hIndex (int [] citations) { Arrays.sort(citations); int h = 0 , i = citations.length - 1 ; while (i >= 0 && citations[i] > h) { h++; i--; } return h; } }
时间复杂度:O(nlogn)
空间复杂度:O(logn)
计数排序 设 n 为 citations 的长度,即这名研究者发表的论文数。根据题意,h 不可能超过 n,所以对于引用次数大于 n 的论文,我们在统计的时候,可以看成是引用次数等于 n 的论文。
创建一个长为 n+1 的 cnt 数组,统计 min(citations[i],n) 的出现次数。设 s 为引用次数 ≥i 的论文数,我们需要算出满足 s≥i 的最大的 i。
从 i=n 开始倒序循环,每次循环,把 cnt[i] 加到 s 中。由于我们是倒序循环的,只要 s≥i 成立,此时的 i 就是满足 s≥i 的最大的 i,直接返回 i 作为答案。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution { public int hIndex (int [] citations) { int n = citations.length; int [] cnt = new int [n + 1 ]; for (int citation : citations) { cnt[Math.min(citation, n)]++; } int s = 0 ; for (int i = n; ; i--) { s += cnt[i]; if (s >= i) { return i; } } } }
二分查找 设查找范围的初始左边界 left 为 0,初始右边界 right 为 n。每次在查找范围内取中点 mid,同时扫描整个数组,判断是否至少有 mid 个数大于 mid。如果有,说明要寻找的 h 在搜索区间的右边,反之则在左边。
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 int hIndex (int [] citations) { int n = citations.length; int left = 0 , right = n; while (left < right) { int mid = (left + right + 1 ) >> 1 ; int cnt = 0 ; for (int i = 0 ; i < n; i++) { if (citations[i] >= mid) { cnt++; } } if (cnt >= mid) { left = mid; } else { right = mid - 1 ; } } return left; } }
时间复杂度:O(nlogn)
空间复杂度:O(1)
复习一下股票和跳跃游戏。
变长数组+哈希表 变长数组中存储元素,哈希表中存储每个元素在变长数组中的下标。
插入:
判断是否存在于哈希表,不存在则插入;
插入变长数组末尾;
将元素值和下标插入哈希表;
删除:
判断是否存在于哈希表,存在则获取其下标删除;
将变长数组的最后一个元素 last 移动到下标 index 处,在哈希表中将 last 的下标更新为 index;
在变长数组中删除最后一个元素,在哈希表中删除 val;
获取随机数:
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 class RandomizedSet { List<Integer> nums; Map<Integer, Integer> hash; Random random; public RandomizedSet () { nums = new ArrayList <Integer>(); hash = new HashMap <Integer, Integer>(); random = new Random (); } public boolean insert (int val) { if (hash.containsKey(val)) { return false ; } int index = nums.size(); nums.add(val); hash.put(val, index); return true ; } public boolean remove (int val) { if (!hash.containsKey(val)) { return false ; } int index = hash.get(val); int n = nums.size(); int last = nums.get(n - 1 ); nums.set(index, last); hash.put(last, index); nums.remove(n - 1 ); hash.remove(val); return true ; } public int getRandom () { int randomIndex = random.nextInt(nums.size()); return nums.get(randomIndex); } }
时间复杂度:所有操作的时间复杂度都是O(1)
空间复杂度:O(n),存储元素的数组和哈希表需要 O(n) 的空间。
前缀积+后缀积 题目要求不能用除法,考虑前缀和的思想,prefix保存前缀积,suffix保存后缀积,对于每一个元素它最终的结果都是prefix[i] * suffix[i]
。
prefix[i] = prefix[i-1] * nums[i-1]
suffix[i] = prefix[i+1] * nums[i+1]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Solution { public int [] productExceptSelf(int [] nums) { int n = nums.length; int [] prefix = new int [n]; int [] suffix = new int [n]; prefix[0 ] = 1 ; for (int i = 1 ; i < n; i++) { prefix[i] = prefix[i-1 ] * nums[i-1 ]; } suffix[n-1 ] = 1 ; for (int i = n - 2 ; i >= 0 ; i--) { suffix[i] = suffix[i+1 ] * nums[i+1 ]; } for (int i = 0 ; i < n; i++) { prefix[i] *= suffix[i]; } return prefix; } }
空间优化 由于输出数组不算在空间复杂度内,那么我们可以将前后缀数组用输出数组来计算。先把输出数组当作前缀数组来计算,然后再动态构造后缀数组得到结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution { public int [] productExceptSelf(int [] nums) { int n = nums.length; int [] answer = new int [n]; answer[0 ] = 1 ; for (int i = 1 ; i < n; i++) { answer[i] = answer[i-1 ] * nums[i-1 ]; } int suf = 1 ; for (int i = n - 1 ; i >= 0 ; i--) { answer[i] *= suf; suf *= nums[i]; } return answer; } }
复习昨天做的题目。
贪心 计算从每个站点出发能否完成一圈,找到最优的起点,避免直接从每个站点暴力判断。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution { public int canCompleteCircuit (int [] gas, int [] cost) { int n = gas.length; int spare = 0 ; int minSpare = Integer.MAX_VALUE, minIndex = 0 ; for (int i = 0 ; i < n; i++) { spare += gas[i] - cost[i]; if (spare < minSpare) { minSpare = spare; minIndex = i; } } return spare < 0 ? -1 : (minIndex + 1 ) % n; } }
两次遍历 初始化答案数组每个都是1:
当ratings[i] > ratings[i-1]时,第 i 个孩子的糖果数量比第i - 1个孩子的糖果数量多;
当ratings[i] > ratings[i+1]时,第 i 个孩子的糖果数量比第i + 1个孩子的糖果数量多。
因此分别从左到右遍历一次,再从右向左遍历一次,更新状态方式:
从左到右:如果ratings[i] > ratings[i-1]
,则sweets[i] = sweets[i-1] + 1
;
从右到左:如果ratings[i] > ratings[i+1]
,则sweets[i] = max(sweets[i], sweets[i+1] + 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 class Solution { public int candy (int [] ratings) { int n = ratings.length; int [] sweets = new int [n]; Arrays.fill(sweets, 1 ); for (int i = 1 ; i < n; i++) { if (ratings[i] > ratings[i-1 ]) { sweets[i] = sweets[i-1 ] + 1 ; } } for (int i = n - 2 ; i >= 0 ; i--) { if (ratings[i] > ratings[i+1 ]) { sweets[i] = Math.max(sweets[i], sweets[i+1 ] + 1 ); } } int ans = 0 ; for (int i = 0 ; i < n; i++) { ans += sweets[i]; } return ans; } }
常数空间遍历 翻译了一下别人的python代码,不太理解。
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 class Solution { public int candy (int [] ratings) { int n = ratings.length; int res = 1 ; int pre = 1 ; int desNum = 0 ; for (int i = 1 ; i < n; i++) { if (ratings[i] >= ratings[i-1 ]) { if (desNum > 0 ) { res += (1 + desNum) * desNum / 2 ; if (pre <= desNum) { res += desNum- pre + 1 ; } pre = 1 ; desNum = 0 ; } if (ratings[i] == ratings[i - 1 ]) { pre = 1 ; } else { pre += 1 ; } res += pre; } else { desNum++; } } if (desNum > 0 ) { res += ((1 + desNum) * desNum) / 2 ; if (pre <= desNum) { res += desNum - pre + 1 ; } } return res; } }
复习接雨水等单调栈问题。
分析
设 x=s[i−1],y=s[i],这是两个相邻的罗马数字。
如果 x 的数值小于 y 的数值,那么 x 的数值要取相反数。例如 IV 中的 I 相当于 −1,CM 中的 C 相当于 −100。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Solution { private static final Map<Character, Integer> ROMAN = Map.of( 'I' , 1 , 'V' , 5 , 'X' , 10 , 'L' , 50 , 'C' , 100 , 'D' , 500 , 'M' , 1000 ); public int romanToInt (String s) { int n = s.length(); int ans = 0 ; for (int i = 1 ; i < n; i++){ int x = ROMAN.get(s.charAt(i-1 )); int y = ROMAN.get(s.charAt(i)); ans += x < y ? -x : x; } return ans + ROMAN.get(s.charAt(n-1 )); } }
分析 硬编码:
1 2 3 4 5 6 7 8 9 10 11 12 class Solution { private static final String[][] R = new String [][]{ {"" , "I" , "II" , "III" , "IV" , "V" , "VI" , "VII" , "VIII" , "IX" }, {"" , "X" , "XX" , "XXX" , "XL" , "L" , "LX" , "LXX" , "LXXX" , "XC" }, {"" , "C" , "CC" , "CCC" , "CD" , "D" , "DC" , "DCC" , "DCCC" , "CM" }, {"" , "M" , "MM" , "MMM" }, }; public String intToRoman (int num) { return R[3 ][num / 1000 ] + R[2 ][num / 100 % 10 ] + R[1 ][num / 10 % 10 ] + R[0 ][num % 10 ]; } }
分析 从后往前找到第一个不为空格的字符,从此开始计数,然后找到为空格的位置,计数结束。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution { public int lengthOfLastWord (String s) { int n = s.length(); int ans = 0 ; int i = n - 1 ; while (s.charAt(i) == ' ' ) { i--; } while (i >= 0 && s.charAt(i) != ' ' ) { ans++; i--; } return ans; } }
最长公共前缀 按列遍历 即外层循环枚举字符串的每一位,内层循环枚举每一个字符串,当遇到不符合条件的就可以直接返回结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution { public String longestCommonPrefix (String[] strs) { int n = strs.length; for (int i = 0 ; i < strs[0 ].length(); i++) { String s0 = strs[0 ]; for (String s : strs) { if (i == s.length() || s.charAt(i) != s0.charAt(i)) { return s.substring(0 , i); } } } return strs[0 ]; } }
分析 首先用replaceAll和trim方法去除多余的空格,然后逆转字符串,再按照逐个单词进行翻转。
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 class Solution { public String reverseWords (String s) { s = s.replaceAll("\\s+" , " " ).trim(); char [] charArray = s.toCharArray(); int n = charArray.length; reverse(charArray, 0 , n - 1 ); int start = 0 , end = 0 ; while (end < n) { if (charArray[end] == ' ' ) { reverse(charArray, start, end - 1 ); start = end + 1 ; } end++; } reverse(charArray, start, n - 1 ); return new String (charArray); } private void reverse (char [] charArray, int start, int end) { while (start < end) { char temp = charArray[start]; charArray[start] = charArray[end]; charArray[end] = temp; start++; end--; } } }
使用语言特性 1 2 3 4 5 6 7 8 9 10 11 12 class Solution { public String reverseWords (String s) { s = s.trim(); List<String> wordList = Arrays.asList(s.split("\\s+" )); Collections.reverse(wordList); return String.join(" " , wordList); } }
双端队列 由于双端队列支持从队列头部插入的方法,因此我们可以沿着字符串一个一个单词处理,然后将单词压入队列的头部,再将队列转成字符串即可。
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 String reverseWords (String s) { int n = s.length(); int left = 0 , right = n - 1 ; while (left <= right && s.charAt(left) == ' ' ) { left++; } while (right >= left && s.charAt(right) == ' ' ) { right--; } Deque<String> que = new ArrayDeque <>(); StringBuilder word = new StringBuilder (); while (left <= right) { Character ch = s.charAt(left); if (word.length() != 0 && ch == ' ' ) { que.offerFirst(word.toString()); word.setLength(0 ); } else if (ch != ' ' ) { word.append(ch); } left++; } que.offerFirst(word.toString()); return String.join(" " , que); } }
分析 模拟这个行索引的变化,在遍历 s 中把每个字符填到正确的行。
res[i] += c: 把每个字符 c 填入对应行;
i += flag: 更新当前字符 c 对应的行索引;
flag = - flag: 在达到 Z 字形转折点时,执行反向。
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 String convert (String s, int numRows) { if (numRows < 2 ) { return s; } int n = s.length(); List<StringBuilder> rows = new ArrayList <StringBuilder>(); for (int i = 0 ; i < numRows; i++) { rows.add(new StringBuilder ()); } int r = 0 , flag = -1 ; for (int i = 0 ; i < n; i++) { rows.get(r).append(s.charAt(i)); if (r == 0 || r == numRows - 1 ) { flag = -flag; } r += flag; } StringBuilder res = new StringBuilder (); for (StringBuilder sb : rows) { res.append(sb); } return res.toString(); } }
暴力解法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution { public int strStr (String haystack, String needle) { int m = haystack.length(); int n = needle.length(); if (n > m) { return -1 ; } for (int i = 0 ; i + n <= m; i++) { if (haystack.substring(i, i + n).equals(needle)) { return i; } } return -1 ; } }
KMP算法
next[i] 表示在模式串的前 i 个字符中,模式串的前缀和后缀的最大匹配长度(不包括 i 本身)。
当遇到不匹配的字符时,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 class Solution { public int strStr (String haystack, String needle) { if (needle.length() == 0 ) { return 0 ; } int n = haystack.length(); int m = needle.length(); int [] next = new int [m]; next[0 ] = 0 ; int j = 0 ; for (int i = 1 ; i < m; i++) { while (j > 0 && needle.charAt(i) != needle.charAt(j)) { j = next[j - 1 ]; } if (needle.charAt(i) == needle.charAt(j)) { j++; } next[i] = j; } int i = 0 ; j = 0 ; while (i < n) { if (haystack.charAt(i) == needle.charAt(j)) { i++; j++; if (j == m) { return i - j; } } else { if (j != 0 ) { j = next[j - 1 ]; } else { i++; } } } return -1 ; } }
时间复杂度:O(m + n)
空间复杂度:O(m),m为模式串的长度。
KMP不太好想,之后再看看好好理解一下,然后结合马拉车算法一起看一下。
贪心
当前行是最后一行:单词左对齐,且单词之间应只有一个空格,在行末填充剩余空格;
当前行不是最后一行,且只有一个单词:该单词左对齐,在行末填充空格;
当前行不是最后一行,且不只一个单词:设当前行单词数为 numWords,空格数为 numSpaces,我们需要将空格均匀分配在单词之间,则单词之间应至少有$\frac{numSpaces}{numWords-1}$个空格。对于多出来的$extraSpaces=numSpacesmod(numWords−1)$个空格,应填在前 extraSpaces 个单词之间。因此,前 extraSpaces 个单词之间填充 avgSpaces+1 个空格,其余单词之间填充 avgSpaces 个空格。
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 class Solution { public List<String> fullJustify (String[] words, int maxWidth) { List<String> result = new ArrayList <>(); int index = 0 ; while (index < words.length) { int lineStart = index; int lineLength = 0 ; while (index < words.length && lineLength + words[index].length() + (index - lineStart) <= maxWidth) { lineLength += words[index].length(); index++; } StringBuilder line = new StringBuilder (); int numWords = index - lineStart; int totalSpaces = maxWidth - lineLength; if (index == words.length || numWords == 1 ) { for (int i = lineStart; i < index; i++) { line.append(words[i]); if (i < index - 1 ) { line.append(" " ); } } while (line.length() < maxWidth) { line.append(" " ); } } else { int spacesPerWord = totalSpaces / (numWords - 1 ); int extraSpaces = totalSpaces % (numWords - 1 ); for (int i = lineStart; i < index; i++) { line.append(words[i]); if (i < index - 1 ) { for (int j = 0 ; j < spacesPerWord; j++) { line.append(" " ); } if (extraSpaces > 0 ) { line.append(" " ); extraSpaces--; } } } } result.add(line.toString()); } return result; } }
时间复杂度:O(m),其中 m 是数组 words 中所有字符串的长度之和。
空间复杂度:O(m)
总结 字符串的很多题目并不难,但具体分析起来会有多种情况,每一种思考起来并不容易,只能多做多复习。