一 挑战字符串
1 无重复字符的最长子串(见leetcode bug free)
2 最长公共前缀(见leetcode bug free)
3 字符串的排列
给定两个字符串 s1 和 s2,写一个函数来判断 s2 是否包含 s1 的排列。
示例1:
输入: s1 = "ab" s2 = "eidbaooo"
输出: True
解释: s2 包含 s1 的排列之一 ("ba"). 示例2:
输入: s1= "ab" s2 = "eidboaoo"
输出: False
analyse:
滑动窗口
code:
class Solution {
public boolean checkInclusion(String s1, String s2) {
if (s1 == null || s2 == null || s1.length() > s2.length()) {
return false;
}
int[] map1 = new int[26];
int[] map2 = new int[26];
int i = 0;
int j = 0;
while (j < s1.length()) {
char c1 = s1.charAt(j);
char c2 = s2.charAt(j);
map1[c1 - 'a'] += 1;
map2[c2 - 'a'] += 1;
j++;
}
j = s1.length() - 1;
while (j < s2.length()) {
if (arrayEqual(map1, map2)) {
return true;
}
j++;
i++;
if (j < s2.length()) {
char new_c = s2.charAt(j);
char old_c = s2.charAt(i - 1);
map2[new_c - 'a'] += 1;
map2[old_c - 'a'] -= 1;
}
}
return false;
}
public boolean arrayEqual(int[] a, int[] b) {
for (int i = 0; i < a.length; i++) {
if (a[i] != b[i]) {
return false;
}
}
return true;
}
}
4 字符串相乘
两个以字符串形式表示的非负整数 num1
和 num2
,返回 num1
和 num2
的乘积,它们的乘积也表示为字符串形式。不能直接将字符串转化为大整数求乘积。
示例 1: 输入: num1 = "2", num2 = "3"
输出: "6"
示例 2: 输入: num1 = "123", num2 = "456"
输出: "56088"
说明: num1 和 num2 的长度小于110。
num1 和 num2 只包含数字 0-9。
num1 和 num2 均不以零开头,除非是数字 0 本身。
不能使用任何标准库的大数类型(比如 BigInteger)或直接将输入转换为整数来处理。
analyse:
要求结果里的第k个数字(k从0开始,k为0时表示个位,1表示十位),需要将满足i + j = k的所有数字乘积累加,再加上carry。(i和j分别为num1和num2的第i和第j个数字,定义同k)
code:
class Solution {
public String multiply(String num1, String num2) {
if (num1 == null || num2 == null || num1.length() == 0 || num2.length() == 0) {
return null;
}
if (num1.equals("0") || num2.equals("0")) {
return "0";
}
int m = num1.length();
int n = num2.length(); StringBuilder sb = new StringBuilder();
int k = 0;
int carry = 0;
while (k <= m + n - 2) {
int sum = 0;
for (int i = 0; i <= Math.min(k, m - 1); i++) {
int j = k - i;
if (j >= n) {
continue;
}
int digit1 = num1.charAt(m - i - 1) - '0';
int digit2 = num2.charAt(n - j - 1) - '0';
sum += digit1 * digit2;
}
sum += carry;
sb.append(String.valueOf(sum % 10));
carry = sum / 10;
k++;
}
if (carry != 0) {
String cs = String.valueOf(carry);
String r_cs = (new StringBuilder(cs)).reverse().toString();
sb.append(r_cs);
}
return sb.reverse().toString();
}
}
5 反转字符串
给定一个字符串,逐个翻转字符串中的每个单词。
示例 1: 输入: "the sky is blue"
输出: "blue is sky the"
示例 2: 输入: " hello world! "
输出: "world! hello"
解释: 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。
示例 3: 输入: "a good example"
输出: "example good a"
解释: 如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。 说明: 无空格字符构成一个单词。
输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。
如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。
analyse:
几个要点:
(1)定义两个指针p1,p2,p1指向待填充元素的位置,p2往后扫。
(2)如何保证开头的空格被去掉:当p1为0时,p2扫到空格不加入。
(3)如何保证单词间的空格只剩一个: 当p1 - 1为空格时,p2扫到空格不加入。
(4)如何实现单词反转:实现一个字符串反转的方法。每次往前加入一个空格时,就反转一下上一个加入的单词。最后还要对整体反转一遍。
(5)如何保证末尾的空格被去掉:扫完一遍后,检查p1-1是否为空格,是就p--。不是记得反转一下最后一个单词。
code:
class Solution {
public String reverseWords(String s) {
if (s == null || s.length() == 0) {
return "";
}
char[] sa = s.toCharArray();
int p = 0;
int last_word_start = 0;
for (int i = 0; i < sa.length; i++) {
char c = sa[i];
if (c == ' ') {
if (p == 0 || sa[p - 1] == ' ') {
continue;
}
reverse(sa, last_word_start, p - 1);
sa[p++] = c;
last_word_start = p;
} else {
sa[p++] = c;
}
}
if (p > 0 && sa[p - 1] == ' ') {
p--;
} else {
reverse(sa, last_word_start, p - 1);
}
reverse(sa, 0, p - 1);
return new String(sa, 0, p);
}
public void reverse(char[] sa, int start, int end) {
while (start < end) {
char tmp = sa[start];
sa[start] = sa[end];
sa[end] = tmp;
start++;
end--;
}
}
}
6 简化路径
题目太长
以 Unix 风格给出一个文件的绝对路径,你需要简化它。或者换句话说,将其转换为规范路径。 在 Unix 风格的文件系统中,一个点(.)表示当前目录本身;此外,两个点 (..) 表示将目录切换到上一级(指向父目录);两者都可以是复杂相对路径的组成部分。更多信息请参阅:Linux / Unix中的绝对路径 vs 相对路径 请注意,返回的规范路径必须始终以斜杠 / 开头,并且两个目录名之间必须只有一个斜杠 /。最后一个目录名(如果存在)不能以 / 结尾。此外,规范路径必须是表示绝对路径的最短字符串。 示例 1: 输入:"/home/"
输出:"/home"
解释:注意,最后一个目录名后面没有斜杠。
示例 2: 输入:"/../"
输出:"/"
解释:从根目录向上一级是不可行的,因为根是你可以到达的最高级。
示例 3: 输入:"/home//foo/"
输出:"/home/foo"
解释:在规范路径中,多个连续斜杠需要用一个斜杠替换。
示例 4: 输入:"/a/./b/../../c/"
输出:"/c"
示例 5: 输入:"/a/../../b/../c//.//"
输出:"/c"
示例 6: 输入:"/a//b////c/d//././/.."
输出:"/a/b/c"
analyse:
先根据“/”split,遍历split后的每个元素,用一个栈来搞,
code:
class Solution {
public String simplifyPath(String path) {
Stack<String> s = new Stack<String>();
String[] dirs = path.split("/");
for (String dir : dirs) {
if (dir.equals("") || dir.equals(".")) {
continue;
} else if (dir.equals("..")) {
if (!s.isEmpty()) {
s.pop();
}
} else {
s.push(dir);
}
}
if (s.isEmpty()) {
return "/";
}
List<String> dir_list = new ArrayList<String>();
while (!s.isEmpty()) {
dir_list.add(s.pop());
}
StringBuilder sb = new StringBuilder();
for (int i = dir_list.size() - 1; i >= 0; i--) {
sb.append("/");
sb.append(dir_list.get(i));
}
return sb.toString();
}
}
7 复原IP地址
给定一个只包含数字的字符串,复原它并返回所有可能的 IP 地址格式。(ip格式:只能是四段,每段值范围在0~255,除0外不能以0开头)。
示例: 输入: "25525511135"
输出: ["255.255.11.135", "255.255.111.35"]
analyse:
dfs
code:
class Solution {
public List<String> restoreIpAddresses(String s) {
List<String> res = new ArrayList<String>();
helper(res, s, 0, "", 0);
return res;
}
public void helper(List<String> res, String s, int index, String cur, int count) {
if (count == 4) {
if (index >= s.length()) {
res.add(cur.substring(0, cur.length() - 1));
} else {
return;
}
}
for (int i = index; i < s.length(); i++) {
if (i - index > 2) {
break;
}
String p = s.substring(index, i + 1);
int pv = Integer.parseInt(p);
if (pv > 255 || (p.charAt(0) == '0' && p.length() > 1)) {
continue;
}
helper(res, s, i + 1, cur + p + ".", count + 1);
}
}
}
二 数组与排序
1 岛屿的最大面积
给定一个包含了一些 0 和 1的非空二维数组 grid
, 一个 岛屿 是由四个方向 (水平或垂直) 的 1
(代表土地) 构成的组合。你可以假设二维矩阵的四个边缘都被水包围着。
找到给定的二维数组中最大的岛屿面积。(如果没有岛屿,则返回面积为0。)
示例 1: [[0,0,1,0,0,0,0,1,0,0,0,0,0],
[0,0,0,0,0,0,0,1,1,1,0,0,0],
[0,1,1,0,1,0,0,0,0,0,0,0,0],
[0,1,0,0,1,1,0,0,1,0,1,0,0],
[0,1,0,0,1,1,0,0,1,1,1,0,0],
[0,0,0,0,0,0,0,0,0,0,1,0,0],
[0,0,0,0,0,0,0,1,1,1,0,0,0],
[0,0,0,0,0,0,0,1,1,0,0,0,0]]
对于上面这个给定矩阵应返回 6。注意答案不应该是11,因为岛屿只能包含水平或垂直的四个方向的‘1’。 示例 2: [[0,0,0,0,0,0,0,0]]
对于上面这个给定的矩阵, 返回 0。 注意: 给定的矩阵grid 的长度和宽度都不超过 50。
analyse:
并查集
code:
class Solution {
int[] father;
int[] size;
int max_size;
public int maxAreaOfIsland(int[][] grid) {
int m = grid.length;
int n = grid[0].length;
father = new int[m * n];
size = new int[m * n];
max_size = 0;
for (int i = 0; i < m * n; i++) {
father[i] = i;
size[i] = 1;
}
int[] dx = {0, 0, -1, -1};
int[] dy = {1, -1, 0, 0};
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
int index = i * n + j;
if (grid[i][j] == 0) {
continue;
}
max_size = Math.max(1, max_size);
for (int k = 0; k < 4; k++) {
int nx = i + dx[k];
int ny = j + dy[k];
if (nx >= 0 && nx < m && ny >= 0
&& ny < n && grid[nx][ny] == 1) {
union(index, nx * n + ny);
}
}
}
}
return max_size;
}
public int find(int a) {
if (father[a] == a) {
return a;
}
return father[a] = find(father[a]);
} public void union(int a, int b) {
int root_a = find(a);
int root_b = find(b);
if (root_a != root_b) {
father[root_a] = root_b;
size[root_b] += size[root_a];
max_size = Math.max(max_size, size[root_b]);
}
}
}
2 最长连续递增序列
给定一个未经排序的整数数组,找到最长且连续的的递增序列。
示例 1: 输入: [1,3,5,4,7]
输出: 3
解释: 最长连续递增序列是 [1,3,5], 长度为3。
尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为5和7在原数组里被4隔开。
示例 2: 输入: [2,2,2,2,2]
输出: 1
解释: 最长连续递增序列是 [2], 长度为1。
注意:数组长度不会超过10000。
analyse:
遍历即可
code:
class Solution {
public int findLengthOfLCIS(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
int start = 0;
int res = 1;
for (int i = 1; i < nums.length; i++) {
if (nums[i - 1] >= nums[i]) {
start = i;
}
res = Math.max(res, i - start + 1);
}
return res;
}
}
3 最长连续序列
给定一个未排序的整数数组,找出最长连续序列的长度。
示例: 输入: [100, 4, 200, 1, 3, 2]
输出: 4
解释: 最长连续序列是 [1, 2, 3, 4]。它的长度为 4。
analyse:
带size的并查集和hash表
code:
class Solution {
int[] father;
int[] size;
int max_size;
public int longestConsecutive(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
father = new int[nums.length];
size = new int[nums.length];
max_size = 1;
for (int i = 0; i < nums.length; i++) {
father[i] = i;
size[i] = 1;
}
for (int i = 0; i < nums.length; i++) {
if (map.containsKey(nums[i])) {
continue;
}
if (map.containsKey(nums[i] - 1)) {
union(map.get(nums[i] - 1), i);
}
if (map.containsKey(nums[i] + 1)) {
union(map.get(nums[i] + 1), i);
}
map.put(nums[i], i);
}
return max_size;
} public int find(int a) {
if (father[a] == a) {
return a;
}
return father[a] = find(father[a]);
} public void union(int a, int b) {
int root_a = find(a);
int root_b = find(b);
if (root_a != root_b) {
father[root_a] = root_b;
size[root_b] += size[root_a];
max_size = Math.max(size[root_b], max_size);
}
}
}
4 朋友圈
班上有 N 名学生。其中有些人是朋友,有些则不是。他们的友谊具有是传递性。如果已知 A 是 B 的朋友,B 是 C 的朋友,那么我们可以认为 A 也是 C 的朋友。所谓的朋友圈,是指所有朋友的集合。 给定一个 N * N 的矩阵 M,表示班级中学生之间的朋友关系。如果M[i][j] = 1,表示已知第 i 个和 j 个学生互为朋友关系,否则为不知道。你必须输出所有学生中的已知的朋友圈总数。 示例 1: 输入:
[[1,1,0],
[1,1,0],
[0,0,1]]
输出: 2
说明:已知学生0和学生1互为朋友,他们在一个朋友圈。
第2个学生自己在一个朋友圈。所以返回2。
示例 2: 输入:
[[1,1,0],
[1,1,1],
[0,1,1]]
输出: 1
说明:已知学生0和学生1互为朋友,学生1和学生2互为朋友,所以学生0和学生2也是朋友,所以他们三个在一个朋友圈,返回1。
注意: N 在[1,200]的范围内。
对于所有学生,有M[i][i] = 1。
如果有M[i][j] = 1,则有M[j][i] = 1。
analyse:
即求无向图的连通块数量,bfs或并查集
code:
class Solution { public int findCircleNum(int[][] M) {
Set<Integer> set = new HashSet<Integer>();
int count = 0;
int n = M.length;
for (int i = 0; i < n; i++) {
if (set.contains(i)) {
continue;
}
count++;
Queue<Integer> q = new LinkedList<Integer>();
q.offer(i);
set.add(i);
while (!q.isEmpty()) {
int cur = q.poll();
for (int j = 0; j < n; j++) {
if (M[cur][j] == 1 && !set.contains(j)) {
q.offer(j);
set.add(j);
}
}
}
}
return count;
}
}
class Solution {
int[] father;
int count;
public int findCircleNum(int[][] M) {
int n = M.length;
father = new int[n];
count = n;
for (int i = 0; i < n; i++) {
father[i] = i;
}
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (M[i][j] == 1) {
union(i, j);
}
}
}
return count;
} public int find(int a) {
if (father[a] == a) {
return a;
}
return father[a] = find(father[a]);
} public void union(int a, int b) {
int root_a = find(a);
int root_b = find(b);
if (root_a != root_b) {
father[root_a] = root_b;
count--;
}
}
}
5 合并区间(见leetcode bug free)
6 接雨水
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
analyse:
两个指针指向两边已经灌完水的柱子,灌水的时候从两边往中间灌,哪边低就先灌哪边。
code:
class Solution {
public int trap(int[] height) {
if (height == null || height.length <= 2) {
return 0;
}
int i = 0;
int j = height.length - 1;
int res = 0;
int left = 0;
int right = 0;
while (i + 1 < j) {
if (height[i] + left < height[j] + right) {
left = Math.max(height[i] + left - height[i + 1], 0);
res += left;
i++;
} else {
right = Math.max(height[j] + right - height[j - 1], 0);
res += right;
j--;
}
}
return res;
}
}
7 第k个排列
给出集合 [1,2,3,…,n],其所有元素共有 n! 种排列。 按大小顺序列出所有排列情况,并一一标记,当 n = 3 时, 所有排列如下: "123"
"132"
"213"
"231"
"312"
"321"
给定 n 和 k,返回第 k 个排列。 说明: 给定 n 的范围是 [1, 9]。
给定 k 的范围是[1, n!]。
示例 1: 输入: n = 3, k = 3
输出: "213"
示例 2: 输入: n = 4, k = 9
输出: "2314"
analyse:
递归。首先把1~n加入到list中,求第一个数时,设i = k - 1, 应该找list.get(i / (n - 1)!),然后list删除这个数,i设为i % (n - 1)!。类似的接下来去找第2~n个数。
code:
class Solution {
public String getPermutation(int n, int k) {
int factorial = 1;
List<Integer> numbers = new ArrayList<Integer>();
for (int i = 1; i <= n; i++) {
factorial *= i;
numbers.add(i);
}
StringBuilder sb = new StringBuilder();
helper(sb, k - 1, numbers, factorial / n);
return sb.toString();
}
public void helper(StringBuilder sb, int i, List<Integer> numbers, int factorial) {
int num_index = i / factorial;
sb.append(numbers.get(num_index));
numbers.remove(num_index);
if (numbers.size() == 0) {
return;
}
helper(sb, i % factorial, numbers, factorial / numbers.size());
}
}
四 动态和贪心
1 最大正方形(见leetcode bug free)
2 俄罗斯套娃信封问题
给定一些标记了宽度和高度的信封,宽度和高度以整数对形式 (w, h) 出现。当另一个信封的宽度和高度都比这个信封大的时候,这个信封就可以放进另一个信封里,如同俄罗斯套娃一样。 请计算最多能有多少个信封能组成一组“俄罗斯套娃”信封(即可以把一个信封放到另一个信封里面)。 说明:
不允许旋转信封。 示例: 输入: envelopes = [[5,4],[6,4],[6,7],[2,3]]
输出: 3
解释: 最多信封的个数为 3, 组合为: [2,3] => [5,4] => [6,7]。
analyse:
先把信封按宽度从小到大排序,dp状态dp[i]表示为第i个信封为最外层信封时的最大信封数。
code:
class Solution {
public int maxEnvelopes(int[][] envelopes) {
if (envelopes == null || envelopes.length == 0) {
return 0;
}
List<Pair> list = new ArrayList<Pair>(); for (int i = 0; i < envelopes.length; i++) {
list.add(new Pair(envelopes[i][0], envelopes[i][1]));
}
Comparator<Pair> c = new Comparator<Pair>() {
public int compare(Pair e1, Pair e2) {
return e1.w - e2.w;
}
};
Collections.sort(list, c);
int[] dp = new int[envelopes.length]; int res = 1;
for (int i = 0; i < dp.length; i++) {
Pair cur = list.get(i);
dp[i] = 1;
for (int j = 0; j < i; j++) {
Pair last = list.get(j);
if (cur.w > last.w && cur.h > last.h) {
dp[i] = Math.max(dp[i], 1 + dp[j]);
}
}
res = Math.max(res, dp[i]);
}
return res;
}
}
class Pair {
int w;
int h;
Pair(int w, int h) {
this.w = w;
this.h = h;
}
}
五 数据结构
1 全O(1)的数据结构
实现一个数据结构支持以下操作: Inc(key) - 插入一个新的值为 1 的 key。或者使一个存在的 key 增加一,保证 key 不为空字符串。
Dec(key) - 如果这个 key 的值是 1,那么把他从数据结构中移除掉。否者使一个存在的 key 值减一。如果这个 key 不存在,这个函数不做任何事情。key 保证不为空字符串。
GetMaxKey() - 返回 key 中值最大的任意一个。如果没有元素存在,返回一个空字符串""。
GetMinKey() - 返回 key 中值最小的任意一个。如果没有元素存在,返回一个空字符串""。
挑战:以 O(1) 的时间复杂度实现所有操作。
analyse:
(1)两个map
map1存key到count的映射关系
map2存count到DoubleListNode的映射关系 (2)DoubleListNode
存对应的count,以及该count对应哪些key,以及prev和next (3)插入key时,做以下几件事:
a 更新map1,更新后假设该key的频次为count
b 找到count - 1对应的节点,记该节点为prev
c 删除prev中该key
d 假设prev的next节点的val不是count,需要新建一个节点,插入到prev后面
f prev.next加入key
g 假设prev中key数量已经被删除到0了,则删除prev这个节点(如果当前count为1则不删除) (4)删除key:类似于(3)
(5)getMaxKey
获取tail.prev.keys中的任意一个元素
(6)getMinKey
获取head.next.keys中的任意一个元素
code:
class AllOne {
Map<String, Integer> map1;
Map<Integer, DoubleListNode> map2;
DoubleListNode head;
DoubleListNode tail; public AllOne() {
map1 = new HashMap<String, Integer>();
map2 = new HashMap<Integer, DoubleListNode>();
head = new DoubleListNode(0, new HashSet<String>());
tail = new DoubleListNode(0, new HashSet<String>());
head.next = tail;
tail.prev = head;
map2.put(0, head);
} public void inc(String key) {
if (!map1.containsKey(key)) {
map1.put(key, 0);
}
map1.put(key, map1.get(key) + 1); int count = map1.get(key);
DoubleListNode prev = map2.get(count - 1);
prev.keys.remove(key); if (prev.next.val != count) {
DoubleListNode new_n = new DoubleListNode(count, new HashSet<String>());
map2.put(count, new_n);
insertAfterPos(prev, new_n);
}
prev.next.keys.add(key); if (prev.keys.size() == 0 && count != 1) {
deleteNode(prev);
map2.remove(count - 1);
}
} public void dec(String key) {
if (!map1.containsKey(key)) {
return;
}
int count = map1.get(key) - 1; map1.remove(key);
DoubleListNode next = map2.get(count + 1);
next.keys.remove(key);
for (String s : map1.keySet()) {
System.out.println(s);
}
if(count != 0) {
map1.put(key, count);
if (next.prev.val != count) {
DoubleListNode new_n = new DoubleListNode(count, new HashSet<String>());
map2.put(count, new_n);
insertBeforePos(next, new_n);
}
next.prev.keys.add(key);
}
if (next.keys.size() == 0) {
deleteNode(next);
map2.remove(count + 1);
}
} public String getMaxKey() {
if (map1.size() == 0) {
return "";
}
return tail.prev.keys.iterator().next();
} public String getMinKey() {
if (map1.size() == 0) {
return "";
}
return head.next.keys.iterator().next();
} private void deleteNode(DoubleListNode node) {
DoubleListNode prev = node.prev;
DoubleListNode next = node.next;
prev.next = next;
next.prev = prev;
} private void insertAfterPos(DoubleListNode pos, DoubleListNode node) {
DoubleListNode pos_next = pos.next;
pos.next = node;
pos_next.prev = node;
node.prev = pos;
node.next = pos_next;
} private void insertBeforePos(DoubleListNode pos, DoubleListNode node) {
DoubleListNode pos_prev = pos.prev;
pos.prev = node;
pos_prev.next = node;
node.prev = pos_prev;
node.next = pos;
}
} class DoubleListNode {
int val;
Set<String> keys;
DoubleListNode next;
DoubleListNode prev;
DoubleListNode(int val, Set<String> keys) {
this.val = val;
this.keys = keys;
}
}
六 扩展练习
1 平方根
实现 int sqrt(int x) 函数
analyse:
二分,注意用long防止溢出
code:
class Solution {
public int mySqrt(int x) {
long start = 0;
long end = x;
while (start + 1 < end) {
long mid = (start + end) / 2;
if (mid * mid <= x) {
start = mid;
} else {
end = mid;
}
}
if (end * end <= x) {
return (int)end;
}
return (int)start;
}
}