> 技术文档 > 分治算法 (Divide and Conquer)原理、及示例-JS版

分治算法 (Divide and Conquer)原理、及示例-JS版


文章目录

    • 1. 算法原理
      • 基本步骤:
      • 适用条件:
    • 2. JavaScript 实现示例
      • 2.1 归并排序 (Merge Sort)
      • 2.2 快速排序 (Quick Sort)
      • 2.3 二分搜索 (Binary Search)
      • 2.4 最大子数组和 (Maximum Subarray Sum)
      • 2.5 大整数乘法 (Karatsuba Algorithm)
    • 3. 实际应用
      • 3.1 在前端开发中的应用
        • 3.1.1 DOM 树遍历
        • 3.1.2 树形菜单渲染
      • 3.2 在算法优化中的应用
        • 3.2.1 快速傅里叶变换 (FFT) - 简化版
      • 3.3 在数据处理中的应用
        • 3.3.1 分治法处理大数据集
    • 4. 复杂度分析
      • 时间复杂度
      • 空间复杂度
    • 5. 优缺点
      • 优点:
      • 缺点:

1. 算法原理

分治算法是一种重要的算法设计范式,它将一个复杂问题分解为若干个规模较小的相同问题,递归地解决这些子问题,然后将子问题的解合并得到原问题的解。

基本步骤:

  1. 分解 (Divide): 将原问题分解为若干个规模较小的子问题
  2. 解决 (Conquer): 递归地解决各个子问题。如果子问题足够小,则直接求解
  3. 合并 (Combine): 将子问题的解合并为原问题的解

适用条件:

  • 问题可以分解为规模较小的相同问题
  • 子问题相互独立,不包含公共子问题
  • 子问题的解可以合并为原问题的解

2. JavaScript 实现示例

2.1 归并排序 (Merge Sort)

