- 浏览: 42272 次
- 性别:
- 来自: 北京
文章分类
最新评论
-
lost_alien:
复杂的sql恐怕用这个就够呛了。。。。
DSL-SQL源码分析 -
liaobinxu:
shaopei3344 写道递归不是会引起栈溢出吗。说说递归在 ...
递归算法 -
shaopei3344:
递归不是会引起栈溢出吗。
说说递归在现实中的例子
递归算法
什么是递归?
递归是一个重要的概念。我们在开发中排序方法以及定义和少秒线性数据结构的主干部分使用递归。递归运用在运筹学模型、博弈论以及图的研究中。
递归运用到什么方面?
一个计算机文件系统由拥有的文件和其他目录(名为子目录)的根组成。到你需要复制一个文件夹的内容到你的移动硬盘的时候。程序首先会把根目录下面的文件复制到移动硬盘中,然后在继续前进到子目录。针对每一个子目录, 再次重复同样的处理过程: 复制文件并移动到子目录(也就是根下面的子目录)。最终,你会在子目录层次结构中一直向下前进到只存在文件位置,此时复制完成。 文件夹复制是一个递归过程, 这是因为它涉及的一个过程会产生一个具有相同动作的相同过程。在每一个步骤, 磁盘备份只完成复制文件的最终, 递归步骤会到达只复制文件的停止条件。
1下面介绍一个示例:
1.1求一个非负整数n的阶乘?
阶乘的公式:n!=n * n-1* n-2 * ...*2*1
这个迭代过程其实可以使用循环
1.2我们使用递归方法来实现阶乘
1.3上面两个方法的测试用例
2.递归算法的描述或实现
2.1递归算法的要数:
(1)一个或多个能够确定实参直接求值的停止条件(stopping condition)
(2)一个或多个递归步骤(recursive step),在递归步骤中,要计算递归算法的当前值,需要重复调用具有实参的方法, 这些实参最终达到某个停止条件。
2.2递归的实现
设计递归算法时,必须小心区分停止条件和递归步骤。使用if-else语法来实现这个区别
2.3递归的工作方式
现在我们跟踪factorial()方法的执行:
将factorial(n)视作一台为n-Fact的机器,它通过执行乘法n*(n-1)!来计算n!的值。为了操作这台机器,它必须与由后向前传递信息的其他一些列n-Fact机互联。 0-Fact是一个例外,它可以独立工作, 并且不需要另外一个台机器的帮组就可以返回一个结果1.
如图所示
步骤1:4-Fact启动3-Fact机, 等待来自3-Fact机的返回值3!:计算4*3!=24并返回给main
步骤2:3-Fact启动2-Fact机, 等待来自2-Fact机的返回值2!:计算3*2!=6并返回给4-Fact
步骤3:2-Fact启动1-Fact机, 等待来自1-Fact机的返回值1!:计算2*1!=2并返回给3-Fact
步骤4:1-Fact启动0-Fact机, 等待来自0-Fact机的返回值0!:计算1*1!=1并返回给2-Fact
步骤5:0-Fact确定0!=1 将1返回给1-Fact
3.递归的应用
递归中最强大是允许程序人员能够解决难于采用迭代处理进行设计和实现的问题。下面的示例是汉诺塔(Hanoi)问题。
3.1Hanoi塔
问题:Hanoi塔包括一堆尺寸不等的圆盘和三根分别为A、B、C的针, 起始设置是将n个圆盘放在A上。Hanoi的任务就是把针A上的圆盘移动C上,一次只能移动一个圆盘,并且要以相同顺序重新构建n个圆盘。在移动中, 尺寸大的圆盘不允许放在尺寸小的圆盘上。
分析:
设三根针分别为initNeedle,tempNeedle,endNeedle.
递归步骤持续将堆叠得越来越少的圆盘从一根针移动到另一根针, 直到只剩下一根圆盘,此时递归步骤到达停止条件,解决的问题的方法就是简单地将这个圆盘移动到正确的针上。
下面是hanoi塔的实现:
* hanoi塔算法
* @param n 从大到小
* @param initNeedel 针A
* @param endNeedel 针B
* @param tempNeedle 针C
运行结果
n=3,initNeeelde = A, endneedle=C, tempNeedle=B
Move A to C //阶段一
Move A to B
Move C to B
Move A to C //阶段二
Move B to A //阶段三
Move B to C
Move A to C
4递归的评价
递归的方法的价值取决与问题。对于hannoi塔问题递归就是最好的选择,但是正对fibonacci数列就不一定了
4.1fibonacci数列
fib(n)= 0(n=0)或=1(n=1)或=fib(n-1)+fib(n-2)(n>=2)
使用递归方法实现fibonacci数列
我们以fibonacci(5)为例
fib()产生多次迭代,造成巨大的人冗余。fib(5)的值需要两次fib(3)和五次fib(1)。调用次数numCall(n)=2*fib(n+1)-1;
对于5来说numbCall(5)=2*fib(6)-1=2*8-1=15
对于35来说,numCall(35)=2*fib(36)-1=2*149303352-1=29,860,703
4.2使用迭代的方式实现fibonacci数列fibIter()
4.3对比Fibonacci数列的两种方法
测试用例
5递归的使用方法
outline:
测试用例
不会溢栈吧, 递归从面向过程语言C,还是在面向对象语言java,都在广泛使用,只有在一些算法的时间复杂度不同而已,
比如递归解决hanoi比较好, 在斐波那契数列就不好,因为存在重复调用的问题,这里有一个关于斐波那契数列递归算法Fibonacci数列的一种经典递归实现
递归是一个重要的概念。我们在开发中排序方法以及定义和少秒线性数据结构的主干部分使用递归。递归运用在运筹学模型、博弈论以及图的研究中。
递归运用到什么方面?
一个计算机文件系统由拥有的文件和其他目录(名为子目录)的根组成。到你需要复制一个文件夹的内容到你的移动硬盘的时候。程序首先会把根目录下面的文件复制到移动硬盘中,然后在继续前进到子目录。针对每一个子目录, 再次重复同样的处理过程: 复制文件并移动到子目录(也就是根下面的子目录)。最终,你会在子目录层次结构中一直向下前进到只存在文件位置,此时复制完成。 文件夹复制是一个递归过程, 这是因为它涉及的一个过程会产生一个具有相同动作的相同过程。在每一个步骤, 磁盘备份只完成复制文件的最终, 递归步骤会到达只复制文件的停止条件。
1下面介绍一个示例:
1.1求一个非负整数n的阶乘?
阶乘的公式:n!=n * n-1* n-2 * ...*2*1
这个迭代过程其实可以使用循环
public static int factorial(int n) { if(n<=0) throw new IllegalArgumentException(" Recursion: factorial must be postive"); int factValue = 1; while (n >= 1) { factValue *= n; n--; } return factValue; }
1.2我们使用递归方法来实现阶乘
public static int factorial2(int n){ if(n<=0) throw new IllegalArgumentException(" Recursion: factorial must be postive"); if(n==0) return 1; return n* factorial2(n-1); }
1.3上面两个方法的测试用例
/** * 测试: Recursion的factorial()方法 */ public void factroial(){ int n=3; System.out.println(Recursion.factorial(n)); } /** * 测试: Recursion的factorial()方法 */ public void factroialWithRecursion(){ int n=3; System.out.println(Recursion.factorialWithRecursion(n)); }
2.递归算法的描述或实现
2.1递归算法的要数:
(1)一个或多个能够确定实参直接求值的停止条件(stopping condition)
(2)一个或多个递归步骤(recursive step),在递归步骤中,要计算递归算法的当前值,需要重复调用具有实参的方法, 这些实参最终达到某个停止条件。
引用
递归算法中存在递归步骤和递归条件
2.2递归的实现
设计递归算法时,必须小心区分停止条件和递归步骤。使用if-else语法来实现这个区别
引用
使用if-else选择语句来标识停止条件(if部分)和递归步骤(else部分)
2.3递归的工作方式
现在我们跟踪factorial()方法的执行:
将factorial(n)视作一台为n-Fact的机器,它通过执行乘法n*(n-1)!来计算n!的值。为了操作这台机器,它必须与由后向前传递信息的其他一些列n-Fact机互联。 0-Fact是一个例外,它可以独立工作, 并且不需要另外一个台机器的帮组就可以返回一个结果1.
如图所示
步骤1:4-Fact启动3-Fact机, 等待来自3-Fact机的返回值3!:计算4*3!=24并返回给main
步骤2:3-Fact启动2-Fact机, 等待来自2-Fact机的返回值2!:计算3*2!=6并返回给4-Fact
步骤3:2-Fact启动1-Fact机, 等待来自1-Fact机的返回值1!:计算2*1!=2并返回给3-Fact
步骤4:1-Fact启动0-Fact机, 等待来自0-Fact机的返回值0!:计算1*1!=1并返回给2-Fact
步骤5:0-Fact确定0!=1 将1返回给1-Fact
3.递归的应用
递归中最强大是允许程序人员能够解决难于采用迭代处理进行设计和实现的问题。下面的示例是汉诺塔(Hanoi)问题。
3.1Hanoi塔
问题:Hanoi塔包括一堆尺寸不等的圆盘和三根分别为A、B、C的针, 起始设置是将n个圆盘放在A上。Hanoi的任务就是把针A上的圆盘移动C上,一次只能移动一个圆盘,并且要以相同顺序重新构建n个圆盘。在移动中, 尺寸大的圆盘不允许放在尺寸小的圆盘上。
分析:
设三根针分别为initNeedle,tempNeedle,endNeedle.
引用
阶段1, 使用tempNeedle作为临时存储器, 将n-1个圆盘从tempNeedle移动到endNeedle(递归步骤)
hanoi(n-1, tempNeedle, endNeedle, initNeedle);
引用
阶段2,将initNeedle上的一个圆盘移动到endNeedle(停止条件)。
System.out.println("move "+initNeedle+" to "+ endNeedle);
引用
阶段3,在阶段1中,使用initNeedle作为临时存储器,将n-1个圆盘从tempNeedle移动到endNeedle(递归步骤)
hanoi(n-1,tempNeedle,endNeedle,initNeedle)
递归步骤持续将堆叠得越来越少的圆盘从一根针移动到另一根针, 直到只剩下一根圆盘,此时递归步骤到达停止条件,解决的问题的方法就是简单地将这个圆盘移动到正确的针上。
下面是hanoi塔的实现:
public static void hanoi(int n,String initNeedle, String endNeedle,String tempNeedle){ if(n==1){ System.out.println("Move " +initNeedle+" to "+endNeedle); }else { hanoi(n-1, initNeedle, tempNeedle, endNeedle); System.out.println("Move " +initNeedle+" to "+endNeedle); hanoi(n-1, tempNeedle, endNeedle, initNeedle); } }
* hanoi塔算法
* @param n 从大到小
* @param initNeedel 针A
* @param endNeedel 针B
* @param tempNeedle 针C
运行结果
引用
n=3,initNeeelde = A, endneedle=C, tempNeedle=B
Move A to C //阶段一
Move A to B
Move C to B
Move A to C //阶段二
Move B to A //阶段三
Move B to C
Move A to C
4递归的评价
递归的方法的价值取决与问题。对于hannoi塔问题递归就是最好的选择,但是正对fibonacci数列就不一定了
4.1fibonacci数列
fib(n)= 0(n=0)或=1(n=1)或=fib(n-1)+fib(n-2)(n>=2)
使用递归方法实现fibonacci数列
引用
fib()方法是fibonacci()的简写
public static int fibonacci(int n){ if (n<=1) { return n; }else { return fibonacci(n-1)+fibonacci(n-2); } }
我们以fibonacci(5)为例
fib()产生多次迭代,造成巨大的人冗余。fib(5)的值需要两次fib(3)和五次fib(1)。调用次数numCall(n)=2*fib(n+1)-1;
对于5来说numbCall(5)=2*fib(6)-1=2*8-1=15
对于35来说,numCall(35)=2*fib(36)-1=2*149303352-1=29,860,703
引用
fib(n)产生巨大的冗余调用
4.2使用迭代的方式实现fibonacci数列fibIter()
public static int fibInter(int n){ int oneback=1,twoback=0,current=0; for (int i = 2; i <= n; i++) { current=oneback+twoback; twoback=oneback; oneback=current; } return current; }
迭代算法fibIter()是一个线性算法
4.3对比Fibonacci数列的两种方法
测试用例
public void fibonacci(){ Timing time=new Timing(); time.start(); Recursion.fibonacci(35); System.out.println(time.stop()); time.start(); Recursion.fibInter(35); time.stop(); System.out.println(time.stop()); }
引用
输出
0.141
0.0
0.141
0.0
5递归的使用方法
outline:
package ds.util; /** * 通过将一个问题分割为使用相同的算法来解决若干子问题, 某个算法可以解决这个问题, 此时就会应用术语“递归(recursive)”。对于阶乘算法来说, * 分割过程首先计算(n-1)!来解决确定的n!的值, 然后以同样方式继续进行计算。 在递归的算法执行期间, 分割过程不能无限地继续下去。 * 分割过程必须终止于一个或多个能够直接解决的简单。我们将这些简单问题算法称为停止条件(Stopping condition), * 其原因在于它们不需要进一步分割就能被解决。 * * 递归算法设计要素: (1)一个或多个能够确定实参直接求值的停止条件(stopping condition)。 (2)一个或多个递归步骤(recursive * step),在递归步骤中,要计算递归方法的当前值, 需要重复调用具有实参的方法, 这些实参最终会达成某个停止条件 * * 递归算法的实现: 设计递归算法时,必须小心区分停止条件和递归步骤。 编程人员使用if-else语句来实现这个区别。其中if部分处理停止条件, * 而else部分处理递归步骤。 * * @author Administrator * */ public class Recursion { /** * 求一个整整数的阶乘,使用循环的方式,这也是我们最容易接受的方式 * * @param n * @return */ public static int factorial(int n) { if (n <= 0) throw new IllegalArgumentException( " Recursion: factorial must be postive"); int factValue = 1; while (n >= 1) { factValue *= n; n--; } return factValue; } /** * 使用递归来求n的阶乘 n*(n-1)*(...)*2*1 * * @param n * @return */ public static int factorialWithRecursion(int n) { if (n <= 0) throw new IllegalArgumentException( " Recursion: factorial must be postive"); if (n == 0) return 1; return n * factorialWithRecursion(n - 1); } /** * hanoi塔算法 * * @param n * 从大到小 * @param initNeedel * 针A * @param endNeedel * 针B * @param tempNeedle * 针C */ public static void hanoi(int n, String initNeedle, String endNeedle, String tempNeedle) { if (n == 1) { System.out.println("Move " + initNeedle + " to " + endNeedle); } else { hanoi(n - 1, initNeedle, tempNeedle, endNeedle); System.out.println("Move " + initNeedle + " to " + endNeedle); hanoi(n - 1, tempNeedle, endNeedle, initNeedle); } } /** * fibonacci数列:fib(n)=0(n=0)或1(n=1)或fib(n-1)+fib(n-2)(n>=2) * * @param n * @return */ public static int fibonacci(int n) { if (n <= 1) { return n; } else { return fibonacci(n - 1) + fibonacci(n - 2); } } /** * 使用迭代来实现fabionacci数组的算法 * * @param n * @return */ public static int fibInter(int n) { int oneback = 1, twoback = 0, current = 0; for (int i = 2; i <= n; i++) { current = oneback + twoback; twoback = oneback; oneback = current; } return current; } /** * 使用递归求n,n-1,...,2,1的和 * * @param n * @return */ public static int sumToN(int n) { if (n <= 0) throw new IllegalArgumentException( " Recursion: factorial must be postive"); if (n == 1) return 1; return sumToN(n - 1) + n; } /** * isPal()方法判断字符串str是否为同一个简单的回文.简单的回是一个完全由字符"a"-"z"组成的、并且正读反读都是相同的串 * * @param str * @param startIndex * @param endIndex * @return */ public static boolean isPal(String str, int startIndex, int endIndex) { if (startIndex >= endIndex) return true; if (str.charAt(startIndex) != str.charAt(endIndex)) return false; return isPal(str, ++startIndex, --endIndex); } /** * 递归方法baseString()提供以2到16之间的任何值为基数的非负数值为基数的非负数表示方法, 该方法接受一个十进制数n和一个基数b(其中2<=b<=16),并且返回一个给出对应的表示法串。 * * @param n * @param b * @return */ public static String baseString(int n, int b) { String str = "", digitChar = "0123456789abcef"; if (n == 0)// 停止条件 return ""; else { str = baseString(n / b, b);// 利用n/b作为减小条件 return str + digitChar.charAt(n % b);// 递归步骤 } } /** * 上面的方法实现十进制从2到16之间的任何值为基数进制转换,现在要实现一个十进制数转换一个指定长度length的二进制字符串 * * @param n * @param length * @return */ public static String base2String(int n, int length) { if (n > (Math.log(length) / Math.log(2))) throw new IllegalArgumentException( " n must be litter than log(length)"); if (n == 0) {// 停止条件1 if (length > 0)// 停止条件2 return base2String(0, length - 1) + "0";// 递归步骤2 return ""; } else return base2String(n / 2, length - 1) + (n % 2);// 递归步骤1 } /** * 在Arrays和GenericUtil里实现了的二分查询binSearch(),现在我们要使用递归的思想来实现它们: * 该方法接受一个数组arr(用索引first和last来描述一个索引)和一个target值,并且扫描查找匹配的列表. 这个方法返回匹配的索引, * 或者在没有匹配的情况下返回-1. 使用分治侧率实现一个递归形式的二分查询搜索算法. * * @param <T> * 泛型类型T * @param arr * 反省类型数组 * @param first * 数组arr的头索引 * @param last * 数组arr的尾索引 * @param target * 搜索的目标值 * @return */ public static <T extends Comparable<? super T>> int binSearch(T[] arr, int first, int last, T target) { int mid = (first + last) / 2; if (first < last) { // 运行条件1 if (arr[mid].compareTo(target) > 0) { // 停止条件1 return binSearch(arr, first, mid, target); // 递归步骤1 } else if (arr[mid].compareTo(target) < 0) { // 停止条件2 return binSearch(arr, mid + 1, last, target); // 递归步骤2 } else { return mid; // 返回步骤1 } } else if (first == last) { // 运行条件2 if (arr[mid].compareTo(target) == 0) { // 停止条件1 return mid; // 返回步骤1 } else { return -1; // 返回步骤2 } } else { // 运行条件3 return -1; // 返回步骤2 } } /** * 方法numToNames()方法将一个正数转换一个串,例如n=372,那么numberToNames()方法返回串“three seven * two”。 * * @return */ public static String numToNames(int n) { if (n < 10) { return numToName(n); } else { return numToNames(n / 10) + " " + numToName(n % 10); } } private static String numToName(int n) { String value = ""; switch (n) { case 0: value = "zero"; break; case 1: value = "one"; break; case 2: value = "two"; break; case 3: value = "three"; break; case 4: value = "four"; break; case 5: value = "five"; break; case 6: value = "six"; break; case 7: value = "seven"; break; case 8: value = "eight"; break; case 9: value = "nine"; break; default: break; } return value; } }
测试用例
package test.ds.util; import ds.time.Timing; import ds.util.GenericUtil; import ds.util.Recursion; import junit.framework.TestCase; public class RecursionTestCase extends TestCase { /** * 测试: Recursion的factorial()方法 */ public void factroial() { int n = 3; System.out.println(Recursion.factorial(n)); } /** * 测试: Recursion的factorial()方法 */ public void factroialWithRecursion() { int n = 3; System.out.println(Recursion.factorialWithRecursion(n)); } /** * 测试: Recursion的isPal()方法 */ public void isPal() { String str = "amanaplanacanalpanama"; System.out.println(Recursion.isPal(str, 0, str.length() - 1));//判断串str是否是回字 String str1 = "gohangasalamiimalasagnahog"; System.out.println(Recursion.isPal(str1, 0, str1.length() - 1)); } /** * * 测试: Recursion的baseString()方法 */ public void baseString(){ System.out.println(Recursion.baseString(95, 8));//十进制转n机制 System.out.println(Recursion.baseString(450, 16)); } /** * * 测试: Recursion的base2String()方法 */ public void base2String(){ System.out.println(Recursion.base2String(11,5)); } /** * 测试: 测试Rescursion的binSearch()方法 */ public void binSearch(){ String[] arr=ArraysUtil.getRandomStrings(10,5);//产生大小为10长度为5的字符串 GenericUtil.selectionSort(arr);//保证arr必须是按升序排列 ArraysUtil.printArrays(arr); System.out.println(Recursion.binSearch(arr, 6,9, arr[5]));//二分搜索 } /** * 测试hanoi()塔 */ public void hanoi(){ System.out.println("n=3,initNeeelde = A, endneedle=C, tempNeedle=B"); Recursion.hanoi(3,"A","C","B"); } /** * 测试fibonacci数列 */ public void fibonacci(){ Timing time=new Timing(); time.start(); Recursion.fibonacci(35); System.out.println(time.stop()); time.start(); Recursion.fibInter(35); time.stop(); System.out.println(time.stop()); } public void numToNames(){ System.out.println(Recursion.numToNames(372)); } }
评论
2 楼
liaobinxu
2009-10-18
shaopei3344 写道
递归不是会引起栈溢出吗。
说说递归在现实中的例子
说说递归在现实中的例子
不会溢栈吧, 递归从面向过程语言C,还是在面向对象语言java,都在广泛使用,只有在一些算法的时间复杂度不同而已,
比如递归解决hanoi比较好, 在斐波那契数列就不好,因为存在重复调用的问题,这里有一个关于斐波那契数列递归算法Fibonacci数列的一种经典递归实现
1 楼
shaopei3344
2009-10-14
递归不是会引起栈溢出吗。
说说递归在现实中的例子
说说递归在现实中的例子
发表评论
-
树的学习
2009-11-02 15:18 01.树的定义: (1)有且仅有一个根节点(root) (2)n ... -
排序算法
2009-10-28 23:27 1024排序方法 包括方法是插入排序,归并排序,通用排序,快速排序,第 ... -
排序算法
2009-10-18 22:12 01.插入排序 算法:排序关系假定第一个已经排好序了,需要从1, ... -
泛型和方法
2009-10-09 16:43 1884如何在java类中一些通用方法, 特别是一些静态的工具方法? ... -
java数据结构-Arrays类
2009-10-07 15:06 1154选择排序 选择排序是一种简单直观的排序算法。它的工作原理如下。 ... -
java 数据结构 - 类与对象
2009-10-01 00:48 880动态数组 ArrayList 这种数据结构具有优秀的索引功能 ... -
java 数据结构名词介绍
2009-09-30 23:31 1441数据结构: 围绕定义集 ...
相关推荐
.net 递归算法.net 递归算法.net 递归算法.net 递归算法.net 递归算法.net 递归算法.net 递归算法.net 递归算法
VC对磁盘文件遍历搜索的递归算法和非递归算法 里面的文档是讲解递归算法和递归算法的 里面还有一个Vc工程文件,是我自己写的,关于非递归算法,其实里面那些被注释掉的部分是递归算法,大家仔细看看就知道了,
递归算法详解递归算法详解递归算法详解递归算法详解
快速排序算法设计与分析总结 二叉树与树的转换前序、后序的递归、非递归算法,层次序的非递归算法的实现 二叉树与树的转换前序、后序的递归、非递归算法,层次序的非递归算法的实现,应包含建树的实现 实现树与...
5!递归算法和非递归算法,面试专用,适合新手
acm递归算法总结acm递归算法总结!!!!!!!!!!!!!!!!!!!!!!!
18.递归算法与递归算法应用.ppt
折半查找的递归算法,非常实用,可以实现的C语言程序
方法一:递归算法 /// /// 一列数的规则如下: 1、1、2、3、5、8、13、21、34求第30位数是多少, 用递归算法实现。(C#语言) /// /// <param name=pos></param> /// <returns></returns> public int GetNumberAtPos...
利用递归算法求阶乘(VB6.0源代码)利用递归算法求阶乘。VB6.0源代码
递归算法转为非递归算法。方法、过程,用栈的原理
实现树与二叉树的转换的实现。以及树的前序、后序的递归、非递归算法,层次序的非递归算法的实现,应包含建树的实现。
数据结构DFS深度优先遍历非递归算法实现,是自己编写的,可靠。
用C++实现汉诺塔的递归算法,定义了类和方法。
合并排序递归和非递归算法的实现可以让人理解到递归算法的实现有时候比非递归算法效率高很多,人只需要给出一个递归公式和一个递归出口,所有的事都可以交给计算机来完成了
递归算法计算二叉树中叶子节点的数目
主要介绍了Python基于递归算法实现的走迷宫问题,结合迷宫问题简单分析了Python递归算法的定义与使用技巧,需要的朋友可以参考下
递归算法与循环算法的分析
用非递归解决八皇后的问题,是经典的非递归算法,学习数据结构中很有用
Java递归算法构造JSON树形结构,Java递归算法构造JSON树形结构Java递归算法构造JSON树形结构