一文了解分而治之和动态规则算法在前端中的应用

简介: 该文章详细介绍了分而治之策略和动态规划算法在前端开发中的应用,并通过具体的例子和LeetCode题目解析来说明这两种算法的特点及使用场景。

分而治之和动态规则

众多周知,分而治之算法和动态规则算法是前端面试中的“宠儿”。而在我们的日常生活中,这两个场景的应用也相对比较广泛。比如,分而治之算法常用于翻转二叉树、快速搜索等场景中,而动态规则算法,则常用于最少硬币找零问题、背包问题等场景中。

在下面的这篇文章中,将讲解分而治之和动态规则的常用场景以及对 leetcode 的一些经典例题进行解析。

一、分而治之

1、分而治之是什么?

  • 分而治之是算法设计中的一种方法。
  • 它将一个问题成多个和原问题相似的小问题,递归解决小问题再将结果合并以解决原来的问题。

2、应用场景

  • 归并排序
  • 快速搜索
  • 二分搜索
  • 翻转二叉树
  • ……

3、场景剖析:归并排序和快速排序

(1)场景一:归并排序

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

(2)场景二:快速排序

  • :选基准,按照基准把数组分成两个子数组。
  • :递归地对两个子数组进行快速排序。
  • :对两个子数组进行合并。

二、动态规则

1、动态规则是什么?

  • 动态规则是算法设计中的一种方法;
  • 它将一个问题分解为相互重叠的子问题,通过反复求解子问题,来解决原来的问题。

看到这里,很多小伙伴会想着,动态规则和分而治之不是解决同样的问题吗?其实不是的。

注意:

  • 动态规则解决相互重叠的子问题。

  • 分而治之解决的是相互独立的子问题。

这样说可能还有点抽象,稍后将在第3点的时候做详细解析。

2、应用场景

  • 最少硬币找零问题
  • 背包问题
  • 最长公共子序列
  • 矩阵链相乘
  • ……

3、场景剖析:斐波那契数列

斐波那契数列是一个很典型的数学问题。斐波那契数列指的是这样一个数列:

斐波那契数列

这个数列从第3项开始,每一项都等于前两项之和。即:

Fibonacci[n]= $\begin{cases} 0,n=0 \\ 1,n=1 \\ Fibonacci[n-1]+Fibonacci[n-2],n>1 \end{cases}$ ​

那么我们来梳理一下,斐波那契数列是怎么运用动态规则算法的。主要有以下两点:

  • 定义子问题:F(n)=F(n - 1) + F(n - 2);
  • 反复执行:从2循环到n,执行上述公式。

4、动态规则VS分而治之

看完上面的内容,我们来梳理下动态规则和分而治之的区别。先用一张图展示两者的区别。

动态规则和分而治之的区别

大家可以看到,左边的斐波那契数列是将所有问题分解为若干个相互重叠的问题,每个问题的解法都一样。

右边的翻转二叉树,左右子树是相互独立的,需先翻转左右子树,且在翻转过程中,它们各自翻转,互不干扰,左子树干左子树的活,右子树干右子树的活。

不像斐波那契数列那样,每一层都是相互依赖的,一层嵌套一层,相互重叠。

这就是动态规则和分而治之的区别。

三、分而治之算法常见应用

引用leetcode的几道经典题目来强化分而治之算法

1、leetcode 374:猜数字大小

(1)题意

这里附上原题链接

猜数字游戏的规则如下:

  • 每轮游戏,我都会从 1n 随机选择一个数字。 请你猜选出我选的是哪个数字。
  • 如果你猜错了,我会告诉你,你猜测的数字比我选出的数字是大了还是小了。

你可以通过调用一个预先定义好的接口 int guess(int num) 来获取猜测结果,返回值一共有 3 种可能的情况(-1,1 或 0):

  • -1 :我选出的数字比你猜的数字小 pick < num
  • 1 :我选出的数字比你猜的数字大 pick > num
  • 0 :我选出的数字和你猜的数字一样。恭喜!你猜对了!pick == num

返回我选出的数字。

(2)解题思路

  • 二分搜索,同样具备“分、解、合”的特性。
  • 考虑选择分而治之。

(3)解题步骤

  • :计算中间元素,分割数组。
  • :递归地在较大或者较小的数组进行二分搜索。
  • :不需要此步,因为在子数组中搜到就返回了。

(4)代码实现

/** 
 * Forward declaration of guess API.
 * @param {number} num   your guess
 * @return                 -1 if num is lower than the guess number
 *                         1 if num is higher than the guess number
 *                       otherwise return 0
 * var guess = function(num) {}
 */

