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

所有人都听过这样一个歌谣:从前有座山,山里有座庙,庙里有个和尚在讲故事:从前有座山。。。。,虽然这个歌谣并没有一个递归边界条件跳出循环,但无疑地,这是递归算法最朴素的落地实现,本次我们使用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代码可读性更高。

结语

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

0 条评论
请不要发布违法违规有害信息,如发现请及时举报或反馈
还没有人评论呢,速度抢占沙发!
相关文章
  • 采用一致性hash算法将key分散到不同的节点,客户端可以连接到集群中任意一个节点 https://github.com/csgopher/go-redis 本文涉及以下文件: consistenth...

  • 官方资料 官方解释: https://pkg.go.dev/cmd/go#hdr-Build_constraints ,go help buildconstraint 也能看到描述 根据官方描述,go...

  • 学习Go快两年了,一些资料进行整理。 Go语言基础书籍 Go语言圣经——《Go程序设计语言》机械工业出版社作 【推荐】 在线版:Go 语言设计与实现 | Go 语言设计与实现 (dravenes...

  • 1 实验问题描述 设计程序模拟先进先出FIFO,最佳置换OPT和最近最久未使用LRU页面置换算法的工作过程。假设内存中分配给每个进程的最小物理块数为m,在进程运行过程中要访问的页面个数为n,页面访问序...

  • 概述问题:Go 中 Map 的 key 可以是哪些数据类型呢? 我们来看看具体的规则。比较运算符 用来比较两个操作数并返回一个 bool 值,常见的比较运算符:== 等于 != 不等于 ...

  • 本文参与了思否技术征文,欢迎正在阅读的你也加入。前言这是Go常见错误系列的第15篇:interface使用的常见错误和最佳实践。素材来源于Go布道者,现Docker公司资深工程师Teiva Harsa...

  • 1. 简介 本文将介绍 Go 语言中的 sync.Cond 并发原语,包括 sync.Cond的基本使用方法、实现原理、使用注意事项以及常见的使用使用场景。能够更好地理解和应用 Cond 来实现 go...

  • Hello,Golang 一、开发环境搭建 1. 下载 SDK // Go官网下载地址 https://golang.org/dl/ // Go官方镜像站(推荐) https://go...

  • 服务端 package main import ( "errors" "fmt" "log" "net" "net/rpc" "net/rpc/jsonrpc" "os" ) // ...

  • dongle 是一个轻量级、语义化、对开发者友好的 Golang 编码解码和加密解密库Dongle 已被 awesome-go 收录, 如果您觉得不错,请给个 star 吧github.com/gol...

  • 概述Map 的遍历是无序的,这意味着不能依赖遍历的键值顺序。如果想实现 Map 遍历时顺序永远一致,一个折中的方案时预先给 Map 的 键 排序,然后根据排序后的键序列遍历 Map, 这样可以保证每次...

  • 往期回顾: Go语言开发小技巧&易错点100例(一) 本期看点(技巧类用【技】表示,易错点用【易】表示): (1)Go Module中对依赖库版本的升级与降级【技】 (2...

  • 概述Go 是强类型语言,因此不会进行隐式类型转换 (例如不能直接将一个 浮点型 转换为 整型)。任何不同类型之间的转换都必须显式说明。在类型转换时,要注意两边的值类型大小,可以将一个较小的值类型转换为...

  • 一、方法 1、方法是作用在指定的数据类型上,和指定的数据类型绑定,因此自定义类型都可以有方法,而不仅仅是struct; 2、方法的申明和格式调用: package main import ( ...

  • 对于无类型常量,可能大家是第一次听说,但这篇我就不放进拾遗系列里了。 因为虽然名字很陌生,但我们每天都在用,每天都有无数潜在的坑被埋下。包括我本人也犯过同样的错误,当时代码已经合并并发布了,当我意识到...

  • hello 大家好呀,我是小楼,这是系列文《Go底层原理剖析》的第三篇,依旧分析 Http 模块。我们今天来看 Go内置的 RPC。说起 RPC 大家想到的一般是框架,Go 作为编程语言竟然还内置了 ...

  • dongle 是一个轻量级、语义化、对开发者友好的 Golang 编码解码和加密解密库Dongle 已被 awesome-go 收录, 如果您觉得不错,请给个 star 吧github.com/gol...

  • 一 jaeger链路追踪介绍 什么是链路追踪: 分布式链路追踪就是将一次分布式请求还原成调用链路,将一次分布式请求的调用情况集中展示,比如各个服务节点上的耗时、请求具体到达哪台机器上、每个服务节点的...

  • 概述调用 log 包即可,包里面的方法输出日志时会自动加上日期时间前缀字符。例子输出到终端package main import ( "log" "os" ) func main(...

  • 概述建议先阅读 字符串, 切片 两个小节。由于字符串不可变,如果每次以 重新赋值 的方式改变字符串,效率会非常低,这时应该使用 []byte 类型,[]byte 元素可以被修改。因为 byte 类型是...