算法导论知识梳理(一):函数增加符号及分治法

前言

之因此想写这一系列文章,主要是由于最近有在看算法导论。为何要看这本书,虽然有些人会认为对前端来讲,数据结构和算法可能并非那么重要。其实否则,这一块的知识其实应该算是重中之重,只有真正理解这一块知识,才能提高咱们的核心竞争力。不少时候,咱们与别人的差距可能就差在这些通用知识上,而不只仅是某一块具体的知识领域。前端

根据我目前的阅读状况来讲,对其中的内容其实也都处于一种只知其一;不知其二的状态吧,因此才打算经过写文章的方式来进行巩固和梳理,也算是一种复习吧。固然这本书我也还没看完,纯粹就是一边看一边写,因此更新进度彻底随缘。算法

渐近记号Θ、Ο、o、Ω、ω

在开始以前,先来了解如下几个函数增加的渐进记号。渐近记号值得是对于给定的函数g(n),用渐近记号来表示如下函数的集合:数组

渐近紧确界记号:Θ(big-theta)

Θ(g(n))={ f(n):存在正常量c1、c2和n0,使的对全部n >= n0,有0 <= c1g(n) <= f(n) <= c2g(n) }数据结构

渐近紧确上界记号:Ο(big-order)

Ο(g(n))={ f(n):存在正常量c和n0,使的对全部n >= n0,有0 <= f(n) <= cg(n) }cors

非渐近紧确上界记号:o(small-order)

o(g(n))={ f(n):对任意正常量c > 0,存在常量n0 > 0,使的对全部n >= n0,有0 <= f(n) < cg(n) }数据结构和算法

渐近紧确下界记号:Ω(big-omege)

Ω(g(n))={ f(n):存在正常量c和n0,使的对全部n >= n0,有0 <= cg(n) <= f(n) }函数

非渐近紧确下界记号:ω(small-omege)

ω(g(n))={ f(n):对任意正常量c > 0,存在常量n0 > 0,使的对全部n >= n0,有0 <= cg(n) < f(n) }优化

说明

更直观地,能够经过图来查看 ui

1-1

咱们能够经过下面的表格来加深理解spa

记号 含义 简单理解
Θ 渐近紧确 至关于"="
Ο 渐近紧确上界 至关于"<="
o 非渐近紧确上界 至关于"<"
Ω 渐近紧确下界 至关于">="
ω 非渐近紧确下界 至关于">"

在平时,咱们更多地探讨的实际上是O,由于在大多数算法的复杂度中,咱们只须要关心其上界,而不是其下界,而对于Θ而言,咱们须要同时证实其上界和下界。(PS:这句话的意思,指的并非算法的最好状况和最差状况,而是对于某一具体状况下,其时间复杂度T(n)是一个多项式,而咱们只关心多项式的上界,例如:在平均状况下,某算法的时间复杂度计算出来为T(n)=c1n2+c2n+c0,则其复杂度为O(n2),固然也能够用Θ(n2)表示其复杂度)

分治法

分治法即将原问题分解为几个规模较小,但相似于原问题的子问题,递归地求解这些子问题,而后再合并这些子问题的解来创建原问题的解。通常的,其有如下三个步骤:

  1. 分解原问题为若干子问题,这些子问题是原问题的规模较小的实例
  2. 解决这些子问题,递归地求解各子问题,然而,若子问题的规模足够小,则直接求解
  3. 合并这些子问题的解,组合成原问题的解

使用分治法求解最大子数组

比较典型的的例子有归并排序和快速排序,关于这两个排序,下面将会说起。这里,我先从另外一个经典的例子开始:给定一个数组A=[13,-3,-25,20,-3,-16,-23,18,20,-7,12,-5,-22,15,-4,7],求解其最大子数组Max。(最大子数组Max定义:Max为A的子数组,而且对于任意A的子数组a来讲,都有sum(Max) >= sum(a)。数组A一定是同时包含正负值的,由于若是全是正值或全是负值,此问题将毫无心义。映射到现世生活中的一个典型的例子就是给定某一个时间段内股票的涨跌,计算在该时间段内如何买入卖出才能得到最大收益。)

假定咱们须要寻找a[low...high]的最大子数组,使用分治法就要求咱们将其分解为两个规模尽可能相等的子数组,假设mid为其中央位置。那么其最大子数组Max必然属于如下状况之一:

  1. 彻底位于子数组[low...mid]中
  2. 彻底位于子数组[mid+1...high]中
  3. 跨越了mid,一部分位于[low...mid]中,另外一部分位于[mid+1...high]

