dfs求连通块
递归
递归是什么?绝大部分人都会说:自己调用自己,刚开始我也是这样理解递归的。确实没错,递归的确是自己调用自己。递归简单的应用:编写一个能计算斐波那契数列的函数,也就是这样:
int fb(int n){
if(n == 1 || n == 2) return 1;
return fb(n-1) + fb(n-2);
}
相信绝大部分人都能看懂这段代码。递归除了可以用自己调用自己这样描述之外,还可以这样表示递归函数:递推式+边界处理。很显然,fb(n) = fb(n-1) + fb(n-2)
就是这个计算斐波那契数列的递推式,而上面的if语句就是边界处理。但是,当我接触到二叉树这个数据结构时,这样的递归定义显然还不够完整,还差一点。我们先介绍一个数据结构,链表,链表是常见的基础数据结构,对于链表的实现书和网上都有讲,那么我们可不可以给链表这样一个递归定义:
链表:要么结点为空,要么由结点和子节点构成
这样定义了链表后,其实二叉树的递归定义就好理解了:
二叉树:要么为空,要么由根结点、左子树和右子树组成,而左子树和右子树分别是一棵二叉树。 摘自《算法竞赛入门经典》
看完了二叉树,就可以想到二叉树的结点是可以用类似与实现链表的方法来进行实现:
struct node{
int value;
node *left, *right;
};
和链表相比,二叉树只不过是多了一个指针而已。但是呢,多了一个指针有变得有点麻烦:我们在遍历链表时,只需要用一个循环就能遍历完链表中的结点。
struct node{
int value;
node *next;
};
void fun(node* root){ //root是链表头,即链表第一个元素的地址(指针)
for(node *p = root; p != NULL; p=p->next){
//操作
}
}
而对于二叉树要写多少个循环呢,一个,两个?我们发现,二叉树并不能像链表一样简单的遍历,因为二叉树每到一个结点就有两个方向可以走,并不像一个for循环只规定一个方向。还有一个问题:当我们从根节点出发时,如果按照我们普通的方法遍历,应该是从左到右遍历,也就是先遍历左子树,遍历完后再遍历右子树。
当遍历左子树时,我们发现,这个左子树的跟结点也连接有左子树和右子树。
这时遍历过程如果用循环写就变得异常复杂,最关键的是,怎样从左子树遍历完后开始右子树的遍历。有的人说,到了树的末端就停止左子树的遍历,然后进行右子树的遍历。但是右子树又要从哪个结点开始遍历呢,所以我们还要写一个回溯的代码,而这仅仅用循环是很难实现的,下面是回溯图:
看起来非常复杂,其实我们用递归就可以解决这个问题,关键是我们对递归怎样进一步地去理解。我们再次看回斐波那契函数的代码:
int fb(int n){
if(n == 1 || n == 2) return 1;
return fb(n-1) + fb(n-2);
}
其中if(n == 1 || n == 2) return 1;
之前被认为是边界处理,其实这里还有个操作:回溯,也就是return 1;
这个语句。在递归到达边界后,就会把值返回给上一个状态。下面的return fb(n-1) + fb(n-2);
中的return
的作用也是回溯的操作。那么,这个递归函数还有什么值得研究的吗?之前我们说的递推式fb(n-1) + fb(n-2)
,在这里我们把它称为要重复做的事。所以,根据上面的解释,我们又可以这样理解递归:递归是可以帮你完成要重复做的事情,只要你规定好边界和处理好回溯的问题。那么递归相比于我们普通写的循环(递推)有什么优势呢?首先,相同点我们都知道,就是同样可以完成要重复做的事,不同在于循环一般是完成单方向的重复做的事,如果是多方向的重复做的事可能要写多重循环,甚至多重循环都不一定解决的了,代码实现相对较难。而递归呢,则单方向和多方向要完成重复做的事都可以,而且关注点只是重复做的事情,处理好边界和回溯问题就行了,减少思考的时间(这个时间因情况而定,如果你每一步递归全都要思考一遍,把过程写出来,自然是会消耗不少时间,减少时间的前提是你把要重复做的事抽象化出来,处理好边界问题后相信递归能计算出来),我先摆上遍历二叉树代码:
struct node{
int value;
node *left, *right;
};
void dfs(node* root){ //root为二叉树的根节点的地址(指针)
if(root == NULL) return; //边界处理,如果到达边界就回溯
//重复要做的事
dfs(root->left);
dfs(root->right);
return; //遍历完左子树和右子树后返回
}
是不是递归函数的代码很简洁?我们分析为什么遍历二叉树可以这样写:看回二叉树的递归定义:结点,左子树和右子树。所以我们在遍历时重复的操作是遍历左子树和右子树,那么怎样遍历左子树和右子树呢?首先肯定是要到左子树和右子树的根节点才能继续遍历。于是完整要做的重复事情是:到达一个节点后,遍历它的左结点和右结点。于是代码就变成这样:
void dfs(node* root){ //到达一个结点
dfs(root->left); //遍历左结点
dfs(root->right); //遍历右结点
}
这时递归函数的主要框架已经完成,也就是我们搞定了要重复做的事。接下来就要考虑边界和回溯的问题。首先考虑边界吧。当到达树的底部时:
我们怎样停止遍历,也就是判断的依据是什么?
我们可以看到上图,一个结点是边界的标志是它的左右子节点都为空,也就是:
root->left == NULL && root->right == NULL
于是原来的代码可以这样写:
void dfs(node* root){ //到达一个结点
if(root->left == NULL && root->right == NULL) return; //如果左右结点为空则不再遍历左结点和右结点
dfs(root->left); //遍历左结点
dfs(root->right); //遍历右结点
}
当然,也可以这样写:
void dfs(node* root){ //到达一个结点
if(root == NULL) return; //当结点为空时就回溯
dfs(root->left); //遍历左结点
dfs(root->right); //遍历右结点
}
这样写看起来更简洁一些,为什么可行呢?当我们遍历到最后一个结点时,这个代码会继续遍历左结点,然后到了左结点这个状态。检查这个结点,发现为空,所以返回。返回后遍历右结点,发现右结点也为空,所以返回。然后遍历完左结点和右结点后返回。这里有些小伙伴可能会有些疑问?为什么遍历完左结点和右结点后会返回呢?这里没有返回代码啊!其实这里的返回只是省略不写,因为是void类型啊,执行完后就会自动返回。所以完整的遍历二叉树的代码是:
struct node{
int value;
node *left, *right;
};
void dfs(node* root){
if(root == NULL) return;
//其他操作可以写在这里,比如查找值等等
dfs(root->left);
dfs(root->right);
}
这里有个小坑:如果你的边界处理是这样:if(root->left == NULL && root->right == NULL) return;
你要执行的操作应该在这个语句前面,否则会导致最后一个点遍历不了。所以最好写成上面完整代码的形式。
dfs
终于讲到dfs,我要die了 dfs:深度优先搜索,英文全称:Depth-First-Search。刚刚遍历二叉树时,我们的函数名是不是写成了dfs?对,没错,刚刚遍历二叉树的方法就是一种dfs。那么,dfs如何实现呢?我想大家应该都猜到了:递归。所以理解递归尤为关键。这里给一道dfs的经典例题:https://www.cnblogs.com/happy-MEdge/p/10544533.html
dfs求连通块的更多相关文章
- DFS入门之二---DFS求连通块
用DFS求连通块也是比较典型的问题, 求多维数组连通块的过程也称为--“种子填充”. 我们给每次遍历过的连通块加上编号, 这样就可以避免一个格子访问多次.比较典型的问题是”八连块问题“.即任意两格子所 ...
- UVA 572 Oil Deposits油田(DFS求连通块)
UVA 572 DFS(floodfill) 用DFS求连通块 Time Limit:1000MS Memory Limit:65536KB 64bit IO Format: ...
- [C++]油田(Oil Deposits)-用DFS求连通块
[本博文非博主原创,均摘自:刘汝佳<算法竞赛入门经典>(第2版) 6.4 图] [程序代码根据书中思路,非独立实现] 例题6-12 油田(Oil Deposits,UVa572) 输入一个 ...
- HDU1241 Oil Deposits —— DFS求连通块
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1241 Oil Deposits Time Limit: 2000/1000 MS (Java/Othe ...
- UVA 572 -- Oil Deposits(DFS求连通块+种子填充算法)
UVA 572 -- Oil Deposits(DFS求连通块) 图也有DFS和BFS遍历,由于DFS更好写,所以一般用DFS寻找连通块. 下述代码用一个二重循环来找到当前格子的相邻8个格子,也可用常 ...
- UVA 572 dfs求连通块
The GeoSurvComp geologic survey company is responsible for detecting underground oil deposits. GeoSu ...
- 用DFS求连通块(种子填充)
[问题] 输入一个m行n列的字符矩阵,统计字符“@”组成多少个八连块.如果两个字符“@”所在的格子相邻(横.竖或者对角线方向),就说它们属于同一个八连块.例如,图6-9中有两个八连块. 图6-9 [分 ...
- 图-用DFS求连通块- UVa 1103和用BFS求最短路-UVa816。
这道题目甚长, 代码也是甚长, 但是思路却不是太难.然而有好多代码实现的细节, 确是十分的巧妙. 对代码阅读能力, 代码理解能力, 代码实现能力, 代码实现技巧, DFS方法都大有裨益, 敬请有兴趣者 ...
- UVa 572 油田(DFS求连通块)
https://uva.onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&page=show_problem& ...
- UVa572 Oil Deposits DFS求连通块
技巧:遍历8个方向 ; dr <= ; dr++) ; dc <= ; dc++) || dc != ) dfs(r+dr, c+dc, id); 我的解法: #include< ...
随机推荐
- java中的装箱及拆箱
java中存在8中基本的数据类型,每一种数据类型都有包装类型. 包装类型:每一个基本的数据类型都会------对应一个包装类型. boolean------------------>Boolea ...
- 新闻类爬虫库:Newspaper
newspaper库是一个主要用来提取新闻内容及分析的Python爬虫框架.此库适合抓取新闻网页.操作简单易学,即使对完全没了解过爬虫的初学者也非常的友好,简单学习就能轻易上手,除此之外,使用过程你不 ...
- Redis五大类型及底层实现原理
目录 简单动态字符串链表字典跳跃表整数集合压缩列表对象 对象的类型与编码字符串对象列表对象哈希对象 集合对象有序集合对象类型检查与命令多态内存回收对象共享对象的空转时长 简单动态字符串 导读 Red ...
- ARMv7-A 架构下的MMU
ARM架构中MMU的作用主要就是将CPU发出的虚拟地址转换成为物理地址,从实现内核和用户程序和用户程序之间的虚拟地址空间隔离.MMU的主要组成为两部分,其一是内部的TLBs缓存:还有就是转换表装换单元 ...
- ARM汇编---程序获取符号的物理地址
在移植u-boot的过程看到过u-boot在重定向时的实现,当时不知道怎么就觉得很好理解就把这个知识点没怎么深入的理解,最近在看华为的鸿蒙OS在Cortex-A平台上的实现过程时再次遇到一时间看不太懂 ...
- vue watch All In One
vue watch All In One var vm = new Vue({ data: { a: 1, b: 2, c: 3, d: 4, e: { f: { g: 5 } } }, watch: ...
- DNS & HTTPS bug
DNS & HTTPS bug SSL protocol version bug https://typescript-4.x-tutorials.xgqfrms.xyz/ errors Th ...
- Node.js Backend Developer
Node.js Backend Developer refs xgqfrms 2012-2020 www.cnblogs.com 发布文章使用:只允许注册用户才可以访问!
- React useEffect in depth
React useEffect in depth useEffect class DogInfo extends React.Component { controller = null state = ...
- JavaScript Number Type Checker
JavaScript Number Type Checker Number.isInteger // static 方法 Number.isInteger(value) https://develop ...