学习递归之前,请先点击此文章了解,一些数据结构"栈"的概念以及特点
分而治之
,即 先将一个规模较大的大问题分解成若干个规模较小的小问题,再对这些小问题进行解决,得到的解,在将其组合起来得到最终的解 。 而分治与递归很多情况下都是一起结合使用的。方法自己调用自己
称为递归
注意事项:
- 要有出口,(是一个判断条件,一般要和我们if语句搭钩);
- 次数不宜过多(因为方法调用要开栈,栈内存是有限的,很容易溢出);
- 如果递归不结束,则会报错。
java.lang.StackOverflowError: 栈内存溢出错误
- 递归会内存溢出的原因:
方法不停地进栈而不出栈,导致栈内存不足。
- 递归并不能解决所有的问题。有的问题适合使用递归而不是循环。如果使用循环并不困难的话,最好使用循环
递归结构
递归的优缺点:
优点:
1.代码结构简单;
2.如在树结构的遍历中,递归的实现明显比循环简单。缺点:
自己调用自己,每次都会分配栈内存来保存参数值。导致时间和内存消耗,从而降低效率。内存大量使用可能会导致栈内存溢出风险。
递归的分类:
在日常开发中,我们使用循环语句远远大于递归,但这不能说明递归就没有用武之地,实际上递归算法的解决问题的步骤更符合人类解决问题的思路,这是递归算法的优点,同时也是它的缺点。
.
排序算法里面的快速排序和归并排序
,这两种算法采用的都是分治思想来处理排序问题,所以递归在这里就出现了,如果不理解递归算法,就去学习这两种排序算法,可能理解起来就非常费事,尽管你知道这两种排序的算法原理和它的时间及空间复杂度,但就是不知道它是如何使用递归完成的,所以学习和理解递归算法是非常有必要的。
对递归和循环的生动解释:
递归: 你打开面前这扇门,看到屋里面还有一扇门。你走过去,发现手中的钥匙还可以打开它,你推开门,发现里面还有一扇门,你继续打开它。若干次之后,你打开面前的门后,发现只有一间屋子,没有门了。然后,你开始原路返回,每走回一间屋子,你数一次,走到入口的时候,你可以回答出你到底用这你把钥匙打开了几扇门。
循环: 你打开面前这扇门,看到屋里面还有一扇门。你走过去,发现手中的钥匙还可以打开它,你推开门,发现里面还有一扇门(若前面两扇门都一样,那么这扇门和前两扇门也一样;如果第二扇门比第一扇门小,那么这扇门也比第二扇门小,你继续打开这扇门,一直这样继续下去直到打开所有的门。但是,入口处的人始终等不到你回去告诉他答案。
public void recursiveTest(){
recursiveTest(); //自己调用自己,就叫递归
}
执行结果:
上面就是一个错误
的递归算法,一旦运行起来就会抛出栈内存溢出异常
,原因是: 没有退出条件,所以就会进入死循环中,一直都在重复调用自己。
递归底层是对栈的操作
底层
其实是对线程栈的压栈和出栈操作
,每调用一次都会压栈一次,并记录相关的局部变量信息。线程栈的内存是非常有限的
,而递归调用如果是无限的,那么很快就会消耗完所有的内存资源,最终导致内存溢出,这一点与空的while死循环是不一样的,单纯的死循环会大量的消耗cpu资源,但不会占用内存资源
,所以不会导致程序异常。编写正确的递归算法,一定要有 ”归“
的步骤,也就是说递归算法,在分解问题到不能再分解的步骤时
,要让递归有退出
的条件,否则就会陷入死循环
,最终导致内存不足引发栈溢出异常
。
public static int factrial(int n){
if(n<1){
return 1;
}
return n*factrial(n-1);
}
改造了一下,实现的是同样的功能,但有详细的步骤,如下:
public static int factrialDetail(int n) {
if (n == 1) {
System.out.println("拆解问题完毕,开始分而治之");
return 1;
}
System.out.println("f(" + n + ")=" + n + " * f(" + (n - 1) + ")");
int z = n * factrialDetail(n - 1);
System.out.println("f(" + n + ")=" + z);
return z;
}
实际上运算逻辑
入栈顺序:最先入栈的在栈底,最后入栈的在栈顶
factrialDetail(2) => 2 * factrialDetail(1)
factrialDetail(3) => 3 * factrialDetail(2)
factrialDetail(4) => 4 * factrialDetail(3)
factrialDetail(5) => 5 * factrialDetail(4)
出栈顺序:后进先出
factrialDetail(2) => 2 * factrialDetail(1) => 2 * (1) = 2
factrialDetail(3) => 3 * factrialDetail(2) => 3 * (2*(1)) = 6
factrialDetail(4) => 4 * factrialDetail(3) => 4 * (3*(2*(1))) = 24
factrialDetail(4) => 5 * factrialDetail(4) => 5 * (4*(3*(2*(1)))) = 120
从上面的步骤我们可以清晰的看到:
分治
,把复杂的大的问题,给拆分成一个一个小问题,直到不能再拆解,通过退出条件retrun
,然后再从最小的问题开始解决
,只到所有的子问题解决完毕,那么最终的大问题就迎刃而解。后进先出
,通过把所有的子问题压栈之后,然后再一个个出栈,从最简单的步骤计算,最终解决大问题,非常形象。如下图:
第一阶段,递推分解任务:
第 二阶段:回归分治任务:
心得:有些兄dei可能认为递归即是自己调用自己,那岂不是死循环了。对,如果递归写的不合理,那就是死循环了。但是如果写的合理,加上“边界条件”,程序执行到底的时候,会逐层返回。就像我们爬山一样,我们绕着山路爬上一层又一层,如果没有山顶,我们会一直往上爬。但如果到了山顶,就按照上山时候的步骤一层一层的往下爬。
@Slf4j
public class DeleteDir {
public static void main(String[] args) {
// 封装目录
File srcFolder = new File("E:\\data\\platform");
deleteAllFile(srcFolder);
}
/**
* 递归删除文件
*
* boolean delete():删除此路径名表示的文件或目录。如果此路径名表示一个目录,则该目录必须为空才能删除。
*
* @param file
* @return
*/
public static boolean deleteFiles(File file) {
log.info("1.[{}]进入方法", file.getAbsolutePath());
if (!file.exists()) {
log.info("=>路径[{}]不存在", file.getAbsolutePath());
return false;
}
if (file.isFile()) {
log.info("2.当前[{}]为文件,直接删除", file.getAbsolutePath());
return file.delete();
}
if (file.isDirectory()) {
log.info("3.当前[{}]为目录,获取所有子File,执行递归删除逻辑", file.getAbsolutePath());
File[] files = file.listFiles();
if (files == null || files.length <= 0) {
log.info("4.当前[{}]为空目录,直接删除", file.getAbsolutePath());
return file.delete();
}
for (int i = 0; i < files.length; i++) {
String typeName = files[i].isFile() ? "文件" : "目录";
log.info("5.1.[{}]=>递归删除子[{}]=>[{}],start", i, typeName, files[i].getAbsolutePath());
deleteFiles(files[i]);
log.info("5.2.[{}]=>递归删除子[{}]=>[{}],end", i, typeName, files[i].getAbsolutePath());
}
log.info("6.本轮递归已完成,删除目录:[{}]", file.getAbsolutePath());
return file.delete();
}
return false;
}
}
16:21:34.120 - 1.[E:\data\platform]进入方法
16:21:34.124 - 3.当前[E:\data\platform]为目录,获取所有子File,执行递归删除逻辑
16:21:34.125 - 5.1.[0]=>递归删除子[目录]=>[E:\data\platform\path_a],start
16:21:34.125 - 1.[E:\data\platform\path_a]进入方法
16:21:34.125 - 3.当前[E:\data\platform\path_a]为目录,获取所有子File,执行递归删除逻辑
16:21:34.125 - 5.1.[0]=>递归删除子[文件]=>[E:\data\platform\path_a\a111.txt],start
16:21:34.125 - 1.[E:\data\platform\path_a\a111.txt]进入方法
16:21:34.125 - 2.当前[E:\data\platform\path_a\a111.txt]为文件,直接删除
16:21:34.126 - 5.2.[0]=>递归删除子[文件]=>[E:\data\platform\path_a\a111.txt],end
16:21:34.126 - 5.1.[1]=>递归删除子[文件]=>[E:\data\platform\path_a\a222.txt],start
16:21:34.126 - 1.[E:\data\platform\path_a\a222.txt]进入方法
16:21:34.126 - 2.当前[E:\data\platform\path_a\a222.txt]为文件,直接删除
16:21:34.126 - 5.2.[1]=>递归删除子[文件]=>[E:\data\platform\path_a\a222.txt],end
16:21:34.126 - 5.1.[2]=>递归删除子[文件]=>[E:\data\platform\path_a\a333.txt],start
16:21:34.126 - 1.[E:\data\platform\path_a\a333.txt]进入方法
16:21:34.127 - 2.当前[E:\data\platform\path_a\a333.txt]为文件,直接删除
16:21:34.127 - 5.2.[2]=>递归删除子[文件]=>[E:\data\platform\path_a\a333.txt],end
16:21:34.127 - 5.1.[3]=>递归删除子[目录]=>[E:\data\platform\path_a\anull1],start
16:21:34.127 - 1.[E:\data\platform\path_a\anull1]进入方法
16:21:34.127 - 3.当前[E:\data\platform\path_a\anull1]为目录,获取所有子File,执行递归删除逻辑
16:21:34.127 - 4.当前[E:\data\platform\path_a\anull1]为空目录,直接删除
16:21:34.127 - 5.2.[3]=>递归删除子[目录]=>[E:\data\platform\path_a\anull1],end
16:21:34.127 - 5.1.[4]=>递归删除子[目录]=>[E:\data\platform\path_a\path_a_1],start
16:21:34.127 - 1.[E:\data\platform\path_a\path_a_1]进入方法
16:21:34.127 - 3.当前[E:\data\platform\path_a\path_a_1]为目录,获取所有子File,执行递归删除逻辑
16:21:34.128 - 5.1.[0]=>递归删除子[文件]=>[E:\data\platform\path_a\path_a_1\file111.txt],start
16:21:34.128 - 1.[E:\data\platform\path_a\path_a_1\file111.txt]进入方法
16:21:34.128 - 2.当前[E:\data\platform\path_a\path_a_1\file111.txt]为文件,直接删除
16:21:34.128 - 5.2.[0]=>递归删除子[文件]=>[E:\data\platform\path_a\path_a_1\file111.txt],end
16:21:34.128 - 5.1.[1]=>递归删除子[文件]=>[E:\data\platform\path_a\path_a_1\file222.txt],start
16:21:34.128 - 1.[E:\data\platform\path_a\path_a_1\file222.txt]进入方法
16:21:34.128 - 2.当前[E:\data\platform\path_a\path_a_1\file222.txt]为文件,直接删除
16:21:34.128 - 5.2.[1]=>递归删除子[文件]=>[E:\data\platform\path_a\path_a_1\file222.txt],end
16:21:34.129 - 5.1.[2]=>递归删除子[文件]=>[E:\data\platform\path_a\path_a_1\file333.txt],start
16:21:34.129 - 1.[E:\data\platform\path_a\path_a_1\file333.txt]进入方法
16:21:34.129 - 2.当前[E:\data\platform\path_a\path_a_1\file333.txt]为文件,直接删除
16:21:34.129 - 5.2.[2]=>递归删除子[文件]=>[E:\data\platform\path_a\path_a_1\file333.txt],end
16:21:34.129 - 5.1.[3]=>递归删除子[目录]=>[E:\data\platform\path_a\path_a_1\nullpath_a_1],start
16:21:34.129 - 1.[E:\data\platform\path_a\path_a_1\nullpath_a_1]进入方法
16:21:34.129 - 3.当前[E:\data\platform\path_a\path_a_1\nullpath_a_1]为目录,获取所有子File,执行递归删除逻辑
16:21:34.129 - 4.当前[E:\data\platform\path_a\path_a_1\nullpath_a_1]为空目录,直接删除
16:21:34.130 - 5.2.[3]=>递归删除子[目录]=>[E:\data\platform\path_a\path_a_1\nullpath_a_1],end
16:21:34.130 - 6.本轮递归已完成,删除目录:[E:\data\platform\path_a\path_a_1]
16:21:34.130 - 5.2.[4]=>递归删除子[目录]=>[E:\data\platform\path_a\path_a_1],end
16:21:34.130 - 6.本轮递归已完成,删除目录:[E:\data\platform\path_a]
16:21:34.130 - 5.2.[0]=>递归删除子[目录]=>[E:\data\platform\path_a],end
16:21:34.130 - 6.本轮递归已完成,删除目录:[E:\data\platform]
@Slf4j
public class DirSize {
public static void main(String[] args) {
// 封装目录
File srcFolder = new File("E:\\data\\egos");
System.out.println(getDirLength(srcFolder));
}
/**
* long length(): 表示的文件的长度(以字节为单位)
*
* @param dir
* @return
*/
public static long getDirLength(File dir) {
long len = 0;
if (dir == null || !dir.exists()) {
log.info("[{}]不存在", dir.getAbsolutePath());
return 0;
}
if (dir.isFile()) {
log.info("[{}]不是一个目录", dir.getAbsolutePath());
return dir.length();
}
File[] files = dir.listFiles();
if (files != null) {
for (int i = 0; i < files.length; i++) {
if (files[i].isFile()) {
len += files[i].length();
} else {
len += getDirLength(files[i]);
}
}
}
log.info("path={},len={}",dir.getAbsolutePath(),len);
return len;
}
}
public class ShowDir {
public static void main(String[] args) {
// 封装目录
File srcFolder = new File("E:\\data\\platform");
List<FileVO> dirList = getDirList(srcFolder);
System.out.println(dirList);
}
/**
* 源目录-必须保证src传的是一个目录
* @param src
* @return
*/
public static List<FileVO> getDirList(File src) {
if (src == null || !src.exists()) {
System.out.println("路径不存在");
return new ArrayList<>(1);
}
if (src.isFile()) {
System.out.println("当前根File是一个目录");
return new ArrayList<>(1);
}
//获取所有文件夹
File[] files = src.listFiles();
if (files == null || files.length == 0) {
return new ArrayList<>(1);
}
List<FileVO> rootFileList = new ArrayList<>();
for (File file : files) {
FileVO fileVO = new FileVO();
fileVO.setName(file.getName());//当前文件或者目录名字
fileVO.setAbsPath(file.getAbsolutePath());//保存绝对路径
fileVO.setFile(file.isFile());//是否是文件
if (file.isFile()) {
fileVO.setChildrenList(new ArrayList<>(1));
} else {
//是文件夹时进入递归处理
fileVO.setChildrenList(getDirList(file));
}
rootFileList.add(fileVO);
}
return rootFileList;
}
@Data
public static class FileVO {
//文件或者目录名称
private String name;
//绝对路径
private String absPath;
//是否是文件
private boolean isFile;
//当前目录下的所有子文件获取目录
private List<FileVO> childrenList;
}
}
@Slf4j
public class CopyDir {
public static void main(String[] args) {
// 源目录
File srcFolder = new File("E:\\usr\\local\\img");
// 目标目录
File targetFolder = new File("E:\\data\\newHome");
copyFiles(srcFolder, targetFolder);
}
/**
* 递归克隆文件夹(在目标文件夹中创建原来文件夹进行克隆 如E:/usr/local/img 克隆到 E:/data/newHome 后生成的为 E:/data/newHome/img)
* 源目录 E:/usr/local/img
* 目标目录 E:/data/newHome
*
* @param src 源文件夹
* @param target 目录文件夹
* @return
*/
public static void copyFiles(File src, File target) {
//1,在目标文件夹中创建原来文件夹(目标文件夹=目录文件夹+源文件夹名称 E:/data/newHome/ img)
File newDir = new File(target, src.getName());
//2.创建目标文件夹
if (!newDir.exists()) {
newDir.mkdirs();
}
//3,获取原文件夹中所有的文件和文件夹,存储在File数组中
File[] subFiles = src.listFiles();
for (int i = 0; i < subFiles.length; i++) {
//如果是文件就用io流读写
if (subFiles[i].isFile()) {
try (
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(subFiles[i]));//获取源文件输入流程
//目标文件夹 E:/data/newHome/img/ aaa.txt =》(目标文件夹=新文件夹+当前文件名称)
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(new File(newDir, subFiles[i].getName())));//输出流到指定目录
) {
int len = 0;
byte[] bytes = new byte[1024];
while ((len = bis.read(bytes, 0, bytes.length)) != -1) {
bos.write(bytes, 0, bytes.length);
}
} catch (Exception e) {
e.printStackTrace();
}
//如果是文件夹就递归调用
} else {
//源文件夹=当前文件夹 E:/usr/local/img/aaa 目标文件夹=E:/data/newHome/img
copyFiles(subFiles[i], newDir);
}
}
}
/**
* 递归克隆文件夹(在目标文件夹中创建原来文件夹进行克隆 如E:/usr/local/img 克隆到 E:/data/newHome 后生成的为 E:/data/newHome/)
*
* @param src 源文件夹
* @param target 目标文件夹
* @return
*/
public static void copyAllFile(File src, File target) {
//2.创建目标文件夹
if (!target.exists()) {
target.mkdirs();
}
//3,获取原文件夹中所有的文件和文件夹,存储在File数组中
File[] subFiles = src.listFiles();
for (int i = 0; i < subFiles.length; i++) {
//如果是文件就用io流读写
if (subFiles[i].isFile()) {
try (
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(subFiles[i]));//获取源文件输入流程
//目标文件夹=当前文件夹 E:/data/newHome + aaa.text =》(目标文件夹=-原文件夹+当前文件名称)
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(new File(target, subFiles[i].getName())));//输出流到指定目录
) {
int len = 0;
byte[] bytes = new byte[1024];
while ((len = bis.read(bytes, 0, bytes.length)) != -1) {
bos.write(bytes, 0, bytes.length);
}
} catch (Exception e) {
e.printStackTrace();
}
//如果是文件夹就递归调用
} else {
//源文件夹=当前文件夹 E:/usr/local/img/aaa 目标文件夹=目标文件夹+当前文件夹名称 E:/data/newHome + aaa
copyAllFile(subFiles[i], new File(target, subFiles[i].getName()));
}
}
}
}
菜单数据从这里取
public class MenuTree {
public static void main(String[] args) {
//获取classpath下面的menuList.json文件流程
InputStream is = MenuTree.class.getClassLoader().getResourceAsStream("menuList.json");
//通过缓冲字符串流程读取json文件
try (
BufferedReader br = new BufferedReader(new InputStreamReader(is));
) {
//保存每次读取的一行数据
String line = null;
//保存所有读取数据
StringBuilder stringBuilder = new StringBuilder();
while ((line = br.readLine()) != null) {
stringBuilder.append(line);
}
//反序列化成MenuVO集合
List<MenuVO> menuList = JSONArray.parseArray(stringBuilder.toString(), MenuVO.class);
//获取菜单树
List<MenuVO> treeMenuList = findTree(menuList);
System.out.println(treeMenuList);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 生成树的主体方法
*
* @param allMenu 所有菜单信息
* @return
*/
public static List<MenuVO> findTree(List<MenuVO> allMenu) {
//1.保存所有根节点
List<MenuVO> rootMenuList = new ArrayList<>();
//2.获取parentId=0的所有根节点
for (MenuVO menu : allMenu) {
if (menu.getMenuParentId() == 0L) {
rootMenuList.add(menu);
}
}
//3.根据orderNum排序根节点
rootMenuList.sort(order());
//4.递归获取根节点下的所有子节点,getClild()是递归调用的
for (MenuVO menu : rootMenuList) {
menu.setChildrenList(getChild(menu.getMenuId(), allMenu));//保存当前根节点下的所有子节点
}
//5.返回根节点
return rootMenuList;
}
/**
* 获取子节点
* @param menuId 父节点id
* @param allMenu 所有菜单列表
* @return 每个根节点下,所有子菜单列表
*/
public static List<MenuVO> getChild(Long menuId, List<MenuVO> allMenu) {
//1.保存父节点下的所有子节点
List<MenuVO> childrenMenuList = new ArrayList<>();
//2.获取父节点下的所有子节点
for (MenuVO menu : allMenu) {
//遍历所有节点,将所有菜单的父id与传过来的根节点的menuId比较(相等说明:为该根节点的子节点。)
if (menu.getMenuParentId().equals(menuId)) {
childrenMenuList.add(menu);
}
}
//3.根据orderNum排序子节点
childrenMenuList.sort(order());
//4.递归获取子节点下面的子节点
for (MenuVO menu : childrenMenuList) {
// 保存当前子节点下的所有子节点
menu.setChildrenList(getChild(menu.getMenuId(), allMenu));
}
//5. 如果节点下没有子节点,返回一个空List(递归退出条件)
if (childrenMenuList.isEmpty()) {
return new ArrayList<>();
}
return childrenMenuList;
}
/**
* 返回根据orderNum排序的比较器
* "1".compareTo("2");//小于 负数
* "3".compareTo("1"));//大于 正数
* "1".compareTo("1");//等于 0
*
* @return
*/
public static Comparator<MenuVO> order() {
return (o1, o2) -> {
return o1.getOrderNum().compareTo(o2.getOrderNum());
};
}
}
java递归和循环