/**
 * @param {number} n
 * @return {number}
 */
let guessNumber = function(n) {
   

    const rec = (low, high) => {
   
        if(low > high){
   
            return;
        }

       // 1.计算中间元素,分割数组
        const mid = Math.floor((low + high) / 2);

        // 2.与猜测的数字进行比较
        const res = guess(mid);

        // 3.递归地在较大或者较小子数组进行二分搜索
        if(res === 0){
   
            return mid;
        }else if(res === 1){
   
            return rec(mid + 1, high);
        }else{
   
            return rec(low, mid - 1);
        }
    }
    return rec(1, n);
};

2、leetcode 226:翻转二叉树

(1)题意

这里附上原题链接

翻转一棵二叉树。

翻转二叉树

(2)解题思路

  • 先翻转左右子树,再将子树换个位置。
  • 符合“分、解、合”特性。
  • 考虑选择分而治之。

(3)解题步骤

  • :获取左右子树。
  • :递归地翻转左右子树。
  • :将翻转后的左右子树换个位置放到根节点上。

(4)代码实现

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {TreeNode}
 */
 var invertTree = function(root) {
   
    if(!root){
   
        return null;
    }
    return{
   
        //1.根节点值不变
        val:root.val,
        //2.递归地将左子树与右子树结点变换
        left:invertTree(root.right),
        //3.递归地将右子树与左子树结点变换
        right:invertTree(root.left)
    }
};

3、leetcode 100:相同的树

(1)题意

这里附上原题链接

给你两棵二叉树的根节点 pq ,编写一个函数来检验这两棵树是否相同。

如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。

相同的树

(2)解题思路

  • 两棵树:根节点的值相同,左子树相同,右子树相同。
  • 符合“分、解、合”特性。
  • 考虑选择分而治之。

(3)解题步骤

  • :获取两棵树的左子树和右子树。
  • :递归地判断两棵树的左子树是否相同,右子树是否相同。
  • :将上述结果合并,如果根节点的值也相同,两棵树就相同。

(4)代码实现

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} p
 * @param {TreeNode} q
 * @return {boolean}
 */
let isSameTree = function(p, q) {
   
    if(!p && !q){
   
        return true;
    }
    /**
     * 判断条件:
     * 1.p树和q树同时存在;
     * 2.每遍历一个节点,两棵树的节点值都存在;
     * 3.递归左子树,比较每个节点值;
     * 4.递归右子树,比较每个节点值。
     */
    if(p && q && p.val === q.val &&
        isSameTree(p.left, q.left) &&
        isSameTree(p.right, q.right)
    ){
   
        return true;
    }
    return false;
};

4、leetcode 101:对称二叉树

(1)题意

这里附上原题链接

给定一个二叉树,检查它是否是镜像对称的。

对称二叉树

(2)解题思路

  • 转化为:左右子树是否镜像。
  • 分解为:树1的左子树和树2的右子树是否镜像,树1的右子树和树2的左子树是否镜像。
  • 符合“分、解、合”特性,考虑选择分而治之。

(3)解题步骤

  • :获取两棵树的左子树和右子树。
  • :递归地判断树1的左子树和树2的右子树是否镜像,树1的右子树和树2的左子树是否镜像。
  • :如果上述成立,且根节点值也相同,两棵树就镜像。

(4)代码实现

let isSymmetric = function(root){
   
    if(!root){
   
        return true;
    }

    const isMirror = (l, r) => {
   
        if(!l && !r){
   
            return true;
        }

        /**
         * 判断条件:
         * 1.左子树和右子树同时存在;
         * 2.左子树和右子树的根节点相同;
         * 3.左子树的左节点和右子树的右节点镜像相同;
         * 4.左子树的右结点和右子树的左结点镜像相同
         */
        if(l && r && l.val === r.val &&
            isMirror(l.left, r.right) &&
            isMirror(l.right, r.left)
        ){
   
            return true;
        }
        return false;
    }
    return isMirror(root.left, root.right);
}

四、动态规则算法常见应用

引用leetcode的几道经典题目来强化动态规则算法

1、leetcode 70:爬楼梯

(1)题意

这里附上原题链接

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

**注意:**给定 n 是一个正整数。

(2)解题思路

  • 爬到第n阶可以在第n - 1阶爬1个台阶,或者在第n - 2阶爬2个台阶。
  • F(n) = F(n - 1) + F(n - 2)。
  • 使用动态规则。

(3)解题步骤

  • 定义子问题:F(n) = F(n - 1) + F(n - 2)。
  • 反复执行:从2循环到n,执行上述公式。

