高频算法题

124 阅读8分钟

高频题

  • 反转链表(链表)
  • 链表是否有环(快慢指针)
  • 两数之和 (数组、哈希表)
  • 接雨水(栈、数组、双指针)
  • 最长回文子串(动态规划)
  • 爬楼梯(动态规划)
  • 买卖股票(动态规划)
  • 零钱兑换(动态规划)
  • 二叉树的中序遍历(二叉树):递归+迭代
  • 二叉树的右视图(二叉树、头条高频)

排序

快排 nlogn

  • 快速排序是找出一个元素作为基准(pivot),然后对数组进行分区操作,使基准左边元素的值都不大于基准值,基准右边的元素值 都不小于基准值,如此作为基准的元素调整到排序后的正确位置。
  • 递归快速排序,将其他 n-1 个元素也调整到排序后的正确位置。 所以快速排序算法的核心算法是分区操作,即如何调整基准的位置以及调整返回基准的最终位置以便分治递归。

image.png

var arr=[38,26,97,19,66,1,5,49];
quickSort(arr, 0, arr.length - 1)

function quickSort(arr, begin, end) {
  // 递归终止条件
  if (begin >= end) return

  // 用第一个值当基准值
  let i = begin, j = end, vot = arr[i]

  while (i !== j) {
    //从后向前寻找较小值,较小数值向前移动  
    while (i < j && vot <= arr[j]) j-- 
    if (i < j) arr[i++] = arr[j]

    //从前往后寻找较大值,较大值向后移动  
    while (i < j && arr[i] <= vot) i++ 
    if (i < j) arr[j--] = arr[i]
  }

  arr[i] = vot // 一趟下来基本值到了正确的位置上

  // 分治递归
  quickSort(arr, begin, j - 1)
  quickSort(arr, i + 1, end)
}

冒泡排序

每次比较相邻的两个值,如果左边的更大,就进行交换,每一趟把最大的值,交换到最后一位。 有交换才进行下一趟,最多n-1趟。

var exchange = true;
for (var i = 0; i < arr.length && exchange; i++) {
  exchange = false;
  for (var j = 0; j < arr.length; j++) {
    // 相邻位置比较,左边的大,则交换位置
    if (arr[j] > arr[j + 1]) { 
      // j 和 j+1 交换位置
      [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
      exchange = true;
    }
  }
}

洗牌算法【头条】

打乱数组的标准:n个数产生的结果必须有n!种可能

function shuffle(arr) {
  const random = (l, r) => Math.floor(Math.random() * (r - l + 1)) + l
  const len = arr.length

  for (let i = 0; i < len; i++) {
    const rand = random(i, len - 1) // 从[i, len-1]找随机数

    [arr[i], arr[rand]] = [arr[rand], arr[i]]
  }

  return nums
}

[中等]56.合并区间

[[1,3],[2,6],[4,10],[15,18]]

👇

[[1,10],[15,18]]

image.png

var merge = function (intervals) {
  if (intervals.length === 0) return []

  var res = []

  intervals.sort((a, b) => a[0] - b[0])
  res.push(intervals[0])

  for (var i = 1; i < intervals.length; i++) {
    if (intervals[i][0] > res[res.length - 1][1]) res.push(intervals[i])
    else if (intervals[i][1] > res[res.length - 1][1]) res[res.length - 1][1] = intervals[i][1]
  }

  return res
}

动态规划

1.【递归/动态规划】斐波那契数列

要求:

用 JavaScript 实现斐波那契数列函数,返回第n个斐波那契数。 f(1) = 1, f(2) = 1 等 斐波那契数列:1、1、2、3、5、8…… (后一位是前两位之和)

fibonacci(6); //8

题解:

缺点:超时;很多相同的结果没有缓存,数据大的时候性能很差;时间复杂度:O(2n)

//递归的方法
var fib = function(n) {
  if (n === 1 || n === 2) return 1

  return fib(n - 1) + fib(n-2)
};

动态规划的方式:

优点:时间复杂度降为O(n),使用了一个数组空间来做缓存

var fib = function(n) {
  let arr = new Array(n)
  arr[0] = 0
  arr[1] = 1

  for (let i = 2; i <= n; i++) {
    arr[i] = (arr[i - 1] + arr[i - 2]) % 1000000007
  }

  return arr[n]
};

2. [中等] 3. 无重复字符的最长子串(字节)

var lengthOfLongestSubstring = function (str) {
  let arr = [], max = 0

  for (let i = 0; i < str.length; i++) {
    const char = str.charAt(i)
    let index = arr.indexOf(char)

    if (index !== -1) arr.splice(0, index + 1);

    arr.push(char)
    max = Math.max(arr.length, max)
  }

  return max
};

// test

strlen('abcdabca') // 4

3. [中等] 5. 最长回文子串

var longestPalindrome = function (s) {
  let len = s.length, longest = ''

  if (len <= 1) return s

  const dp = Array(len).fill().map(() => Array(len).fill())

  for (let r = 1; r < s.length; r ++) {
    for (let l = 0; l <= r; l ++) {
      if (s.charAt(l) === s.charAt(r) && (r - l <= 2 || dp[l+1][r-1])) {
        dp[l][r] = true
        longest = r - l + 1 > longest.length ? s.slice(l, r+1) : longest
      }
    }
  }

  return longest
}

4. [简单] 70. 爬楼梯

var climbStairs = function(n) {
  const dp = []

  dp[0] = 1
  dp[1] = 2

  for (let i = 2; i < n; i++) dp[i] = dp[i - 1] + dp[i - 2]

  return dp[n - 1]
};

5. [中等] 🌸322. 零钱兑换

var coinChange = function(coins, amount) {
  let len = coins.length
  const dp = new Array(amount + 1).fill(amount + 1)

  dp[0] = 0
  for (let i = 1; i <= amount; i++) {
    for (let j = 0; j < len; j++) {
      if (coins[j] <= i) dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1)
    }
  }

  return dp[amount] > amount ? -1 : dp[amount]
};