function mergeSort(arr) { // 基础情况:数组长度小于等于1时无需排序 if (arr.length <= 1) { return arr; } // 分解:将数组分为两半 const mid = Math.floor(arr.length / 2); const left = arr.slice(0, mid); const right = arr.slice(mid); // 解决:递归排序左右两部分 const sortedLeft = mergeSort(left); const sortedRight = mergeSort(right); // 合并:将两个已排序的数组合并为一个 return merge(sortedLeft, sortedRight);}function merge(left, right) { let result = []; let leftIndex = 0; let rightIndex = 0; // 比较两个数组的元素,将较小的放入结果数组 while (leftIndex < left.length && rightIndex < right.length) { if (left[leftIndex] <= right[rightIndex]) { result.push(left[leftIndex]); leftIndex++; } else { result.push(right[rightIndex]); rightIndex++; } } // 将剩余元素添加到结果数组 return result .concat(left.slice(leftIndex)) .concat(right.slice(rightIndex));}// 使用示例const unsortedArray = [38, 27, 43, 3, 9, 82, 10];console.log(\'原数组:\', unsortedArray);console.log(\'排序后:\', mergeSort(unsortedArray));

2.2 快速排序 (Quick Sort)

function quickSort(arr) { // 基础情况 if (arr.length <= 1) { return arr; } // 分解:选择基准元素并分区 const pivot = arr[Math.floor(arr.length / 2)]; const left = []; const right = []; const equal = []; // 将元素分为三组:小于、等于、大于基准元素 for (let element of arr) { if (element < pivot) { left.push(element); } else if (element > pivot) { right.push(element); } else { equal.push(element); } } // 解决:递归排序左右两部分 const sortedLeft = quickSort(left); const sortedRight = quickSort(right); // 合并:连接排序后的数组 return [...sortedLeft, ...equal, ...sortedRight];}// 使用示例const arrayToSort = [5, 3, 7, 6, 2, 9, 1, 8, 4];console.log(\'原数组:\', arrayToSort);console.log(\'排序后:\', quickSort(arrayToSort));

2.3 二分搜索 (Binary Search)

function binarySearch(arr, target, left = 0, right = arr.length - 1) { // 基础情况:未找到目标元素 if (left > right) { return -1; } // 分解:找到中间位置 const mid = Math.floor((left + right) / 2); // 解决:比较中间元素与目标元素 if (arr[mid] === target) { return mid; // 找到目标元素 } else if (arr[mid] > target) { // 在左半部分搜索 return binarySearch(arr, target, left, mid - 1); } else { // 在右半部分搜索 return binarySearch(arr, target, mid + 1, right); }}// 使用示例const sortedArray = [1, 3, 5, 7, 9, 11, 13, 15];const target = 7;const index = binarySearch(sortedArray, target);console.log(`元素 ${target} 在数组中的索引为: ${index}`);

2.4 最大子数组和 (Maximum Subarray Sum)

function maxSubarraySum(arr) { function maxSubarrayHelper(arr, low, high) { // 基础情况:只有一个元素 if (low === high) { return arr[low]; } // 分解:找到中点 const mid = Math.floor((low + high) / 2); // 解决:递归计算左半部分、右半部分和跨越中点的最大子数组和 const leftSum = maxSubarrayHelper(arr, low, mid); const rightSum = maxSubarrayHelper(arr, mid + 1, high); const crossSum = maxCrossingSum(arr, low, mid, high); // 合并:返回三者中的最大值 return Math.max(leftSum, rightSum, crossSum); } function maxCrossingSum(arr, low, mid, high) { // 计算包含中点的左侧最大和 let leftSum = Number.NEGATIVE_INFINITY; let sum = 0; for (let i = mid; i >= low; i--) { sum += arr[i]; if (sum > leftSum) { leftSum = sum; } } // 计算包含中点右侧的最大和 let rightSum = Number.NEGATIVE_INFINITY; sum = 0; for (let i = mid + 1; i <= high; i++) { sum += arr[i]; if (sum > rightSum) { rightSum = sum; } } // 返回跨越中点的最大子数组和 return leftSum + rightSum; } if (arr.length === 0) return 0; return maxSubarrayHelper(arr, 0, arr.length - 1);}// 使用示例const array = [-2, -5, 6, -2, -3, 1, 5, -6];console.log(\'数组:\', array);console.log(\'最大子数组和:\', maxSubarraySum(array));

2.5 大整数乘法 (Karatsuba Algorithm)

function karatsubaMultiply(x, y) { // 基础情况:数字较小直接相乘 if (x < 10 || y < 10) { return x * y; } // 将数字转换为字符串以获取长度 const xStr = x.toString(); const yStr = y.toString(); const n = Math.max(xStr.length, yStr.length); const half = Math.floor(n / 2); // 分解:将数字分为高低两部分 const high1 = Math.floor(x / Math.pow(10, half)); const low1 = x % Math.pow(10, half); const high2 = Math.floor(y / Math.pow(10, half)); const low2 = y % Math.pow(10, half); // 解决:递归计算三个乘积 const z0 = karatsubaMultiply(low1, low2); const z1 = karatsubaMultiply((low1 + high1), (low2 + high2)); const z2 = karatsubaMultiply(high1, high2); // 合并:组合结果 return (z2 * Math.pow(10, 2 * half)) + ((z1 - z2 - z0) * Math.pow(10, half)) + z0;}// 使用示例const num1 = 1234;const num2 = 5678;console.log(`${num1} × ${num2} = ${karatsubaMultiply(num1, num2)}`);console.log(\'验证:\', num1 * num2);

3. 实际应用

3.1 在前端开发中的应用

3.1.1 DOM 树遍历
function traverseDOM(node, callback) { // 解决:对当前节点执行回调 callback(node); // 分解与解决:递归遍历子节点 const children = node.children; for (let i = 0; i < children.length; i++) { traverseDOM(children[i], callback); }}// 使用示例:查找所有包含特定类名的元素function findElementsByClass(root, className) { const results = []; traverseDOM(root, (node) => { if (node.classList && node.classList.contains(className)) { results.push(node); } }); return results;}
3.1.2 树形菜单渲染
function renderMenu(menuData) { // 基础情况 if (!menuData || menuData.length === 0) { return \'\'; } let html = \'
    \'; // 分解与解决:递归渲染每个菜单项 menuData.forEach(item => { html += \'
  • \'; html += item.name; // 如果有子菜单,递归渲染 if (item.children && item.children.length > 0) { html += renderMenu(item.children); } html += \'
  • \'
    ; }); html += \'
\'
; return html;}// 使用示例const menuData = [ { name: \'首页\', children: [] }, { name: \'产品\', children: [ { name: \'产品A\', children: [] }, { name: \'产品B\', children: [] } ] }, { name: \'关于我们\', children: [] }];console.log(renderMenu(menuData));

3.2 在算法优化中的应用

3.2.1 快速傅里叶变换 (FFT) - 简化版
function fft(coefficient) { const n = coefficient.length; // 基础情况 if (n === 1) { return [coefficient[0]]; } // 分解:将系数分为偶数索引和奇数索引 const even = []; const odd = []; for (let i = 0; i < n; i += 2) { even.push(coefficient[i]); if (i + 1 < n) { odd.push(coefficient[i + 1]); } } // 解决:递归计算偶数和奇数部分的FFT const evenFFT = fft(even); const oddFFT = fft(odd); // 合并:组合结果 const result = new Array(n); for (let i = 0; i < n / 2; i++) { const theta = -2 * Math.PI * i / n; const t = Math.exp(1j * theta) * oddFFT[i]; // 简化的复数表示 result[i] = evenFFT[i] + t; result[i + n / 2] = evenFFT[i] - t; } return result;}

3.3 在数据处理中的应用

3.3.1 分治法处理大数据集
// 分治法计算数组的和function divideAndConquerSum(arr, start = 0, end = arr.length - 1) { // 基础情况 if (start === end) { return arr[start]; } if (start > end) { return 0; } // 分解:找到中点 const mid = Math.floor((start + end) / 2); // 解决:递归计算左右两部分的和 const leftSum = divideAndConquerSum(arr, start, mid); const rightSum = divideAndConquerSum(arr, mid + 1, end); // 合并:返回两部分的和 return leftSum + rightSum;}// 使用示例const largeArray = Array.from({length: 1000000}, () => Math.floor(Math.random() * 100));console.time(\'分治法求和\');const sum1 = divideAndConquerSum(largeArray);console.timeEnd(\'分治法求和\');console.time(\'传统方法求和\');const sum2 = largeArray.reduce((a, b) => a + b, 0);console.timeEnd(\'传统方法求和\');console.log(\'分治法结果:\', sum1);console.log(\'传统方法结果:\', sum2);

4. 复杂度分析

时间复杂度

分治算法的时间复杂度通常可以用递推关系表示:

T(n) = aT(n/b) + f(n)

其中:

  • a 是子问题的数量
  • n/b 是子问题的规模
  • f(n) 是分解和合并步骤的时间复杂度

根据主定理 (Master Theorem):

  1. 如果 a > b^k,则 T(n) = O(n^(log_b(a)))
  2. 如果 a = b^k,则 T(n) = O(n^k * log n)
  3. 如果 a < b^k,则 T(n) = O(n^k)

空间复杂度

分治算法的空间复杂度主要由递归调用栈的深度决定,通常是 O(log n) 到 O(n)。

5. 优缺点

优点:

  1. 自然性:许多问题本身具有递归性质,分治法与之天然契合
  2. 效率性:对于某些问题,分治法能显著降低时间复杂度
  3. 可并行性:子问题相互独立,易于并行处理
  4. 清晰性:算法结构清晰,易于理解和实现

缺点:

  1. 递归开销:递归调用会产生额外的时间和空间开销
  2. 重复子问题:某些问题可能存在重复子问题,需要额外优化
  3. 栈溢出风险:深度递归可能导致栈溢出