算法自学。

回溯

  1. 思想

  2. 什么时候需要回溯
    当问题需要 “回头”,以此来查找出所有的解的时候,使用回溯算法。即满足结束条件或者发现不是正确路径的时候(走不通),要撤销选择,回退到上一个状态,继续尝试,直到找出所有解为止

  3. 步骤
    ①画出递归树,找到状态变量(回溯函数的参数),这一步非常重要※
    ②根据题意,确立结束条件
    ③找准选择列表(与函数参数相关),与第一步紧密关联※
    ④判断是否需要剪枝
    ⑤作出选择,递归调用,进入下一层
    ⑥撤销选择

  4. 例题
    leetcode 46 全排列
    leetcode 31 下一个排列
    leetcode 22 括号生成
    leetcode 78 子集生成
    leetcode 90 子集II

DFS

1. 网格类DFS
2. 步骤

void dfs(int[][] grid, int r, int c) {
    // 判断 base case
    if (!inArea(grid, r, c)) {
        return;
    }
    // 如果这个格子不是岛屿,直接返回
    if (grid[r][c] != 1) {
        return;
    }
    grid[r][c] = 2; // 将格子标记为「已遍历过」
    
    // 访问上、下、左、右四个相邻结点
    dfs(grid, r - 1, c);
    dfs(grid, r + 1, c);
    dfs(grid, r, c - 1);
    dfs(grid, r, c + 1);
}

// 判断坐标 (r, c) 是否在网格中
boolean inArea(int[][] grid, int r, int c) {
    return 0 <= r && r < grid.length 
        	&& 0 <= c && c < grid[0].length;
}

3. 例题
leetcode 200 岛屿数量

动态规划

子序列问题

  1. 分类
  • 第一种思路模板是一个一维的 dp 数组:
    在子数组array[0…i]中,以array[i]结尾的目标子序列(最长递增子序列)的长度是dp[i]。
  • 第二种思路模板是一个二维的 dp 数组:
    2.1 涉及两个字符串/数组时(比如最长公共子序列),dp 数组的含义如下:
    子序列:在子数组arr1[0…i]和子数组arr2[0…j]中,我们要求的子序列(最长公共子序列)长度为dp[i][j]。
    子数组:在子数组arr1[0…i]和子数组arr2[0…j]中,以arr1[i],arr2[j]结尾的子数组长度为dp[i][j]。
    2.2 只涉及一个字符串/数组时(比如本文要讲的最长回文子序列),dp 数组的含义如下:
    子序列:在子数组array[i…j]中,我们要求的子序列(最长回文子序列)的长度为dp[i][j]。注意对角线遍历
    子串:子数组array[i…j]中,数组i-j满足条件当dp[i+1][j-1]且array[i]=array[j]
  1. 例题
    leetcode 53 最大子数组和
    leetcode 300 最长递增子序列
    leetcode 72 编辑距离
    leetcode 1143 最长公共子序列
    leetcode 718 最长重复子数组
    leetcode 53 最长回文子序列

其他问题

背包问题

  1. 思想
    给定一个背包容量target,再给定一个数组nums(物品),能否按一定方式选取nums中的元素得到target
    注意:
    1、背包容量target和物品nums的类型可能是数,也可能是字符串
    2、target可能题目已经给出(显式),也可能是需要我们从题目的信息中挖掘出来(非显式)(常见的非显式target比如sum/2等)
    3、选取方式有常见的一下几种:每个元素选一次/每个元素选多次/选元素进行排列组合
  2. 背包分类
    常见的背包类型主要有以下几种:
    1、0/1背包问题:每个元素最多选取一次
    2、完全背包问题:每个元素可以重复选择
    3、组合背包问题:背包中的物品要考虑顺序
    4、分组背包问题:不止一个背包,需要遍历每个背包

