leetcode刷题笔记

180 阅读5分钟

大部分都是看了题解才做出来的,所以思路和实现都跟题解差不多

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,数字组成就变为了小数字位+大数字位,这时候数值的大小==大数字-小数字
  • 得出结论:
    1. 当左边罗马数字>右边的时候,数字大小==左边罗马数字对应的大小+右边对应的大小(大+小)
    2. 当左边<右边时,数字大小==左边罗马数的负值+右边罗马数(-小+大,即大-小)
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 递归

image.png

  • 当需要翻译 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;
};