Algorithm

2022-08-12· 25min

#相关定义

#时间复杂度

  • 定义:执行算法所需要的计算工作量,其复杂度反映了程序执行时间随输入规模增长而增长的量级
  • 通常用大 O 符号表述,定义为 (n) = O(f(n))
    • Ο(1) 常数型 < Ο(log n) 对数型 < Ο(n) 线性型< Ο(nlog n) 线性对数型 < Ο(n^2) 平方型 <Ο(n^3) 立方型 < ... < O(n^k)k 次方型 < O(2^n) 指数型 < O(n!) 阶乘型

#空间复杂度

  • 定义:执行算法所需内存的大小
  • O(1): 所需要的临时空间不随着某个变量 n 的大小而变化,即此算法空间复杂度为一个常量
function fff() {
  return 1 + 1;
}
  • O(n): 定义一个数组的空间,数组的长度随着 n 的规模不同,会不一样
function fff(n) {
  let arr = [];
  for (let i = 0; i < n; i++) {
    arr.push(i);
  }
  return arr;
}
console.log(fff(2)); // [0, 1]
  • O(n^2): 定义二维数组的空间
function fff(n) {
  let arr = [];
  for (let i = 0; i < n; i++) {
    arr[i] = [];
    for (let j = 0; j < n; j++) {
      arr[i][j] = j;
    }
  }
  return arr;
}

console.log(fff(2)); // [[0,1], [0,1]]

#数据结构

#栈(Stack)

  • 后入先出(LIFO, Last In First Out)的数据结构
class Stack {
  constructor() {
    this.items = [];
  }
  // 入栈
  push(i) {
    this.items.push(i);
  }
  // 出栈
  pop() {
    return this.items.pop();
  }
  // 查看栈顶元素
  peek() {
    return this.items[this.items.length - 1];
  }
  // 获取栈的长度
  size() {
    return this.items.length;
  }
  //清空栈
  clear() {
    this.items = [];
  }
  // 判断栈是否空
  isEmpty() {
    return this.items.length === 0;
  }
}

#队列(Queue)

  • 先进先出(FIFO, First In First Out)的数据结构
class Queue {
  constructor() {
    this.items = [];
  }
  // 入队
  push(i) {
    this.items.push(i);
  }
  // 出队
  pop() {
    return this.items.shift();
  }
  // ...
}

#链表(Linked List)

  • 一种线性表,但并不会按线性的顺序存储数据,链表中每一个节点中存储了到下一个节点的指针
// 节点
class Node {
  constructor(val) {
    this.val = val;
    this.next = null;
  }
}
  • 常见结构:单链表、循环链表、双链表、双循环链表
    • 单链表(Singly linked list):每一个节点中存储了到下一个节点的指针,若结点为链表的尾端,则尾节点指针指向null

    • 循环链表(Circular linked list):头节点和尾节点被连接在一起,它的第一个节点之前就是最后一个节点

    • 双链表(Doubly linked list):每个节点存储两个指针,一个指向前一个节点,另一个指向下一个节点

    • 双循环链表:头节点的前趋指针指向尾节点,尾节点的后驱指针指向头节点

#热热身

#509. 斐波那契数

/**
 * 该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和
 *
 * 用数列表示为:0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144…,数列中的每一项都被称为斐波那契数
 * 用符号Fn表示:F(0)=1,F(1)=1, F(n)=F(n - 1)+F(n - 2)(n ≥ 2,n ∈ N)。
 *
 * @param {number} n
 * @return {number}
 *
 * 复杂度分析
 * 时间复杂度:O(n)
 * 空间复杂度:O(1)
 *
 */

var fib = function (n) {
  if (n < 2) return n;
  let p = 0;
  let q = 0;
  let r = 1;
  for (let i = 2; i <= n; i++) {
    p = q;
    q = r;
    r = p + q;
  }
  return r;
};

fib(3); // F(3) = F(2) + F(1) = 1 + 1 = 2
fib(5); // 2 + 3 = 5

// n     =  0   1   2   3   4   5   6   7    8
// f(n)  =  0   1   1   2   3   5   8   13 21

function fib2(n) {
  if (n <= 1) return n;
  let fib = [0, 1]; // 保存斐波那契数列的结果
  for (let i = 2; i <= n; i++) {
    fib[i] = fib[i - 1] + fib[i - 2]; // 计算第 i 个斐波那契数
  }
  return fib[n];
}

#796. 旋转字符串

function rotateString(s, gloal) {
  return s.length && gloal.length && (s + s).includes(gloal);
}
console.log(rotateString("abcde", "cdeab")); // true
console.log(rotateString("abcde", "abced")); // false

#112. 路径总和

/**
 * 给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和 targetSum。如果存在,返回 true; 否则,返回 false 。
 * 叶子节点 是指没有子节点的节点。
 *
 *  * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 *
 * 复杂度分析
 * 时间复杂度:O(n),其中 n 为二叉树的节点个数。
 * 空间复杂度:O(n),最坏情况下,二叉树退化成一条链,递归需要 O(n) 的栈空间。
 *
 * @param {TreeNode} root
 * @param {number} targetSum
 * @return {boolean}
 */

var hasPathSum = function (root, targetSum) {
  if (root === null) return false;

  targetSum -= root.val;
  if (root.left === root.right) return targetSum === 0;

  return hasPathSum(root.left, targetSum) || hasPathSum(root.right, targetSum);
};

#415. 字符串相加

/**
 * 给定两个字符串形式的非负整数 num1 和 num2 ,计算它们的和并同样以字符串形式返回。
 *
 * 时间复杂度:O(max(len1​,len2​))
 * 空间复杂度:O(1)
/**
 * @param {string} num1
 * @param {string} num2
 * @return {string}
 */