6. 买卖股票(动态规划)

7. 矩阵[动态规划]

矩阵,从左上走到右下,最近距离,每一个格子一个距离数字

const arr = [
  [1, 3, 1],
  [1, 5, 1],
  [4, 2, 1],
];

console.log(minPathSum(arr))
var minPathSum = function (grid) {
  const rowLen = grid.length;
  const columnLen = grid[0].length
  const dp = [];

  for (let i = 0; i < rowLen; i++) {
    dp[i] = []
    for (let j = 0; j < columnLen; j++) {
      if (i === 0 && j === 0) dp[0][0] = grid[i][j]
      else if (i === 0) dp[i][j] = dp[i][j - 1] + grid[i][j]
      else if (j === 0) dp[i][j] = dp[i - 1][j] + grid[i][j]
      else dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j]

    }
  }

  return dp[rowLen - 1][columnLen - 1]
};

树的遍历有7中方式

  • BFS 层序遍历 (队列)
  • DFS 前序、中序、后续 (栈)【递归、非递归】

0. 二叉树的层序遍历(BFS、DFS)

/**
 * BFS方式
 * @param {TreeNode} root
 * @return {number[][]}
 */
var levelOrder = function(root) {
  if (!root) return []

  let res = [], queue = [root]

  while(queue.length){
      // arr存放每一层的数据
      let arr = [], queueLen = queue.length

      // 遍历每一层
      while(queueLen --) {
          let curr = queue.shift();
          
          arr.push(curr.val)
          if(curr.left) queue.push(curr.left)
          if(curr.right) queue.push(curr.right)

      }

      res.push(arr)
  }

return res
};

1. 二叉树的前序遍历(递归、非递归)

递归的方式

var preorderTraversal = function(root) {
  var res= []

  var preorder = (node) => {
    if(node) {
      res.push(node.val)
      preorder(node.left)
      preorder(node.right)
    }
  }

  preorder(root)

  return res;
};

非递归

function PreOrder(root) {
  const res = []
  const stack = [];
  let node = root;

  while (stack.length || node) {
    if (node) {
      res.push(node.val)  // 重点这句的位置
      stack.push(node)
      node = node.left
    } else {
      node = stack.pop();
      node = node.right;
    }
  }

  return res
}

2. 二叉树的中序遍历(递归、非递归)

递归

var inorderTraversal = function(root) {
  var res= []

  var inorder = (node) => {
    if(node) {
      inorder(node.left)
      res.push(node.val)
      inorder(node.right)
    }
  }

  postorder(root)

  return res;
};

非递归

function InOrder(root) {
  const res = []
  const stack = [];
  let node = root;

  while (stack.length || node) {
    if (node) {
      stack.push(node)
      node = node.left; // // 一直找left,直到没有
    } else {
      node = stack.pop();
      res.push(node.val); // 重点这句的位置
      node = node.right;
    }
  }

  return res
}

3. 二叉树的后序遍历(递归、非递归)

递归

