网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
深度优先搜索在实践中有许多应用
- 查找图的最长路径
- 解决八皇后之类的迷宫问题
而在实现形式上
- 递归是一种非常经典的分层思想,但是如果函数调用时不断压栈,可能导致栈内存超出限制,这对于 Go 语言来说会有栈扩容的成本,并且在实践中也不太好调试。
- 非递归: 借助堆栈先入后出的特性来实现它,不过需要开辟额外的空间来模拟堆栈。
《The Go Programming Language》这本书里有一个很恰当的案例,我们以它为基础进一步说明一下。
假设我们都是计算机系的大学生,需要选修一些课程。但是要选修有的课程必须要先学习它的前序课程。
例如,学习网络首先要学习操作系统的知识,而要学习操作系统的知识必须首先学习数据结构的知识。如果我们现在只知道每门课程的前序课程,不清楚完整的学习路径,我们要怎么设计这一系列课程学习的顺序,确保我们在学习任意一门课程的时候,都已经学完了它的前序课程呢?
这个案例非常适合使用深度优先搜索算法来处理。下面是这个案例的实现代码:
// 计算机课程和其前序课程的映射关系
var prereqs = map[string][]string{
"algorithms": {"data structures"},
"calculus": {"linear algebra"},
"compilers": {
"data structures",
"formal languages",
"computer organization",
},
"data structures": {"discrete math"},
"databases": {"data structures"},
"discrete math": {"intro to programming"},
"formal languages": {"discrete math"},
"networks": {"operating systems"},
"operating systems": {"data structures", "computer organization"},
"programming languages": {"data structures", "computer organization"},
}
func main() {
for i, course := range topoSort(prereqs) {
fmt.Printf("%d:\t%s\n", i+1, course)
}
}
func topoSort(m map[string][]string) []string {
var order []string
seen := make(map[string]bool)
var visitAll func(items []string)
visitAll = func(items []string) {
for \_, item := range items {
if !seen[item] {
seen[item] = true
visitAll(m[item])
order = append(order, item)
}
}
}
var keys []string
for key := range m {
keys = append(keys, key)
}
sort.Strings(keys)
visitAll(keys)
return order
}
广度优先搜索算法
广度优先搜索指的是**从根节点开始,逐层遍历树的节点,直到所有节点均被访问为止。**我们还是以之前的拓扑结构为例,广度优先搜索会首先查找 A、接着查找 B、C,最后查找 D、E、F、G。Dijkstra 最短路径算法和 Prim 最小生成树算法都采用了和广度优先搜索类似的思想。
实现广度优先搜索最简单的方式是使用队列。这是由于队列具有先入先出的属性。以上面的拓扑结构为例,我们可以构造一个队列,然后先将节点 A 放入到队列中。接着取出 A 来处理,并将与 A 相关联的 B、C 放入队列末尾。接着取出 B,将 D、E 放入队列末尾,接着取出 C,将 F、G 放入队列末尾。以此类推。
广度优先搜索在实践中的应用也很广泛。
- 要计算两个节点之间的最短路径
- 即时策略游戏中的找寻路径问题都可以使用它
- Go 语言垃圾回收在并发标记阶段也是用广度优先搜索查找当前存活的内存的
下面是一段利用广度优先搜索爬取网站的例子。其中,urls 是一串 URL 列表,exactUrl 抓取每一个网站中要继续爬取的 URL,并放入到队列 urls 的末尾,用于后续的爬取。
func breadthFirst(urls []string) {
for len(urls) > 0 {
items := urls
urls = nil
for \_, item := range items {
urls = append(urls, exactUrl(item)...)
}
}
}
用广度优先搜索实战爬虫
根据爬取目标的不同,可以灵活地选择广度优先和深度优先算法。但一般广度优先搜索算法会更加简单直观一些。下面我用广度优先搜索来实战爬虫,这一次我们爬取的是豆瓣小组中的数据。
首先,让我们在 collect 中新建一个 request.go 文件,对 request 做一个简单的封装。Request 中包含了一个 URL,表示要访问的网站。这里的 ParseFunc 函数会解析从网站获取到的网站信息,并返回 Requesrts 数组用于进一步获取数据。而 Items 表示获取到的数据。
type Request struct {
Url string
ParseFunc func([]byte) ParseResult
}
type ParseResult struct {
Requesrts []\*Request
Items []interface{}
}
豆瓣小组是一个个的兴趣小组,小组内的组员可以发帖和评论。我们以 深圳租房 这个兴趣小组为例,这个网站里有许多的租房帖子。
不过我们没法一次性将所有的帖子查找出来,因为每一页只会为我们展示 25 个帖子,要看后面的内容需要点击下方具体的页数,进入到第 2 页、第 3 页。不过这难不倒我们,稍作分析就能发现,“第 1 页”的网站是:https://www.douban.com/group/szsh/discussion?start=0,“第 2 页”的网址是:https://www.douban.com/group/szsh/discussion?start=25,豆瓣是通过 HTTP GET 参数中 start 的变化来标识不同的页面的。所以我们可以用循环的方式把初始网站添加到队列中。如下所示,我们准备抓取前 100 个帖子:
func main(){
var worklist []\*collect.Request
for i := 25; i <= 100; i += 25 {
str := fmt.Sprintf("<https://www.douban.com/group/szsh/discussion?start=%d>", i)
worklist = append(worklist, &collect.Request{
Url: str,
ParseFunc: ParseCityList,
})
}
}
下一步,我们要解析一下抓取到的网页文本。
新建一个文件夹“parse”来专门存储对应网站的规则。
对于首页样式的页面,我们需要获取所有帖子的 URL,这里使用正则表达式的方式来实现。匹配到符合帖子格式的 URL 后,我们把它组装到一个新的 Request 中,用作下一步的爬取。
const cityListRe = `(<https://www.douban.com/group/topic/[0-9a-z]+/>)"[^>]\*>([^<]+)</a>`
func ParseURL(contents []byte) collect.ParseResult {
re := regexp.MustCompile(cityListRe)
matches := re.FindAllSubmatch(contents, -1)
result := collect.ParseResult{}
for \_, m := range matches {
u := string(m[1])
result.Requesrts = append(
result.Requesrts, &collect.Request{
Url: u,
ParseFunc: func(c []byte) collect.ParseResult {
return GetContent(c, u)
},
})
}
return result
}
新的 Request 需要有不同的解析规则,这里我们想要获取的是正文中带有“阳台”字样的帖子(注意不要匹配到侧边栏的文字)。
查看 HTML 文本的规则会发现,正本包含在xxxx当中,所以我们可以用正则表达式这样书写规则函数,意思是当发现正文中有对应的文字,就将当前帖子的 URL 写入到 Items 当中。
const ContentRe = `<div class="topic-content">[\s\S]\*?阳台[\s\S]\*?<div`
func GetContent(contents []byte, url string) collect.ParseResult {
re := regexp.MustCompile(ContentRe)
ok := re.Match(contents)
if !ok {
return collect.ParseResult{
Items: []interface{}{},
}
}
result := collect.ParseResult{
Items: []interface{}{url},
}
![img](https://img-blog.csdnimg.cn/img_convert/ff618d1f5b7f0e85e1212c3b56ed3e94.png)
![img](https://img-blog.csdnimg.cn/img_convert/4751c788fa05ee473cf38b14584f1be2.png)
![img](https://img-blog.csdnimg.cn/img_convert/72d355a1fea643a73d0936f2760f56a2.png)
**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!**
**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**
**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618658159)**
...(img-BAgMh2Zr-1715739950488)]
**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!**
**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**
**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618658159)**