var addStrings = function (num1, num2) {
  let i = num1.length - 1;
  let j = num2.length - 1;
  let add = 0;
  const ans = [];
  while (i >= 0 || j >= 0 || add != 0) {
    const x = i >= 0 ? num1.charAt(i) - "0" : 0;
    const y = j >= 0 ? num2.charAt(j) - "0" : 0;
    const result = x + y + add;
    ans.push(result % 10);
    add = Math.floor(result / 10);
    i -= 1;
    j -= 1;
  }
  return ans.reverse().join("");
};
addStrings("110", "22"); // '132'

#5. 最长回文子串

/**
 * 给你一个字符串 s,找到 s 中最长的回文子串。
 *
 * @param {string} s
 * @return {string}
 *
 * 时间复杂度:𝑂(𝑁^2)
 */

var longestPalindrome = function (s) {
  if (s.length < 2) return s;
  let res = "";
  for (let i = 0; i < s.length; i++) {
    // 回文子串长度是奇数
    helper(i, i);
    // 回文子串长度是偶数
    helper(i, i + 1);
  }
  function helper(m, n) {
    while (m >= 0 && n < s.length && s[m] == s[n]) {
      m--;
      n++;
    }
    // 注意此处 m,n 的值循环完后  是恰好不满足循环条件的时刻
    // 此时m到n的距离为 n-m+1,但是 mn 两个边界不能取 所以应该取 m+1 到 n-1 的区间  长度是 n-m-1
    if (n - m - 1 > res.length) {
      // slice 也要取[ m+1,n-1] 这个区间
      res = s.slice(m + 1, n);
    }
  }
  return res;
};

longestPalindrome("dddccee"); // ddd
longestPalindrome("abba"); // abba
  • 稀疏数组:数组的元素不是连续有值
function convertToSparseArray(originalArray) {
  let sparseArray = [];
  const rowCount = originalArray.length;
  const colCount = originalArray[0].length;
  let nonZeroCount = 0;

  for (let i = 0; i < rowCount; i++) {
    for (let j = 0; j < colCount; j++) {
      if (originalArray[i][j] !== 0) {
        nonZeroCount++;
        sparseArray.push([i, j, originalArray[i][j]]);
      }
    }
  }
  sparseArray = [[rowCount, colCount, nonZeroCount], ...sparseArray];
  return sparseArray;
}

var originalArray = [
  [1, 0, 0, 0, 0],
  [0, 1, 0, 0, 0],
];

var sparseArray = convertToSparseArray(originalArray);
console.log(sparseArray);
[
  [2, 5, 2], // row: 2, col: 5, nonZeroCount: 2
  [0, 0, 1],
  [1, 1, 1],
];
  • Array 转 TreeNode
function TreeNode(val, left, right) {
  this.val = val === undefined ? 0 : val;
  this.left = left === undefined ? null : left;
  this.right = right === undefined ? null : right;
}

function arrayToTreeNode(arr) {
  if (!arr.length) return null;

  const root = new TreeNode(arr[0]);
  const queue = [root];
  let i = 1;

  while (i < arr.length) {
    const current = queue.shift();

    if (arr[i] !== null) {
      current.left = new TreeNode(arr[i]);
      queue.push(current.left);
    }
    i++;

    if (i < arr.length && arr[i] !== null) {
      current.right = new TreeNode(arr[i]);
      queue.push(current.right);
    }
    i++;
  }

  return root;
}

const arr = [1, 2, 3];
const treeRoot = arrayToTreeNode(arr);
console.log(treeRoot);

// {
//     "val": 1,
//     "left": {
//         "val": 2,
//         "left": null,
//         "right": null
//     },
//     "right": {
//         "val": 3,
//         "left": null,
//         "right": null
//     }
// }

#稀疏数组搜索

/**
 * 稀疏数组搜索。有个排好序的字符串数组,其中散布着一些空字符串,编写一种方法,找出给定字符串的位置。
 *
 * @param {string[]} words
 * @param {string} s
 * @return {number}
 */
var findString = function (words, s) {
  // 时间复杂度是 O(n)
  // 方式1:
  // for (const i in words) if (words[i] === s) return i;
  // return -1;
  // 方式2:
  // return words.indexOf(s)

  // 时间复杂度是 O(logn)
  // 方式: 二分查找
  // 1. 初始左右指针
  let left = 0;
  let right = words.length - 1;

  while (left <= right) {
    // 2. 计算中间位置
    let mid = Math.floor((left + right) / 2);

    // 3. 处理空字符串
    if (words[mid] === "") {
      let leftMid = mid - 1;
      let rightMid = mid + 1;
      while (true) {
        if (leftMid < left && rightMid > right) {
          return -1;
        } else if (rightMid <= right && words[rightMid] !== "") {
          mid = rightMid; // 找到右侧最近的非空字符串
          break;
        } else if (leftMid >= left && words[leftMid] !== "") {
          mid = leftMid; // 找到左侧最近的非空字符串
          break;
        }
        rightMid++;
        leftMid--;
      }
    }

    // 4. 比较中间元素与目标字符串
    // (字符串比较会先转成 ascii 码,"ball".charCodeAt())
    if (words[mid] === s) {
      return mid;
    } else if (words[mid] < s) {
      left = mid + 1;
    } else {
      right = mid - 1;
    }
  }

  return -1;
};

var words = ["at", "", "", "", "ball", "", "", "car", "", "", "dad", "", ""],
  s = "ball";
findString(words, s); // 4