Leetcode - 分类经典题目汇总

 

回到顶部(go to top)

回溯算法

 

 

 

回到顶部(go to top)

动态规划

通解通法

动态规划问题的一般形式就是求最值

判断算法问题是否具备「最优子结构」,是否能够通过子问题的最值得到原问题的最值

明确 base case -> 明确「状态」-> 明确「选择」 -> 定义 dp 数组/函数的含义

「状态」就是指,f(n) 把参数 n 想做一个状态,这个状态 n 是由状态 n - 1 和状态 n - 2 转移(相加)而来,这就叫状态转移

 

最长递增子序列

原文:https://labuladong.gitee.io/algo/di-er-zhan-a01c6/zi-xu-lie--6bc09/dong-tai-g-6ea57/

一维最长递增子序列

二维最长递增子序列

 

 

回到顶部(go to top)

二叉树

0.通解通法

遇到一道二叉树的题目时的通用思考过程是:

1、是否可以通过遍历一遍二叉树得到答案?如果可以,用一个 traverse 函数配合外部变量来实现。

2、是否可以定义一个递归函数,通过子问题(子树)的答案推导出原问题的答案?如果可以,写出这个递归函数的定义,并充分利用这个函数的返回值。

3、无论使用哪一种思维模式,你都要明白二叉树的每一个节点需要做什么,需要在什么时候(前中后序)做。

 

1.以树的视角看动归/回溯/DFS算法

动归/DFS/回溯算法都可以看做二叉树问题的扩展,只是它们的关注点不同:

  • 动态规划算法属于分解问题的思路,它的关注点在整棵「子树」。
  • 回溯算法属于遍历的思路,它的关注点在节点间的「树枝」。
  • DFS 算法属于遍历的思路,它的关注点在单个「节点」。

有了这些铺垫,你就很容易理解为什么回溯算法和 DFS 算法代码中「做选择」和「撤销选择」的位置不同了。回溯算法必须把「做选择」和「撤销选择」的逻辑放在 for 循环里面,否则怎么拿到「树枝」的两个端点。

怎么理解?我分别举三个例子你就懂了:

第一个例子

给你一棵二叉树,请你用分解问题的思路写一个 count 函数,计算这棵二叉树共有多少个节点。代码很简单,上文都写过了。

你看,这就是动态规划分解问题的思路,它的着眼点永远是结构相同的整个子问题,类比到二叉树上就是「子树」。

复制代码
// 定义:输入一棵二叉树,返回这棵二叉树的节点总数
int count(TreeNode root) {
    if (root == null) {
        return 0;
    }
    // 我这个节点关心的是我的两个子树的节点总数分别是多少
    int leftCount = count(root.left);
    int rightCount = count(root.right);
    // 后序位置,左右子树节点数加上自己就是整棵树的节点数
    return leftCount + rightCount + 1;
}
复制代码

 

第二个例子

给你一棵二叉树,请你用遍历的思路写一个 traverse 函数,打印出遍历这棵二叉树的过程,你看下代码:

你看,这就是回溯算法遍历的思路,它的着眼点永远是在节点之间移动的过程,类比到二叉树上就是「树枝」。

复制代码
void traverse(TreeNode root) {
    if (root == null) return;
    printf("从节点 %s 进入节点 %s", root, root.left);
    traverse(root.left);
    printf("从节点 %s 回到节点 %s", root.left, root);

    printf("从节点 %s 进入节点 %s", root, root.right);
    traverse(root.right);
    printf("从节点 %s 回到节点 %s", root.right, root);
}
复制代码

 

第三个例子

我给你一棵二叉树,请你写一个 traverse 函数,把这棵二叉树上的每个节点的值都加一。很简单吧,代码如下:

你看,这就是 DFS 算法遍历的思路,它的着眼点永远是在单一的节点上,类比到二叉树上就是处理每个「节点」。

void traverse(TreeNode root) {
    if (root == null) return;
    // 遍历过的每个节点的值加一
    root.val++;
    traverse(root.left);
    traverse(root.right);
}

 

好,请你仔细品一下上面三个简单的例子,是不是像我说的:动态规划关注整棵「子树」,回溯算法关注节点间的「树枝」,DFS 算法关注单个「节点」。

有了这些铺垫,你就很容易理解为什么回溯算法和 DFS 算法代码中「做选择」和「撤销选择」的位置不同了,看下面两段代码:

复制代码
// DFS 算法把「做选择」「撤销选择」的逻辑放在 for 循环外面
void dfs(Node root) {
    if (root == null) return;
    // 做选择
    print("我已经进入节点 %s 啦", root)
    for (Node child : root.children) {
        dfs(child);
    }
    // 撤销选择
    print("我将要离开节点 %s 啦", root)
}

// 回溯算法把「做选择」「撤销选择」的逻辑放在 for 循环里面
void backtrack(Node root) {
    if (root == null) return;
    for (Node child : root.children) {
        // 做选择
        print("我站在节点 %s 到节点 %s 的树枝上", root, child)
        backtrack(child);
        // 撤销选择
        print("我站在节点 %s 到节点 %s 的树枝上", child, root)
    }
}
复制代码

 

 

2.后序遍历的特殊之处

前序位置的代码只能从函数参数中获取父节点传递来的数据,而后序位置的代码不仅可以获取参数数据,还可以获取到子树通过函数返回值,传递回来的数据。

那么换句话说:

  • 只有后序位置才能通过返回值获取子树的信息。
  • 一旦你发现题目和子树有关,那大概率要给函数设置合理的定义和返回值,在后序位置写代码了。

 

回到顶部(go to top)

字符串

1.辗转相除法

 

 

回到顶部(go to top)

数组

0.只要数组有序,就应该想到双指针技巧

1.双指针法:左右指针

 

2.双指针法:快慢指针

这里要注意:

1)不必两两交换,时间复杂度高

2)直接快慢指针,慢指针找到第一个0的位置,快指针找到第一个非0的位置,不交换,直接把快指针位置的值,覆盖慢指针位置的值。

然后重复该步骤,直到快指针到头,把慢指针之后的元素再都变成0即可

 

 

回到顶部(go to top)

链表

1. 虚拟头结点

Tips: 利用虚拟头结点,简化边界问题

 

当你需要创造一条新链表的时候,可以使用虚拟头结点简化边界情况的处理。

  • 比如说,让你把两条有序链表合并成一条新的有序链表,是不是要创造一条新链表?
  • 再比你想把一条链表分解成两条链表,是不是也在创造新链表?

这些情况都可以使用虚拟头结点简化边界情况的处理。

 

2 双指针法:快慢指针

找出环形列表开始的节点:

1.先通过快慢指针,速度为2和1,找到相遇的节点

2.然后一个指针回到head,一个指针从相遇节点触发,速度均为1,再次相遇的节点就是环开始的节点。

 

posted on   frank_cui  阅读(92)  评论(0编辑  收藏  举报

相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
历史上的今天:
2021-06-14 Docker - Swarm 集群
< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

导航

统计

levels of contents
点击右上角即可分享
微信分享提示