而每个背包问题要求的也是不同的,按照所求问题分类,又可以分为以下几种:
1、最值问题:要求最大值/最小值
2、存在问题:是否存在…………,满足…………
3、组合问题:求所有满足……的排列组合

  1. 解题模板
    首先是背包分类的模板:
    1、0/1背包:外循环nums,内循环target,target倒序且target>=nums[i];
    2、完全背包:外循环nums,内循环target,target正序且target>=nums[i];
    3、组合背包(考虑顺序):外循环target,内循环nums,target正序且target>=nums[i];
    4、分组背包:这个比较特殊,需要三重循环:外循环背包bags,内部两层循环根据题目的要求转化为1,2,3三种背包类型的模板
    然后是问题分类的模板:
    1、最值问题: dp[i] = max/min(dp[i], dp[i-nums]+1)或dp[i] = max/min(dp[i], dp[i-num]+nums);
    2、存在问题(bool):dp[i]=dp[i]||dp[i-num];
    3、组合问题:dp[i]+=dp[i-num];

  2. 解题步骤

    • 确定dp数组以及下标的含义

    • 确定递推公式

    • 确定遍历顺序
      如果求组合数就是外层for循环遍历物品,内层for遍历背包。
      如果求排列数就是外层for遍历背包,内层for循环遍历物品。

  3. 模板

    int dp[N+1][amount+1]
    dp[0][..] = 0
    dp[..][0] = 1
    
    for i in [1..N]:
        for j in [1..amount]:
            把物品 i 装进背包,
            不把物品 i 装进背包
    return dp[N][amount]
    
  4. 例题
    leetcode322 零钱兑换

贪心算法

  1. 思想
    贪心的本质是选择每一阶段的局部最优,从而达到全局最优。

  2. 时机
    刷题或者面试的时候,手动模拟一下感觉可以局部最优推出整体最优,而且想不到反例,那么就试一试贪心。

  3. 步骤
    将问题分解为若干个子问题
    找出适合的贪心策略
    求解每一个子问题的最优解
    将局部最优解堆叠成全局最优解

  4. 例题
    leetcode 122. 买卖股票的最佳时机 II
    局部最优:收集每天的正利润-> 整体最优 :求得最大利润
    leetcode 135 分发糖果
    局部最优:对于右评分大于左评分 糖果数加一 对于左评分大于右评分 糖果数取现有糖果数和右糖果数加一的最大值 -> 整体最优
    leetcode 55 跳跃游戏
    局部最优: 每次跳跃取最大的覆盖范围 -> 整体最优: 整体跳跃的最大覆盖范围
    leetcode 435 无重叠子区间
    右边界排序之后,局部最优:优先选右边界小的区间,所以从左向右遍历,留给下一个区间的空间大一些,从而尽量避免交叉。全局最优:选取最多的非交叉区间。
    leetcode 56 合并区间
    按照左边界排序,排序之后局部最优:每次合并都取最大的右边界,这样就可以合并更多的区间了,整体最优:合并所有重叠的区间。

BFS

1. 思想
把一些问题抽象成图,从一个点开始,向四周开始扩散。一般来说,我们写 BFS 算法都是用「队列」这种数据结构,每次将一个节点周围的所有节点加入队列。
2.场景
问题的本质就是让你在一幅「图」中找到从起点start到终点target的最近距离

字符串相关

滑动窗口

  1. 思想
  2. 场景
  3. 步骤
/* 滑动窗口算法框架 */
void slidingWindow(string s, string t) {
    unordered_map<char, int> need, window;
    for (char c : t) need[c]++;

    int left = 0, right = 0;
    int valid = 0; 
    while (right < s.size()) {
        // c 是将移入窗口的字符
        char c = s[right];
        // 右移窗口
        right++;
        // 进行窗口内数据的一系列更新
        ...

        /*** debug 输出的位置 ***/
        printf("window: [%d, %d)\n", left, right);
        /********************/

        // 判断左侧窗口是否要收缩
        while (window needs shrink) {
            // d 是将移出窗口的字符
            char d = s[left];
            // 左移窗口
            left++;
            // 进行窗口内数据的一系列更新
            ...
        }
    }
}
  1. 例题
    leetcode 76 最小覆盖子串
    leetcode 3 无重复字符的最长子串
    剑指 Offer II 014. 字符串中的变位词

字符串相加

  1. 例题
    leetcode 165. 比较版本号
    leetcode 415. 字符串相加
    leetcode 8.字符串转换整数 (atoi)
    leetcode 151.翻转字符串里的单词

