本文为xdfApp团队成员文章,原文链接:https://blog.csdn.net/sinat_37380158/article/details/106866970

作者介绍:韩沛沛, 北京邮电大学本科硕士毕业,在阿里大文娱(优酷)工作6年,后任马蜂窝后端技术专家, 现任新东方后端JAVA技术专家。

0 前言

昨天突然到来的代码训练营中,我被叫起来讲两周前的一道题,有点懵,有同学听完之后表示没太明白,可能我当时表述的比较着急所以没讲清楚。现在特别整理了一下DFS的解题模板,并挑选了一系列leetcode的相关题目(从easy到hard),希望大家看完之后能对DFS有个更好的认识。
本文内容比较基础,只适用于对DFS了解不深的同学;不过欢迎所有的同学交流和指正,大家一起努力提高~

1 DFS简介:

引用自leetcode网站关于DFS的介绍:

深度优先搜索算法(英语:Depth-First-Search,DFS)是一种用于遍历或搜索树或图的算法。沿着树的深度遍历树的节点,尽可能深的搜索树的分支。当节点v的所在边都己被探寻过,搜索将回溯到发现节点v的那条边的起始节点。这一过程一直进行到已发现从源节点可达的所有节点为止。如果还存在未被发现的节点,则选择其中一个作为源节点并重复以上过程,整个进程反复进行直到所有节点都被访问为止。属于盲目搜索。
    深度优先搜索是图论中的经典算法,利用深度优先搜索算法可以产生目标图的相应拓扑排序表,利用拓扑排序表可以方便的解决很多相关的图论问题,如最大路径问题等等。
    因发明「深度优先搜索算法」,约翰 · 霍普克洛夫特与罗伯特 · 塔扬在1986年共同获得计算机领域的最高奖:图灵奖。

2 DFS模板

DFS的一般模板(解题一般套路):

//参数用来表示当前状态;
//返回值是我们dfs完成之后想要获取的数据,如果不需要返回值或者通过全局变量来记录状态的话ReturnType可以为void
//函数名可以换成更有意义的名字
ReturnType dfs(param1,params2,...)
{
if(终点状态 || 非法状态 || 需要剪枝)
{
... //退出前处理
return;
}
for(每一个当前状态相关的下一个状态)
{
if(该状态合法 && 该状态未被标记)
{
...; // 当前状态应该做的处理(遍历前需要的处理)(根据实际情况来判断是否需要)
标记当前状态;
dfs();
...; // 当前状态应该做的处理(遍历后需要的处理)(根据实际情况来判断是否需要)
(还原标记); //可选操作, 如果加上这句就是"回溯法"
} }
}

3 DFS实战

我们从一系列实战例题来逐步加深对DFS模板的理解。
    说明:实战部分的代码均为博主手敲,主要是用来和大家一起熟悉思路,可能不是最优雅的解法。

实战一:简单DFS

题目: LeetCode No.100 相同的树 (简单) 原题链接

给定两个二叉树,编写一个函数来检验它们是否相同。如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。

题目分析
树的遍历,可以用dfs解决。从根结点出发,如果根结点相同 && 根结点的左子树相同 && 根结点的右子树相同,则可以判断两个二叉树相同。
java代码:
应用DFS模板很容易写出下面的代码:

class Solution {
// 当前状态(两个树同一位置的某个节点)可用参数p和q表示;返回值(是否相同)显然是boolean
public boolean isSameTree(TreeNode p, TreeNode q) {
// 终止状态 直接返回
if (p == null && q == null) return true;
if (p == null || q == null) retrun false;
if (p.val != q.val) return false;
// 当前状态相关的下一个状态有两个: 比较树的左子和右子
// 因为当前节点不会再次遍历,省略当前状态的标记处理和标记还原操作
boolean leftSame = isSameTree(p.left, q.left);
boolean rightSame = isSameTree(p.right, q.right);
// 遍历后需要的处理
return leftSame && rightSame;
}
}

实战二:稍复杂的DFS

题目: LeetCode No.112 路径总和 (简单) 原题链接

给定一个二叉树和一个目标和,判断该树中是否存在根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和。
说明: 叶子节点是指没有子节点的节点。

题目分析
树的遍历,可以用dfs解决。从根结点出发,如果 (根结点.val == 目标和 && 根结点为叶子节点) || (根结点.val + 根结点的左子树.val == 目标和) || (根结点.val + 根结点的右子树.val == 目标和),则可以判断存在满足题意的路径。
java代码:
应用DFS模板很容易写出下面的代码:

