所有人都听过这样一个歌谣:从前有座山,山里有座庙,庙里有个和尚在讲故事:从前有座山。。。。,虽然这个歌谣并没有一个递归边界条件跳出循环,但无疑地,这是递归算法最朴素的落地实现,本次我们使用Golang1.18回溯递归与迭代算法的落地场景应用。

递归思想与实现

递归思想并非是鲜为人知的高级概念,只不过是一种相对普遍的逆向思维方式,这一点我们在:人理解迭代,神则体会递归,从电影艺术到Python代码实现神的逆向思维模式中已经探讨过,说白了就是一个函数直接或者间接的调用自己,就是递归,本文开篇和尚讲故事的例子中,和尚不停地把他自己和他所在的庙和山调用在自己的故事中,因此形成了一个往复循环的递归故事,但这个故事有个致命问题,那就是停不下来,只能不停地讲下去,所以一个正常的递归必须得有一个递归边界条件,用来跳出无限递归的循环:



package main  

import (
"fmt"
) func story(n int) int {
if n <= 0 {
return 0
}
return story(n - 1) } func main() { res := story(5) fmt.Println(res) }

这里我们声明了一个故事函数,参数为n,即讲n遍同样的故事,并且调用自己,每讲一次n减1,即减少一次讲故事总数,但如果我们不设置一个递归边界条件,那么函数就会无限递归下去,所以如果n小于等于0了,那么我们就结束这个故事:

➜  mydemo git:(master) ✗ go run "/Users/liuyue/wodfan/work/mydemo/tests.go"
0

所以 if n <= 0 就是递归边界条件。

那么递归的底层是如何实现的呢?假设我们要针对n次故事做一个高斯求和:

package main  

import (
"fmt"
) func story(n int) int {
if n <= 0 {
return 0
}
return n + story(n-1) } func main() { res := story(5) fmt.Println(res) }

程序输出:

➜  mydemo git:(master) ✗ go run "/Users/liuyue/wodfan/work/mydemo/tests.go"
15

那么这一次递归高斯求和函数的底层实现应该是这样:

5+story(4)
5+(4+ story(3))
5+(4+(3+ story(2)))
5+(4+(3+(2+ story(1))))
5+(4+(3+(2+1)))
15

当story函数每次被调用时,都会在内存中创建一个帧,来包含函数的局部变量和参数,对于递归函数,栈上可能同时存在多个函数帧。当每调用一次函数story(n)时,栈顶指针就会往栈顶移动一个位置,直到满足退出递归的条件(n<=0)之后再依次返回当前的结果直接,栈顶指针被压入栈底方向。

也就是说,内存栈会存储每一次递归的局部变量和参数,这也就是递归算法的性能被人们所诟病的原因,即不是自己调用自己而性能差,而是自己调用自己时,系统需要保存每次调用的值而性能差。

尾递归优化

尾递归相对传统的普通递归,其是一种特例。在尾递归中,先执行某部分的计算,然后开始调用递归,所以你可以得到当前的计算结果,而这个结果也将作为参数传入下一次递归。这也就是说函数调用出现在调用者函数的尾部,因为是尾部,所以其有一个优越于传统递归之处在于无需去保存任何局部变量,从内存消耗上,实现节约特性:



package main  

import (
"fmt"
) func tail_story(n int, save int) int { if n <= 0 {
return save
}
return tail_story(n-1, save+n) } func main() { save := 0 res := tail_story(5, save) fmt.Println(res) }

程序返回:

➜  mydemo git:(master) ✗ go run "/Users/liuyue/wodfan/work/mydemo/tests.go"
15

可以看到,求和结果和普通递归是一样的,但过程可不一样:

tail_story(5,0)
tail_story(4,5)
tail_story(3,9)
tail_story(2,12)
tail_story(1,14)
tail_story(0,15)

因为尾递归通过参数将计算结果进行传递,递归过程中系统并不保存所有的计算结果,而是利用参数覆盖旧的结果,如此,就不会到处栈溢出等性能问题了。

递归应用场景

在实际工作中,我们当然不会使用递归讲故事或者只是为了计算高斯求和,大部分时间,递归算法会出现在迭代未知高度的层级结构中,即所谓的“无限极”分类问题:

package main  