(4)代码实现

 /*
 * @param {number} n
 * @return {number}
 */
// 数组方法
var climbStairs = function(n) {
   
    if(n < 2){
   
        return 1;
    }
     // 记录第0阶和第1阶可以走多少步
    const dp = [1, 1];
    // 从第2阶开始遍历,直至第5阶
    for(let i = 2; i <= n; i++){
   
        dp[i] = dp[i - 1] + dp[i - 2];
    }
    return dp[n];
};

如果dp用一维数组来记录的话,时间复杂度和空间复杂度都为O(n),这样子的话效率还是偏低的。

那么有什么方法可以来降低它的复杂度呢?

可以采用变量的方法。从上面的代码中我们可以看出,dp的值用一个数组存着,一直在线性增长。那么这个时候我们可以考虑把这个一维数组变换成单变量的形式,不断进行替换,来降低空间复杂度

下面用代码实现一遍。

let climbStairs2 = function(n){
   
    if(n < 2){
   
        return 1;
    }
    //定义一个变量,记录 n - 2 时的台阶数
    let dp0 = 1;
    //定义一个变量,记录 n - 1 时的台阶数
    let dp1 = 1;
    for(let i = 2; i <= n; i++){
   
        const temp = dp0;
        //每遍历一次,就让dp0指向下一个数的值,即dp1
        dp0 = dp1;
        //每遍历一次,就让dp1指向dp1下一个数的值,即前两个数的和,也就是dp1和原来dp0的值
        dp1 = dp1 + temp;
    }
    return dp1;
}

从上面的代码中可以看出,没有了数组或者像矩阵一样线性增长的数组,空间复杂度就变为了O(1)。

2、leetcode 198:打家劫舍

(1)题意

这里附上原题链接

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

(2)解题思路

  • f(k) = 从前k个房屋中能偷窃到的最大数额。
  • Ak = 第k个房屋的钱数。
  • f(k) = max(f(k - 2) + Ak, f(k - 1))。
  • 考虑使用动态规则。

(3)解题步骤

  • 定义子问题:f(k) = max(f(k - 2) + Ak, f(k - 1))。
  • 反复执行:从2循环到n,执行上述公式。

(4)代码实现

/**
 * @param {number[]} nums
 * @return {number}
 */

let rob1 = function(nums) {
   
    if(nums.length === 0){
   
        return 0;
    }
    // 前0个房屋和前1个房屋能劫持到的金钱数
    const dp = [0,nums[0]];
    for(let i = 2; i <= nums.length; i++){
   
        dp[i] = Math.max(dp[i - 2] + nums[i - 1], dp[i - 1]);
    }
    return dp[nums.length];
};

与爬楼梯同样,如果dp用一维数组来记录的话,时间复杂度和空间复杂度都为O(n),这样子的话效率还是偏低的。

那这个时候就可以采用单变量的方法,来降低空间复杂度

下面用代码实现一遍。

let rob2 = function(nums) {
   
    if(nums.length === 0){
   
        return 0;
    }
    let dp0 = 0;
    let dp1 = nums[0];
    for(let i = 2; i <= nums.length; i++){
   
        const dp2 = Math.max(dp0 + nums[i - 1], dp1);
        dp0 = dp1;
        dp1 = dp2;
    }
    return dp1;
};

此时空间复杂度自然也就变为O(1)了。

3、leetcode 62:不同路径

(1)题意

这里附上原题链接

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

问总共有多少条不同的路径?

不用路径

(2)解题思路

  • 每一步只能向下或者向右移动一步,因此想要走到(i,j),如果向下走一步,那么从(i-1,j)走过来;如果向右走一步,那么从(i,j-1)走过来。
  • f(i, j) = f(i-1, j) + f(i, j-1)。
  • 使用动态规则。

(3)解题步骤

  • 定义子问题:f(i, j) = f(i-1, j) + f(i, j-1)。
  • 反复执行:从2循环到n,执行上述公式。

(4)代码实现

let uniquePaths = function(m, n){
   
    const f = new Array(m).fill(0).map(() => new Array(n).fill(0));
    for(let i = 0; i < m; i++){
   
        // 将第一列全部补上1
        f[i][0] = 1;
    }
    for(let j = 0; j < n; j++){
   
        // 将第一行全部补上1
        f[0][j] = 1;
    }

    for(let i = 1; i < m; i++){
   
        for(let j = 1; j < n; j++){
   
            f[i][j] = f[i - 1][j] + f[i][j - 1];
        }
    }
    return f[m - 1][n - 1];
}

五、结束语