class Solution {
// 当前状态除了树的当前节点root,还有当前期望的和sum;
// 返回值(是否存在路径)显然是boolean
public boolean hasPathSum(TreeNode root, int sum) {
// 终止状态 直接返回
if (root.left == null && root.right == null && root.val == sum) {
return true;
}
// 当前状态相关的下一个状态有两个: 比较树的左子和右子
// 邻节点dfs之前应该做的处理(设定expectSum)
int expectSum = sum - root.val;
// 因为当前节点不会再次遍历,省略当前状态的标记处理和标记还原操作
boolean leftRes = hasPathSum(root.left, expectSum);
boolean rightRes = hasPathSum(root.right, expectSum);
// 遍历后需要的处理
return leftSame && rightSame;
}

相比较前一题,本题在dfs时除了关注树本身节点外还需要关注当前期望和sum,这里刚开始学习dfs的同学可能会觉得有一点绕,理解的关键还是要搞清楚dfs遍历时都有哪些数据在发生变化(刚开始初学时,如果不确定dfs方法需要哪些参数,可以把这些会发生变化的数据都当作方法参数) 。刚开始学习dfs的部分同学对于dfs执行的顺序也可能感到有点难理解,这个问题可以通过不断练习针对不同的输入调试跟踪dfs遍历的过程来解决。

实战三:DFS+回溯

题目: LeetCode No.113 路径总和 II (中等) 原题链接

给定一个二叉树和一个目标和,找到所有从根节点到叶子节点路径总和等于给定目标和的路径。
说明: 叶子节点是指没有子节点的节点。

题目分析
遍历树可以得到所有满足条件的路径,可以用dfs解决。
从根结点出发对树进行完整遍历,如果 (当前节点为叶子结点 && 从叶子节点往上所有祖先.val之和 == 目标和),则将该路径加入到结果集合。
仔细思考dfs的过程,和当前状态有关的变量可能有:当前的节点root、当前目标和sum、当前路径path、当前结果集res。其中与当前状态强相关的变量是:当前的节点root、当前目标和sum;起支持作用的变量是:当前路径path、当前结果集res。一般习惯将强相关的变量放到dfs的参数列表中;起支持作用的变量可以放到dfs参数列表中,也可以放到全局变量(之后dfs过程中能用到就好)。
java代码:
应用DFS模板很容易写出下面的代码:

class Solution {
// 用全局变量res来记录结果(当然也可以将res当作当前状态的一部分放到dfs的参数列表中)。
private List<List<Integer>> res = new ArrayList<>(); public List<List<Integer>> pathSum(TreeNode root, int sum) {
if (root == null) {
return res;
}
// 仔细思考dfs的状态,除了和当前的节点root、当前目标和sum有关,还和当前路径path有关。
// (当然也可以将res当作当前状态的一部分放到dfs的参数列表中, 这里我们认为res只是一个结果收集器,与当前状态无关,放到全局变量中)
List<Integer> path = new ArrayList<>();
dfs(root, sum, path);
return res;
} // 因为在遍历过程中会做结果集的收集,dfs不需要返回值
private void dfs(TreeNode root, int sum, List<Integer> path) {
// 终点状态1, 直接返回
if(root == null) {
return;
} // 终点状态2,需要做退出前处理(收集新路径)
if (root.left == null && root.right == null && root.val == sum) {
// 标记当前状态 - 路径加入当前节点
path.add(root.val);
// 结果加入当前路径
// 因为path是全局唯一对象,用来记录遍历过程中当前状态的路径,所以不能直接将path放到结果集中,需要深拷贝
res.add(new ArrayList<>(path));
// (还原标记) - 为了不影响后续遍历,需要回溯去掉path里的当前节点
path.remove(path.size() - 1);
return;
}
// 当前状态相关的下一个状态有两个: 比较树的左子和右子
// 邻节点dfs之前应该做的处理(设定expectSum)
int expectSum = sum - root.val;
// 标记当前状态 - 路径加入当前节点
path.add(root.val);
dfs(root.left, expectSum, path);
dfs(root.right, expectSum, path);
// (还原标记) - 为了不影响后续遍历,需要回溯去掉path里的当前节点
path.remove(path.size() - 1);
}
}

如果有对回溯不太熟悉的同学,在刚开始的时候可能感到有点难理解。其实回溯的本质很简单,用下面模板来解释:

for(需要遍历的每一个item) {
doSomething(item); // 前行
process(item);
undoSomeThing(item); // 回退(回溯)
}

结合本例,在采集结果或者对非叶子结点dfs时,我们先将当前节点加入当前路径,等结果采集完毕或者子节点dfs结束后将当前节点从当前路径中去除,这样就能保证遍历下一个元素的时候,path里面永远是正确的当前路径内容。
回溯也需要多加练习,才能掌握比较好。
下面我们再通过一个题目来巩固dfs+回溯。

实战四:DFS+回溯

题目: LeetCode No.46 全排列(中等) 原题链接

给定一个 没有重复 数字的序列,返回其所有可能的全排列。

题目分析
本题可以有很多种解法,当然也可以用dfs解决。用dfs也有多种思路,我们以每次选择一个新元素为例。

java代码:
应用DFS模板很容易写出下面的代码:

class Solution {
// 结果集;用全局变量res来记录结果
List<List<Integer>> res = new ArrayList<>();
// 仔细思考dfs遍历时的当前状态,可以用(数组nums、路径path、状态traveled)来表示。
// 这里我们在做一个小的变化,将当前状态(路径、状态)也放到全局变量中
// 当前状态 - 路径(当前遍历过的所有节点的路径)
List<Integer> path = new ArrayList<>();
// 当前状态 - 状态(当前遍历过哪些节点)
Set<Integer> traveled = new HashSet<>(); public List<List<Integer>> permute(int[] nums) {
dfs(nums);
return res;
}
private void dfs(int[] nums) {
// 终点状态,需要做退出前处理(收集新路径)
if (path.size() == nums.length) {
// 需要深拷贝
res.add(new ArrayList<>(path));
return;
}
// for(每一个当前状态相关的下一个状态)。
// 注意这里和之前树的dfs不一样,树的dfs很多时候不用考虑重复遍历,这里就需要考虑了(根据标记状态判断就可以去除重复遍历)
for (int i=0;i<nums.length;i++) {
// if(该状态被标记) 直接跳过
if (traveled.contains(i)) {
continue;
}
// 标记当前状态:同时处理路径path和状态traveled
traveled.add(i);
path.add(nums[i]);
dfs(nums);
// (还原标记)/回溯:同时回溯路径path和状态traveled
path.remove(path.size() - 1);
traveled.remove(i);
}
}
}

从上面代码可以看出,对于dfs最重要的几点就是:确定如何来表示/切换当前状态,确定如何标记/回溯,确定终止/剪枝条件。
推荐阅读:
从全排列问题开始理解「回溯」算法(深度优先遍历 + 状态重置 + 剪枝)

实战五:DFS+二维

题目: LeetCode No.695 岛屿的最大面积(中等) 原题链接

给定一个包含了一些 0 和 1 的非空二维数组 grid 。
一个 岛屿 是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在水平或者竖直方向上相邻。你可以假设 grid 的四个边缘都被 0(代表水)包围着。
找到给定的二维数组中最大的岛屿面积。(如果没有岛屿,则返回面积为 0 。)
示例 1:
[[0,0,1,0,0,0,0,1,0,0,0,0,0],
[0,0,0,0,0,0,0,1,1,1,0,0,0],
[0,1,1,0,1,0,0,0,0,0,0,0,0],
[0,1,0,0,1,1,0,0,1,0,1,0,0],
[0,1,0,0,1,1,0,0,1,1,1,0,0],
[0,0,0,0,0,0,0,0,0,0,1,0,0],
[0,0,0,0,0,0,0,1,1,1,0,0,0],
[0,0,0,0,0,0,0,1,1,0,0,0,0]]
对于上面这个给定矩阵应返回 6。注意答案不应该是 11 ,因为岛屿只能包含水平或垂直的四个方向的 1 。
示例 2:
[[0,0,0,0,0,0,0,0]]
对于上面这个给定的矩阵, 返回 0。

题目分析
本题可以有很多种解法,当然也可以用dfs解决。用dfs也有多种思路:比如每次选择一个新元素,比如每次交换相邻元素等。我们以每次选择一个新元素为例。

java代码
有了前面的基础,应用DFS模板很容易写出相应代码(肯定要比回溯简单)。

class Solution {
public int maxAreaOfIsland(int[][] grid) {
if (grid == null || grid.length == 0 || grid[0].length == 0) {
return 0;
}
int m = grid.length;
int n = grid[0].length;
int max = 0; // 对二维数组每一个岛屿进行dfs,dfs可以返回当前岛屿的面积,由此可得最大岛面积
for (int i=0;i<m;i++) {
for (int j=0;j<n;j++) {
if (grid[i][j] == 1) {
max = Math.max(dfs(grid, i, j), max);
}
}
}
return max;
} // dfs遍历时的当前状态比较明显,就是二维数组的某个元素,可以用(数组grid, 横坐标 i, 纵坐标 j)来表示。
private int dfs(int[][] grid, int i, int j) {
int m = grid.length;
int n = grid[0].length;
// 标记当前状态;已经标记过的元素后面不会再次访问
grid[i][j] = -1;
int res = 1;
// 对当前状态的4个方向(如果有的话)分别进行dfs累加当前岛面积
if (i > 0 && grid[i-1][j] == 1) {
res += dfs(grid, i-1, j);
}
if (i < m - 1 && grid[i+1][j] == 1) {
res += dfs(grid, i+1, j);
}
if (j > 0 && grid[i][j-1] == 1) {
res += dfs(grid, i, j-1);
}
if (j < n - 1 && grid[i][j+1] == 1) {
res += dfs(grid, i, j+1);
}
// 返回当前岛面积
return res;
}
}

上面dfs()方法返回了当前岛的面积。我们也可以思考一下场景来解决类似的更复杂问题。
比如如果每次完成dfs时我们将当前岛的面积都记录下来,就可以得到所有岛屿的面积。
比如不同的岛屿构成了不同的连通分量,我们可以判断任意两个点是否在同一个岛,也可以计算不同的岛屿间的最近距离。
比如假设我们有能力将某一块海洋变成陆地(将二维数组中某一个值为0的元素变成1),变动哪块海洋之后能得到最大岛?

实战六:DFS+二维+着色

题目: LeetCode No.827 最大人工岛 (困难) 原题链接

在二维地图上, 0代表海洋, 1代表陆地,我们最多只能将一格 0 海洋变成 1变成陆地。
进行填海之后,地图上最大的岛屿面积是多少?(上、下、左、右四个方向相连的 1 可形成岛屿)
示例 1:
输入: [[1, 0], [0, 1]]
输出: 3
解释: 将一格0变成1,最终连通两个小岛得到面积为 3 的岛屿。
示例 2:
输入: [[1, 1], [1, 0]]
输出: 4
解释: 将一格0变成1,岛屿的面积扩大为 4。
示例 3:
输入: [[1, 1], [1, 1]]
输出: 4
解释: 没有0可以让我们变成1,面积依然为 4。
说明:
1 <= grid.length = grid[0].length <= 50
0 <= grid[i][j] <= 1

题目分析
1 暴力解:很容易想到,对grid中每一个为0的元素将其变成1后进行dfs看其所在的岛屿面积,取其中最大岛屿面积即可,只是复杂度比较高,需要优化剪枝。dfs完了之后还要进行回溯(再将1变回0)。
2 优化暴力解:显然暴力解中没必要改变所有为0的元素,只需要改变近海元素即可(近海元素:紧挨着1的0)。
3 优化暴力解:对同一个岛一次dfs之后,就知道了该岛的面积,没必要多次重复对该岛dfs。
4 基于以上分析,我们可以先对grid做一个整体dfs,来给各个岛屿着色(对应的元素都相同的编号),并用一个map来记录每个着色的岛屿面积;然后对所有的近海元素,将0变成1,再从四个方向上累加新链接上的不同岛屿(着色不同)的面积,即可得到变更后此近海元素对应的岛屿面积。整个过程中记录最大岛屿面积即可。

java代码

class Solution {
// 用全局变量color来记录当前岛屿的着色(这里为了后面方便判断,颜色去了负值;其实取值多少无所谓,只要不同岛屿着色不同就行)
int color = -100;
// 用全局变量colorAreaMap来记录每个颜色对应的岛屿面积
Map<Integer, Integer> colorAreaMap = new HashMap<>(); public int largestIsland(int[][] grid) {
// 假设res就是我们要求的填海后的最大岛面积,它有两种可能: 1 未填海之前的最大岛屿面积(比如grid全为1);2 填海之后的最大岛屿面积
int res = 0; // 对二维数组内每一个岛屿进行dfs
for (int i = 0; i < grid.length; i++) {
for (int j = 0; j < grid[0].length; j++) {
// 如果发现新的陆地,改变颜色,后面邻接的陆地都将染色成新颜色
if (grid[i][j] == 1) {
color--;
}
// dfs对岛屿着色并返回岛屿面积
int area = dfs(grid, i, j);
if (area > 0) {
// 记录color对应的岛屿面积
colorAreaMap.put(color, area);
// 更新res
res = Math.max(res, area);
}
}
} // 对每个海域grid[i][j],寻找它相邻岛屿着色集合colorSet,计算填海后的岛屿面积
for (int i = 0; i < grid.length; i++) {
for (int j = 0; j < grid[0].length; j++) {
if (grid[i][j] == 0) {
// 海域grid[i][j]相邻的岛屿着色集合colorSet
Set<Integer> colorSet = new HashSet<>();
if (i > 0 && grid[i-1][j] < 0) {
colorSet.add(grid[i-1][j]);
}
if (i < grid.length - 1 && grid[i+1][j] < 0) {
colorSet.add(grid[i+1][j]);
}
if (j > 0 && grid[i][j-1] < 0) {
colorSet.add(grid[i][j-1]);
}
if (j < grid.length - 1 && grid[i][j+1] < 0) {
colorSet.add(grid[i][j+1]);
}
// 计算填海后的岛屿面积
int area = 1;
for (Integer c: colorSet) {
area += colorAreaMap.getOrDefault(c, 0);
}
res = Math.max(res, area);
}
}
}
return res;
} // dfs对岛屿着色并返回岛屿面积
private int dfs(int[][] grid, int i, int j) {
// 数组越界 或者 非陆地 或者 已遍历过,返回0
if (i < 0 || j < 0 || i >= grid.length || j >= grid[0].length || grid[i][j] < 1) {
return 0;
}
// 着色/染色
if (grid[i][j] == 1) {
grid[i][j] = color;
}
// 返回岛屿面积
return 1 + dfs(grid, i-1, j) + dfs(grid, i+1, j) + dfs(grid, i, j-1) + dfs(grid, i, j+1);
} }

4 DFS周边

DFS与BFS

DFS深度优先遍历,BFS广度优先遍历,二者都常见于树/图的遍历。
一般BFS常借助于队列实现,DFS常借助于栈/递归(系统栈)实现。从编码的角度讲,一般DFS要更容易实现。
DFS经常和回溯法搭配使用,这是因为DFS在遍历的当前状态和下一状态一般是相邻的,我们可以轻松的从一个状态变更到另一个状态。BFS遍历时从浅层转到深层状态的变化很大,通常需要额外变量去保存这些信息,性能往往也没DFS好。

DFS与UnionFind

DFS:深度优先遍历,常用来解决树/图的遍历、连通性、路径等问题。
UnionFind:并查集,一般用来解决图的连通性问题,不能解决路径相关问题。
DFS功能强于UnionFind,但是并查集更易于理解,代码也相对固定,不失为一种解决问题的好方法。
并查集将在下一期内容详细讲解。

————————————————
版权声明:本文为CSDN博主「ppprog」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/sinat_37380158/article/details/106866970

DFS常规解题套路的更多相关文章

