算法基础

208 阅读5分钟

时间、空间复杂度

时间复杂度

```js
function traverse(arr) {
  var len = arr.length
  for(var i=0;i<len;i++) {
      console.log(arr[i])
  }
}

T(n) = 1 + 1 + (n+1) + n + n + n + n*(n+1) + n*n + n*n = 3n^2 + 5n + 3
```
- 若 T(n) 是常数,那么无脑简化为1
- 若 T(n) 是多项式,我们只保留次数最高那一项,并且将其常数系数无脑改为1。

``` js 
O(n)=n^2
```

> 常见的时间复杂度按照从小到大的顺序排列,有以下几种:
> 常数时间:O(1) 对数时间:O(logn) 线性时间:O(n) 线性对数时间:O(nlogn) 二次时间:O(n^2) 三次时间:O(n^3) 指数时间O(2^n)

空间复杂度

```js
function traverse(arr) {
  var len = arr.length
  for(var i=0;i<len;i++) {
      console.log(arr[i])
  }
}
```
- 循环体在执行时,并没有开辟新的内存空间,整个 traverse 函数对内存的占用量是恒定的,它对应的空间复杂度就是 O(1)。

```js
function init(n) {
  var arr = []
  for(var i=0;i<n;i++) {
      arr[i] = i
  }
  return arr
}
```
- arr最终的大小是由输入的 n 的大小决定的,它会随着 n 的增大而增大、呈一个线性关系。因此这个算法的空间复杂度就是 O(n)。

DFS(深度优先搜索)和BFS(广度优先搜索)

const root = {
  val: "A",
  left: {
    val: "B",
    left: {
      val: "D"
    },
    right: {
      val: "E"
    }
  },
  right: {
    val: "C",
    right: {
      val: "F"
    }
  }
};
  • 先序遍历过程
// 所有遍历函数的入参都是树的根结点对象
function DFS(root) {
    // 递归边界,root 为空
    if(!root) {
        return 
    }
    
    // 输出当前遍历的结点值
    console.log('当前遍历的结点值是:', root.val)  
    // 递归遍历左子树 
    preorder(root.left)  
    // 递归遍历右子树  
    preorder(root.right)
}
  • 二叉树的层序遍历
function BFS(root) {
  const queue = [] // 初始化队列queue
  // 根结点首先入队
  queue.push(root)
  // 队列不为空,说明没有遍历完全
  while(queue.length) {
      const top = queue[0] // 取出队头元素  
      // 访问 top
      console.log(top.val)
      // 如果左子树存在,左子树入队
      if(top.left) {
          queue.push(top.left)
      }
      // 如果右子树存在,右子树入队
      if(top.right) {
          queue.push(top.right)
      }
      queue.shift() // 访问完毕,队头元素出队
  }
}

换个角度思考

空间换时间,Map类型(思想)存数据

> 描述: 给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
> 示例: 给定 nums = [2, 7, 11, 6, 3, 15], target = 9
> 返回 [[0, 1], [3, 4]]

```js
/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
const twoSum = function (nums, target) {
  const res = []
  // 这里我用对象来模拟 map 的能力
  const diffs = {};
  // 缓存数组长度
  const len = nums.length;
  // 遍历数组
  for (let i = 0; i < len; i++) {
    // 判断当前值对应的 target 差值是否存在(是否已遍历过)
    if (diffs[target - nums[i]] !== undefined) {
      // 若有对应差值
      res.push([diffs[target - nums[i]], i])
    }
    // 若没有对应差值,则记录当前值
    diffs[nums[i]] = i;
  }
  return res 
};
```

双指针

> 描述给你一个整数 x ,如果 x 是一个回文整数,返回 true ;否则,返回 false 。回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。
> 输入: 121
> 输出: true


