前言
我们把自己做的算法题进行归类总结,进行一个目录导航
算法不是一个速成的东西,算法就像做数学题,各种题型在一个根题型的基础上进行变化。
我的学习方法
-
写文章 ---- 为啥要写文章?因为算法题目太多,2千多道题目,你不可能都记得住。就算你现在记住了,后面也会忘的。所以一定要写文章,后面的话方便快速复习。而且写文章也能检测你是否真正掌握了这个题目,如果你没有真正掌握,那么你也写不出来,每篇文章不能少于600字
-
总结模板 ---- 就算你第一遍掌握了,那么这么多道题,你还是会忘,所以我们要找到这些题目中共性的东西总结出来,总结不出来,说明你还是没学会。
-
同一道题最少做5遍 ---- 第一遍你可能懵懵懂懂,然后你写了文章。第二遍,你总结了模板,印象加深了。第三遍,把模板进行了丰富,有所掌握。第四遍,题目做的快了很多。第五遍,完全掌握。我说的这些只是参考,以实际为准。
我们要达成的效果就是一道题目能在5分钟内写出来,并且准确率能有100%,高级一点的话,能用两种方法写出来
- 所有的题目第一步上来是判断给的参数
- 写好以后一定要把参数带到题目里面去检查一遍
排序算法
1. 冒泡排序
2. 选择排序
3. 插入排序
冒大(后)选小(前)插入(把右边的插入到左边合适的位置这个比较难,要好好理解理解,其实还是交换,插入的时候倒排,然后比大小,所有的交换一下位置,看上去像插入)
4. 归并排序,从中间分开(合并两个有序数组) 难点在边界问题,
5. 快速排序,(一定要选取最右侧的,记得每次分左右数组的时候不要带最后一个数)
数组和字符串
双指针解法(数组必须有序)
解析: 1.前提是必须先排序 2.循环条件是while(left<right)
-
js算法题解(第四天)---LeetCode:11 盛最多水的容器
- 注意理解题意:取短的那根板
-
js算法题解(第六天)---LeetCode:680. 验证回文字符串 Ⅱ和28.实现 strStr()
- 像这种查找回文串和strStr都重新再定义一个函数,可以让代码变得清晰
下标法
中心扩散法(专门用于找回文子串的)
- js算法题解(第七天)---LeetCode: 5.最长回文子串 - 掘金 (juejin.cn)* 思路:遍历每一个索引,以这个索引为中心,利用“回文串”中心对称的特点,往两边扩散,看最多能扩散多远。
模板
var centerSpread = (str, left, right) => {
// 双指针判断条件
while (left >= 0 && right < str.length) {
if (str[left] === str[right]) {
left--;
right++;
} else {
break;
}
}
return str.substring(left + 1, right);
};
哈希法
解析:最快的查询方法
- js算法题解(第一天)---LeetCode:1.两数之和
- 几乎所有的求和问题都可以用求差问题来解决
- 只要是查找最快的方法,立马想到哈希表
链表
定义头结点(只要是需要操作第一个节点的,都需要使用头结点)
模板
if(!head||!head.next){
return head;
}
let dummy = new ListNode();
dummy.next = head;
let cur = dummy;
while(cur){
if(){
// 主要代码部分
}else{
// 循环链表必写代码
cur = cur.next;
}
}
return dummy.next;
判断条件主要看子作用域中如何使用的 例如
子作用域 | 判断条件 |
---|---|
cur.val | cur |
cur.next.val | cur&&cur.next |
cur.next.next.val | cur&&cur.next&&cur.next.next |
子里面使用几个next
,那么父就要判断
几个next
- js算法题解(第十天)---LeetCode:82. 删除排序链表中的重复元素 II
- js算法题解(第九天)---LeetCode:83.删除排序链表中的重复元素
- js算法题解(第八天)---LeetCode: 21. 合并两个有序链表
删除链表
cur.next = cur.next.next;
快慢指针
快指针在前面走,慢指针在后面追他
模板
let dummy = new ListNode();
dummy.next = head;
let quick = dummy;
let slow = dummy;
let count = 0;
while(quick){
if(count>n){
slow = slow.next;
}
quick = quick.next;
count++;
}
多指针
- js算法题解(第十二天)---206. 反转链表(3个指针)
专门用于链表反转
定义好pre,cur,next三个指针
模板
// 注意初始pre必须是null,因为他是反转后的最后一个元素
let pre = null;
let cur = dummy.next;
while(cur){
// next不能写到外面,如果cur为null的话,那么cur.next就报错了
let next = cur.next;
}
会给你left(开始反转的节点),right(结束反转的节点) 所以要有两个循环,一个循环到left,一个循环到right
环
设立flag
head.flag = true;
栈
对称性
辅助栈
- js算法题解(第十六天)---155. 最小栈 - 掘金 (juejin.cn)
-
求最小值用升序,求最大值用降序
此题的注意点就是pop的时候会把当前的最小值的数字pop掉
-
队列
八字转换
辅助队列
- js算法题解(第十八天)----剑指 Offer 59 - II. 队列的最大值和239. 滑动窗口最大值
- 求最小值用升序,求最大值用降序
树
广度优先遍历(BFS模板)
function BFS(入口坐标) {
const queue = [] // 初始化队列queue
// 入口坐标首先入队
queue.push(入口坐标)
// 入口坐标是第一层也就是0,所以level从1开始,代表遍历第二层
let level = 1;
// 队列不为空,说明没有遍历完全
while(queue.length) {
// 当前queue的长度,一定要先声明长度,因为queue一直在变
let length = queue.length;
// for循环使当前层的都出列,下一层的都入列
for(let i=0;i<length;i++){
// 上面的i不要用,因为我们
let node = queue.shift();
// 从node上取出元素,放到queue队列中
queue.push(取出的元素)
}
// 层数+1;
level++;
}
}
回溯算法
要使用回溯算法,那么需要搞清三个东西
- 1.路径(track):也就是已经做出的选择
- 2.选择列表(nums):也就是你当前可以做的选择
- 3.结束条件:也就是无法在做选择的条件
并记住下面这两张图
result = [];
let backTrack = function(选择列表,路径){
if(满足条件){
result.push(路径);
return;
}
for(选择 in 选择列表){
添加到路径中
backTrack(选择列表,路径)
从路径中撤销
}
}
二叉树遍历
前序遍历
var preorderTraversal = function(root) {
// 当前节点
let cur = root;
// 深度遍历和栈有关栈
let stack = [];
// 结果
let result = [];
// 为什么while结束条件是cur||stack.length呢?
// 因为初始化stack中没有值
while(cur||stack.length)
// 左节点一杆子打到底,右节点的话会存一下数据
while(cur){
result.push(cur.val);
stack.push(cur);
cur=cur.left;
}
// 每弹出一个栈,就到达右孩子
let tmp = stack.pop();
cur = tmp.right;
}
return result;
};
中序遍历
var inorderTraversal = function(root) {
// 返回的结果
let result = [];
// 当前节点
let cur = root;
// 深度遍历离不开栈
let stack = [];
while(cur||stack.length){
//
while(cur){
stack.push(cur);
cur = cur.left;
}
let tmp = stack.pop();
result.push(tmp.val);
cur = tmp.right;
}
return result;
};
和前序遍历的区别就是result在pop后面
后序遍历
var postorderTraversal = function(root) {
let result = [];
let cur = root;
let stack = [];
while(cur||stack.length){
while(cur){
result.push(cur.val);
stack.push(cur);
cur = cur.right;
}
let tmp = stack.pop();
cur = tmp.left;
}
return result.reverse();
};
写法几乎和前序遍历一模一样,就是right和left遍历调换一下位置
深度优先遍历(dfs模板)
递归
搜索二叉树
有效 二叉搜索树定义如下:
- 节点的左子树只包含 小于 当前节点的数。
- 节点的右子树只包含 大于 当前节点的数。
- 所有左子树和右子树自身必须也是二叉搜索树。
模板
原理,搜索二叉树的中序遍历,每一个节点的值都大于前一个节点值
var isValidBST = function(root) {
// 初始化前一个数为最小值
let pre = -Infinity;
let flag = true;
const travers = function(root){
// 判断空值二叉搜索树
// 如果是空树的,也是二叉搜索树,返回true
if(!root){
return true;
}
// 判断有值二叉搜索树
travers(root.left)
if(pre>=root.val){
flag = false;
}
pre = root.val;
travers(root.right)
}
travers(root);
return flag;
};
平衡二叉树
抓住其中的三个关键字:
- 任意结点
- 左右子树高度差绝对值都不大于1
- 二叉搜索树
const dfs = function(root){
// 递归结束条件
if(!root){
// 一定要注意返回0,否则就计算不了高度了
return 0;
}
let left = dfs(root.left);
let right = dfs(root.right);
// 逆想思维求高度
return Math.max(left,right)+1;
}
二分查找
搜索一个元素
搜索一个元素时,搜索区间两端闭。
while条件带等号,否则需要打补丁。
if相等就返回,其他的事甭操心。
mid必须加减一,因为区间两端闭。
while结束就凉凉,凄凄惨惨返-1。
搜索多个元素
搜索左右区间时,搜索区间要阐明。
if相等别返回,利用mid锁边界。
while结束不算完,因为你还没返回。
索引可能出边界,if检查保平安。
动态规划
解题模板
- 1.定义新数组
动态规划题目一般需要定义一个新数组let a=[]或let a=[][]之类的
- 2.找到最简单的子问题
这个子问题一般都是a[0],a[1]
之类的,不可再分解的,这个其实就是递归的边界条件
- 3.找到父问题
要能看出来让你求什么,一般也就是数组的第几个值
- 4.找到如何把父问题化成子问题(自顶向下推理不出来,就自底向上推理试试,想办法怎么把子问题在父问题循环里面使用)
根据题目中的条件,想一下子问题是如何构成父问题的,这是最关键的一步,也是最难的一步,这个只能多做
动态规划题型
动态规划题目大概有三类题型
- 计数(多少种方法)
- 求最值
- 求最小值,一般先声明res为最大值;
- 求最大值,一般先声明res为最小值,
一定不要忘了加一
- 求存在性(后面会讲到)
求最值的动态规划也是有模板的
求最值伪代码
var findMaxValue = function(coins, amount){
// 定义一个空数组
let arr = [];
// 最简单的值,是已知条件减一,value是不可在分割的一部分
arr[0] = 0;
// 求出父问题前面的所有子问题,一定要有等于,因为=才是我们要求的答案
for(let i=1;i<=amount;i++){
// 求最小值一定要先声明最大值
let res = Infinity;
// 遍历所有的子条件
for(let j=0;j<coins.length;j++){
// 数组一定要考虑边界问题
if(i-coins[j]>=0){
// 注意加一,建立i与j的关系
res = Math.min(res,arr[i-coins[j]]+1);
}
}
arr[i] = res;
}
return a[amount]===Infinity?-1:a[amount];
}