算法通关模板----助你快速掌握算法

497 阅读6分钟

前言

我们把自己做的算法题进行归类总结,进行一个目录导航

算法不是一个速成的东西,算法就像做数学题,各种题型在一个根题型的基础上进行变化。

我的学习方法

  1. 写文章 ---- 为啥要写文章?因为算法题目太多,2千多道题目,你不可能都记得住。就算你现在记住了,后面也会忘的。所以一定要写文章,后面的话方便快速复习。而且写文章也能检测你是否真正掌握了这个题目,如果你没有真正掌握,那么你也写不出来,每篇文章不能少于600字

  2. 总结模板 ---- 就算你第一遍掌握了,那么这么多道题,你还是会忘,所以我们要找到这些题目中共性的东西总结出来,总结不出来,说明你还是没学会。

  3. 同一道题最少做5遍 ---- 第一遍你可能懵懵懂懂,然后你写了文章。第二遍,你总结了模板,印象加深了。第三遍,把模板进行了丰富,有所掌握。第四遍,题目做的快了很多。第五遍,完全掌握。我说的这些只是参考,以实际为准。

我们要达成的效果就是一道题目能在5分钟内写出来,并且准确率能有100%,高级一点的话,能用两种方法写出来

  1. 所有的题目第一步上来是判断给的参数
  2. 写好以后一定要把参数带到题目里面去检查一遍

排序算法

1. 冒泡排序
2. 选择排序
3. 插入排序

冒大(后)选小(前)插入(把右边的插入到左边合适的位置这个比较难,要好好理解理解,其实还是交换,插入的时候倒排,然后比大小,所有的交换一下位置,看上去像插入)


4. 归并排序,从中间分开(合并两个有序数组) 难点在边界问题,
5. 快速排序,(一定要选取最右侧的,记得每次分左右数组的时候不要带最后一个数)

数组和字符串

双指针解法(数组必须有序)

解析: 1.前提是必须先排序 2.循环条件是while(left<right)


下标法


中心扩散法(专门用于找回文子串的)

模板

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);
};


哈希法

解析:最快的查询方法


链表

定义头结点(只要是需要操作第一个节点的,都需要使用头结点)

模板

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.valcur
cur.next.valcur&&cur.next
cur.next.next.valcur&&cur.next&&cur.next.next

子里面使用几个next,那么父就要判断几个next


删除链表

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++;
  }

多指针

专门用于链表反转

定义好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


image.png

设立flag

head.flag = true;

对称性


辅助栈


队列

八字转换


辅助队列


广度优先遍历(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.结束条件:也就是无法在做选择的条件

image.png

并记住下面这两张图

image.png

image.png

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];
}