var postorderTraversal = function (root) {
  let result = []

  var postorder = (node) => {
    if (node) {
      postorder(node.left) // 先遍历左子树
      postorder(node.right)  // 再遍历右子树
      result.push(node.val) // 最后根节点
    }
  }

  postorder(root)

  return result
};

非递归

function postOrder(root) {
  const res = []
  const stack = [];
  let node = root, pre = null;

  while (stack.length || node) {
    if (node) {
      stack.push(node)
      node = node.left;
    } else {
      node = stack.pop();

      if (!node.right || node.right === pre) {//没有右子树或刚访问过右子树
        res.push(node.val)
        pre = node
        node = null;
      } else {//有右子树并且没有访问
        stack.push(node);
        stack.push(node.right);//右子树入栈
        node = node.right.left;//转向右子树的左子树
      }
    }
  }

  return res
}

4. [中等] 199. 二叉树的右视图

递归

var rightSideView = function (root) {
  let result = []
  
  var dfs = (node, depth) => {
    if (node) {
      if (depth === result.length) result.push(node.val)
      depth++
      dfs(node.right, depth) // 先遍历右子树
      dfs(node.left, depth)  // 再遍历左子树
    }
  }

  dfs(root, 0)

  return result
};

非递归

var rightSideView = function(root) {
  if(!root) return [];

  let depthMap = new Map(); // key: 节点深度 value:值
  let queue = [[root, 0]];

  while(queue.length) {
    let [ {val, left, right}, depth ] = queue.shift(); // 取出队首元素

    depthMap.set(depth, val); // 最后一次set一定是同层depth的最后一个

    depth += 1;
    
    if(left) queue.push([left, depth]); // 仅将存在的节点推入队列中
    if(right) queue.push([right, depth]); // 仅将存在的节点推入队列中
  }
  
  return [...depthMap.values()]; 
}

链表

🌸1. [简单]206. 反转链表

image.png

var reverseList = function(head) {
  let curr = head
  let prev = null

  while (curr) {
    const next = curr.next // 先保存下一个值

    curr.next = prev // 核心,指向前一位
    prev = curr // 往前移动一位
    curr = next // 后移移动一位
  }

  return prev
};

递归

var reverseList = function(head) {
  // 递归终止条件
  if (head == null || head.next == null) return head;

  const p = reverseList(head.next); // 递归,p一直都是最后一个节点
  
  head.next.next = head // 反转的核心, 4->5 4.next.next = 4 即5.next = 4
  head.next = null // 清除原来的指针 4指向5的指针清除

  return p
};

image.png

2. [简单] 141. 环形链表

判断链表是否有环,可以使用快慢指针的方式。也可以通过哈希表存储节点来判断。

var hasCycle = function(head) {
    let fast = head
    let slow = head

    while (fast && fast.next) {
      slow = slow.next
      fast = fast.next.next

      if (slow === fast) return true
    }

    return false
};

3. [困难] 25. K 个一组翻转链表 (还蛮常考的,但是有点难)

const myReverse = (head, tail) => {
    let prev = tail.next;
    let p = head;
  
    while (prev !== tail) {
        const nex = p.next;
        p.next = prev;
        prev = p;
        p = nex;
    }
  
    return [tail, head];
}

var reverseKGroup = function(head, k) {
    const hair = new ListNode(0);
    hair.next = head;
    let pre = hair;

    while (head) {
        let tail = pre;
        // 查看剩余部分长度是否大于等于 k
        for (let i = 0; i < k; ++i) {
            tail = tail.next;
            if (!tail)  return hair.next;
        }
      
        const nex = tail.next;
        [head, tail] = myReverse(head, tail);
        // 把子链表重新接回原链表
        pre.next = head;
        tail.next = nex;
        pre = tail;
        head = tail.next;
    }
  
    return hair.next;
};

哈希

1. [简单] 两数之和

var twoSum = function(nums, target) {
  const map = new Map()

  for (let i = 0; i < nums.length; i ++) {
    const num = nums[i]
    const otherIndex = map.get(target - num)

    if (otherIndex !== undefined) return [otherIndex, i]

    map.set(num, i)
  }
};

top k问题

找出最大的k个数,

  • 全局排序:快排,全部都排序了没必要 O(n*lg(n))
  • 局部排序:冒泡,冒k个泡,就得到TopK。 O(n*k)
  • 堆: O(n*lg(k))

