前端工作中很少用到算法,但是面试或多或少都会问到这方面,尤其最近在学习Java,如果要学全栈,那么算法就是必须要迈过去的一道坎.
开始总结掌握的算法吧家人们。。。
先来个简单的
整数反转
核心思路如下:
while(num!==0){
result=result*10+num%10;
num= (num/10)|0;
}
1. 扁平化数组转换为树形结构
思路:
递归 父级id
let transArr=(arrs) =>{
let res=[];
let go=(val,arrs,Arr)=>{
arrs.forEach(ele => {
if(ele.parent_id == val){
Arr.push(ele);
}
});
Arr.forEach(ele=>{
ele.children=[];
go(ele.id,arrs,ele.children);
})
}
go(0,arrs,res);
return res;s
}
console.log(transArr(menu_list));
2.扁平化对象转换为树形对象
思路:
指针,引用类型赋值操作的浅拷贝,递归
let obj = {
"a.b.c": 1,
"a.d": 2,
"e.f.g": 3
}
let TansObject=(obj)=>{
let result = {};
let cur;
for (const key in obj) {
let keyList = key.split(".");
let val = obj[key];
cur = result;
for (let i = 0; i < keyList.length-1; i++) { //这里遍历keyList 要少遍历一个节点,为了最后获取值
cur[keyList[i]] = cur[keyList[i]] || {};
cur = cur[keyList[i]]; //在这里改变指向
}
cur[keyList[keyList.length-1]] = val;
}
return result;
}
console.log(TansObject(obj));
3.把上面的转换的对象再变回来~(字节面试题是上面那道,我自己研究了一下如何转换回来)
思路:
递归,初始值,实例判断
这道题有难度,要注意递归函数中传递的keys参数,在方法体内部判断时需要对keys的值进行判断(null or string)
let tansObj = (obj) => {
let result = {};
let go = (object, keys) => {
for (const key in object) {
if (object[key] instanceof Object) {
if (keys == null) {
go(object[key], key)
} else {
go(object[key], keys +"."+ key)
}
} else {
if(keys==null){
result[key] = object[key];
}else{
result[keys +"."+ key] = object[key];
}
}
}
}
go(obj, null);
return result;
}
console.log(tansObj(obj2));
4.最长无重复子串
思路:
处理字符串需要split为数组,在合适的时候使用while循环
let str = "aabbsd";
let getMaxStr=(str)=>{
let tempArr=[];
let maxLen = 0;
let strArr= str.split("");
strArr.forEach(ele => {
while(tempArr.includes(ele)){
tempArr.shift();
}
tempArr.push(ele);
maxLen = Math.max(maxLen,tempArr.length);
});
return maxLen;
}
console.log(getMaxStr(str));
5.二分查找
思路:
while循环中动态获取middle,根据目标值改变下一次循环开始和结束的索引
function search(list,item){
count =1;//计数出现的次数
start = 0;
end = list.length-1;
while(start<=end){
middle =Math.floor((start+end)/2); //取中间下标
guess = list[middle];
if(guess==item){
return middle; //返回位置
}
if(guess>item){
end = middle;
}else{
start = middle+1
}
count++;
}
return "查不到";
}
let result = search(list,4);
6.打家劫舍
这个方法空间复杂度和时间复杂度几乎最优,重要的是无需递归
let rob=(arrs)=>{
if(arrs.length==1){return arrs}
if(arrs.length==2){return Math.max(arrs[0],arrs[1])}
let result =[];
for (let i = 2; i < arrs.length; i++) {
result[i] = Math.max(arrs[i]+arrs[i-2],arrs[i-1])
}
return result[arrs.length-1]
}
7.最长递增子序列
给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。
dp存放的是递增数据的长度
/**
* @param {number[]} nums
* @return {number}
*/
var findLengthOfLCIS = function(nums) {
const dp = [1]
for(let i=1;i<nums.length;i++){
if(nums[i]>nums[i-1]){
dp[i]=dp[i-1]+1
}else{
dp[i]=1
}
}
return Math.max(...dp)
};
8. 买彩票的最佳时机
思路:
(这道题很难,巨难,难的一批!动态规划求解)
/*
第一天没买,收入为0
第一天买了,收入--
*/
dp[i][j][0] = 0;
dp[i][j][1] = -prices[i];
/*
第i天没买,两种情况取最优
1.昨天也没买
2.昨天买了,但是卖出去了,收入++
第i天买了,两种情况取最优
1.昨天也买了
2.昨天没买,今天第一次买,收入--
*/
dp[i][j][0] = Math.max(
dp[i - 1][j][0],
dp[i - 1][j][1] + prices[i]
);
dp[i][j][1] = Math.max(
dp[i - 1][j - 1][0] - prices[i],
dp[i - 1][j][1]
);
var maxProfit = function (prices) {
let len = prices.length;
let k = 2;
if (len == 0) {
return;
}
let dp = Array.from(new Array(len), () =>
new Array(k + 1).fill(0).map(() => new Array(2).fill(0))
);
for (let i = 0; i < len; i++) {
for (let j = k; j > 0; j--) {
if (i == 0) {
dp[i][j][0] = 0;
dp[i][j][1] = -prices[i];
continue;
}
dp[i][j][0] = Math.max(
dp[i - 1][j][0],
dp[i - 1][j][1] + prices[i]
);
dp[i][j][1] = Math.max(
dp[i - 1][j - 1][0] - prices[i],
dp[i - 1][j][1]
);
}
}
return dp[len - 1][k][0];
};
maxProfit([3, 3, 5, 0, 0, 3, 1, 4]);
Z字形变换
思路:
第一行和最后一行按照 row*2-2 的索引进行递增
其余行按照 n+((row*2)-2)-1*i进行递增
n 代表当前行字符索引,i代表行数 ,row为总行数
let TransStr=(str,row)=>{
let result='';
for (let i = 0; i < row; i++) {
if(i==0){
for (let k = 0; k < str.length; k+=(row*2)-2) {
result+= str.charAt(k);
}
}else if(i=row-1){
for (let j = row-1; j < str.length; j+=(row*2)-2) {
result+= str.charAt(j);
}
}else{
for (let k = i; k < str.length; k+=(row*2)-2) {
result= result+str.charAt(k)+str.charAt(k+((row*2)-2)-2*i)
}
}
}
return result;
}
盛最多水的容器
思路:
比较左右两侧大小,小的乘以数组长度得到水的容积
/**
* @param {number[]} height
* @return {number}
*/
var maxArea = function(height) {
let start =0;
let end=height.length-1;
let max=0;
while(start<end){
max=Math.max(max,(end-start)*height[start]<height[end]?height[start++]:height[end--])
}
return max;
};
整数转换为罗马数字
思路: 有一个由大到小的整数构成的数组以及对应的罗马字符数组,依次遍历数组的索引,根据索引找到对应数字,当num大于索引对应数字时,num-数字,res+字符
var intToRoman = function (num) {
let intArr = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1];
let RomanArr = [
"M",
"CM",
"D",
"CD",
"C",
"XC",
"L",
"XL",
"X",
"IX",
"V",
"IV",
"I",
];
let index = 0;
let res='';
while(index<13){
while (num>=intArr[index]) {
res+=RomanArr[index];
num-= intArr[index]
}
index++;
}
return res;
};
console.log(intToRoman(300));
罗马数字转换为整数
思路:
建立罗马字符和value的映射,
遍历罗马字符,当前字符代表值和下一个字符的值比较,大的res+=value,小的res-=value
const romanToInt = s => {
let map = new Map([['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++) {
let left = map.get(s[i]);
let right = map.get(s[i + 1]);
res += left < right ? -left : left
}
return res
};
最接近目标值的三数之和
思路:
给定一个暂定值,可以是Number类的最大值,也可以是随便一个值,
判断三数之和和target之间的绝对值和暂定值与target绝对值的大小,越小说明越接近目标值
/**
* @param {number[]} nums
* @param {number} target
* @return {number}
*/
var threeSumClosest = function(nums, target) {
let result =Number.MAX_SAFE_INTEGER;
nums.sort((a,b)=>a-b);
for(let i=0;i<nums.length;i++){
let left = i+1;
let right = nums.length-1;
while(left<right){
let sum = nums[i]+nums[left]+nums[right];
if(Math.abs(target-sum)<Math.abs(target-result)){
result = sum;
}
if(sum>target){
right--;
}else if(sum<target){
left++;
}else{
return sum;
}
}
}
return result
};
最接近目标值的n数之和
思路:
两数之和可以采用双指针来找到最接近的两数,三数之和在此基础上,target-nums[i] 等于两数的target,求得两数之和和nums[i]相加,四数之和以此类推,从而获取状态转移方程
for(let i= start; i<len;i++){
let res= getNumSum(arrs,start++,n-1,target-num[i]);
res.forEach((item)=>{
item.push(num[i]);
res.push(item);
})
}
/**
* @param {number[]} nums
* @param {number} target
* @return {number[][]}
*/
var fourSum = function (nums, target) {
// 先排序
nums.sort((a, b) => a - b);
/*
注意:调用这个函数之前一定要先给 nums 排序
n 填写想求的是几数之和,start 从哪个索引开始计算(一般填 0),target 填想凑出的目标和
*/
const nSumTarget = (nums, n, start, target) => {
let size = nums.length;
let res = [];
// 至少是 2Sum,且数组大小不应该小于 n
if (n < 2 || size < n) return res;
// 2Sum 是 base case
if (n == 2) {
// 双指针那一套操作
let lo = start,
hi = size - 1;
while (lo < hi) {
let sum = nums[lo] + nums[hi];
let left = nums[lo],
right = nums[hi];
if (sum < target) {
while (lo < hi && nums[lo] == left) lo++;
} else if (sum > target) {
while (lo < hi && nums[hi] == right) hi--;
} else {
res.push([left, right]);
while (lo < hi && nums[lo] == left) lo++;
while (lo < hi && nums[hi] == right) hi--;
}
}
} else {
// n > 2 时,递归计算 (n-1)Sum 的结果
for (let i = start; i < size; i++) {
let sub = nSumTarget(nums, n - 1, i + 1, target - nums[i]);
for (let arr of sub) {
arr.push(nums[i]);
res.push(arr);
}
while (i < size - 1 && nums[i] == nums[i + 1]) i++;
}
}
return res;
};
// n 为 4,从 nums[0] 开始计算和为 target 的四元组
return nSumTarget(nums, 4, 0, target);
};
最长公共字符串
思路:找第一个字符串当对比,用数组其余字符串与其进行比较(从第一个字符开始)
相同则使用substring拼接
function longestCommonPrefix(strs: string[]): string {
if(strs.length ==0){return null}
let result:string = strs[0];
for(let i=1;i<strs.length;i++){
let j=0;
let len = result.length;
for(;j<len;j++){
if(result[j]!=strs[i][j]) break;
}
result = result.substring(0,j)
}
return result;
};
合并两个有序链表
链表真的很抽象,我理解不了...
思路:
边界:其中一个链表为null时返回另一个。 两个都为null则返回null
判断链表头部val,将小的返回
小的节点的next指向 新的递归 (再次调用合并函数,将val值较小的链表剩余节点和 另一个链表传入)
let mergeListNode=(L1,L2)=>{
if (L1==null) {
return L2;
}
if(L2==null){
return L1;
}
if(L1.val>L2.val){
L2.next = mergeListNode(L1,L2.next);
return L2;
}else{
L1.next = mergeListNode(L1.next,L2);
return L1;
}
}
生成指定对数的括号(随意排列)
指定对数 n,代表最后返回的括号字符串长度为n*2
左右括号的长度分别为n
两种递归条件:
当左括号存在,递归拼接左括号
右括号存在,递归拼接右括号
var generateParenthesis = function (n) {
const res = [];
const dfs = (lRemain, rRemain, str) => { // 左右括号所剩的数量,str是当前构建的字符串
if (str.length == 2 * n) { // 字符串构建完成
res.push(str); // 加入解集
return; // 结束当前递归分支
}
if (lRemain > 0) { // 只要左括号有剩,就可以选它,然后继续做选择(递归)
dfs(lRemain - 1, rRemain, str + "(");
}
if (lRemain < rRemain) { // 右括号比左括号剩的多,才能选右括号
dfs(lRemain, rRemain - 1, str + ")"); // 然后继续做选择(递归)
}
};
dfs(n, n, ""); // 递归的入口,剩余数量都是n,初始字符串是空串
return res;
};
链表两两反转
思路:
第二个节点指向第一个节点,再把第一个节点指向后面剩余节点,
利用递归以及指针改变链表的next实现反转(链表是一个很抽象的内容,实在是理解困难..)
var swapPairs = function(head) {
if(head==null||head.next==null){
return head
}
let newHead= head.next;
head.next = swapPairs(newHead.next);
newHead.next = head;
return newHead
};
以K为组进行链表反转
思路: 从head开始依次取出链表K个元素并用stack作为栈来存放,取出后node指向下一个
var reverseKGroup = function(head, k) {
if (!head) return head;
if (k < 2) return head;
//边界
let newHead = head;
const travel = (node, preNode) => {
let i = 0;
let stack = [];
while (node && i < k) {
stack.push(node);
node = node.next; // 每添加一个元素到栈,node就改变下一个指向
i++;
}
let cur = stack.pop(); // 后入先出,此刻从后面获取
if (i === k) {
if (newHead === head) newHead = cur;
if (preNode) preNode.next = cur;
while (stack.length) {
cur.next = stack.pop();
cur = cur.next; // 节点反转的同时要改变cur的next指向
}
cur.next = node; // 反转好的链表和剩余节点连接
}
if (node) {
return travel(node, cur); //把剩余node和 反转后链表的尾节点再次递归
}
}
travel(head, null);
return newHead;
};
链表反转
思路: 给定res做最后节点,每次反转相当于将当前节点的next指向res,,之后将该节点赋值给res
let reverseList = (list)=>{
let res= null;
let cur = list;
while(cur){
let next = cur.next;
cur.next = res;
next.next = cur;
res = cur;
}
return res;
}
和为K的子数组个数
思路: 前缀和思路
sum[0] = nums[0]
sum[1] = nums[0]+nums[1],
sum[2] = nums[0]+nums[1]+nums[2] ...以此类推
以上求出的sum数组为 前缀和数组,
根据规律可求得 N数和 = sum[n] ,n 到 m 的和 sum[m] - sum[n]
目标值为K,代表N数之和,求得公式 sum[i] - sum[j-1] =K;
利用哈希表和前缀和求得题解
·
class Solution {
public int subarraySum(int[] nums, int k) {
if(nums == null || nums.length == 0){
return 0;
}
int pre = 0;
int ans = 0;
Map<Integer, Integer> map = new HashMap<>();
map.put(0, 1);//错误:少了这行代码
for(int i = 0; i < nums.length; i++){
pre += nums[i];
//遍历求得前i数之和
int target = pre - k;
//求得差值
ans += map.getOrDefault(target, 0);
map.put(pre, map.getOrDefault(pre, 0) + 1);
//统计满足条件的前缀和出现次数,最后通过ans返回
}
return ans;
}
}
给定除数和被除数,不用运算符求出商
思路:
用map存放除数和对应数字, 递增实现阶乘 c+=c
var divide = function(dividend, divisor) {
// 获取符号
let a = dividend>=0,b = divisor>=0
// 全部转正数处理
dividend = Math.abs(dividend)
divisor = Math.abs(divisor)
// 对除数进行翻倍 存储待用
// 最终 map = [...., [4,divisor*4] , [2,divisor*2] , [1,divisor*1]]
let c = 1
let map = []
let temp = divisor
while(dividend >= temp){
map.unshift([c,temp])
temp += temp
c += c
}
// 让被除数不断减去map中小于它的除数 统计次数 则为结果
let sum = 0
for(let i = 0;i< map.length && dividend > 0;i++){
let [c,divisor] = map[i]
if(dividend >= divisor){
dividend-=divisor
sum +=c
}
}
return a == b?Math.min(result,2147483648-1):-Math.min(result,2147483648)
};
下一个排列
思路: 数组全排列之后,找出给定排列的下一个排列
双指针法,两个相邻指针,从后往前进行遍历,找到数组中第一个升序排列的两个元素,
再找到从尾指针到数组末尾中最小的比头指针大的元素并调换位置,之后将尾指针之后的数组元素反转,得到下一个排列。
var nextPermutation = function(nums) {
if(nums.length == 1) return nums;//如果长度为1,直接返回
let i = nums.length - 2;
let j = nums.length - 1;
let k = nums.length - 1;
while(nums[i] >= nums[j] && i >= 0) {//找到相邻两位为升序排列的nums[i],nums[j]
i--;
j--;
}
if(i < 0) return nums.reverse();//如果i<0,表示从后往前,左边每一位数字都大于等于右边相邻数字,当前排列是最大值,反转数组返回最小值
while(nums[i] >= nums[k]) {//从右往左找到第一个比nums[i]大的数字
k--;
}
[nums[i], nums[k]] = [nums[k], nums[i]];//交换
for(let l = nums.length - 1; l > j; l--, j++) {//从nums[j]到nums[nums.length - 1]都是降序,反转使其变成升序
[nums[l], nums[j]] = [nums[j], nums[l]];
}
return nums;
};
最长有效括号
思路:用栈的思想来做,当出现开口,push,当出现闭口pop,判断栈的长度,来获取最长有效括号
var longestValidParentheses = function (s) {
let maxLen = 0
let stack = []
stack.push(-1) // 初始化一个参照物
for (let i = 0; i < s.length; i++) {
if (s[i] === '(') {
// ( 入栈 )出栈
stack.push(i)
} else {
// )的情况 出栈
stack.pop()
if (stack.length) {
// 每次出栈 计算下当前有效连续长度
// 如何计算连续长度 当前位置 - 栈顶下标
maxLen = Math.max(maxLen, i - stack[stack.length - 1])
} else {
stack.push(i) //栈为空时 放入右括号参照物 表示从这个下标开始 需要重新计算长度
}
}
}
return maxLen
};
数组查找(哈希法)
var search = function(nums, target) {
let map = new Map;
for(let i = 0;i < nums.length;i++)
map.set(nums[i],i);
if(map.has(target)) return map.get(target);
else return -1;
};
查找数组中元素出现的第一个和最后一个位置
思路:二分查找
判断条件有两种,左边界为第一个等于target的数字,右边界为第一个大于target的数字索引减一
解法一:
const findIndex = (nums, target, ifLeft) => {
let left = 0;
let right = nums.length - 1;
let ans = nums.length;
while(left <= right) {
const mid = (left + right) >> 1;
//nums[mid] > target用于找到大于目标值的下标,(ifLeft && nums[mid] >= target)用于找到大于等于目标值的下标
if(nums[mid] > target || (ifLeft && nums[mid] >= target)) {
right = mid - 1;//缩小范围,帮助确认ans的最小值,即第一个符合条件的下标
ans = mid;//更新下标值
} else {
left = mid + 1;//缩小范围,帮助确认ans的最小值,即第一个符合条件的下标
}
}
return ans;
}
var searchRange = function(nums, target) {
let ans = [-1, -1];
const leftIndex = findIndex(nums, target, true);//第一个大于等于目标值的下标
const rightIndex = findIndex(nums, target, false) - 1;//第一个大于目标值的下标 - 1
//防止leftIndex和rightIndex不合理,比如[5,7,7,8,8,10],target值是6,rightIndex是1;需要再次确定
if(leftIndex <= rightIndex && rightIndex < nums.length && nums[leftIndex] == target && nums[rightIndex] == target) {
ans = [leftIndex, rightIndex];
}
return ans;
}
解法二:
var searchRange = function(nums, target) {
let ans = [-1, -1],
len = nums.length,
left = 0,
right = len - 1;
// 迭代查找右边界
while (left <= right) {
let mid = left + Math.floor((right - left) / 2);
if (nums[mid] === target && (mid === len - 1 || nums[mid + 1] > target)) {
ans[1] = mid;
break;
}
if (nums[mid] <= target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
// 迭代查找左边界
left = 0;
while (left <= right) {
let mid = left + Math.floor((right - left) / 2);
if (nums[mid] === target && (mid === 0 || nums[mid - 1] < target)) {
ans[0] = mid;
break;
}
if (nums[mid] >= target) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return (ans[0] === -1 || ans[1] === -1) ? [-1, -1] : ans;
};
给定一个数,求对应的描述数组的内容
思路: 描述数组第一项为1, 第二项 11(一个一) ,第三项 21(两个一),第四项 1211(一个二一个一) ...
每一项都是前一项内容的描述,使用动态规划来求解
let getDiscribeNmm = (n) =>{
let dp = ["1"];
for(let i=1;i<n;i++){
dp[i] = currentNum(dp[i-1]);
}
}
let currentNum=(cur)=>{
let res = '';
let left =0;
while(left<cur.length){
let sum =1;
while(cur[left] == cur[left+1]{
left++;
sum++
}
res = res + sum + cur[left];
}
return res;
}
跳跃游戏
给定一个数组,每一位代表可以跳跃的位数,初始为0,如果可以跳到最后一位返回true
思路:
使用动态规划求解,从倒数第二位开始,当前位的数字大于当前索引到结尾的长度即可,改变尾指针指向,指向当前索引。
var canJump = function(nums) {
// 必须到达end下标的数字
let end = nums.length - 1;
for (let i = nums.length - 2; i >= 0; i--) {
if (end - i <= nums[i]) {
end = i;
}
}
return end == 0;
};
区间合并
思路: 按照每个数组的首个元素进行排序,之后遍历通过指针进行合并
var merge = function(intervals) {
if (intervals.length === 0) return [];
// 按每个区间的开头大小排序
intervals.sort((a, b) => {
return a[0] - b[0];
});
let res = [];
let tmp = intervals[0];
for (let interval of intervals) {
// 有重叠,可以合并
if (interval[0] <= tmp[1]) {
tmp = [ tmp[0], Math.max(interval[1], tmp[1]) ];
// 指针永远指向数组合并后的容器
} else { // 无重叠, tmp是独立的区间,记录到结果中
res.push([].concat(tmp)); //push进当前指向的容器
tmp = interval; //改变指向到当前数组
}
}
res.push(tmp);// 最后的区间
return res;
};