【Java基础】递归的理解及应用场景

文章目录

    • 前言
    • 分治策略
    • 什么是递归
    • 递归算法的使用
      • 1.递归底层是对栈的操作
      • 2.例子:求阶乘
    • 递归使用场景
      • 1.删除文件夹
      • 2.计算文件夹大小
      • 3.指定目录下的文件树
      • 4.克隆文件夹
      • 5.多级菜单树处理

前言

学习递归之前,请先点击此文章了解,一些数据结构"栈"的概念以及特点

分治策略

  • 分治策略的思想就是分而治之,即 先将一个规模较大的大问题分解成若干个规模较小的小问题,再对这些小问题进行解决,得到的解,在将其组合起来得到最终的解 。 而分治与递归很多情况下都是一起结合使用的。

什么是递归

  1. 方法自己调用自己称为递归

    注意事项:

    1. 要有出口,(是一个判断条件,一般要和我们if语句搭钩);
    2. 次数不宜过多(因为方法调用要开栈,栈内存是有限的,很容易溢出);
    3. 如果递归不结束,则会报错。java.lang.StackOverflowError: 栈内存溢出错误
    4. 递归会内存溢出的原因:方法不停地进栈而不出栈,导致栈内存不足。
    5. 递归并不能解决所有的问题。有的问题适合使用递归而不是循环。如果使用循环并不困难的话,最好使用循环
  2. 递归结构

    • 递归尽头 : 什么时候不调用自己,如果没有头,将陷入死循环(常见的递归头:就是if判断)
    • 递归体:什么时候需要调用自身方法。
  3. 递归的优缺点:

    优点:
    1.代码结构简单;
    2.如在树结构的遍历中,递归的实现明显比循环简单。

    缺点:
    自己调用自己,每次都会分配栈内存来保存参数值。导致时间和内存消耗,从而降低效率。内存大量使用可能会导致栈内存溢出风险。

  4. 递归的分类:

    • 直接递归: 方法自己调用自己,例如:方法A调用方法A (常用)
    • 间接递归: 使用其他方法间接调用自己,例如:方法A调用方法B,方法B调用方法C,方法C调用方法A

在日常开发中,我们使用循环语句远远大于递归,但这不能说明递归就没有用武之地,实际上递归算法的解决问题的步骤更符合人类解决问题的思路,这是递归算法的优点,同时也是它的缺点。
.
排序算法里面的快速排序和归并排序,这两种算法采用的都是分治思想来处理排序问题,所以递归在这里就出现了,如果不理解递归算法,就去学习这两种排序算法,可能理解起来就非常费事,尽管你知道这两种排序的算法原理和它的时间及空间复杂度,但就是不知道它是如何使用递归完成的,所以学习和理解递归算法是非常有必要的。

对递归和循环的生动解释:

  • 递归: 你打开面前这扇门,看到屋里面还有一扇门。你走过去,发现手中的钥匙还可以打开它,你推开门,发现里面还有一扇门,你继续打开它。若干次之后,你打开面前的门后,发现只有一间屋子,没有门了。然后,你开始原路返回,每走回一间屋子,你数一次,走到入口的时候,你可以回答出你到底用这你把钥匙打开了几扇门。

  • 循环: 你打开面前这扇门,看到屋里面还有一扇门。你走过去,发现手中的钥匙还可以打开它,你推开门,发现里面还有一扇门(若前面两扇门都一样,那么这扇门和前两扇门也一样;如果第二扇门比第一扇门小,那么这扇门也比第二扇门小,你继续打开这扇门,一直这样继续下去直到打开所有的门。但是,入口处的人始终等不到你回去告诉他答案。

递归算法的使用

1.递归底层是对栈的操作

public void recursiveTest(){
     
    recursiveTest();  //自己调用自己,就叫递归
}

执行结果:
【Java基础】递归的理解及应用场景_第1张图片
上面就是一个错误的递归算法,一旦运行起来就会抛出栈内存溢出异常,原因是: 没有退出条件,所以就会进入死循环中,一直都在重复调用自己。

递归底层是对栈的操作

  • 递归调用在底层其实是对线程栈的压栈和出栈操作,每调用一次都会压栈一次,并记录相关的局部变量信息。
  • 线程栈的内存是非常有限的,而递归调用如果是无限的,那么很快就会消耗完所有的内存资源,最终导致内存溢出,这一点与空的while死循环是不一样的,单纯的死循环会大量的消耗cpu资源,但不会占用内存资源,所以不会导致程序异常。

编写正确的递归算法,一定要有 ”归“ 的步骤,也就是说递归算法,在分解问题到不能再分解的步骤时,要让递归有退出的条件,否则就会陷入死循环,最终导致内存不足引发栈溢出异常

2.例子:求阶乘

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;
    }

执行结果
【Java基础】递归的理解及应用场景_第2张图片

实际上运算逻辑

入栈顺序:最先入栈的在栈底,最后入栈的在栈顶
  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

从上面的步骤我们可以清晰的看到:

  1. 递归算法的第一步是分治把复杂的大的问题,给拆分成一个一个小问题,直到不能再拆解,通过退出条件retrun,然后再从最小的问题开始解决,只到所有的子问题解决完毕,那么最终的大问题就迎刃而解。
  2. 上面的打印信息,符合栈数据结构的定义,后进先出,通过把所有的子问题压栈之后,然后再一个个出栈,从最简单的步骤计算,最终解决大问题,非常形象。

如下图:
第一阶段,递推分解任务:
【Java基础】递归的理解及应用场景_第3张图片
第 二阶段:回归分治任务:
【Java基础】递归的理解及应用场景_第4张图片

心得:有些兄dei可能认为递归即是自己调用自己,那岂不是死循环了。对,如果递归写的不合理,那就是死循环了。但是如果写的合理,加上“边界条件”,程序执行到底的时候,会逐层返回。就像我们爬山一样,我们绕着山路爬上一层又一层,如果没有山顶,我们会一直往上爬。但如果到了山顶,就按照上山时候的步骤一层一层的往下爬。

递归使用场景

1.删除文件夹

@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; } }

【Java基础】递归的理解及应用场景_第5张图片
【Java基础】递归的理解及应用场景_第6张图片

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]

2.计算文件夹大小

@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;
    }
}

【Java基础】递归的理解及应用场景_第7张图片

3.指定目录下的文件树

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;
    }
}

4.克隆文件夹

@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()));
            }
        }
    }
}

5.多级菜单树处理

菜单数据从这里取

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递归和循环

你可能感兴趣的:(Java基础,递归,算法,分治策略,堆栈,内存)