import (
"fmt"
) type cate struct {
id int
name string
pid int
} func main() {
allCate := []cate{
cate{1, "计算机课程", 0},
cate{2, "美术课程", 0},
cate{3, "舞蹈课程", 0},
cate{4, "Golang", 1},
cate{5, "国画", 2},
cate{6, "芭蕾舞", 3},
cate{7, "Iris课程", 4},
cate{8, "工笔", 5},
cate{9, "形体", 6},
} fmt.Println(allCate) }

程序输出:

[{1 计算机课程 0} {2 美术课程 0} {3 舞蹈课程 0} {4 Golang 1} {5 国画 2} {6 芭蕾舞 3} {7 Iris课程 4} {8 工笔 5} {9 形体 6}]

可以看到,结构体cate中使用pid来记录父分类,但展示的时候是平级结构,并非层级结构。

这里使用递归算法进行层级结构转换:

type Tree struct {
id int
name string
pid int
son []Tree
}

新增加一个Tree的结构体,新增一个子集的嵌套属性。

随后建立递归层级结构函数:

func CategoryTree(allCate []cate, pid int) []Tree {
var arr []Tree
for _, v := range allCate {
if pid == v.pid {
ctree := Tree{}
ctree.id = v.id
ctree.pid = v.pid
ctree.name = v.name sonCate := CategoryTree(allCate, v.id) ctree.son = sonCate arr = append(arr, ctree)
}
}
return arr
}

随后调用输出:

package main  

import (
"fmt"
) type cate struct {
id int
name string
pid int
} type Tree struct {
id int
name string
pid int
son []Tree
} func CategoryTree(allCate []cate, pid int) []Tree {
var arr []Tree
for _, v := range allCate {
if pid == v.pid {
ctree := Tree{}
ctree.id = v.id
ctree.pid = v.pid
ctree.name = v.name sonCate := CategoryTree(allCate, v.id) ctree.son = sonCate arr = append(arr, ctree)
}
}
return arr
} func main() {
allCate := []cate{
cate{1, "计算机课程", 0},
cate{2, "美术课程", 0},
cate{3, "舞蹈课程", 0},
cate{4, "Golang", 1},
cate{5, "国画", 2},
cate{6, "芭蕾舞", 3},
cate{7, "Iris课程", 4},
cate{8, "工笔", 5},
cate{9, "形体", 6},
} arr := CategoryTree(allCate, 0)
fmt.Println(arr) }

程序返回:

[{1 计算机课程 0 [{4 Golang 1 [{7 Iris课程 4 []}]}]} {2 美术课程 0 [{5 国画 2 [{8 工笔 5 []}]}]} {3 舞蹈课程 0 [{6 芭蕾舞 3 [{9 形体 6 []}]}]}]

这里和Python版本的无限极分类:使用Python3.7+Django2.0.4配合vue.js2.0的组件递归来实现无限级分类(递归层级结构)有异曲同工之处,但很显然,使用结构体的Golang代码可读性更高。

结语

递归并非是刻板印象中的性能差又难懂的算法,正相反,它反而可以让代码更加简洁易懂,在程序中使用递归,可以更通俗、更直观的描述逻辑。