先用前k个元素生成一个小顶堆,这个小顶堆用于存储,当前最大的k个元素。接着,从第k+1个元素开始扫描,和堆顶(堆中最小的元素)比较,如果被扫描的元素大于堆顶,则替换堆顶的元素,并调整堆,以保证堆内的k个元素,总是当前最大的k个元素。

heap[k] = make_heap(arr[1, k]);

for(i=k+1 to n){
  adjust_heap(heep[k],arr[i]);
}

return heap[k];

其他

4. 大数相加 (腾讯、头条)

大数指一个大于2的53次方的数。Number是双精度浮点数,在 -(2^53 -1) 到 2^53-1之间时是精确的。 竖式运算:一种从个位往前一个一个相加求和的方式

代码思路:利用竖式运算的方式,从末尾一直向前加,当两数相加大于10时便向前进一位,同理我们可以将这里的“大数加法”运算变成两个超大数字从末尾一个一个向前加求和的过程。

  • 检查传进来的参数,必须是字符串类型的数字(如果是Number类型,传进来的时候已经丢失精度了,就如 如果传入11111111111111111,处理的时候已经是丢失精度的11111111111111112)
  • 将传入的数据进行反转,取两个数组长度大者,进行循环,从前向后依次加和
  • 超过10就进位,通过temp存储进位
  • 最后反转,join成字符串,返回。要注意最高位进位问题。
function addBigNum(num1, num2) {
  const checkNum = num => typeof num === 'string' && !isNaN(Number(num))

  if (!checkNum(num1) || !checkNum(num2)) return 'big number type error'

  const result = []
  const tmp1 = num1.split('').reverse()
  const tmp2 = num2.split('').reverse()
  const numLen = Math.max(tmp1.length, tmp2.length);
  let temp = 0

  const format = val => val ? Number(val) : 0

  for (let i = 0; i <= numLen; i++) {
    const addTmp = format(tmp1[i]) + format(tmp2[i]) + temp

    result[i] = addTmp % 10
    temp = addTmp > 9 ? 1 : 0; // 进位
  }

  result.reverse()

  const resultNum = result[0] > 0 ? result.join('') : result.join('').slice(1)

  return resultNum
}

可追问:两个超大浮点数相加

八皇后变种问题 (腾讯)

1. 8x8棋盘,随机有N个车,要求获取到不被车攻击的所有点。

function getSafePoint(arr) {
  const column = new Set()
  const row = new Set()
  const res = []

  for (let i = 0; i < arr.length; i++) {
    for (let j = 0; j < arr[i].length; j++) {
      if (arr[i][j] === 1) {
        row.add(i)
        column.add(j)
      }
    }
  }

  for (let i = 0; i < arr.length; i++) {
    for (let j = 0; j < arr[i].length; j++) {
      if (!row.has(i) && !column.has(j)) res.push([i, j])
    }
  }

  return res
}

2. 8x8棋盘,要求随机生成一张棋盘,值分别为1、0,1代表有车,0代表没车,并返回这张棋盘是否存在相互攻击的情况。

给定一个8x8的棋盘, 上面有若干个车(Rook),写⼀个函数检查这些车有没有互相攻击的情况. 如果横列或者竖列同样也有1,即为互相攻击,输入参数是一个由0和1组成的二维数组(1代表有车,0代表没车)。

let arr = [  [0, 0, 0, 1, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 1, 0, 0, 0, 0]
];
checkConflict(arr);

解答: 随机生成一个8*8数组,值为1、0

function initArr(n) {
  const arr = new Array(n);

  for(let i = 0; i < n; i++) {
    arr[i] = []
    for(let j = 0; j < n; j++) {
      arr[i][j] = Math.floor(2*Math.random())
    }
  }

  return arr
}

initArr(8)

实现checkAttack 思路:存储已经有车的行和列;如果缓存中已有行和列,就说明被攻击了。

function checkAttack(arr) {
  const column = new Set()
  const row = new Set()

  for(let i = 0; i< arr.length; i++){
    for(let j = 0; j< arr[i].length; j++){
      if (arr[i][j] === 1) {
        if (row.has(i)) return true
        if (column.has(j)) return true
        row.add(i)
        column.add(j)
      }
    }
  }

  return false
}

面试遇到过的题

  • 递增数组发生一次旋转后,寻找数组中的最小值,例如数组 [1,2,3,5,6,7] 旋转后变为 [5,6,7,1,2,3] ,返回数组最小值,要求时间复杂度为 O(log(n))
  • 爬楼梯
  • k个一组反转单链表
  • 合并区间
  • "189641" 寻找一个字符串中的最长升序子串 (重点:手写快排)
  • 大数相加