【前端算法系列】算法总结

七分理解,三分记忆

数组

技巧:三指针

  • 使用遍历的话,不需要遍历

字符串

反转
回文(反转后相同;中间劈开对称性)

技巧:双指针

  • 左右指针是否相等str[left]===str[right]
  • 跳过左指针[left+1, right] 或 右指针[left, right-1] 是否相等

场景:

  1. 最大、最小值(使用辅助栈,当大于或小于栈顶时才push进去,然后便可取栈顶的值作为最大/小值)

链表

链表处理:合并、删除
链表反转
链表成环

完全二叉树:可以不需要有两个子结点
满二叉树:必须有两个子结点

特殊的二叉树:
1、二叉搜索树
特点:可以为空树;左结点<=根结点<=右结点;有序

常见操作:搜索、插入、删除、遍历(前中后序)

  • 遍历
    • 深度优先遍历:前中后序(递归/栈实现)
      • 穷举(找深度)一般会用到dfs,只要用到dfs,就想到树形思维方式
    • 广度优先遍历:层序遍历(栈实现)

2、平衡二叉树:叶子节点深度差不超过1的二叉搜索树(所以有二叉搜索树的特点)

场景:构建平衡树、验证是否平衡树)

  • 可以用中序遍历求出有序数组
  • 二分递归思想

  • 堆是一种特殊的完全二叉树的数组对象(最后一层结点不需要是满的)
  • 所有的节点都大于等于(最大堆)或小于等于(最小堆)它的子节点
    • 大堆顶 a[i] >= a[2i + 1] && a[i] >= a[2i + 2]
    • 小堆顶 a[i] <= a[2i + 1] && a[i] <= a[2i + 2]

索引:

  1. 父结点:索引为 (n-1)/2
  2. 左子结点:索引2*n+1
  3. 右子结点:索引2*n+2

场景:

  1. 堆能高效、快速地找出最大值和最小值,时间复杂度是O(1)
  2. 找出第K个最大/小元素

map

集合、字典都是基于哈希表,key都是无序且不能重复

排序和搜索

排序

  • 冒泡排序: 相邻两个数进行对比交换
  • 选择排序: 不断选择最小的,让最小和第一个索引位置做交换
  • 插入排序:分未排序和已排序,让已排序第一个去跟未排序的遍历做比较并交换
  • 希尔排序(插入排序的高级版本)选定增长量:分为未排序和已排序,让已排序去和为排序的遍历做交换,减少增长量,直到增长量大于等于1
  • 归并排序(火狐):按中间件值不断分左右两边,直到不存在,再分别进行排序,最后合并
  • 快速排序(谷歌):随便选一基准比如索引0位置,让所有数跟中间值做比较,比它小的放左边,大的放右边;再不断递归左右两边

冒泡、选择、插入、希尔、归并、快速(前三个O(n²),后三个O(n*logN))
少于23个项目,V8采用插入排序法,大于用快速排序(问题是不稳定性,影响重绘重排)
归并排序是快速排序的竞争对手,火狐和Safari用归并

二分查找

使用二分查找的三个条件:

  • 所要查找的树在有序数组里(递增或递减查找某个元素)
  • 存在明确的上下界
  • 能够通过索引访问(如数组,而链表不适合二分查找)

二分思想:在有序数组里,每次把它一分为二,看它和中间元素对比

左边小于右边就不断查找,得到中间数((左键+右键)/2),再判断中间数和要找的target是否相等,是就表示找到,
如果没有找到,目标数大于中间数,说明数组是递增的,只能去数组右半部分查找(mid+1);
如果目标数小于中间件数,说明是递减的,只能在数组左边查找(mid-1)

// 模板
function fn(arr, target){
	let left, right=0
	while(left<=right){
		let mid = (left+right)/2
		if(arr[mid] == target){
			return arr[mid]
		}else if(arr[mid]<target){
			left = mid+1 // 说明在中间数右边查找,所以不断mid+1
		}else{
			left = mid-1
		}
	}
}

算法设计思想

分而治之

将一个问题分成多个和原问题相似的小问题,递归解决小问题,再将结果合并以解决原来的问题
分 -> 递归解决 -> 合

