前言
由于很多小伙伴看了 《普通人如何进大厂》这篇文章对我的笔记比较感兴趣,所以我花时间整理提炼了一下跟大家分享,不保证内容完全正确,仅供参考哈
本文内容来自修言的《前端算法与数据结构面试:底层逻辑解读与大厂真题训练》掘金小册子的核心总结,侵权删。
其他笔记传送门
数据结构类型:数组、栈、队列、链表、树
栈 - 先进后出
对应数组的 进:push ,出:pop
进栈出栈
let stack = []
stack.push('a')
stack.push('b')
stack.push('c')
while(stack.length){
const top = stack[stack.length-1]
console.log('出栈顺序',top)
stack.pop()
}
队列 queue - 先进先出
对应数组的进:push ,出:shift
排队出队
let queue = []
queue.push('a')
queue.push('b')
queue.push('c')
while(queue.length){
const top = queue[0]
console.log('出栈顺序',top)
stack.shift()
}
链表
function ListNode(val) {
this.val = val;
this.next = null;
}
const node = new ListNode(1)
node.next = new ListNode(2)
形成一个2个结点的链表
查找链表的第10个元素
let index = 10
let node = head
while(index<10 && node){
node = node.next
index++
}
二叉树
1、是一个空树
2、不是空树,则必须由跟结点和左结点和右结点组成,并且子结点也是二叉树
二叉树的遍历
- 递归遍历
- 前序遍历
- 中序遍历
- 后序遍历
- 迭代遍历
- 层次遍历
// 先序遍历
function preorder(root){
if(!root) return
console.log(root.val)
preorder(root.left)
preorder(root.right)
}
// 中序遍历
function inorder(root){
if(!root) return
preorder(root.left)
console.log(root.val)
preorder(root.right)
}
// 后序遍历
function postorder(root){
if(!root) return
preorder(root.left)
preorder(root.right)
console.log(root.val)
}
数组的应用
合并两个有序数组-双指针
const merge = function(nums1,m,num2,n){
let i = m-1,j=n-1,k=m+n-1
while(i>=0 && j>=0){
if(nums1[i]>=nums2[j]){
nums1[k]=nums1[i]
i--
}else{
nums1[k]=nums2[j]
j--
}
k--
}
while(j>=0){
nums1[k]=nums2[j]
k--
j--
}
}
三数求和-对碰指针
/**
* @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
};
字符串的应用
判断是否回文
// 简单方式
function isPalindrome(str){ return str.split('').reverse().join()===srt}
// 对折方式
function isPalindrome(str){
const len =str.length
for(let i =0;i<len/2;i++){
if(srt[i]!==str[len-i-1]){
return false
}
}
return true
}
删除一个数字是否成为回文
原理:对折判断字符串是否相等,如不想等则往前或者往后跳一步,若前后组成的字符串是回文则命题成立
var validPalindrome = function (str) {
let len = str.length;
let i = 0,
j = len - 1;
while (i < j && str[i] === str[j]) {
i++;
j--;
}
if (isPalindrome(i + 1, j)) {
return true;
}
if (isPalindrome(i, j + 1)) {
return true;
}
return false;
function isPalindrome(start, end) {
while (start < end) {
if (str[start] !== str[end]) {
return false;
}
start++;
end--;
}
return true;
}
};
取整数中的数字
const myAtoi=function(str){
const max=Math.pow(2,31)-1,min=-max-1
const group=str.match(/^\s*([-+]?[0-9]+).*$/)
let num=0
if(group) {
num=+group[1]
}
if(num>max){num=max}
if(num<min){num=min}
return num
}
链表的应用
链表合并
const mergeTwoLists = function(l1, l2) {
// 定义头结点,确保链表可以被访问到
let head = new ListNode()
// cur 这里就是咱们那根“针”
let cur = head
// “针”开始在 l1 和 l2 间穿梭了
while(l1 && l2) {
// 如果 l1 的结点值较小
if(l1.val<=l2.val) {
// 先串起 l1 的结点
cur.next = l1
// l1 指针向前一步
l1 = l1.next
} else {
// l2 较小时,串起 l2 结点
cur.next = l2
// l2 向前一步
l2 = l2.next
}
// “针”在串起一个结点后,也会往前一步
cur = cur.next
}
// 处理链表不等长的情况
cur.next = l1!==null?l1:l2
// 返回起始结点
return head.next
};
删除重复的值
function delDuplicates(head) {
let curr = head;
while (curr && curr.next) {
if (curr.val === curr.next.val) {
curr.next = curr.next.next;
} else {
curr = curr.next;
}
}
return head;
}
快慢指针
// 删除第n个结点
function delDuplicatesII(head,n) {
let dummy = new NodeList()
dummy.next = head
let fast = dummy,slow=dummy
while(n>0){
fast=fast.next
n--
}
while(fast.next){
fast=fast.next
slow = slow.next
}
slow.next=slow.next.next
return dummy.next
}
多指针
// 反转链表
const reverseList=function(head){
let cur=head
let pre=null
while(cur){
let next = curr.next
cur.next=pre
pre=cur
cur = next
}
return pre
}
环形链表
// 求环形起点
const cycleList = function(head){
while(head){
if(head.flag){ return head}
else{ head.flag = true;
head = head.next
}
}
return null
}
栈与队
有效括号-对称性
const isValid = function(s){
let letftToright = {
'(':')',
'{':'}',
'[':']'
}
let res =[]
for(let i =0;i<s.length;i++){
if(s[i]==='('||s[i]==='{'||s[i]==='['){
res.push(letftToright[s[i]])
}else{
if(res.length<1 || s[i]!==res.pop()){
return false
}
}
}
return true
}
每日温度
维持一个递减序列
const dailyTemprature = function(arr){
let res = (new Array(arr.length)).fill(0)
let stack = []
for(let i =0;i<arr.length;i++){
while(stack.length>0 && arr[stack[stack.length-1]]<arr[i]) {
let j= stack.pop()
res[j]=i-j
}
stack.push(i)
}
return res
}
最小栈问题
var MinStack = function() {
this.stack=[]
this.stack2=[]
};
MinStack.prototype.push = function(x) {
this.stack.push(x)
if(this.stack2.length===0 || this.stack2[this.stack2.length-1]>=x){
this.stack2.push(x)
}
};
MinStack.prototype.pop = function() {
if(this.stack.pop()===this.stack2[this.stack2.length-1]){
this.stack2.pop()
}
};
MinStack.prototype.top = function() {
return this.stack[this.stack.length-1]
};
MinStack.prototype.getMin = function() {
return this.stack2[this.stack2.length-1]
};
滑窗最大值
const maxSlidingWindow = function (nums, k) {
// 缓存数组的长度
const len = nums.length;
// 初始化结果数组
const res = [];
// 初始化双端队列
const deque = [];
// 开始遍历数组
for (let i = 0; i < len; i++) {
// 当队尾元素小于当前元素时
while (deque.length && nums[deque[deque.length - 1]] < nums[i]) {
// 将队尾元素(索引)不断出队,直至队尾元素大于等于当前元素
deque.pop();
}
// 入队当前元素索引(注意是索引)
deque.push(i);
// 当队头元素的索引已经被排除在滑动窗口之外时
while (deque.length && deque[0] <= i - k) {
// 将队头元素索引出队
deque.shift();
}
// 判断滑动窗口的状态,只有在被遍历的元素个数大于 k 的时候,才更新结果数组
if (i >= k - 1) {
res.push(nums[deque[0]]);
}
}
// 返回结果数组
return res;
};
DFS 与 BFS
dfs 和二叉树先序遍历类似,不撞南墙不回头,穷举首选
// 所有遍历函数的入参都是树的根结点对象
function preorder(root) {
// 递归边界,root 为空
if(!root) {
return
}
// 输出当前遍历的结点值
console.log('当前遍历的结点值是:', root.val)
// 递归遍历左子树
preorder(root.left)
// 递归遍历右子树
preorder(root.right)
}
BFS实战:二叉树的层序遍历
function bfs(root){
let queue=[]
queue.push(root)
while(queue.length>0){
let top = queue[0]
console.log('top',top.val)
if(top.left) { queue.push(top.left)}
if(top.right) { queue.push(top.right)}
queue.shift()
}
}
递归和回溯
回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为 “回溯点”。
序列全排
要点:
-
穷举首先想到 dfs
-
触底条件是遍历层数>nums.length
-
返回递归的路径,用 visited 记录当前路径节点避免重复,完成之后释放进行下一个路径
const premute = function (nums) {
let path = [],
res = []
let visited = {}
let len = nums.length
dfs(0)
function dfs(nth) {
if (nth === len) {
res.push(path.slice())
return
}
for (let i = 0; i < len; i++) {
if (!visited[nums[i]]) {
visited[nums[i]] = true
path.push(nums[i])
dfs(nth + 1)
path.pop()
visited[nums[i]] = false
}
}
}
}
子组合问题
要点:
- 穷举首先想到 dfs
- dfs 是基于 i 非层级
const subsets= function(nums){
const res = []
const len = nums.length
const subset=[]
dfs(0)
function dfs(index){
res.push(subset.slice())
for(let i=index;i<len;i++){
subset.push(nums[i])
dfs(i+1)
subset.pop()
}
}
return res
}
限定组合问题
要点:
-
穷举首先想到 dfs
-
dfs 是基于 i 非层级
-
触底条件为 组合长度===k
//
const combine(num,k){
const res = []
const subset=[]
dfs(1)
function dfs(index){
if(subset.length===k){
res.push(subset.slice())
return
}
for(let i=index;i<=num;i++){
subset.push(i)
dfs(i+1)
subset.pop()
}
}
return res
}
二叉树问题
迭代方式实现先序排序
要点:利用栈后进先出原理,先压入右结点再压入左结点,不断出栈,取栈顶结点遍历,重复以上过程
function preorderTraversal(root) {
let res = [];
if (!root) return res;
let stack = [];
stack.push(root);
while (stack.length) {
let top = stack.pop();
res.push(top.val);
if (top.right) {
stack.push(top.right);
}
if (top.left) {
stack.push(top.left);
}
}
return res;
}
迭代方式实现后序排序
要点:
-
先序排序为根->左->右,而后序排序为左->右->根
-
通过相反的添加数据,即头部添加 unshift 方式实现右->左->根
-
通过调换一下左右结点入栈顺序即可实现左->右->根
function postorderTraversal(root) {
let res = [];
if (!root) return res;
let stack = [];
stack.push(root);
while (stack.length) {
let top = stack.pop();
res.unshift(top.val);
if (top.left) {
stack.push(top.left);
}
if (top.right) {
stack.push(top.right);
}
}
return res;
}
迭代方式实现中序排序
要点:
-
通过遍历左结点并推入栈中
-
当到达叶子结点取值,然后通过 pop 回溯到父结点
-
取当前结点值,然后遍历右结点
function inorderTraversal(root) {
let res = [];
if (!root) return res;
let stack = [];
let cur = root
while (cur || stack.length) {
while(cur.left){ // 找到叶子结点,并把左结点入栈
stack.push(cur.left)
cur=cur.left
}
cur=stack.pop()
res.push(cur.val)
cur=cur.right // 遍历右结点
}
return res;
}
5大排序
冒泡排序
要点:每轮相邻前后的值比较如后边的值比前边的值小,则交互位置
每轮获取一个最大值
复杂度:最优n,平均n^2
function bubbleSort(arr) {
const len = arr.length;
for (let i; i < len; i++) {
for (let j = 0; j < len - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
}
}
}
return arr;
}
选择排序
要点:找最小值
每轮记一个最小值,若当前比最小值小,则更改最小值
复杂度:最优n^2,平均n^2
function selectSort(arr) {
const len = arr.length;
let minIndex = 0;
for (let i = 0; i < len; i++) {
minIndex = i;
for (let j = i + 1; j < len; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
if (minIndex !== i) {
[arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];
}
}
return arr;
}
插入排序
要点:找到当前元素插入到,插入到前面有序列表中正确到位置
复杂度:最优n,平均n^2
function insertSort1(arr) {
const len = arr.length;
for (let i = 1; i < len; i++) {
let j = i ;
let temp = arr[i];
while (j > 0 && arr[j-1] > temp) {
arr[j] = arr[j - 1];
j--;
}
arr[j] = temp;
}
return arr;
}
// 插入排序
function insertSort(arr) {
const len = arr.length;
for (let i = 0; i < len; i++) {
for (let j = i + 1; j > 0; j--) {
// console.log('ij',i,j)
if (arr[j] < arr[j - 1]) {
let temp = arr[j];
arr[j] = arr[j - 1];
arr[j - 1] = temp;
// console.log('arr[j]',j,arr[j-1],arr[j])
// [arr[j], arr[j - 1]] = [arr[j - 1], arr[j]];
}
}
}
return arr;
}
归并排序
分割数组为左右组,知道分割长度变成1,则合并
function merge(l1, l2) {
console.log(l1, ':', l2);
let len1 = l1.length;
let len2 = l2.length;
let i = 0;
let j = 0;
let res = [];
while (len1 > i && len2 > j) {
if (l1[i] < l2[j]) {
res.push(l1[i]);
i++;
} else {
res.push(l2[j]);
j++;
}
}
if (i < len1) {
return res.concat(l1.slice(i));
} else {
return res.concat(l2.slice(j));
}
}
function mergeSort(arr) {
const len = arr.length;
if (len <= 1) return arr;
const mid = parseInt(len / 2);
let leftArr = mergeSort(arr.slice(0, mid));
let rightArr = mergeSort(arr.slice(mid, len));
arr = merge(leftArr, rightArr);
return arr;
}
快速排序
要点:不断把数组拆分,设置基准值,小的值放左边大的值放右边,
细分回溯合并
function quickSort(arr) {
let len = arr.length;
if (len <= 1) return arr;
const index = parseInt(arr.length / 2);
const flagValue = arr[index];
let leftArr= [];
let rightArr=[]
for (let i = 0; i < len; i++) {
if(i===index) continue
if(arr[i]<=flagValue){
leftArr.push(arr[i])
}else{
rightArr.push(arr[i])
}
}
return [...quickSort(leftArr),flagValue,...quickSort(rightArr)]
}
动态规划
运用条件:
- 要求你给出达成某个目的的解法个数
- 不要求你给出每一种解法对应的具体路径
思想:找状态转移和边界
树形思维模型将帮助我们更迅速地定位到状态转移关系,边界条件往往对应的就是已知子问题的解
最少硬币数
原理:
-
找拿了一个硬币之后硬币总数最小值
-
最小硬币数 关键找底和底-1的硬币数最小值 Math.min(f[i], f[i - coins[j]]+1)
-
例如 36 则 min(min(36-1),min(36-2),min(36-5))
const coinChange = function (coins, amount) {
let f = [];
f[0] = 0;
for (let i = 1; i <= amount; i++) {
f[i] = Infinity;
for (let j = 0; j < coins.length; j++) {
if (coins[j] <= i) {
f[i] = Math.min(f[i], f[i - coins[j]] + 1);
}
}
}
if (f[amount] === Infinity) return -1;
return f[amount];
};