Repository navigation
【018-week4】算法训练营第四周学习总结 #205
Description
学习笔记
LeetCode_720_词典中最长的单词
题目
给出一个字符串数组words组成的一本英语词典。从中找出最长的一个单词,该单词是由words词典中其他单词逐步添加一个字母组成。若其中有多个可行的答案,则返回答案中字典序最小的单词。
若无答案,则返回空字符串。
示例 1:
输入:
words = ["w","wo","wor","worl", "world"]
输出: "world"
解释:
单词"world"可由"w", "wo", "wor", 和 "worl"添加一个字母组成。
示例 2:
输入:
words = ["a", "banana", "app", "appl", "ap", "apply", "apple"]
输出: "apple"
解释:
"apply"和"apple"都能由词典中的单词组成。但是"apple"得字典序小于"apply"。
注意:
所有输入的字符串都只包含小写字母。
words数组长度范围为[1,1000]。
words[i]的长度范围为[1,30]。
解法一
思路
使用字符串排序的方法,原因:
- 长的单词必定需要由短的单词扩展而来,所以按照字符长度遍历的顺序和解题的要求符合
- 如果结果长度相同,按照字符ASCII大小排序
过程:
- 有一个Set set存储符合要求的word,有一个result变量存储最终结果
- 对String数组 words排序
- 遍历数组,判断word,如果长度==1或者set里面有当前word去掉最后一个字符的字(因为是排序过的,所以比当前word短的word都判断过了,排序后的好处),就存到set里
- 通过字符的长度决定result是哪个
- 如果遍历到的字比结果长度长,就替换result
- 如果遍历到的字比结果长度短,result不变
- 如果遍历到的字和结果长度相同,result也不变,因为它的ASCII码肯定比result大,排序的好处
- 遍历结束,返回result
代码
class Solution {
public String longestWord(String[] words) {
Arrays.sort(words);
String result = "";
Set<String> set = new HashSet<>();
for (String word: words) {
if (word.length() == 1 || set.contains(word.substring(0, word.length() - 1))) {
set.add(word);
result = word.length() > result.length() ? word : result;
}
}
return result;
}
}解法二
思路
构建一个字典树来解这道题。
- 字典树节点属性:
- 当前节点的字符内容data
- 当前节点是否有效isWord
- 保存下层节点指针的数组next
- 字典树函数:
- 插入insert():
- 把字符串中每个字符按顺序生成字典树节点放入字典树中
- 将最有一个节点的isWord属性设置为true(起初在自己思考时,一直想不清如果我把每个字符串的字符都放入字典树中,怎么判断这个字符串是有效的,后来才想通,因为这个单词是依赖整个数组中所有单词衍生而来,所以只要标记最后一个字符,在后面检查的阶段中,只要这个单词的前几个字符所对应的字典树节点的isWord属性没有设置为true,就说明不符合题意)
- 判断是否是符合要求的字符串isBuild()
- 遍历整个字符数组
- 从根节点出发依次必对字符对应的节点isWord属性是否为true
- 插入insert():
- 主方法:
- 遍历整个字符串数组,依次insert,生成字典树
- 再遍历字符串数组,获得符合题目要求同时字符串长度最长的单词
- 如果有若干单词长度一样的,就再依次比对字符,获得最后的结果
代码
class Solution {
public String longestWord(String[] words) {
TrieTree trieTree = new TrieTree();
for (String word: words) {
trieTree.insert(word);
}
String result = "";
int len = 0;
for (String word: words) {
if (trieTree.isBuild(word) && word.length() > len) {
result = word;
len = result.length();
} else if (trieTree.isBuild(word) && word.length() == len) {
char[] wordCs = word.toCharArray();
char[] resultCs = result.toCharArray();
for (int i = 0; i < len; i++) {
if (wordCs[i] < resultCs[i]) {
result = word;
break;
} else if (wordCs[i] > resultCs[i]) {
break;
}
}
}
}
return result;
}
private class TrieTreeNode {
private char data;
private boolean isWord;
private TrieTreeNode[] next;
public TrieTreeNode(char data) {
this.data = data;
this.next = new TrieTreeNode[26];
}
}
private class TrieTree {
TrieTreeNode root = new TrieTreeNode('/');
public void insert(String word) {
char[] cs = word.toCharArray();
TrieTreeNode node = root;
for (char c: cs) {
int index = c - 'a';
if (node.next[index] == null) {
node.next[index] = new TrieTreeNode(c);
}
node = node.next[index];
}
node.isWord = true;
}
public boolean isBuild(String word) {
char[] cs = word.toCharArray();
TrieTreeNode node = root;
for (char c: cs) {
int index = c - 'a';
if (!node.next[index].isWord) {
return false;
}
node = node.next[index];
}
return true;
}
}
}收获
熟悉和练习了字典树结构及使用的解法。
LeetCode_169_求众数
题目
给定一个大小为 n 的数组,找到其中的众数。众数是指在数组中出现次数大于 ⌊ n/2 ⌋ 的元素。
你可以假设数组是非空的,并且给定的数组总是存在众数。
示例 1:
输入: [3,2,3]
输出: 3
示例 2:
输入: [2,2,1,1,1,2,2]
输出: 2
解法一
思路
暴力解法,直接用哈希表统计num的个数,当超过n/2的时候就返回。
- 时间复杂度:O(N),最坏情况遍历整个数组
- 空间复杂度:O(N),最坏情况保存n/2个元素
代码
class Solution {
public int majorityElement(int[] nums) {
Map<Integer, Long> map = new HashMap<>();
int len = nums.length / 2;
for (Integer num: nums) {
map.compute(num, (k, v) -> v == null ? 1 : ++v);
if (map.get(num) > len) {
return num;
}
}
return 0;
}
}解法二
思路
对数组排序,因为众数的定义是大于n/2个,所以在对数组排序后,下标n/2对应的元素一定是众数
代码
class Solution {
public int majorityElement(int[] nums) {
Arrays.sort(nums);
return nums[nums.length / 2];
}
}解法三
思路
使用分治算法:
- 不断切分整个数组,直到数组长度为1
- 切下的左右两个子区间,去求它们的众数
- 如果众数是一样的,那么就返回这个数
- 如果众数不同,谁众数的数量多,那个就是众数,返回
- 回溯到最后,返回那个值
代码
class Solution {
public int majorityElement(int[] nums) {
return divideConquer(nums, 0, nums.length - 1);
}
private int divideConquer(int[] nums, int left, int right) {
if (left == right) {
return nums[left];
}
int mid = (right - left) / 2 + left;
int leftMajorityNum = divideConquer(nums, left, mid);
int rightMajorityNum = divideConquer(nums, mid + 1, right);
if (leftMajorityNum == rightMajorityNum) {
return leftMajorityNum;
}
long leftCount = count(nums, leftMajorityNum, left, mid);
long rightCount = count(nums, rightMajorityNum, mid + 1, right);
return leftCount > rightCount ? leftMajorityNum : rightMajorityNum;
}
private int count(int[] nums, int num, int left, int right) {
int count = 0;
for (int i = left; i <= right; i++) {
if (nums[i] == num) {
count++;
}
}
return count;
}
}收获
熟悉和练习了分治算法
LeetCode_455_分饼干
题目
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。对每个孩子 i ,都有一个胃口值 gi ,这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j ,都有一个尺寸 sj 。如果 sj >= gi ,我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
注意:
你可以假设胃口值为正。
一个小朋友最多只能拥有一块饼干。
示例 1:
输入: [1,2,3], [1,1]
输出: 1
解释:
你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。
虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。
所以你应该输出1。
示例 2:
输入: [1,2], [1,2,3]
输出: 2
解释:
你有两个孩子和三块小饼干,2个孩子的胃口值分别是1,2。
你拥有的饼干数量和尺寸都足以让所有孩子满足。
所以你应该输出2.
解法一
思路
- 对两个数组从小到大排序
- 使用两个循环,外面的循环遍历小孩的胃口值数组
- 里面的循环遍历饼干分量
- 区别是里面的循环不不需要每次都从头开始遍历,记录迭代的下标位置,每次都从那个位置继续,等同于这块饼干无论符不符合要求,要么被放弃了,要么被吃掉了
- 判断当前的饼干是否满足当前小孩的胃口,如果满足就计数+1,否则就找下一块饼干
- 直到饼干找完,或小孩找完为止,返回count。
代码
class Solution {
public int findContentChildren(int[] g, int[] s) {
Arrays.sort(g);
Arrays.sort(s);
int j = 0, count = 0, sl = s.length;
for (int value : g) {
while (j < sl) {
if (value <= s[j++]) {
count++;
break;
}
}
if (j == sl) {
break;
}
}
return count;
}
}代码优化
class Solution {
public int findContentChildren(int[] g, int[] s) {
Arrays.sort(g);
Arrays.sort(s);
int gi = 0, si = 0, gl = g.length, sl = s.length;
while (gi < gl && si < sl) {
if (g[gi] <= s[si]) {
gi++;
}
si++;
}
return gi;
}
}- 直接移动两个下标,循环时任意一个越界就停止。
- 孩子胃口的下标移动条件是满足饼干满足,同时移动的下标也代表了有多少个孩子吃饱了
- 无论是否满足要求,都移动饼干下标,要么吃掉了,要么遗弃了
时间复杂度:O(NlogN),孩子胃口N,饼干logN
空间复杂度:O(1)
收获
熟悉和练习的贪心算法,在简化代码过程中也得到了学习
LeetCode_784_字母大小写全排列
题目
给定一个字符串S,通过将字符串S中的每个字母转变大小写,我们可以获得一个新的字符串。返回所有可能得到的字符串集合。
示例:
输入: S = "a1b2"
输出: ["a1b2", "a1B2", "A1b2", "A1B2"]
输入: S = "3z4"
输出: ["3z4", "3Z4"]
输入: S = "12345"
输出: ["12345"]
注意:
S 的长度不超过12。
S 仅由数字和字母组成。
解法一
思路
使用回溯算法进行搜索
- 定义一个函数进行回溯,需要如下两个参数
- 被遍历的字符串(补充:因String对象为final修饰的不可变类型,所以每次修改字符串会额外创建一个新的String对象,改为使用StringBuilder对象)
- 当前层需处理的字符下标
- 类似递归,以字符下标作为层数,一层层的下钻处理逻辑
- 处理逻辑:
- 当前要处理的字符是数字就继续下钻
- 否则就分别改变当前字符为大小写,分别下钻。
- 当字符串被搜索完毕,就将字符串放入一个list中,同时返回上一层
- 整个过程结束后,将list返回
代码
class Solution {
List<String> list = new ArrayList<>();
public List<String> letterCasePermutation(String S) {
dfs(new StringBuilder(S), 0);
return list;
}
private void dfs(StringBuilder sb, int index) {
if (index == sb.length()) {
list.add(sb.toString());
return;
}
if (Character.isDigit(sb.charAt(index))) {
dfs(sb, index + 1);
} else {
char c = sb.charAt(index);
sb.setCharAt(index, Character.toLowerCase(c));
dfs(sb, index + 1);
sb.setCharAt(index, Character.toUpperCase(c));
dfs(sb, index + 1);
}
}
}优化代码
参考了国内站的另一种回溯解法,其中讨论了为什么在步骤类似的情况下,这种算法的时间会更短,他的结论是在做判断字符是否为数字的动作时,直接使用ASCII码进行比较比Character.isDigit方法更快。
我也进行了尝试。而我的另一个不同是,我使用的是StringBuilder对象,而其使用的是字符数组。在使用优化的方法后,代码的执行时间果然减少了,同时空间占用的大小也减小了,原因应该是StringBuilder类型的对象中还维护了一些题目中不需要的属性。
class Solution {
List<String> list = new ArrayList<>();
public List<String> letterCasePermutation(String S) {
dfs(S.toCharArray(), 0);
return list;
}
private void dfs(char[] cs, int index) {
if (index == cs.length) {
list.add(new String(cs));
return;
}
if (cs[index] < 'A') {
dfs(cs, index + 1);
} else {
char c = cs[index];
if (c > 64 && c < 91) {
dfs(cs, index + 1);
cs[index] = (char) (cs[index] + 'a' - 'A');
dfs(cs, index + 1);
} else {
dfs(cs, index + 1);
cs[index] = (char) (cs[index] + 'A' - 'a');
dfs(cs, index + 1);
}
}
}
}收获
熟悉和练习了回溯算法
LeetCode_70_爬楼梯
题目
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
示例 1:
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶
示例 2:
输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶
失败解法
思路
使用递归的方法,和之前做的一个求斐波那契数列的题目类似,代码和思路都很直观、简单,但超时了。
时间复杂度:O(2^n)
代码
class Solution {
public int climbStairs(int n) {
return n <= 2 ? n : climbStairs(n - 1) + climbStairs(n - 2);
}
}解法一
思路
求助国内站的官方解法二,其对解法一(思路和我的失败解法类似)进行了优化,用到了记忆化递归。因为每次下钻的过程中,会有重复的步数,把已经计算过的部署放在数组里记录,就可以省去很多的步骤。
代码
class Solution {
public int climbStairs(int n) {
return climbStairs(n, new int[n + 1]);
}
private int climbStairs(int n, int[] nums) {
if (n <= 2) {
return n;
}
if (nums[n] > 0) {
return nums[n];
}
int num = climbStairs(n - 1, nums) + climbStairs(n - 2, nums);
nums[n] = num;
return num;
}
}解法二
思路
看了国内站官方解法三对于动态规划解法的讲解,发现其思路和我解法一二的思路有部分是基本一致的:dp[i] = dp[i - 1] + dp[i - 2]。
从覃老师周三一开始对动态规划的讲解中了解到,这题是属于一维的动态规划的,使用一维数组dp[]
- 数组下标对应的是台阶数
- 下标对应的元素代表的是走当前台阶数有的解法
因为题目定义每次只能是1或者2步,所以到达当前台阶i的必由路径一定是
- i - 1级台阶直接走1级
- i - 2级台阶直接走2级
于是到达i级台阶的方法数dp[i]就是dp[i - 1] + dp[i - 2],也就是从第3级开始,每一级的结果就是前一两级结果之和。
- 时间复杂度:O(N)
- 空间复杂度:O(N)
代码
class Solution {
public int climbStairs(int n) {
if (n <= 2) {
return n;
}
int[] dp = new int[n + 1];
dp[1] = 1;
dp[2] = 2;
for (int i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
}优化代码
在写如上代码时发现,其实整个过程主要出现的就是三个变量
- 当前的数
- 前一位的数
- 前二位的数
所以不需要使用一个n+1位的数组来保存当前值,只需要3个变量就可以了。于是对解法二的代码进行了优化
class Solution {
public int climbStairs(int n) {
if (n <= 2) {
return n;
}
int first = 1, second = 2;
for (int i = 3; i <= n; i++) {
int third = first + second;
first = second;
second = third;
}
return second;
}
}但执行结果显示,空间没有减少,没办法解释,希望有人解惑。
解法三
思路
使用数学公式解,真的快。
代码
class Solution {
public int climbStairs(int n) {
double sqrt5=Math.sqrt(5);
double fibn=Math.pow((1+sqrt5)/2,n+1)-Math.pow((1-sqrt5)/2,n+1);
return (int)(fibn/sqrt5);
}
}收获
练习和熟悉了动态规划及递归,还学了点数学。
LeetCode_208_实现Trie(前缀树)
题目
解法
思路
和720题构建Trie树的思路基本一致。
代码
class Trie {
private static final int SIZE = 26;
private TrieNode root;
public Trie() {
root = new TrieNode();
}
public void insert(String word) {
if (isEmpty(word)) {
return;
}
char[] cs = word.toCharArray();
TrieNode node = root;
for (char c: cs) {
int index = c - 'a';
if (node.next[index] == null) {
node.next[index] = new TrieNode();
node.next[index].data = c;
}
node = node.next[index];
}
node.isWord = true;
}
public boolean search(String word) {
if (isEmpty(word)) {
return false;
}
char[] cs = word.toCharArray();
TrieNode node = root;
for (char c: cs) {
int index = c - 'a';
if (node.next[index] != null) {
node = node.next[index];
} else {
return false;
}
}
return node.isWord;
}
public boolean startsWith(String prefix) {
if (isEmpty(prefix)) {
return false;
}
TrieNode node = root;
char[] cs = prefix.toCharArray();
for (char c: cs) {
int index = c - 'a';
if (node.next[index] != null) {
node = node.next[index];
} else {
return false;
}
}
return true;
}
private boolean isEmpty(String word) {
return word == null || word.length() == 0;
}
private class TrieNode {
private char data;
private TrieNode[] next;
private boolean isWord;
TrieNode() {
this.next = new TrieNode[SIZE];
this.isWord = false;
}
}
}收获
再次熟悉了Trie树的构建代码。同时在构建过程中,发现startWith和search有一部分代码是重复的,在想要抽出这部分代码复用的过程中,没有考虑到函数中的引用指针的问题。
class Problem {
boolean search(String word) {
TrieNode node = root;
boolean hasStr = hasStr(word, node);
return hasStr ? node.isWord : hasStr;
}
private boolean hasStr(String word, TrieNode node) {
for (char c: word.toCharArray()) {
//......
if (node.next[index] == null) {
return false;
}
node = node.next[index];
}
return true;
}
}这里的node其实还是指向的root。最后只能老实的继续在两个函数中写类似的代码。
Activity