```js
  const validPalindrome = function(x) {
    // 1. x < 0 的数字一定不是回文数,因为 -121 不等于 121-
    // 2. 个位数是 0 的数字也一定不是回文数,0 除外
    // if(x < 0 || (!(x % 10) && x)) return false;

    if (x < 0) return false;
    const s = x.toString();

      // 缓存字符串的长度
    const len = s.length

    // i、j分别为左右指针
    let b = 0, e = s.length - 1;

    // 当左右指针均满足对称时,一起向中间前进
    while (b < e) {
      if (s[b] !== s[e]) return false;
      ++b, --e;
    }

    return true;
  };
```




> 描述: 给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。
> 注意:答案中不可以包含重复的三元组。
> 示例:给定数组 nums = [-1, 0, 1, 2, -1, -4], 满足要求的三元组集合为: [ [-1, 0, 1], [-1, -1, 2] ]


```js
  /**
   * @param {number[]} nums
   * @return {number[][]}
   */
  const threeSum = function(nums) {
      // 用于存放结果数组
      let res = [] 
      // 给 nums 排序
      nums = nums.sort((a,b)=>{
          return a-b
      })
      // 缓存数组长度
      const len = nums.length
      // 注意我们遍历到倒数第三个数就足够了,因为左右指针会遍历后面两个数
      for(let i = 0;i < len - 2; i++) {
          // 左指针 j
          let j = i+1 
          // 右指针k
          let k = len-1   
          // 如果遇到重复的数字,则跳过
          if(i > 0 && nums[i] === nums[i - 1]) {
            continue
          }
          while(j<k) {
            // 三数之和小于0,左指针前进
            if(nums[i] + nums[j] + nums[k] < 0) {
                j++
              // 处理左指针元素重复的情况
              while(j < k && nums[j] === nums[j - 1]) {
                    j++
                }
            } else if(nums[i] + nums[j] + nums[k] > 0){
                // 三数之和大于0,右指针后退
                k--
              
              // 处理右指针元素重复的情况
              while(j<k && nums[k] === nums[k+1]) {
                    k--
                }
            } else {
              // 得到目标数字组合,推入结果数组
              res.push([nums[i],nums[j],nums[k]])
              
              // 左右指针一起前进
              j++  
              k--
            
              // 若左指针元素重复,跳过
              while(j<k&&nums[j]===nums[j-1]) {
                  j++
              }  
            
              // 若右指针元素重复,跳过
              while(j<k&&nums[k]===nums[k+1]) {
                k--
              }
            }
        }
      }
      
      // 返回结果数组
      return res
  };
```

动态规划

> 描述:假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
> 每次你可以爬 12 个台阶。你有多少种不同的方法可以爬到楼顶呢?

1. 递归

  ```js
    /**
    * @param {number} n
    * @return {number}
    */
    const climbStairs = function(n) {
        // 处理递归边界
        if(n === 1) {
            return 1
        }
        if(n === 2){
            return 2
        }
        // 递归计算
        return climbStairs(n-1) + climbStairs(n-2)
    };
  ```
2. 空间换时间

  ```js
    /**
    * @param {number} n
    * @return {number}
    */
    // 定义记忆数组 f
    const f = []
    const climbStairs = function(n) {
      if(n==1) {
          return 1
      }
      if(n==2) {
          return 2
      }
      // 若f[n]不存在,则进行计算
      if(f[n]===undefined)  f[n] = climbStairs(n-1) + climbStairs(n-2)
      // 若f[n]已经求解过,直接返回
      return f[n]
    };
  ```
3. 动态规划

  ```js
    /**
    * @param {number} n
    * @return {number}
    */
    const climbStairs = function(n) {
        // 初始化状态数组
        const f = [];
        // 初始化已知值
        f[1] = 1;
        f[2] = 2;
        // 动态更新每一层楼梯对应的结果
        for(let i = 3;i <= n;i++){
            f[i] = f[i-2] + f[i-1];
        }
        console.log(f,"f")
        // 返回目标值
        return f[n];
    };
  ```