高频题
- 反转链表(链表)
- 链表是否有环(快慢指针)
- 两数之和 (数组、哈希表)
- 接雨水(栈、数组、双指针)
- 最长回文子串(动态规划)
- 爬楼梯(动态规划)
- 买卖股票(动态规划)
- 零钱兑换(动态规划)
- 二叉树的中序遍历(二叉树):递归+迭代
- 二叉树的右视图(二叉树、头条高频)
排序
快排 nlogn
- 快速排序是找出一个元素作为基准(pivot),然后对数组进行分区操作,使基准左边元素的值都不大于基准值,基准右边的元素值 都不小于基准值,如此作为基准的元素调整到排序后的正确位置。
- 递归快速排序,将其他 n-1 个元素也调整到排序后的正确位置。 所以快速排序算法的核心算法是分区操作,即如何调整基准的位置以及调整返回基准的最终位置以便分治递归。
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]]
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. 反转链表
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
};
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" 寻找一个字符串中的最长升序子串 (重点:手写快排)
- 大数相加