那么对于状况1和2而言,其实仍是求解最大子数组,只是规模更小而已,因此递归这一过程便可。那么对于状况3来讲,相对左部来讲,只需从mid开始,逐级递减至low,求出最大值;右部则相反。而后将左右部的结果合并便可。

js代码:

const arr = [13,-3,-25,20,-3,-16,-23,18,20,-7,12,-5,-22,15,-4,7];
// 跨越左右数组
function findCrossMaxSubArr(arr, low, mid ,high) {
  let leftMax = rightMax = sum = leftIndex = rightIndex = 0;
  // 求左部最大
  for(let i = mid; i >= low; i--) {
    sum += arr[i];
    if(sum >= leftMax) {
      leftMax = sum;
      leftIndex = i;
    }
  }
  // 求右部最大
  sum = 0;
  for(let i = mid + 1; i <= high; i++) {
    sum += arr[i];
    if(sum >= rightMax) {
      rightMax = sum;
      rightIndex = i;
    }
  }
  return {
    sum: leftMax + rightMax,
    leftIndex,
    rightIndex
  };
}
function findMaxSubArr(arr, low, high) {
  if(low === high) return {
    sum: arr[low],
    leftIndex: low,
    rightIndex: high
  };
  const mid = Math.floor((low + high) / 2);
  // 不跨越左右数组时,直接递归便可
  const leftMax = findMaxSubArr(arr, low, mid);
  const rightMax = findMaxSubArr(arr, mid + 1, high);
  // 跨越左右数组时,求此状况下的最大值
  const corssMax = findCrossMaxSubArr(arr, low, mid, high);
  // 找出三种状况下的最大值
  let max = leftMax;
  if(rightMax.sum > max.sum) max = rightMax;
  if(corssMax.sum > max.sum) max = corssMax;
  return max;
}
// 返回
// {
// leftIndex: 7,
// rightIndex: 10,
// sum: 43
// }
console.log(findMaxSubArr(arr, 0, 15))
复制代码

接下来,就是对复杂度进行分析。可是由于完整的过程比较复杂,因此这里就不进行分析,有兴趣了解的能够去查阅一下完整的推导过程。如下为我我的的简单理解:首先,由于二分策略,因此二分的层级为log2n,而后比较的操做次数为cn次,因此大体估算其复杂度为O(nlogn)。

其余算法求解最大子数组

单单就这一问题而言,大多数人可能想到的第一种方法就是两层循环暴力求解,可是这样复杂度就是O(n2)。而上面所讲到的分治法的复杂度只需O(nlogn),因而可知分治法的优点。可是,除了分治法之外,还能够经过动态规划解决这一问题,其时间复杂度仅需O(n),其对应的状态转移方程为:dp[i] = Max(dp[i-1] + A[i], A[i]),dp[i]表示以arr[i]为结尾的最大连续子数组之和,只需求dp[i]的最大值便可。这一部分打算到后面动态规划的章节再详细讲解,这里先贴一下代码。

// 如需获取下标等信息,只需在此基础上扩展
function findMaxSubArr(arr) {
  const dp = [];
  for(let i = 0; i < arr.length; i++) {
    if(i === 0) dp[i] = arr[i];
    else dp[i] = Math.max(dp[i - 1] + arr[i], arr[i]);
  }
  return Math.max(...dp);
}
复制代码

此算法时间复杂度为O(n),空间复杂度也为O(n)

此外,还有另外一种联机算法也能处理该问题。我的感受应该算是上面算法的优化了空间复杂度的版本。其时间复杂度为O(n),空间复杂度为O(1)

联机算法是在任意时刻算法对要操做的数据只读入(扫描)一次,一旦被读入并处理,它就不须要在被记忆了。而在此处理过程当中算法能对它已经读入的数据当即给出相应子序列问题的正确答案。

function findMaxSubArr(arr) {
  // curSum表示到当前i为止的累加和
  let maxSum = curSum = 0;
  for(let i = 0; i < arr.length; i++) {
    curSum += arr[i];
    if(curSum > maxSum) maxSum = curSum;
    // 若是累加和出现小于0的状况,
    // 则最大和子序列确定不可能包含前面的元素, 
    // 这时将累加和置0,从下个元素从新开始累加
    else if(curSum < 0) curSum = 0;
  }
  return maxSum;
}
复制代码

此章内容先到这里为止,以为有用的话麻烦点个赞呦,谢谢。下一节内容预告:比较排序算法及其下界。

相关文章
相关标签/搜索