分而治之和动态规则算法在前端中的应用还是挺多的,特别是在面试或笔试的时候会经常出现这类题目,大家可以在此之外再继续多刷刷此类 leetcode 的题,做多了慢慢就能举一反三了~

  • 关注公众号 星期一研究室 ,第一时间关注学习干货,更多有趣的专栏待你解锁~
  • 如果这篇文章对你有用,记得点个赞加个关注再走哦~
  • 我们下期见!🥂🥂🥂
相关文章
|
8月前
|
存储 监控 JavaScript
基于布隆过滤器的 Node.js 算法在局域网电脑桌面监控设备快速校验中的应用研究
本文探讨了布隆过滤器在局域网电脑桌面监控中的应用,分析其高效空间利用率、快速查询性能及动态扩容优势,并设计了基于MAC地址的校验模型,提供Node.js实现代码,适用于设备准入控制与重复数据过滤场景。
298 0
|
7月前
|
运维 监控 JavaScript
基于 Node.js 图结构的局域网设备拓扑分析算法在局域网内监控软件中的应用研究
本文探讨图结构在局域网监控系统中的应用,通过Node.js实现设备拓扑建模、路径分析与故障定位,提升网络可视化、可追溯性与运维效率,结合模拟实验验证其高效性与准确性。
410 3
|
7月前
|
前端开发 JavaScript 应用服务中间件
在Docker部署的前端应用中使用动态环境变量
以上步骤展示了如何在 Docker 配置过程中处理并注入环墨遁形成可执行操作流程,并确保最终用户能够无缝地与之交互而无须关心背后复杂性。
388 13
|
7月前
|
机器学习/深度学习 资源调度 算法
遗传算法模型深度解析与实战应用
摘要 遗传算法(GA)作为一种受生物进化启发的优化算法,在复杂问题求解中展现出独特优势。本文系统介绍了GA的核心理论、实现细节和应用经验。算法通过模拟自然选择机制,利用选择、交叉、变异三大操作在解空间中进行全局搜索。与梯度下降等传统方法相比,GA不依赖目标函数的连续性或可微性,特别适合处理离散优化、多目标优化等复杂问题。文中详细阐述了染色体编码、适应度函数设计、遗传操作实现等关键技术,并提供了Python代码实现示例。实践表明,GA的成功应用关键在于平衡探索与开发,通过精心调参维持种群多样性同时确保收敛效率
|
7月前
|
机器学习/深度学习 边缘计算 人工智能
粒子群算法模型深度解析与实战应用
蒋星熠Jaxonic是一位深耕智能优化算法领域多年的技术探索者,专注于粒子群优化(PSO)算法的研究与应用。他深入剖析了PSO的数学模型、核心公式及实现方法,并通过大量实践验证了其在神经网络优化、工程设计等复杂问题上的卓越性能。本文全面展示了PSO的理论基础、改进策略与前沿发展方向,为读者提供了一份详尽的技术指南。
粒子群算法模型深度解析与实战应用
|
7月前
|
机器学习/深度学习 算法 安全
小场景大市场:猫狗识别算法在宠物智能设备中的应用
将猫狗识别算法应用于宠物智能设备,是AIoT领域的重要垂直场景。本文从核心技术、应用场景、挑战与趋势四个方面,全面解析这一融合算法、硬件与用户体验的系统工程。
655 0
|
8月前
|
算法 数据可视化
matlab版本粒子群算法(PSO)在路径规划中的应用
matlab版本粒子群算法(PSO)在路径规划中的应用
|
存储 人工智能 前端开发
前端大模型应用笔记(三):Vue3+Antdv+transformers+本地模型实现浏览器端侧增强搜索
本文介绍了一个纯前端实现的增强列表搜索应用,通过使用Transformer模型,实现了更智能的搜索功能,如使用“番茄”可以搜索到“西红柿”。项目基于Vue3和Ant Design Vue,使用了Xenova的bge-base-zh-v1.5模型。文章详细介绍了从环境搭建、数据准备到具体实现的全过程,并展示了实际效果和待改进点。
1470 14
|
JavaScript 前端开发 程序员
前端学习笔记——node.js
前端学习笔记——node.js
788 0
|
SpringCloudAlibaba JavaScript 前端开发
谷粒商城笔记+踩坑(2)——分布式组件、前端基础,nacos+feign+gateway+ES6+vue脚手架
分布式组件、nacos注册配置中心、openfegin远程调用、网关gateway、ES6脚本语言规范、vue、elementUI
谷粒商城笔记+踩坑(2)——分布式组件、前端基础,nacos+feign+gateway+ES6+vue脚手架

热门文章

最新文章

下一篇
开通oss服务