// 分治模板
function divide(problem, params, param2,...){
    // 1)递归终止条件
    if (!problem){
        // 输出结果
        console.log(result)
        return
    }

    // 2)准备数据和拆分问题
    data = prepare_data(problem)
    subproblems = split_problem(problem, data)

    // 3)调用递归函数:每个子问题都调分治函数进行递归求解,递归完后存在subresult里,相当于下探
    subresult1 = divide(subproblems[0], p1, ...)
    subresult2 = divide(subproblems[0], p1, ...)
    subresult3 = divide(subproblems[0], p1, ...)

    // 4)合并:最后将结果合并到一起
    result = process(subresult1, subresult2, subresult3)

    // 5)恢复当前层状态:当前层状态需要恢复
}

当一个分治问题的子问题具有所谓的重叠或最优子结构的时候,这时候就可以去重或淘汰次优解。 如果能够在中间每一步淘汰次优解的话,就变成了动态规划 动态规划 = 分治 + 最优子结构 (可以从下到上往上推)

应用场景

  • 归并排序(典型的分而治之)

    • 分:把数组从中间一分为二
    • 解:递归地对两个子数组进行归并排序
    • 合:合并有序子数组
  • 快速排序

    • 分:选基准,按基准把数组分成两个子数组(比基准小的左,大的右)
    • 解:递归地对两个子数组进行快速排序
    • 合:对两个子数组进行合并

动态规划

  • 最优子结构(不管前面的决策如何,此后的状态必须是基于当前状态(由上次决策产生)的最优决策)
  • 重叠子问题(在递归过程中,出现返回计算的情况)

子序列问题(序列就是数组,可以利用索引)
对每个元素,考虑取和不取两种选择,得到对应的序列

1.凡是遇到求总类就是累加自身,求乘积就是乘以自身
求最大长度,即每次+1+1
求最大和,即每次加自身: +nums[i]

2.凡是遇到最,最最大最小就是Math.max/min,
如果是取模1e9+71000000007),不能用动态规划了,取余之后max函数就不能用来比大小,可以考虑贪心算法

3. 循环
var lengthOfLIS = function(nums) {
  for(let i=1;i<nums.length;i++){
  /* 1. 只能前面相邻的比较
    if(nums[i]>nums[i-1]){
      console.log(nums[i], nums[i-1])  // 这样的比较是 9跟10,2跟9,5跟2,3跟5...做比较
    }
 */
/* 2. 嵌套循环,跟前面所有数值比较
	for(let j=0; j<i; j++){  
	    console.log(i, nums[i], nums[j]) 
	}
*/
  }
}


4. 多维数组
Array.apply(null, Array(3)).map(()=> Array(2).fill(0)) //  [[0, 0],[0, 0],[0, 0]]
Array.from({length:3}, () => Array.from({length:2}, () => 0)) // [[0, 0],[0, 0],[0, 0]]
Array.from(new Array(3), () => Array.from({length:2}, () => 0))  // [[0, 0],[0, 0],[0, 0]]

回溯算法

是一种渐进式寻找并构建问题解决方式的策略
会从一个可能的动作开始解决问题,如果不行,就回溯并选择另外一个动作,直到将问题解决

即穷举,用到dfs(回溯和树、递归都差不多,只是处理完每一层,都要初始化,回到根节点)

dfs(level){
    // 终止条件,抛出结果并return
    if(level===len) res.push(curr)

    // 处理当前层

    // 下转到下一层
    dfs(level+1)

    // 初始化:清掉visited等
    curr.pop()
    visited[nums[i]] = 0 
}

数组方法

1. push直接改变当前数组;concat不改变当前数组

2. concat、slice是浅拷贝,不会改变当前数组;
比如:res.push(curr.slice())而不是简单的 res.push(curr) ,curr是全局的,会被随时更新,所以要拷贝一份

剪枝:比如八皇后问题,只要列、撇、捺被占了,就马上进行剪枝(判断当前格子不能填皇后)

贪心算法

期盼通过每个阶段的局部最优选择,从而达到全局的最优,结果并不一定最优

在这里插入图片描述

常用技巧方法

1. !0 = true  !1=false ,可以用if(!arr.length)来表示arr为空时做什么
2. 字符串转数字时,乘以1     '2'*1=2

3. 创建二维数组
let arr=[1,3,4,5]
let b = Array(arr.length).fill(0).map(() => Array(2 + 1).fill(0))
// [[0, 0, 0],[0, 0, 0],[0, 0, 0],[0, 0, 0]]
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值