周而复始,往复循环,递归、尾递归算法与无限极层级结构的探究和使用(Golang1.18)的更多相关文章

  1. c#:无限极树形结构

    最近一直在研究树形结构菜单,无意中让我弄了出来.先上代码: 首先需要这个的一个类 public class Tree { public int id { get; set; } public stri ...

  2. SQL 横转竖 、竖专横 (转载) 使用Dapper.Contrib 开发.net core程序,兼容多种数据库 C# 读取PDF多级书签 Json.net日期格式化设置 ASPNET 下载共享文件 ASPNET 文件批量下载 递归,循环,尾递归 利用IDisposable接口构建包含非托管资源对象 《.NET 进阶指南》读书笔记2------定义不可改变类型

    SQL 横转竖 .竖专横 (转载)   普通行列转换 问题:假设有张学生成绩表(tb)如下: 姓名 课程 分数 张三 语文 74 张三 数学 83 张三 物理 93 李四 语文 74 李四 数学 84 ...

  3. 【C/C++】n皇后问题/全排列/递归/回溯/算法笔记4.3

    按常规,先说一下我自己的理解. 递归中的return常用来作为递归终止的条件,但是对于返回数值的情况,要搞明白它是怎么返回的.递归的方式就是自己调用自己,而在有返回值的函数中,上一层的函数还没执行完就 ...

  4. 无限极分类(adjacency list)的三种方式(迭代、递归、引用)

    一般的分类树状结构有两种方式: 一种是adjacency list,也就是是id,parent id这中形式. 另一种是nested set,即左右值的形式. 左右值形式查询起来比较高效,无需递归等, ...

  5. PHP 递归无限极下级

    下面是自己用到的一些递归方法,当然都是借鉴的,各位看官请勿怪 第一种 有层级 $array = array( array('id' => 1, 'pid' => 0, 'n' => ...

  6. PHP实现无限极分类的两种方式,递归和引用

    面试的时候被问到无限极分类的设计和实现,比较常见的做法是在建表的时候,增加一个PID字段用来区别自己所属的分类 $array = array( array('id' => 1, 'pid' =& ...

  7. 逆转序列的递归/尾递归(+destructuring assignment)实现(JavaScript + ES6)

    这里是用 JavaScript 做的逆转序列(数组/字符串)的递归/尾递归实现.另外还尝鲜用了一下 ES6 的destructuring assignment + spread operator 做了 ...

  8. [迷宫中的算法实践]迷宫生成算法——递归分割算法

    Recursive division method        Mazes can be created with recursive division, an algorithm which wo ...

  9. linux循环递归设置权限

    这里给出一个循环递归得到对文件夹和文件分别有效的设置方法: find /path -type f -exec chmod 644 {} \; #对目录和子目录里的文件 find /path -type ...

  10. 递归分治算法之二维数组二分查找(Java版本)

    [java] /** * 递归分治算法学习之二维二分查找 * @author Sking 问题描述: 存在一个二维数组T[m][n],每一行元素从左到右递增, 每一列元素从上到下递增,现在需要查找元素 ...

随机推荐

  1. Activate MFA报错:MFADevice entity at the same path and name already exists

    MFA即:Multi-factor authentication (MFA) 今天在为自己账号Activate MFA时报错,如下图所示: Entity already exists This ent ...

  2. 关于vmware虚拟机的ova/ovf转换成aws上的AMI镜像

    很多时候,我们会有这样的需求,需要将DC中vmware虚拟化的服务器,迁移到aws上,我们就得先将vmware虚拟机导出,然后转换 关于vmvare虚拟的导出备份,一般有ova(Open Virtua ...

  3. 云原生时代的DevOps平台设计之道

    开发人员与运维人员是 IT 领域很重要的两大人群,他们都会参与到各种业务系统的建设过程中去.DevOps 是近年间火爆起来的一种新理念,这种理念被很多人错误的解读为"由开发人员(Dev)学习 ...

  4. emqx启用JWT令牌认证(包含hmac-based和public-key)

    emqx连接启用jwt令牌认证 jwt令牌 概述 JWT 即 JSON Web Tokens 是一种开放的,用于在两方之间安全地表示声明的行业标准的方法(RFC 7519). 组成 令牌的形式 xxx ...

  5. 1.MongoDB之服务启动

    1. 编写docker-compose.yaml文件 version: "3" services: mongo: image: mongo:4.2.6 ports: - 27017 ...

  6. IDEA快速生成数据库表的实体类

    IDEA连接数据库 IDEA右边侧栏有个DataSource,可以通过这个来连接数据库,我们先成功连接数据库 点击进入后填写数据库进行连接,注意记得一定要去Test Connection 确保正常连接 ...

  7. paddle&蜜度 文本智能较对大赛经验分享(17/685)

    引言 我之前参加了一个中文文本智能校对大赛,拿了17名,虽然没什么奖金但好歹也是自己solo拿的第一个比较好的名次吧,期间也学到了一些BERT应用的新视角和新的预训练方法,感觉还挺有趣的,所以在这里记 ...

  8. Teambition企业内部应用开发指南

    Teambition企业内部应用Python开发指南 注意:此文章并非搬运,一小部分仅为借鉴. Teambition提供了API接口,我们可以注册成为开发者,然后通过接口获取Teambition的数据 ...

  9. Codeforces Global Round 23 A-D

    比赛链接 A 题解 知识点:贪心,构造. 注意到有 \(1\) 就一定能构造. 时间复杂度 \(O(n)\) 空间复杂度 \(O(1)\) 代码 #include <bits/stdc++.h& ...

  10. 部署redis-cluster

    1.环境准备 ☆ 每个Redis 节点采用相同的相同的Redis版本.相同的密码.硬件配置 ☆ 所有Redis服务器必须没有任何数据 #所有主从节点执行: [root@ubuntu2004 ~]#ba ...