以下题目为本人在面试中遇到的题,以及在LeetCode刷题过程中,出现率较高的题目,如有错误或者更好的解题思路,欢迎各位大神帮忙提点。
成对的括号
判断括号成对出现
sdfj(nrg(lj()k)nk)sldjwef 合法
q(wdwf()hknkql(whdq)w) 合法
hk)nqeif)liq(h(flq)wj( 不合法
思路:括号存在嵌套的关系,也存在并列关系。可以遍历字符串的每一个字符,使用栈来处理:
(1)遇到左括号,把左括号压入栈中。
(2)遇到右括号,判断栈是否为空,为空说明没有左括号与之对应,则不合法。如果栈不为空,则移除栈顶的左括号---这对括号抵消了。
(3)当遍历结束后,如果栈是空的则合法,否则不合法。
const JudgeBracket = str => {
if(str === '') return true;
if(!str) return false;
const arr = [];
for(let i=0; i<str.length; i++) {
if(str[i] === '(') {
arr.push('(');
}
if(str[i] === ')') {
if(arr.length <= 0) return false;
arr.pop();
}
}
if(arr.length === 0) return true;
return false;
};
const str1 = 'sdfj(nrg(lj()k)nk)sldjwef';
const str2 = 'q(wdwf()hknkql(whdq)w)';
const str3 = 'hk)nqeif)liq(h(flq)wj( ';
console.log(JudgeBracket(str1)); // true
console.log(JudgeBracket(str2)); // true
console.log(JudgeBracket(str3)); // false
冒泡排序
(1)比较相邻的两个元素,如果前一个比后一个大,则交换位置。
(2)第一轮的时候,最后一个元素应该是最大的一个。
(3)按照步骤(1)的方法进行相邻两个元素比较,由于最后一个元素已经是最大的了,所以最后一个元素不用比较。
// 冒泡排序
let arr = [1, 6, 3, 7, 5, 9, 2, 8];
function sort(arr) {
// 升序
console.time("冒泡排序耗时")
let num = null
for(let i=0; i<arr.length-1; i++) {
// 外层循环的作用是:每次循环找出一个最大数放在这个数组的最后面
for(let j=0; j<arr.length-i-1; j++) {
// 内层循环的作用是:比较相邻两个数的大小从而进行交换位置
// 借助一个中间容器来交换位置
if(arr[j] > arr[j+1]){
num = arr[j]
arr[j] = arr[j+1]
arr[j+1] = num
}
}
}
console.log(arr);
console.timeEnd("冒泡排序耗时")
}
sort(arr)
快速排序
解析:快排是对冒泡排序的一种改进,第一趟排序时将数据分成两个部分,一部分比另一部分的所有数据都要小。然后递归调用,在两边都实现快速排序。
(1)从中间取一个数(称之为中位数),然后声明两个空数组
(2)遍历原始数组,小于中位数的放在左边,大于中位数的放右边
(3)递归调用第一步和第二步
快排比冒泡时间复杂度更小,平常用的更多
let arr = [1, 6, 3, 7, 2, 2, 2, 5, 9, 2, 8];
function sort(arr) {
// 递归出口
if(arr.length <= 1) return arr;
let middleIndex = Math.floor(arr.length / 2); // 中位数的下标
let middle = arr.splice(middleIndex, 1)[0]; // 取出中位数
let left = [];
let right = [];
for(let i=0; i<arr.length; i++) {
if(arr[i] < middle) {
left.push(arr[i]);
}else{
right.push(arr[i]);
}
}
return sort(left).concat([middle], sort(right));
}
console.log(sort(arr)); // [1, 2, 2, 2, 2, 3, 5, 6, 7, 8, 9]
零钱兑换
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
示例:
输入: coins = [1, 2, 5], amount = 11
输出: 3
解释: 11 = 5 + 5 + 1
输入: coins = [2], amount = 3
输出: -1
- 用dp[i]来表示找i块钱所需要的最少硬币数
- 外层遍历coins数组,内层遍历所有的amount
- 这种自底向上,每次都用更优解替代之前解法的思想就是动态规划
- 算法的基本思想就是遍历所有的可能,然后给出最优解
var coinChange = function(coins, amount) {
if(amount===0) return 0
let dp = new Array(amount+1).fill(Infinity)
dp[0] = 0 // 0块钱只需要找0枚硬币
for(let coin of coins){
for(let i=1; i<=amount; i++){
if(i-coin >=0){ // 进入条件:当前需要找的钱数大于等于当前循环中的硬币额度时
// i:当前要找零的钱数
// dp[i]:之前钱数为i时的找零方案(这里为使用的硬币数)
// dp[i-coin]+1:当前钱数减去用掉当前循环的一个硬币数,所以最后需要+1
dp[i] = Math.min(dp[i], dp[i-coin]+1)
}
}
}
return dp[amount]===Infinity ? -1 : dp[amount]
};
如果想要知道具体的找零方案是什么,只需要加一个cache缓存就可以了
var coinChange = function(coins, amount) {
if(amount===0) return 0
let dp = new Array(amount+1).fill(Infinity)
let cache = { // 这里缓存找零方案
"0": []
}
dp[0] = 0
for(let coin of coins){
for(let j=1; j<=amount; j++){
if(j-coin >=0){
if(dp[j]>dp[j-coin]+1){
dp[j] = dp[j-coin]+1
cache[j] = [...cache[j-coin], coin]
}
}
}
}
// return dp[amount]===Infinity ? -1 : dp[amount]
return cache[amount] ? cache[amount] : null
};
console.log(coinChange([1,3,4],7)) // [3,4]
动态规划就是要动态的调整最优解,与之对应的就是贪心算法
比如我们要找钱,目前零钱数为[1,3,4]三种硬币,如果我们要找6块钱
动态规划的方式:
- 先给1块钱看看
- 再给1块钱看看
- 再给1块钱,发现可以用1个3取代
- 最后结果:[3,3]
贪心算法的方式:(只考虑局部最优解)
- 上来先给4块
- 再给1块
- 再给1块
- 最后结果:[4,1,1]
接下来使用递归和动态规划的方式来解决找零这个问题
class Change{
constructor(changeType){
this.changeType = changeType
this.cache = {}
}
makeChange(amount){
if(!amount) return []
let min = []
// 开始找钱
if(this.cache[amount]) return this.cache[amount]
for(let i=0; i<this.changeType.length; i++){
// 先找一块钱试试,看看剩多少钱
const leftAmount = amount - this.changeType[i]
let newMin
if(leftAmount>=0){ // 说明没找完,需要再找一次
newMin = this.makeChange(leftAmount) // 这句是动态规划的体现
}
if(leftAmount>=0 && (newMin.length<min.length-1 || !min.length)){
// 说明新的找零的结果长度,小于旧的找零结果长度
// 获得一个硬币更少的找零方案
min = [this.changeType[i]].concat(newMin)
}
}
return this.cache[amount] = min
}
}
const change = new Change([1,3,4])
console.log(change.makeChange(2)) // [ 1, 1 ]
console.log(change.makeChange(6)) // [ 3, 3 ]
console.log(change.makeChange(7)) // [ 3, 4 ]
贪心算法
贪心算法是一种求近似解的思想。当能满足大部分最优解时,就认为符合逻辑要求。
比如刚刚的找零问题,零钱数为[1,3,4],找6块钱,动态规划给出的结果是[3,3],而贪心算法给出的结果是[4,1,1]。贪心算法会从硬币的最大值开始填充。
class Change{
constructor(changeType){
this.changeType = changeType.sort((a,b)=>b-a) // 硬币降序排列
}
makeChange(amount){
let arr = []
for(let i=0; i<this.changeType.length; i++){
while(amount - this.changeType[i] >= 0){
arr.push(this.changeType[i])
amount = amount - this.changeType[i]
}
}
return arr
}
}
const change = new Change([1,3,4])
console.log(change.makeChange(2)) // [ 1, 1 ]
console.log(change.makeChange(6)) // [ 4, 1, 1 ]
console.log(change.makeChange(7)) // [ 4, 3 ]
贪心算法相对简单,就是先怼最大的,大部分情况都没问题,但是有些情况不是最优解。
三数之和
给定一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?找出所有满足条件且不重复的三元组。
注意:答案中不可以包含重复的三元组。
- 首先对数组进行排序,排序后固定一个数nums[i](从数组第一项开始)
- 使用左右两个指针,左指针为nums[L](从 i+1 开始),右指针为nums[R](从 nums.length-1 开始)
- 计算三个数的和是否为0,是0就添加进结果集,进行下一次循环
- 如果nums[i]===nums[i-1],则说明该数字重复,会导致结果重复,所以应该跳过
- 当sum等于0时,如果 nums[L]===nums[L+1],则会导致结果重复,L++
- 当sum等于0时,如果 nums[R]===nums[R-1],则会导致结果重复,R--
- 这里有两层循环,所以时间复杂度为 O(n^2)
/**
* @param {number[]} nums
* @return {number[][]}
*/
var threeSum = function(nums) {
let res = [];
const len = nums.length;
if(nums == null || len < 3) return res;
nums.sort((a, b) => a - b); // 排序
for (let i = 0; i < len ; i++) {
if(nums[i] > 0) break; // 如果当前数字大于0,则三数之和一定大于0,所以结束循环
if(i > 0 && nums[i] == nums[i-1]) continue; // 去重
let L = i+1;
let R = len-1;
while(L < R){ // L和R分别为左指针和右指针
const sum = nums[i] + nums[L] + nums[R];
if(sum == 0){
res.push([nums[i],nums[L],nums[R]]);
while (L<R && nums[L] == nums[L+1]) L++; // 去重
while (L<R && nums[R] == nums[R-1]) R--; // 去重
L++;
R--;
}else if (sum < 0){
L++;
}else if (sum > 0){
R--;
}
}
}
return res;
};
斐波那契数
通常用 F(n) 表示,形成的序列称为斐波那契数列。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:
F(0) = 0, F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N > 1.
给定 N,计算 F(N)。
/**
* @param {number} N
* @return {number}
*/
var fib = function(N) {
if(N === 0 || N === 1){
return N
}
let cache = [] // 自底向上进行规划,这里记录缓存
for(let i=0; i<=N; i++){
if( i == 0 || i == 1 ){
cache[i] = i
}else{
cache[i] = cache[i-1] + cache[i-2]
}
}
return cache[N]
};
当然斐波那契数列简单实现的方式就是递归,但使用上述动态规划的思想,比递归性能会高很多。
有效的括号
给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。
有效字符串需满足:
1、左括号必须用相同类型的右括号闭合。
2、左括号必须以正确的顺序闭合。
/**
* @param {string} s
* @return {boolean}
*/
var isValid = function(s) {
let left = ['(', '[', '{']
let right = [')', ']', '}']
let stack = []
let arr = s.split('')
for(let i=0; i<arr.length; i++){
let temp = arr[i]
if(left.indexOf(temp) !== -1){
stack.push(temp)
}else{
if(left.indexOf(stack.pop()) === right.indexOf(temp)){
continue
}else{
return false
}
}
}
if(stack.length === 0){
return true
}else{
return false
}
};
Pow(x, n)
即计算 x 的 n 次幂
输入: 2.00000, 10
输出: 1024.00000
输入: 2.00000, -2
输出: 0.25000
解释: 2^-2 = 1/2 -> 1/4 = 0.25
/**
* @param {number} x
* @param {number} n
* @return {number}
*/
var myPow = function(x, n) {
if(n === 0) return 1
if(n<0) return 1 / myPow(x, -n)
// 二分+递归的思想,n每次向右移动一位,如果该位上有值(二进制),则结果 * x
// 每次让 x = x*x
if(n%2 === 1){
return myPow(x*x, Math.floor(n/2)) * x
}else{
return myPow(x*x, Math.floor(n/2))
}
};
N皇后
n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
上图为8皇后问题的一种解法。
给定一个整数n,返回所有不同的n皇后问题的解决方案。
每一种解法包含一个明确地n皇后问题的棋子放置方案,该方案中 ‘Q’和‘.’分别代表了皇后和空位。
输入: 4
输出: [
[".Q..", // 解法 1
"...Q",
"Q...",
"..Q."],
["..Q.", // 解法 2
"Q...",
"...Q",
".Q.."]
]
解释: 4 皇后问题存在两个不同的解法。
思路如下:
1、观察皇后攻击的索引特点,除了行、列不能相同之外,发现右侧的斜线 行-列 得到的数值相同,左侧的斜线 行+列 数值相同
- 行不能一样(这里按行查找)
- 列不能一样
- 行-列不能一样
- 行+列不能一样
2、设置一个temp=[],来记录之前的棋子摆放位置。temp的索引是行数据,值是列数据,例如:[2,4,1]代表第一行棋子摆放在2这个位置,第二行棋子摆放在4这个位置,第三行棋子摆放在1这个位置。
/**
* @param {number} n
* @return {string[][]}
*/
var solveNQueens = function(n) {
let ret = []
// 查找第1行
find(0)
return ret
function find(row, tmp=[]){
if(row===n){
// 找完了 n-1就已经是最后一行了 tmp就是所有的摆放位置
ret.push(tmp.map(c=>{
let arr = new Array(n).fill('.')
arr[c] = 'Q'
return arr.join('')
}))
}
for(let col=0; col<n; col++){
// 是不是不能放
let cantSet = tmp.some((ci,ri)=>{
// ci和ri是之前摆放棋子的行列索引
// col和row是当前所在位置的索引
return ci===col ||
(ri-ci)===(row-col) ||
(ri+ci)===(row+col)
})
if(cantSet) continue
// 如果能放,直接下一行
find(row+1, [...tmp, col])
}
}
};
青蛙跳台阶问题
一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。
输入:n = 7
输出:21
/**
* @param {number} n
* @return {number}
*/
var numWays = function(n) {
let cache = {}
if(n<2) return 1
for(let i=0; i<=n; i++){
if(i<2){
cache[i] = 1
}else{
cache[i] = (cache[i-1] + cache[i-2])
}
}
return cache[n]
};
一共n阶台阶,倒数第一步时,无论前面怎么走,都只有两种走法:走一步或走两步。两种走法的总数相加就是n阶台阶情况下的所有方式了,即:
f(n) = f(n-1) + f(n-2)
此时可以很明显的发现就是斐波那契数列。
将数组分成相等的三个部分
给你一个整数数组 A,只有可以将其划分为三个和相等的非空部分时才返回 true,否则返回 false。
形式上,如果可以找出索引 i+1 < j 且满足 (A[0] + A[1] + ... + A[i] == A[i+1] + A[i+2] + ... + A[j-1] == A[j] + A[j-1] + ... + A[A.length - 1]) 就可以将数组三等分。
输出:[0,2,1,-6,6,-7,9,1,2,0,1]
输出:true
解释:0 + 2 + 1 = -6 + 6 - 7 + 9 + 1 = 2 + 0 + 1
思路:
- 想要将数组等分成3段的话,那么数组的累加和就一定能被3整除
- 计算出平均值后,遍历数组。每当值达到平均值的时候,对count进行加1操作
- 当count等于2的时候,说明已经分好了2等分了,这时候判断当前循环的i是否与A.length-1相等,如果相等的话,说明只能分成2等分。否则不管后面有多少个数字,它们的累加和一定等于平均值。
/**
* @param {number[]} A
* @return {boolean}
*/
var canThreePartsEqualSum = function(A) {
let sum = A.mySum(); // 数组的累加和
if(sum % 3 !== 0) return false; // 不能被3整除,则return false
let avgrage = parseInt(sum/3); // 每个部分应该累加的和
let temp = 0; // 记录每部分的累加
let count = 0; // 记录分成部分的个数
for(let i=0;i<A.length;i++) {
temp += A[i];
if(temp === avgrage) { // 累加和达到平均值,说明可分为一个部分
temp = 0; // 重置为0,准备下一部分的累加
count++;
if(count === 2) {
// count为2时,说明已经分好了两个部分
if(i === A.length-1) {
return false;
}
return true;
}
}
}
return false;
};
Array.prototype.mySum = function() {
return this.reduce((a,b)=>a+b, 0);
}
礼物最大价值
在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?
输入:
[
[1,3,1],
[1,5,1],
[4,2,1]
]
输出: 12
解释: 路径 1→3→5→2→1 可以拿到最多价值的礼物
思路:
棋盘问题寻求路径的最大值,如果使用动态规划的思想,可以新建立一个缓存棋盘cache,值为到达每个位置时的最大路径(这里是礼物价值),这个缓存棋盘的行列比原棋盘都多1,可以确保边界问题。
由于这里只能向下或向右前进,每个到达位置的最大路径为:Math.max(上边位置的值,左边位置的值) 加上棋盘当前位置的值
/**
* @param {number[][]} grid
* @return {number}
*/
var maxValue = function(grid) {
const row = grid.length
const col = grid[0].length
let cache = []
// cache缓存的行列都比grid多一条,就不需要考虑边界问题了
for(let i=0; i<row+1; i++){
let arr = new Array(col+1).fill(0)
cache[i] = arr
}
for(let i=0; i<row; i++){
for(let j=0; j<col; j++){
// cache当前位置的值
// 是由上方的值和左方的值比较大小后,加上grid当前的值
cache[i+1][j+1] = Math.max(cache[i][j+1], cache[i+1][j])
+ grid[i][j]
}
}
return cache[row][col]
};
字符串的最大公因子
对于字符串 S 和 T,只有在 S = T + ... + T(T 与自身连接 1 次或多次)时,我们才认定 “T 能除尽 S”。
返回最长字符串 X,要求满足 X 能除尽 str1 且 X 能除尽 str2。
输入:str1 = "ABCABC", str2 = "ABC"
输出:"ABC"
输入:str1 = "ABABAB", str2 = "ABAB"
输出:"AB"
输入:str1 = "LEET", str2 = "CODE"
输出:""
看到标题中有最大公因子这个词,可以考虑一下辗转相除
const gcd = (a, b) => (0 === b ? a : gcd(b, a % b))
- 如果它们有公因子 abc,那么str1就是m个abc的重复,str2就是n个abc的重复,连起来就是m+n个abc。m+n个abc与n+m个abc是一样的。
- 所以如果 str1 + str2 === str2 + str1,就意味着有解
- 当确定有解的情况下,最优解是长度为 gcd(str1.length, str2.length) 的字符串
var gcdOfStrings = function(str1, str2) {
if (str1 + str2 !== str2 + str1) return ''
const gcd = (a, b) => (0 === b ? a : gcd(b, a % b))
return str1.substring(0, gcd(str1.length, str2.length))
};
根据字符出现频率排序
给定一个字符串,请将字符串里的字符按照出现的频率降序排列。
输入: "tree"
输出: "eert"
解释:
'e'出现两次,'r'和't'都只出现一次。
因此'e'必须出现在'r'和't'之前。此外,"eetr"也是一个有效的答案。
输入: "cccaaa"
输出: "cccaaa"
解释:
'c'和'a'都出现三次。此外,"aaaccc"也是有效的答案。
注意"cacaca"是不正确的,因为相同的字母必须放在一起。
输入: "Aabb"
输出: "bbAa"
解释:
此外,"bbaA"也是一个有效的答案,但"Aabb"是不正确的。
注意'A'和'a'被认为是两种不同的字符。
思路:
- 先遍历一次字符串,计算字符串中各个字符出现的次数
- 对数据进行降序排序
- 最后进行字符串的拼接
/**
* @param {string} s
* @return {string}
*/
var frequencySort = function(s) {
let map = new Map()
let temp = []
let str = ''
// 循环字符串,并塞进map(去重+统计次数)
for(let i=0; i<s.length; i++){
let char = s[i]
if(map.has(char)){
map.set(char, map.get(char)+1)
}else{
map.set(char, 1)
}
}
map.forEach((value, key)=>{
temp.push({char:key, numer:value})
})
// 降序排序
temp.sort((a,b)=>{
return b.numer - a.numer
})
// 字符串拼接
temp.forEach(item=>{
str = str + item.char.repeat(item.numer)
})
return str
};
M个人相互传球,由甲开始,经过N次传球后,球仍回到甲手中,则不同的传球方式共有多少种?
(m-2)*(n-1)
思路:球在传递过程中是不能传递给自己的
可以想象它是一颗树
假设:甲乙丙三个人传球,传5次,最后球在甲手中的传球方式。
// 甲乙丙三个人传球,由甲先传,传5次,列出最后球落在甲手上的所有传球方式,这里用 0 1 2 分别代表甲乙丙
function getBool(persons, total) {
const res = [];
find(0);
return res;
// bool:当前球在谁手上
// temp:之前传球的记录
function find(bool, temp = []) {
if (temp.length === total) { // 传球达到了5次
if (bool === 0) { // 当前球在甲手上
res.push([bool, ...temp]); // 最后补上一开始球在谁手上
}
return;
}
for (let i = 0; i < persons.length; i++) {
if (i === bool) continue; // 不能传球给自己
find(i, [...temp, i]);
}
}
}
console.log(getBool([0, 1, 2], 5));