【算法】【更新中】 算法101笔记

285 阅读3分钟

参考

开篇

算法复杂度

假设现有算法 A 和算法 B,其对于同样的输入,总能给到同样的输出,则可以根据两种算法在执行过程中的的时间消耗和空间消耗来比较其优劣。而时间复杂度空间复杂度便是分别用于描述算法时间消耗和空间消耗的指标。时间复杂度用O(f(n))表示,其中f(n)是算法对于数据规模n所消耗时间多少的函数,譬如,对于数据规模n^2,某算法执行耗时为n^2,那么称该算法的时间复杂度为O(n^2);空间复杂度同理。时间复杂度和空间复杂度统称复杂度,复杂度的计算遵循以下原则:

  • 复杂度与具体的常系数无关,如O(n)O(2n)表示同样的复杂度,这是因为O(2n)=O(n)+O(n),即一段复杂度为O(2n)的代码只不过是先后执行两遍复杂度为O(n)的代码而已,所以它们的复杂度是一致的。
  • 当多项式级的复杂度相加时,选择高者作为结果,如O(n^2+n)O(n^2)表示同样的复杂度,这是因为随着n的增加,二阶多项式的增长率远多于一阶多项式,所以只需要通过增长率更高的二阶多项式来表示复杂度。
  • O(1)是一个特殊的复杂度值,其表示算法的时间/空间消耗和数据规模n无关。

时间复杂度

以下是几种常见的时间复杂度:

// n为数据规模
const n = data.length;

// O(1):时间消耗f(n)=c,c为常数
// do something

// O(n):时间消耗f(n)=n
for (let i = 1; i <= n; i++) {
  // do something
}

// O(n^2):时间消耗f(n)=n^2
for (let i = 1; i <= n; i++) {
  for (let j = 0; j <= n; j++) {
    // do something
  }
}

// O(logn):时间消耗f(n)=logn
for (let i = 1; i <= n; i *= 2) {
  // do something
}

空间复杂度

以下是几种常见的空间复杂度:

// n为数据规模
const n = data.length;

// O(1):空间消耗f(n)=c,c为常数
const arr = new Array(1);

// O(n):空间消耗f(n)=n
const arr = new Array(n);

// O(n^2):空间消耗f(n)=n^2
const arr = new Array(n).fill(new Array(n));

总结

常见的复杂度从低到高排序,依次是:O(1)O(logn)O(n)O(nlogn)O(n^2)O(2^n)O(n!)

最后,对于一个算法来说,其时间复杂度和空间复杂度通常是相互影响的,但一般而言,空间廉价,时间昂贵。譬如,为了提高通行流量,可以选择扩建马路或研制速度更快的汽车,但明显前者的成本远低于后者,所以在编写算法时,为换取更优的时间复杂度而牺牲空间复杂度,是非常值得的一件事。

笔者会在以下的题解中,用【】标记出使复杂度提高的语句,其中时间复杂度用【T()】表示,空间复杂度用【Z()】表示。

基础篇

数字

罗马数字转整数

/**
 * 原题:https://leetcode-cn.com/problems/roman-to-integer
 * 时间复杂度:O(n)
 * 空间复杂度:O(1)
 * @param {string} s
 * @return {number}
 */
var romanToInt = function (s) {
  // 定义romanMap,其key和val分别为罗马数字及其对应整数
  const romanMap = new Map([
    ['I', 1],
    ['V', 5],
    ['X', 10],
    ['L', 50],
    ['C', 100],
    ['D', 500],
    ['M', 1000],
  ]);
  // 定义ret,用于表示结果整数
  let ret = 0;
  // 定义max,用于表示当前遇到的最大的罗马数字对应的整数
  let max = 0;
  // 从右向左遍历s【T(n)】
  for (let i = s.length - 1; i >= 0; i--) {
    // cur为当前罗马数字对应整数
    const cur = romanMap.get(s[i]);
    // 如果cur>=max,则cur效益为正,否则cur效益为负,
    // cur效益为正时,还需更新max
    if (cur >= max) {
      ret += cur;
      max = cur;
    } else {
      ret -= cur;
    }
  }
  // 返回结果
  return ret;
};

Fizz Buzz

/**
 * 原题:https://leetcode-cn.com/problems/fizz-buzz
 * 时间复杂度:O(n)
 * 空间复杂度:O(n)
 * @param {number} n
 * @return {string[]}
 */