二叉树

  1. 思想
    二叉树题目的递归解法可以分两类思路,第一类是遍历一遍二叉树得出答案,第二类是通过分解问题计算出答案,这两类思路分别对应着 回溯算法核心框架 和 动态规划核心框架。
    是否可以通过遍历一遍二叉树得到答案?如果不能的话,是否可以定义一个递归函数,通过子问题(子树)的答案推导出原问题的答案?

  2. 思考
    关于回溯遍历问题:

    // 二叉树遍历函数
    void traverse(TreeNode root) {
        if (root == null) {
            return;
        }
        // 前序遍历位置,记录节点值
        path.append(root.val);
        if (root.left == null && root.right == null) {
            // 到达叶子节点,累加路径和
            res += Integer.parseInt(path.toString());
        }
        // 二叉树递归框架,遍历左右子树
        traverse(root.left);
        traverse(root.right);

        // 后续遍历位置,撤销节点值
        path.deleteCharAt(path.length() - 1);

    }

关于后序遍历:
前序位置的代码只能从函数参数中获取父节点传递来的数据,而后序位置的代码不仅可以获取参数数据,还可以获取到子树通过函数返回值传递回来的数据。
一旦你发现题目和子树有关,那大概率要给函数设置合理的定义和返回值,在后序位置写代码了。
关于层序遍历

// 输入一棵二叉树的根节点,层序遍历这棵二叉树
void levelTraverse(TreeNode root) {
    if (root == null) return;
    Queue<TreeNode> q = new LinkedList<>();
    q.offer(root);

    // 从上到下遍历二叉树的每一层
    while (!q.isEmpty()) {
        int sz = q.size();
        // 从左到右遍历每一层的每个节点
        for (int i = 0; i < sz; i++) {
            TreeNode cur = q.poll();
            // 将下一层节点放入队列
            if (cur.left != null) {
                q.offer(cur.left);
            }
            if (cur.right != null) {
                q.offer(cur.right);
            }
        }
    }
}
  1. 框架
void traverse(TreeNode root) {
    if (root == null) {
        return;
    }
    // 前序位置
    traverse(root.left);
    // 中序位置
    traverse(root.right);
    // 后序位置
}
/* 迭代遍历数组 */
void traverse(int[] arr) {
    for (int i = 0; i < arr.length; i++) {

    }
}

/* 递归遍历数组 */
void traverse(int[] arr, int i) {
    if (i == arr.length) {
        return;
    }
    // 前序位置
    traverse(arr, i + 1);
    // 后序位置
}

/* 迭代遍历单链表 */
void traverse(ListNode head) {
    for (ListNode p = head; p != null; p = p.next) {

    }
}

/* 递归遍历单链表 */
void traverse(ListNode head) {
    if (head == null) {
        return;
    }
    // 前序位置
    traverse(head.next);
    // 后序位置
}
  1. 例题
    分解问题
    leetcode 104. 二叉树的最大深度
    leetcode 110. 平衡二叉树
    leetcode 543. 二叉树的直径
    leetcode 101.对称二叉树
    leetcode 226.翻转二叉树
    leetcode 124.二叉树的最大路径和
    回溯遍历
    leetcode 129. 求根节点到叶节点数字之和

位运算

  1. 思想
    a ^ a = 0;
    a ^ b = b ^ a;
    a ^ b ^ c = a ^ (b ^ c) = (a ^ b) ^ c
    a ^ 0 = a
    1 ^ 0 = 1

  2. 步骤
    考虑其中是否需要一下几个

    • 大小写互换

      ('d' ^ ' ') = 'D'
      ('D' ^ ' ') = 'd'
      
    • 不用临时变量交换两个数

      int a = 1, b = 2;
      a ^= b;
      b ^= a;
      a ^= b;
      
    • 消除数字n的二进制表示中最后一个1

      n & (n-1)
      
  3. 例题
    leetcode 67. 二进制求和
    leetcode 136.只出现一次的数字
    leetcode 191.位1的个数
    leetcode 231. 2的幂
    剑指 Offer 53 - II. 0~n-1中缺失的数字
    剑指 Offer 65. 不用加减乘除做加法
    剑指 Offer II 003. 前 n 个数字二进制中 1 的个数

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值