大部分都是看了题解才做出来的,所以思路和实现都跟题解差不多
20-有效的括号
题目:
- 给定一个只包括'(',')','{','}','[',']' 的字符串,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
注意空字符串可被认为是有效字符串。
思路:
- 当开始匹配时,如果是左括号,它要等待跟右括号匹配,此时需要有一个变量来存储这个左括号。
当遍历到右括号时,会让它先跟最近的一个左括号去匹配,那么最后被 push 进来的左括号会是先出去的,这就很符合“后进先出”的栈的特性,所以这个变量就用栈来实现。
const isValid = function (s) {
if (s.length % 2 != 0 || [']', ')', '}'].indexOf(s[0]) != -1) {
return false; //如果长度为奇数或者右括号开头肯定不对
}
//定义一个栈
const stack = [];
for (const cur of s) {
if (cur === '{' || cur === '[' || cur === '(') {
//检测到左括号,推入栈
stack.push(cur);
} else {
// 左括号匹配完毕
//拿到stack中的最后一个左括号
const stackTop = stack[stack.length - 1];
//去跟现在的括号匹配
if (
(stackTop == '(' && cur == ')') ||
(stackTop == '[' && cur == ']') ||
(stackTop == '{' && cur == '}')
) {
//如果匹配成功(stack中的最后一个左括号可以跟当前这个括号组成一对有效的括号),就把这个左括号从stack中拿出去
stack.pop();
} else {
//匹配不成功
return false;
}
}
}
//遍历完毕,如果stack空了,说明全部匹配成功,s有效
//如果stack没空,说明s不是有效字符串
return stack.length == 0;
};
13-罗马数字转整数
题目:
- 将罗马数字转换为整数。输入的数字在 1-3999
罗马数字包含的字符: I(1),V(5),X(10),L(50),C(100),D(500),M(1000)
注意 I 可以放在 V 和 X 左边表示 4 和 9
X 放在 L 和 C 左边表示 40 和 90
C 放在 D 和 M 左边表示 400 和 900
思路:
- 一般情况下,罗马数字大小为:大的数字位+小的数字位,如 VI:6,VII:7,可知数值==大数字+小数字
- 当进位值(个/十/百/千)为 4 或 9,数字组成就变为了小数字位+大数字位,这时候数值的大小==大数字-小数字
- 得出结论:
- 当左边罗马数字>右边的时候,数字大小==左边罗马数字对应的大小+右边对应的大小(大+小)
- 当左边<右边时,数字大小==左边罗马数的负值+右边罗马数(-小+大,即大-小)
const romanToInt = function (s) {
//生成一个hash表存放用于对照的数字
const hashNum = {
I: 1,
V: 5,
X: 10,
L: 50,
C: 100,
D: 500,
M: 1000
};
let res = 0;
for (let i = 0; i < s.length; i++) {
hashNum[s[i]] < hashNum[s[i + 1]] ? (res -= hash[s[i]]) : (res += hashNum[s[i]]);
}
return res;
};
1518-空瓶换酒
题目:
- 用 numExchange 个空酒瓶可以兑换一瓶新酒。你购入了 numBottles 瓶酒。 如果喝掉了酒瓶中的酒,那么酒瓶就会变成空的。 请你计算 最多 能喝到多少瓶酒。
思路 1:
- 按正常流程走,最初买了 numsBottles 瓶酒,那么可以喝到这么多的酒,并且得到同样数量的空瓶,然后得到的空瓶拿去换酒,每换一瓶酒,空瓶的数量就会少 numxExchange 个,然后喝完这瓶酒,喝到的酒喝空瓶的数量又都+1
const numWaterBottles = function (numBottles, numExchange) {
let res = numBottles; // 喝到的酒
let empty = numBottles; // 空瓶
while (empty >= numExchange) {
empty -= numExchange;
res++;
empty++;
}
return res;
};
思路 2:
- 设空瓶价值为 1,那么要兑换一瓶新的酒所需的价值就是 numExchange - 1 现在我们有 numBottles 瓶酒,但是不管我们怎么换新的酒,最后一定会有不足以 numExchange 的数量 这个数量一定是 >= 1,所以我们当前的一个总价值应该为 numBottles * numExchange - 1
const numWaterBottles = function (numBottles, numExchange) {
return Math.floor((numBottles * numExchange - 1) / (numExchange - 1));
};
22-括号生成
题目:
- n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且有效的括号组合
示例: n = 3
输出:[
"((()))",
"(()())",
"(())()",
"()(())",
"()()()"
- 思路: 使用递归。 - 递归终止的条件:生成的括号符合要求,即左右括号的数量都==n - 对当前状态的处理:在要添加括号时,通过数量来判断需要添加左括号还是右括号,添加括号之后下次调用时相应括号数量+1
解法
const generateParenthesis = function (n) {
let res = [];
let help = (s, left, right) => {
//递归终止的条件
if (left == n && right == n) {
return res.push(s);
}
//当left<n,那么左括号的数量还不够,要添加左括号
if (left < n) {
//添加之后left+1
help(s + '(', left + 1, right);
}
//左括号的数量保证了,看右括号
if (right < left) {
help(s + ')', left, right + 1);
}
};
//最开始的时候s是空的,左右括号的数量都是0
help('', 0, 0);
return res;
};
290-单词排列规律
题目:
- 给定一种规律
pattern
和一个字符串str
,判断str
是否遵循相同的规律。str 为用空格分开的英文单词
比如 pattern 为 aba 那么 str 就需要满足 第一个和第三个单词是一样的,比如 str 可以为hello world hello
-
思路:
-
既然是 str 按照 pattern 的规则排列,那么 str 里单词的数量和 pattern 字母的数量就是一致的,即将 str 按空格进行切割为数组后,
str.length === pattern.length
-
如果切割之后,二者长度不一致,那么就必为 false
-
二者规则一致,那么其内元素的排列顺序就是一致的,即对二者相同位置的元素进行 indexOf,返回应是一致的(即使像上边的例子一样有重复单词,只要二者规则一致,那么都会返回第一个 hello 的 index)
-
若不能满足这些,就返回 false
-
解法
const wordPattern = function (pattern, str) {
const arr = str.split(' ');
if (arr.length !== pattern.length) {
return false;
}
for (let i = 0; i < pattern.length; i++) {
if (pattern.indexOf(pattern[i]) != arr.indexOf(arr[i])) {
return false;
}
}
return true;
};
139-单词拆分
题目:
- 给定一个英文字符串 s,和一部词典 wordDict,判断 s 是否能被拆分为在词典中出现的单词,词典中的单词可以多次使用,即:
输入:s = 'dogcatdog',wordDict = ['dog','cat'] //true
输入:s = 'leetcode',wordDict = ['leet','code'] //true
输入:s = 'catdog',wordDict = ['cat','dog','mouse'] //false
解法
-
’leetcode‘能否拆分,可以依次拆分成: i 是否在词典内,剩余子串是否可以 break;le 是否在词典内,剩余子串是否可以 break.....
-
使用 DFS 回溯,从左到右考察所有的可能性
-
如果指针左侧部分是词典中的单词,那么递归考察右边
-
如果指针左边不在词典中,那么不必进行递归,开始考察下一个分支
-
缺陷 DFS 算法中如果不记忆已经做过的计算,那么很有可能因为重复计算过多,造成计算时间过长,从而无法通过测试用例,因此要加入记忆化
const wordBreak = (s, wordDict) => {
const len = s.length;
const wordSet = new Set(wordDict);
const memo = new Array(len); //加入记忆化,记忆已经做过的操作
const canBreak = start => {
//判断从start开始的字符串能否break
//一直break到了最后,递归结束
if (start == len) return true;
//memo本身只有长度没有元素,那么当这个位置的元素不为undefine,说明这里已经遍历过了
if (memo[start] !== undefined) return memo[start];
for (let i = start + 1; i <= len; i++) {
const prefix = s.slice(start, i); //截取看是否在字典里
if (wordSet.has(prefix) && canBreak(i)) {
//这部分字符串可以匹配到字典,而且剩余的进行递归也可以
memo[start] = true;
return true;
}
}
memo[start] = false; //递归完毕不符合条件
return false;
};
return canBreak(0); //开始递归
};
剑指 offer46-翻译数字为字符串
题目:
- 0-25 依次可以翻译为 a-z,如果超过了 25,会有不同的翻译
比如 12258,可以有 5 种翻译:"bccfi", "bwfi", "bczi", "mcfi"和"mzi"
也就是说可以将数字拆开翻译
实现函数,计算输入的数字有几种翻译方法
思路 1 DFS 递归
-
当需要翻译 2163,可以分成:翻译 2、163;翻译 21、63;
-
每次都有两种选择:翻译 1 个数和将两个数作为整体翻译,但如果这个数>25,就无法整体翻译,这时候就只有一种选择
-
指针从左向右,这两种选择对应了两个分支,如果只有一种选择就只有一个分支
-
当指针能到达边界或者越界,代表这条分支成功走到了最后,返回 1。这样无法翻译的分支就不会返回值,单个数值也会直接返回 1
function translateNum(num) {
//转化为字符串
const str = num.toString();
//定义递归函数,随着dfs向下,指针右移
const dfs = (str, pointer) => {
//指针抵达边界、超出边界,返回1
if (pointer >= str.length - 1) return 1;
//考察当前位置和下一位置组成的两位数
const temp = Number(str[pointer] + str[pointer + 1]);
//只有在[10,25]的两位数才能被整体直接翻译
if (temp >= 10 && temp <= 25) {
//将向后一位和向后两位两个分支进行考察,最后将两个分支相加
return dfs(str, pointer + 1) + dfs(str, pointer + 2);
} else {
//temp不能被整体翻译,那么只有一种翻译办法,即两个数字单独翻译
return dfs(str, pointer + 1);
}
};
return dfs(str, 0); //开始递归
}
思路 2 DFS+记忆化
- 在思路 1 的 DFS 中,可以明显看到,位于后边的几位数下的分支被重复计算了很多次
- 如果将计算过的结果进行存放,就可以减少大量计算
- 声明一个备忘录 memo,存入两个处于底部的子树的结果(数字最后两位),当 DFS 运行到符合这一条件,就可以直接使用它。递归的结果从下往上 return 的过程中,子树的运算结果会被不断抄录到 memo 中
const translateNum = num => {
const str = num.toString();
const n = str.length;
const memo = new Array(n);
memo[n - 1] = 1;
memo[n] = 1;
const dfs = (str, pointer, memo) => {
// 计算过程中会存memo,memo里有了说明计算过了,直接取过来用即可
if (memo[pointer]) return memo[pointer];
//取当前和下一位组成一个两位数
const temp = Number(str[pointer] + str[pointer + 1]);
if (temp >= 10 && temp <= 25) {
//计算过的结果存入memo,而不是直接返回
memo[pointer] = dfs(str, pointer + 1, memo) + dfs(str, pointer + 2, memo);
} else {
memo[pointer] = dfs(str, pointer + 1, memo);
}
return memo[pointer]; //计算结果向上return
};
return dfs(str, 0, memo);
};
941-山脉数组
题目:
- 如果满足以下条件就是山脉数组:
- A.length>=3
- 在 0 < i < A.length - 1 条件下,存在 i 使得:A[0] < A[1] < ... A[i-1] < A[i];
A[i] > A[i+1] > ... > A[A.length - 1]
- 简单说,一个数组大于 3 项,从第一项起开始递增,达到最大值后开始递减,就是山脉数组。
//示例1
输入:[2,1]
输出:false
//示例 2:
输入:[3,5,5]
输出:false
//示例 3:
输入:[0,3,2,1]
输出:true
思路
- 双指针,左指针 i 满足
A[i+1]>A[i]
的话就向右走一位,右指针 j 满足A[j-1]<A[j]
的话就向左走一位,那么如果两个指针能走到同一个位置,且至少走了一步,那么这个数组就是山脉数组了
const validMountainArray = function (A) {
const n = A.length;
let i = 0;
let j = n - 1;
while (i + 1 < n && A[i] < A[i + 1]) {
i++;
}
while (j - 1 > 0 && A[j - 1] > A[j]) {
j--;
}
if (i != 0 && i == j && j != n - 1) {
return true;
}
return false;
};