var fizzBuzz = function (n) {
  // 定义ret,用于存放结果【Z(n)】
  const ret = new Array(n);
  // 从数字1遍历到数字n【T(n)】
  for (let i = 1; i <= n; i++) {
    // str为当前数字对应的字符串,初始为空
    let str = '';
    // 如果n是3的倍数,str末尾添加'Fizz',
    // 如果n是5的倍数,str末尾添加'Buzz',
    // 如果都不是,则返回n的字符串形式
    if (i % 3 === 0) str += 'Fizz';
    if (i % 5 === 0) str += 'Buzz';
    if (str === '') str += String(i);
    // 修改ret中对应下标的元素
    ret[i - 1] = str;
  }
  // 返回结果
  return ret;
};

计数质数

/**
 * 原题:https://leetcode-cn.com/problems/count-primes
 * 时间复杂度:O(nlogn)
 * 空间复杂度:O(n)
 * @param {number} n
 * @return {number}
 */
var countPrimes = function (n) {
  // 如果n<=2,则直接返回0
  if (n <= 2) return 0;
  // 定义ret,用于表示当前的质数个数,初始为n-2,减2是因为要排除数字1和数字n
  let ret = n - 2;
  // 定义nums,用于标记非质数,如将nums[i]设置为true,表示将数字i+1标记为非质数【Z(n)】
  const nums = new Array(n);
  // 通过双重循环排除掉非质数,如i=3这轮循环负责排除掉3的倍数,再如j=4表示排除掉3*4=12,
  // 过程中一共要历经n/2+n/3+n/4+...+n/√n次判断【T(nlogn)】
  for (let i = 2; i < Math.sqrt(n); i++) {
    for (let j = 2; i * j < n; j++) {
      // i*j为非质数,所以可将nums[i*j-1]设置为true,
      // 首次标记时,还需将ret减1
      const idx = i * j - 1;
      if (nums[idx] !== true) {
        nums[idx] = true;
        ret--;
      }
    }
  }
  // 返回结果
  return ret;
};

3 的幂

/**
 * 原题:https://leetcode-cn.com/problems/power-of-three
 * 时间复杂度:O(logn)
 * 空间复杂度:O(logn)
 * @param {number} n
 * @return {boolean}
 */
var isPowerOfThree = function (n) {
  // 因为3^0=1,所以当n=1时,返回true
  if (n === 1) return true;
  // 当n!=1且n<3时,返回false
  if (n < 3) return false;
  // 否之,递归做判断,判断次数和调用栈深度相同【T(logn)】【Z(logn)】
  return isPowerOfThree(n / 3);
};

Excel 表列序号

/**
 * 原题:https://leetcode-cn.com/problems/excel-sheet-column-number
 * 时间复杂度:O(n)
 * 空间复杂度:O(1)
 * @param {string} s
 * @return {number}
 */
var titleToNumber = function (s) {
  // 定义ret,用于表示结果
  let ret = 0;
  // 遍历s【T(n)】
  for (let i = 0; i < s.length; i++) {
    // 利用Unicode编码,将字符转化为数字,如字符'A'的编码为65,base=65-64=1
    const base = s[i].charCodeAt() - 64;
    // 计算当前字符对应的进制位数pow
    const pow = s.length - i - 1;
    // 基于26进制转10进制的规则,计算当前字符代表的数值,并累加至ret上
    ret += base * 26 ** pow;
  }
  // 返回结果
  return ret;
};

快乐数

/**
 * 原题:https://leetcode-cn.com/problems/happy-number
 * 时间复杂度:?
 * 空间复杂度:?
 * @param {number} n
 * @return {boolean}
 */
var isHappy = function (n) {
  // 如果n=1,返回true
  if (n === 1) return true;
  // 定义unHappys,用于存放已经访问过的数字,
  // isHappyNumber涵义详见下文
  const unHappys = [];
  return isHappyNumber(n);
  // 利用递归判断x是否为快乐数
  function isHappyNumber(x) {
    // 将x添加到unHappys中
    unHappys.push(x);
    // 计算x的平方和x_
    const x_ = String(x)
      .split('')
      .reduce((prev, cur) => prev + cur * cur, 0);
    // 如果x_=1,则返回false,
    // 如果unHappys中已有x_,说明进入了死循环,此时应返回false,
    // 否则继续做递归判断
    if (x_ === 1) return true;
    else if (unHappys.includes(x_)) return false;
    else return isHappyNumber(x_);
  }
};

阶乘后的零

Pow(x, n)

两数相除

分数到小数和 x 的平方根

字符串

数组

进阶篇

链表

堆、栈和队列

二叉树

高级篇

动态规划

回溯算法

排序与搜索