  1. 3、回溯算法解题套路框架——Go语言版

    前情提示:Go语言学习者.本文参考https://labuladong.gitee.io/algo,代码自己参考抒写,若有不妥之处,感谢指正 关于golang算法文章,为了便于下载和整理,都已开源放在 ...

  2. 深度优先搜索(DFS)解题总结

    定义 深度优先搜索算法(Depth-First-Search),是搜索算法的一种.它沿着树的深度遍历树的节点,尽可能深的搜索树的分支. 例如下图,其深度优先遍历顺序为 1->2->4-&g ...

  3. 广度优先搜索(BFS)解题总结

    定义 广度优先搜索算法(Breadth-First-Search),是一种图形搜索算法. 简单的说,BFS是从根节点开始,沿着树(图)的宽度遍历树(图)的节点. 如果所有节点均被访问,则算法中止. B ...

  4. (step4.3.5)hdu 1501(Zipper——DFS)

    题目大意:个字符串.此题是个非常经典的dfs题. 解题思路:DFS 代码如下:有详细的注释 /* * 1501_2.cpp * * Created on: 2013年8月17日 * Author: A ...

  5. 三解炸弹人——DFS

    原创 枚举解炸弹人—— https://www.cnblogs.com/chiweiming/p/9295262.html BFS解炸弹人—— https://www.cnblogs.com/chiw ...

  6. dfs与dp算法之关系与经典入门例题

    目录 声明 dfs与dp的关系 经典例题-数字三角形 - POJ 1163 题目 dfs思路 解题思路 具体代码 dp思路 解题思路 具体代码 声明 本文不介绍dfs.dp算法的基础思路,有想了解的可 ...

  7. DFS+BFS(广度优先搜索弥补深度优先搜索遍历漏洞求合格条件总数)--09--DFS+BFS--蓝桥杯剪邮票

    题目描述 如下图, 有12张连在一起的12生肖的邮票.现在你要从中剪下5张来,要求必须是连着的.(仅仅连接一个角不算相连)  比如,下面两张图中,粉红色所示部分就是合格的剪取.  请你计算,一共有多少 ...

  8. 1、学习算法和刷题的框架思维——Go版

    前情提示:Go语言学习者.本文参考https://labuladong.gitee.io/algo,代码自己参考抒写,若有不妥之处,感谢指正 关于golang算法文章,为了便于下载和整理,都已开源放在 ...

  9. 大数据入门第二十二天——spark(一)入门与安装

    一.概述 1.什么是spark 从官网http://spark.apache.org/可以得知: Apache Spark™ is a fast and general engine for larg ...

随机推荐

  1. Lesson 12 Life on a desert island

    Lesson 12 Life on a desert island desert island ['dezət 'ailənd] n. 荒岛 uninhabited island coral isla ...

  2. Python爬虫之lxml-etree和xpath的结合使用

    本篇文章给大家介绍的是Python爬虫之lxml-etree和xpath的结合使用(附案例),内容很详细,希望可以帮助到大家. lxml:python的HTML / XML的解析器 官网文档:http ...

  3. sql注入之堆叠注入及waf绕过注入

    #堆叠查询注入 1.堆叠查询概念 stacked injections(堆叠查询注入)从名词的含义就可以看出一应该是一堆(多条)sql语句一起执行.而在真实运用中也是如此,我们知道在mysql中,主要 ...

  4. Centos8部署jdk、mysql8、tomcat,并部署项目到tomcat中

    目录 Linux系统的学习与使用(Centos8) Linux系统的介绍 为什么要选择Linux作为服务器运行的操作系统 目录结构 使Linux系统能够联网(登录root用户) 常用命令 cd命令(用 ...

  5. ListPopupWindow和Popupwindow的阴影相关问题demo总结

    Popupwindow: 优点:可以通过setBackgroundDrawable()来重新设置阴影. 缺点:当AnchorView是可移动的,比如移动到屏幕的左右边界.左下角.右下角时,Popupw ...

  6. Linux 内核的代码仓库太不一样了,光克隆都让我挠头,克隆后居然还丢文件,你肯定也会遇到!!!

    一个肯定能让你节省几个小时的小知识 大家好,我是 小猿来也,一个人称撸(划)码(水)小能手的程序猿. 最近一段时间,每次经过旁边大佬工位,总是发现他在快速的切屏,不知道在搞什么?难道他发现了快乐星球? ...

  7. Servlet中的HttpServletResponse 类

    HttpServletResponse 类的作用:              理解:顾名思义 就是响应客户端的内容, HttpServletResponse 类和 HttpServletRequest ...

  8. Servelt&&JSP进阶

    Servlet与JSP进阶 来自mkw的视频课程的总结 1.前言 内容包括 掌握Java Web核心特性,Servlet核心对象以及JSP九大内置对象.主要有以下的内容: 请求结构 && ...

  9. 基于 CODING CD + Nocalhost 在大型应用的 ChatOps 实践

    本文作者:红亚科技 CTO--卢兴民 红亚科技聚焦信息技术发展,为信息技术相关专业提供优质教学服务 背景 ChatOps 最早起源于 GitHub,它以沟通平台为中心,通过与机器人产生对话和交互,使开 ...

  10. 【ShardingSphere技术专题】「ShardingJDBC」SpringBoot之整合ShardingJDBC实现分库分表(JavaConfig方式)

    前提介绍 ShardingSphere介绍 ShardingSphere是一套开源的分布式数据库中间件解决方案组成的生态圈,它由Sharding-JDBC.Sharding-Proxy和Shardin ...