双指针
// 灵感来源于 LeetCode移动零
let j = 0;
for (let i = 0; i < res.data.Body.length; i++) {
const t = res.data.Body[i];
if (t.StaffId === StaffInfo.StaffId) {
const s = res.data.Body[j]
res.data.Body[j] = t
res.data.Body[i] = s
j++
}
}
我原来是这么实现的
//需求是把数组里的某个复合特定要求的对象,移动到数组最前列
const needShift = [];
for (let i = 0; i < res.data.Body.length; i++) {
const t = res.data.Body[i];
if (t.StaffId === StaffInfo.StaffId) {
const dt = res.data.Body.splice(i, 1);
i--
needShift.unshift(...dt)
}
}
setTags(needShift.concat(res.data.Body))
判断数组是否有重复元素
function check(arr){
return [... new Set(arr)].length !== arr.length
}
求数组内连续元素的最大和
eg:[-2,1,-3,4,-1,2,1,-5,4] => 6
var maxSubArray = function(nums) {
let result = nums[0];
let sum = result;
for(let i = 1;i<nums.length;i++){
const item = nums[i];
if(sum>0){
sum += item
}else{
sum = nums[i]
}
if(sum>result){
result = sum
}
}
console.log( result );
};
思路就是既然是求最大和,那么如果之前累加的和为负数,就不需要向下累加了,因为无论下一个元素是什么,都肯定会被当前的负数sum所拖累。然后每次循环中都拿计算好的sum和缓存的result做比较,给result重新赋值。一定要注意就是循环中 一定要先判断sum是大于0的情况下再进行累加当前数组索引(sum+=item),不要直接累加索引,因为如果sum是负数就完全可以舍弃了,你可能会考虑这种case情况:
[-1,-10000,-200000,-300000]
,这种的显然第一项-1会被舍掉,不会进行后面的进一步累加(但其实本case的最后结果就是-1),但请注意,我们后面有一步非常关键的对比sum和result的操作,我们之前已经将-1缓存到result的了,所以后面的result还是-1,不会变。
找出数组中不重复的两个数(重复的数字均成对出现);
eg:[1,1,2,3,3,5,5,6,4,6] => [2,4]
判断一堆括号中括号花括号是否合法
// 这种解法没什么好说的,算是利用堆栈这种数据结构的一种把。
eg:[[]]() => true ; {{{{}}}}[) => false
var isValid = function(s) {
const flag = {
")":"(",
"}":"{",
"]":"["
}
const stack = [];
for(let i= 0; i< s.length; i++){
const sn = s[i]
const ls = stack[stack.length-1]
if(ls && flag[sn] === ls){ //不要忘记判断stack里面有没有值,如果没有或最后一位的值无法匹配flag表,就往里推
stack.pop()
}else{
stack.push(sn)
}
}
return stack.length === 0
};
// 下面这种解法还是挺好玩的,记录一下
var isValid = function(s){
while(s.includes('()') || s.includes('{}') || s.includes('[]')){
s = s.replace('()','')
s = s.replace('{}','')
s = s.replace('[]','')
}
return s.length === 0
}
数组和对象flat
数组flat
// 递归(感觉有点像深度的意思,递归向下找)
const flags = (arr) => {
let result = [];
for(let i of arr){
if(Array.isArray(i)){
result.push(...flags(i))
}else{
result.push(i)
}
}
return result
}
// 或者直接调用api
arr.flat(Infinity)
// 使用广度优先遍历(先全摊开再说)
function flat(arr){
const queue = arr;
const result = [];
while(queue.length>0){
const _x = queue.shift();
if(Array.isArray(_x)){
queue.push(..._x)
}else{
result.push(_x)
}
}
return result
}
对象flag
// 使用深度优先遍历
const obj = {
a: {
b:1,
c:2,
d:{
e:5
}
},
b:[1,3,{a:2,b:3}],
c:3
}
输出:
{
'a.b':1,
'a.c':2,
'a.d.e':5,
'b[0]':1,
'b[1]':3,
'b[2].a':2,
'b[2].b':3,
c:3
}
// 卡了很久,感觉应该是用深广度遍历的知识,就去学了一下 (总结在这篇文章https://juejin.cn/post/7065876830380621860)。
// 然后写了一个超级丑陋的方法出来
function flatten(obje){
let keys = Object.keys(obje);
const result = {}
for(let key of keys){
const stack = []//每开始一个顶层节点都新声明一个数组(模拟一下栈),
stack.push(key)// 入栈
fla(key,obje,stack,result)
}
console.log(result)
}
function fla (k,ks,stack,res){
if(typeof ks[k] === 'object'){
let keys = Object.keys(ks[k])
for(let key of keys){
Array.isArray(ks[k])?stack.push('['+key+']') : stack.push(key)// 入栈
fla(key,ks[k],stack,res) // 通过递归调用fla不断向下(深)寻找子节点
}
}else{
let _k = stack.join('.').replace('.[','[');
res[_k] = ks[k];
stack.pop() // 当拿到非引用类型的值得时候做出栈操作,然后去寻找同级别的下一个节点
}
}
flatten(obj);
数组快速排序
// 磨磨蹭蹭的搞了一下午,非常简单没有什么好说的,就是递归的应用,注意循环要从1开始,还有就是结束递归的条件考虑好,别忘记
const flags = (arr) => {
if(arr.length === 0) return []
if(arr.length === 1) return arr
let ins = arr[0];
let left = [];
let right = [];
for(let i=1;i<arr.length;i++){
const inx = arr[i]
if(inx>ins){
left.push(inx)
}else{
right.push(inx)
}
}
return [...flags(left),ins,...flags(right)]
}
将数组转为树结构,将树结构摊平
数组转tree
let arr = [ {id: 1, name: '部门1', pid: 0}, {id: 2, name: '部门2', pid: 1}, {id: 3, name: '部门3', pid: 1}, {id: 4, name: '部门4', pid: 3}, {id: 5, name: '部门5', pid: 4},]
=>
[ { "id": 1, "name": "部门1", "pid": 0, "children": [ { "id": 2, "name": "部门2", "pid": 1, "children": [] }, { "id": 3, "name": "部门3", "pid": 1, "children": [ // 结果 ,,, ] } ] } ]
// 标准答案
function arrayToTree(items) {
const result = []; // 存放结果集
const itemMap = {}; //
debugger
// 先转成map存储
for (const item of items) {
itemMap[item.id] = {...item, children: []}
}
for (const item of items) {
const id = item.id;
const pid = item.pid;
const treeItem = itemMap[id];
if (pid === 0) {
result.push(treeItem);
} else {
if (!itemMap[pid]) {
itemMap[pid] = {
children: [],
}
}
itemMap[pid].children.push(treeItem)
}
}
return result;
}
// 我自己瞎写的
const makeObj = (arr) => {
let result = {}
for(let v of arr){
v.children= []
result[v.id] =v
}
return result
}
let cacheObj = makeObj(arr);
let result = [];
for(let v of arr){
if(v.pid !== 0){
cacheObj[v.pid].children.push(v)
}else{
result.push(v)
}
}
console.log(result)
将数组转为树结构这种问题的解决思路非常简单,主要是看你脑袋能不能bie(四声)过来那个弯儿。核心思路就是对象的引用。 说白了就是 你只要把每个一级对象推到结果数组,其他的层级结构的拼装,都依赖你自己提炼出来的Map对象处理,通过map对象的遍历去只安排某个单一层级的依赖关系,也就是相当于你手里有两份数据,一份是输出的result,这个用来保证输出层级结构,另一个是你自己组装的map,这个通过拼装单一级别的数据之后,利用对象的引用原理自动填充到数组的result结构中就可以了(map结构中的children数组变了,因为result中也是同样的引用,所以result里的数据也会跟着变)。
tree转数组
const treeNode = [{
parentId: 0,
title: "目录1",
id:1,
children: [{
parentId: 1,
title: "子目录1-1",
id:22,
},{
parentId: 1,
title: "子目录1-2",
id:33,
}],
},{
parentId: 0,
title: "目录2",
id:2,
children: [{
parentId: 2,
title: "子目录2-1",
id:44,
},{
parentId: 1,
title: "子目录2-2",
id:55,
}],
}];
// 直接广度优先
function fn(tree){
const result = [];
while (tree.length>0) {
const value = tree.shift();
if(value.children){
tree.push(...value.children);
delete value.children;
}
result.push(value)
}
return result
}
用最少的固定面额硬币取出固定面额
function solution(coins, amount){
const amountArr = new Array(amount+1);
for(let i = 0 ; i < amountArr.length; i++){
const price = i;
if(i === 0){
amountArr[i] = 0;
}else if(coins.includes(price)){ //{{1}}
amountArr[i] = 1;
}else {
const _priceArr = [];
for(let j = 0; j < coins.length ; j++){
const coin = coins[j];
if(price > coin) {
const _price = price - coin;
if(_price > 0){
_priceArr.push(amountArr[_price] + 1)
}else {
_priceArr.push(Infinity)
}
}else {
_priceArr.push(Infinity)
}
}
amountArr[i] = Math.min(..._priceArr)
}
}
return amountArr[amountArr.length - 1] === Infinity ? -1 : amountArr[amountArr.length - 1]
}
注意点:
- 注意声明amountArr数组要amount+1,因为数组是从0开始的,否则会取不到最后结果;
- 因为要取最小值,所以不符合的结果全部使用Infinity代替,后序返回的时候替换为-1,不要一上来就用-1,因为计算期间会取最小值,用-1就会有问题
- 要理解解决这种问题的思想
跳台阶
var numWays = function(n) {
let steps = new Array(n + 1);
steps[0] = 1;
steps[1] = 1;
steps[2] = 2;
// n = f(n - 1) + f(n - 2)
if(n > 2){
for(let i = 2; i<steps.length; i++){
steps[i] = steps[i - 2] + steps[i - 1]
}
}
return steps[n]
};
这个不复杂,没什么好说的,n级台阶之前有两种到达n级的办法,跳1步f(n-1)或跳2步f(n-2),这两种不同的跳法把之前的到达n的方式分成了两个不同的集合,把这两个集合加起来就是到达n的所有方法了
斐波那契数列
Z 字形变换
var convert = function(s, numRows) {
if (numRows === 1) return s;
if(s.length === 1 || s.length === 2 ) return s;
let flag = 1;
let handler = [];
let inx = 0;
for(let i = 0; i < numRows; i++){
handler.push([])
};
for(let i = 0; i < s.length; i++){
const curStr = s[i];
if(flag > 0){
handler[inx].push(curStr)
inx++;
if(inx === numRows){
flag = -1;
inx = inx - 2
}
}else {
handler[inx].push(curStr);
inx--;
if(inx === -1){
flag = 1;
inx= inx + 2
}
}
}
return handler.flat(2).join('')
};
核心思路就是利用flag来判断正反方向,然后遍历字符串通过flag来依次将char填入生成的二维数组中即可。
普通接雨水
var maxArea = function(height) {
if (height.length === 2) {
return Math.min(...height);
}
let left = 0,
right = height.length - 1,
result = 0;
while (left < right) {
if (height[left] < height[right]) {
const curResult = (right - left) * height[left];
result = Math.max(result, curResult);
left++;
} else {
const curResult = (right - left) * height[right];
result = Math.max(result, curResult);
right--;
}
}
return result;
};
典型双指针使用场景。主要是要想明白为什么要每次移动短的指针,移动一次短的指针,相当于舍弃了一个边界。具体见题解:leetcode.cn/problems/co…
全排列
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
苦思冥想好久没做出来,看答案了。看答案后的第三天自己按照答案的思路实现了一下。主要翻了一个错误就是忘记深拷贝了,就是slice那里。第一次接触回溯算法。
var permute = function(nums) {
const usedFlag = {}; // 记录当前这条路径下哪个数字被用过了,下次跳过
const result = []; // 结果数组
function getResult(curArr){ // 递归调用方法
if(curArr.length === nums.length) { // 如果满了结束这个路径的查找,这个路径的递归结束
result.push(curArr.slice(0)); // 此处注意拷贝一下数组,防止下面pop的时候会影响结果数组
return;
}
for(let i = 0;i < nums.length; i++){
const curNum = nums[i]
if(usedFlag[curNum])continue // 如果这个数字用过了就跳过
curArr.push(curNum);
usedFlag[curNum] = true;
getResult(curArr)
const popNum = curArr.pop() // 上个路径结束了,推出curArr最后一个,看下一个数字
usedFlag[popNum] = false //推出的数字置为false ,未使用状态
}
}
getResult([])
return result
};
字符串大小写翻转
eg:HeLL => hEll
const input = 'Hello World!';
const reverseCase = txt => txt.replace(/[a-z]/gi, char => String.fromCharCode(char.charCodeAt(0) ^ 32));
console.log(reverseCase(input)); // "hELLO wORLD!"
在 ASCII 码中,同一字母的大小写只有第 6 位不同,大写为 0 小写为 1,因此大小写转换只需要反转第 6 位,也就是 x^(1<<5);
1<<5也就是32的二进制是100000 ,所以前5位的异或操作都是得到需要转换字符的ASCII码本身,只有第六位是1才会取反,达到了转换大小写的目的。注意要用正则拆分出字母,其他的字符不符合这个规则,例如逗号一类的,如果同样被转换可能得到奇怪的符号。
整数转罗马数字
我的解答:
var intToRoman = function(num) {
const StrNum = String(num);
const rapMap = new Map([[1,'I'],[10,'X'],[100,'C'],[1000,'M']]);
const vMap = new Map([[5,'V'],[50,'L'],[500,'D']]);
const createUni = (num,rgp) => new Array(num).fill(0).map(()=>rapMap.get(rgp)).join('');
const result = [];
for(let i = StrNum.length - 1;i>=0;i--){
const s = +StrNum[i];
const pow = StrNum.length - i - 1;
const powValue = Math.pow(10,pow); // 1000 100 10 1
const realValue = powValue*s; // 2000 300 60 5
if(s === 5){
result.push(vMap.get(realValue))
}else if(s < 4){
result.push(createUni(s,powValue))
}else if(s > 5 && s < 9){
result.push(vMap.get(5*powValue) + createUni(s-5,powValue))
}else if(s === 4){
result.push(rapMap.get(powValue) + vMap.get(5*powValue))
}else if(s === 9){
result.push(rapMap.get(powValue)+rapMap.get(powValue*10))
}
}
return result.reverse().reduce((a,b)=>a+b,'')
};
好的解答,贪心算法:
class Solution {
public:
string intToRoman(int num) {
int values[]={1000,900,500,400,100,90,50,40,10,9,5,4,1};
string reps[]={"M","CM","D","CD","C","XC","L","XL","X","IX","V","IV","I"};
string res;
for(int i=0; i<13; i++){
while(num>=values[i]){
num -= values[i];
res += reps[i];
}
}
return res;
}
};
罗马数字转整数
我的解答:
var romanToInt = function(s) {
const signMap = new Map([
['I',1],
['IV',4],
['V',5],
['IX',9],
['X',10],
['XL',40],
['L',50],
['XC',90],
['C',100],
['CD',400],
['D',500],
['CM',900],
['M',1000]
]);
const _s = s.split('');
let result = 0;
for(let i = 0;i<_s.length;i++){
const value = s[i];
const nextValue = s[i] + s[i+1];
if(signMap.has(nextValue)){
result+=signMap.get(nextValue)
i++
}else{
result+=signMap.get(value)
}
}
return result
};
好的解答: leetcode.cn/problems/ro… 这个解答和评论区里的解答都非常有意思;可以看看
写出一个函数trans,将数字转换成汉语的输出,输入为不超过10000亿的数字。
trans(123456) => 十二万三千四百五十六
trans(100010001) => 一亿零一万零一
function trans(number) {
const getLevel1 = (arr) => {
let result = "";
const clone = unit.slice(0);
clone.splice(0, clone.length - arr.length);
arr.forEach((v, i) => {
const t = v == 0 ? "零" : cn[v] + clone[i];
result += t;
});
return result.replace(/零+/, "零").replace(/零$/, "");
};
const unit = ["万", "千", "百", "十", ""];
const cn = {
1: "一",
2: "二",
3: "三",
4: "四",
5: "五",
6: "六",
7: "七",
8: "八",
9: "九",
0: "零",
};
let arr = Array.from(String(number));
if (arr.length < 6) {
console.log(getLevel1(arr));
}
if (arr.length > 5 && arr.length < 9) {
const l1 = arr.slice(arr.length - 4);
const l2 = arr.slice(0, arr.length - 4);
console.log(getLevel1(l2) + "万" + getLevel1(l1));
}
if (arr.length > 8) {
const l1 = arr.slice(0, arr.length - 8);
const l2 = arr.slice(arr.length - 8, arr.length - 4);
const l3 = arr.slice(arr.length - 4);
console.log(
getLevel1(l1) + "亿" + getLevel1(l2) + "万" + getLevel1(l3)
);
}
}
trans(1000099990102);
三数之和
var threeSum = function(nums) {
let result = [];
nums.sort((a,b)=>a-b);
for(let i = 0;i<nums.length;i++){
const cur = nums[i];
if(cur > 0)break;
if(i>0 && cur === nums[i-1])continue;
let left = i+1,right = nums.length - 1;
while(left < right){
let L = nums[left],R = nums[right];
if(cur+L+R === 0){
result.push([cur,L,R]);
left++
while(nums[left]===nums[left-1]){
left++
}
right--
while(nums[right]===nums[right+1]){
right--
}
}else if(cur+L+R > 0){
right--
}else if(cur+L+R < 0){
left++
}
}
}
return result;
};
自己最开始写的方案测试用例超时;以上是看了答案之后过了一周自己又按照答案的思路重新写了一遍,整体思路是对的, 期间还是有一些要注意的地方;
1.首先要第一个要想到的就是将数组要是升序排列;这样当某项大于0的时候后面的元素就直接可以不用考虑了
2.最大的难点是能想到这种双指针的思路,然后还有个难点就是去重,认真考虑几次去重的原因;尤其
if(cur+L+R === 0){}
这个判断内部的两个 去重判断,要想明白;另外比如要考虑好为什么使用了
if(i>0 && cur === nums[i-1])continue;
而没有使用
if(nums[i] === nums[i+1])continue;
来进行判断,乍一看这两种写法似乎都差不多,但你考虑到其中会漏项的原因了吗?
最接近的三数之和
三数之和的变体,连调再猜搞了一下午才写出来,期间跑了n趟厕所,先贴一个最初我写的的超烂版本,完全没优化的
var threeSumClosest = function(nums, target) {
nums.sort((a,b)=>a-b);
let result = Infinity;
for (let i = 0; i < nums.length; i++) {
if (i > 0 && nums[i] === nums[i - 1]) continue;
if (result === target) break;
let L = i + 1;
let R = nums.length - 1;
if(L===R)break;
const sums = nums[i] + nums[L] + nums[R];
const cha = sums - target;
if (Math.abs(cha) < Math.abs(result - target)) {
result = sums;
}
if (cha === 0) {
result = sums;
break;
} else if (cha > 0) {
// 已经大于0了,左指针没必要移动,越移动和target的差值越大,距离越远
R--;
while (L < R) {
const sums = nums[i] + nums[L] + nums[R];
const r = sums - target;
if (Math.abs(r) < Math.abs(result - target)) {
result = sums;
}
if (r < 0) {
L++
}else {
R--
}
}
} else if (cha < 0) {
// 小于0的情况, 移动左指针
L++;
while (L < R) {
const sums = nums[i] + nums[L] + nums[R];
const r = sums - target;
if (Math.abs(r) < Math.abs(result - target)) {
result = sums;
}
if (r < 0) {
L++
}else {
R--
}
}
}
}
return result;
};
实际上两个while循环应该是可以合并的,没时间了,明天补充;
次日补充上面的优化后的版本:
var threeSumClosest = function(nums, target) {
nums.sort((a,b)=>a-b);
let result = Infinity;
for (let i = 0; i < nums.length; i++) {
if (i > 0 && nums[i] === nums[i - 1]) continue;
if (result === target) break;
let L = i + 1;
let R = nums.length - 1;
while (L < R) {
const sums = nums[i] + nums[L] + nums[R];
const r = sums - target;
if (Math.abs(r) < Math.abs(result - target)) {
result = sums;
}
if(r === 0){
result = sums;
break
}
if (r < 0) {
L++
}else {
R--
}
}
}
return result;
};
删掉多余的while循环,以及while循环外面的某些操作本质上都可以放到while循环里面
合并区间 leetcode.cn/problems/me…
var merge = function(intervals) {
intervals.sort((a,b)=>Math.min(...a)-Math.min(...b));
for(let i = 1;i<intervals.length;i++){
const pre = intervals[i-1];
const cur = intervals[i];
const [preS,preE] = pre;
const [curS,curE] = cur;
let newArr;
if(preE > curS){
if(preE<=curE){
newArr = [preS,curE]
}else{
newArr = [preS,preE]
}
}else if(preE === curS){
newArr = [preS,curE]
}
if(newArr){
intervals.splice(i-1,2,newArr)
i--;
}
}
return intervals
};
要优先能想到用每个子项的最小值进行排序
电话号码的字母组合 leetcode.cn/problems/le…
憋了一天半,连蒙带猜外加一顿debug才整出来,算是完全凭直觉写出来的,这种回溯算法确实很难用常规思维去思考,递归很恶心;
var letterCombinations = function(digits) {
if (digits === "") return [];
const numLetter = {
2: ["a", "b", "c"],
3: ["d", "e", "f"],
4: ["g", "h", "i"],
5: ["j", "k", "l"],
6: ["m", "n", "o"],
7: ["p", "q", "r", "s"],
8: ["t", "u", "v"],
9: ["w", "x", "y", "z"],
};
const len = digits.length;
const result = [];
let curStr = "";
function getResult(curStr) {
const curNum = digits[curStr.length];
const curLetter = numLetter[curNum];
for (let i = 0; i < curLetter.length; i++) {
curStr += curLetter[i];
if (curStr.length === len) {
result.push(curStr);
curStr = curStr.slice(0, -1);
} else {
getResult(curStr);
curStr = curStr.slice(0, -1);
}
}
}
getResult(curStr);
return result
};
看题解中,这个使用队列处理的方案很好,比较容易理解
var letterCombinations = function(digits) {
if (digits === "") return [];
const numLetter = {
2: ["a", "b", "c"],
3: ["d", "e", "f"],
4: ["g", "h", "i"],
5: ["j", "k", "l"],
6: ["m", "n", "o"],
7: ["p", "q", "r", "s"],
8: ["t", "u", "v"],
9: ["w", "x", "y", "z"],
};
const result = [""];
for (let i = 0; i < digits.length; i++) {
const digit = digits[i];
let size = result.length;
for (let j = 0; j < size; j++) {
const cur = result.shift();
for (let k = 0; k < numLetter[digit].length; k++) {
result.push(cur + numLetter[digit][k]);
}
}
}
return result;
};
这个解题方法中要关键想明白为什么要用
let size = result.length;
这个size放到循环中,而不是直接这样用:
for(let j = 0; j < resule.length;j++){}
如果直接把取length的操作放到循环体中,那么就死循环了,实际这里的操作是从队列前面shift出一个值,所以只要保证原始队列长度的循环次数即可,就能保证把该队列的所以元素全部shift出来,不会漏掉,因为你不是arr[i]取值,所以这样写没问题,如果arr[i]取值那就拿不准了,这里永远做shift,就是永远拿第一个,所以没事儿。
组合 leetcode.cn/problems/co… 回溯问题
var combine = function (n, k) {
const result = [];
const stack = [];
function getResult(index) {
for (let i = index; i < n; i++) {
const cur = i + 1;
stack.push(cur);
if (stack.length === k) {
result.push([...stack]);
stack.pop();
} else {
getResult(i + 1);
}
}
stack.pop();
}
getResult(0);
return result;
}
这种回溯问题的传统标准解法还是要好好研究研究,准备面试的时候突击一下。几乎每次都是一顿试