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

简介: 该文章详细介绍了分而治之策略和动态规划算法在前端开发中的应用,并通过具体的例子和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 的题,做多了慢慢就能举一反三了~

  • 关注公众号 星期一研究室 ,第一时间关注学习干货,更多有趣的专栏待你解锁~
  • 如果这篇文章对你有用,记得点个赞加个关注再走哦~
  • 我们下期见!🥂🥂🥂
相关文章
|
1天前
|
Rust 前端开发 JavaScript
前端性能革命:WebAssembly在高性能计算中的应用探索
【10月更文挑战第26天】随着Web应用功能的日益复杂,传统JavaScript解释执行模式逐渐成为性能瓶颈。WebAssembly(Wasm)应运而生,作为一种二进制代码格式,支持C/C++、Rust等语言编写的代码在浏览器中高效运行。Wasm不仅提升了应用的执行速度,还具备跨平台兼容性和安全性,显著改善了Web应用的响应速度和用户体验。
9 4
|
1天前
|
前端开发 安全 应用服务中间件
前端性能调优:HTTP/2与HTTPS在Web加速中的应用
【10月更文挑战第26天】随着互联网的快速发展,前端性能调优成为开发者的重要任务。本文探讨了HTTP/2与HTTPS在前端性能优化中的应用,介绍了二进制分帧、多路复用和服务器推送等特性,并通过Nginx配置示例展示了如何启用HTTP/2和HTTPS,以提升Web应用的性能和安全性。
12 3
|
1天前
|
前端开发 JavaScript 数据可视化
前端自动化测试:Jest与Cypress的实战应用与最佳实践
【10月更文挑战第26天】前端自动化测试在现代软件开发中至关重要,Jest和Cypress分别是单元测试和端到端测试的流行工具。本文通过解答一系列问题,介绍Jest与Cypress的实战应用与最佳实践,帮助开发者提高测试效率和代码质量。
14 2
|
1天前
|
前端开发 JavaScript API
前端框架新探索:Svelte在构建高性能Web应用中的优势
【10月更文挑战第26天】近年来,前端技术飞速发展,Svelte凭借独特的编译时优化和简洁的API设计,成为构建高性能Web应用的优选。本文介绍Svelte的特点和优势,包括编译而非虚拟DOM、组件化开发、状态管理及响应式更新机制,并通过示例代码展示其使用方法。
10 2
|
2天前
|
前端开发 JavaScript 开发者
“揭秘React Hooks的神秘面纱:如何掌握这些改变游戏规则的超能力以打造无敌前端应用”
【10月更文挑战第25天】React Hooks 自 2018 年推出以来,已成为 React 功能组件的重要组成部分。本文全面解析了 React Hooks 的核心概念,包括 `useState` 和 `useEffect` 的使用方法,并提供了最佳实践,如避免过度使用 Hooks、保持 Hooks 调用顺序一致、使用 `useReducer` 管理复杂状态逻辑、自定义 Hooks 封装复用逻辑等,帮助开发者更高效地使用 Hooks,构建健壮且易于维护的 React 应用。
9 2
|
7天前
|
JavaScript 前端开发 测试技术
前端全栈之路Deno篇(五):如何快速创建 WebSocket 服务端应用 + 客户端应用 - 可能是2025最佳的Websocket全栈实时应用框架
本文介绍了如何使用Deno 2.0快速构建WebSocket全栈应用,包括服务端和客户端的创建。通过一个简单的代码示例,展示了Deno在WebSocket实现中的便捷与强大,无需额外依赖,即可轻松搭建具备基本功能的WebSocket应用。Deno 2.0被认为是最佳的WebSocket全栈应用JS运行时,适合全栈开发者学习和使用。
|
3天前
|
前端开发 API UED
深入理解微前端架构:构建灵活、高效的前端应用
【10月更文挑战第23天】微前端架构是一种将前端应用分解为多个小型、独立、可复用的服务的方法。每个服务独立开发和部署,但共同提供一致的用户体验。本文探讨了微前端架构的核心概念、优势及实施方法,包括定义服务边界、建立通信机制、共享UI组件库和版本控制等。通过实际案例和职业心得,帮助读者更好地理解和应用微前端架构。
|
7天前
|
人工智能 资源调度 数据可视化
【AI应用落地实战】智能文档处理本地部署——可视化文档解析前端TextIn ParseX实践
2024长沙·中国1024程序员节以“智能应用新生态”为主题,吸引了众多技术大咖。合合信息展示了“智能文档处理百宝箱”的三大工具:可视化文档解析前端TextIn ParseX、向量化acge-embedding模型和文档解析测评工具markdown_tester,助力智能文档处理与知识管理。
|
8天前
|
存储 算法 搜索推荐
这些算法在实际应用中有哪些具体案例呢
【10月更文挑战第19天】这些算法在实际应用中有哪些具体案例呢
16 1
|
8天前
|
监控 算法 数据挖掘
HyperLogLog算法有哪些应用场景呢
【10月更文挑战第19天】HyperLogLog算法有哪些应用场景呢
8 0

热门文章

最新文章