相关概念
图
顶点
边
度
方向
有向图
入度
出度
无向图
权重
带权图
无权图
带权有向图
带权无向图
存储
邻接矩阵
简单,直观,但比较浪费空间。使用比较方便,空间换时间。
邻接表
存储比较节省空间,但是使用比较耗时。因为邻接矩阵是数组存储,邻接表是链表存储,不连续,不支持随机读取。想知道两个顶点间是否存在一条边,得遍历这两个点所有点,对比。
优化:可以将链表换成其他相对高效的动态数据结构如:二叉查找树,跳表,散列表等。
遍历
广度搜索
时间复杂度
广度搜索过程中,queue中所有的顶点要入一次队列,所有顶点要访问一遍记为V,visited保存每条边也要访问一次记为E,所以时间复杂度为:O(V+E)。
广度搜索应用
最短路径
二度好友/可能认识的人
深度优先搜索
走迷宫
时间复杂度
从下面代码实现可以看出,每条边至多访问两次,一次是遍历,一次是回退,所以时间负载度为O(E),E为边数。
空间复杂度
实现中的变量主要有visited,prev与程序运行时候的递归调用栈,这些都跟图的顶点个数V成正比,所以空间复杂度为O(V)。
代码实现
package main
import (
"fmt"
"sync"
)
type Nodee struct {
Value interface{}
}
type Graphh struct {
Nodees []*Nodee
Edges map[Nodee][]*Nodee
Lock sync.RWMutex
}
func (this *Nodee) String() string {
return fmt.Sprintf("%v", this.Value)
}
func NewNode(v interface{}) *Nodee {
return &Nodee{Value: v}
}
func NewGraph() *Graphh {
return &Graphh{
Nodees: []*Nodee{},
Edges: map[Nodee][]*Nodee{},
Lock: sync.RWMutex{},
}
}
func (this *Graphh) AddNodee(v *Nodee) {
this.Lock.Lock()
defer this.Lock.Unlock()
if this.Edges == nil {
this = NewGraph()
}
this.Nodees = append(this.Nodees, v)
}
func (this *Graphh) AddEgde(s, t *Nodee) {
this.Lock.Lock()
defer this.Lock.Unlock()
if this.Edges == nil {
this = NewGraph()
}
this.Edges[*s] = append(this.Edges[*s], t)
this.Edges[*t] = append(this.Edges[*t], s)
}
func (this *Graphh) String() string {
this.Lock.Lock()
defer this.Lock.Unlock()
s := ""
if this.Edges == nil || len(this.Nodees) == 0 {
return s
}
for _, nodeV := range this.Nodees {
s += nodeV.String() + " --> "
for _, item := range this.Edges[*nodeV] {
s += item.String() + " "
}
s += "\n"
}
return s
}
func (this *Graphh) BFS(s, t *Nodee) (b bool, prev map[Nodee]*Nodee) {
this.Lock.Lock()
defer this.Lock.Unlock()
if s == t {
return b, prev
}
// 初始化辅助变量
queue := []Nodee{}
visited := make(map[Nodee]bool, len(this.Nodees))
for _, node := range this.Nodees {
visited[*node] = false
}
prev = make(map[Nodee]*Nodee)
queue = append(queue, *s)
visited[*s] = true
for len(queue) != 0 {
q := queue[0]
queue = queue[1:]
for _, node := range this.Edges[q] {
if visited[*node] {
continue
}
prev[*node] = &q
if node == t {
PrintPrev(prev, s, t)
b = true
return b, prev
}
queue = append(queue, *node)
visited[*node] = true
}
}
return b, prev
}
func (this *Graphh) DFS(s, t *Nodee) {
// 初始化辅助变量
found := false
visited := make(map[Nodee]bool, len(this.Nodees))
for _, node := range this.Nodees {
visited[*node] = false
}
visited[*s] = true
prev := make(map[Nodee]*Nodee)
this.dfsCore(s, t, visited, prev, found)
PrintPrev(prev, s, t)
}
func (this *Graphh) dfsCore(w, t *Nodee, visited map[Nodee]bool, prev map[Nodee]*Nodee, found bool) {
if w == t {
//b, prevRes = false, nil
return
}
if visited == nil {
visited = make(map[Nodee]bool, len(this.Nodees))
}
for _, nodePoint := range this.Edges[*w] {
if visited[*nodePoint] {
continue
}
prev[*nodePoint] = w
visited[*nodePoint] = true
if t == nodePoint {
found = true
//b, prevRes = true, prev
return
}
this.dfsCore(nodePoint, t, visited, prev, found)
}
return
}
func PrintPrev(prev map[Nodee]*Nodee, s, t *Nodee) {
if prev[*t] != nil && s != t {
PrintPrev(prev, s, prev[*t])
}
fmt.Printf("%+v ", t)
}
func main() {
node1 := NewNode(1)
node2 := NewNode(2)
node3 := NewNode(3)
node4 := NewNode(4)
node5 := NewNode(5)
g := NewGraph()
g.AddNodee(node1)
g.AddNodee(node2)
g.AddNodee(node3)
g.AddNodee(node4)
g.AddNodee(node5)
g.AddEgde(node1, node2)
g.AddEgde(node1, node5)
g.AddEgde(node2, node3)
g.AddEgde(node2, node4)
g.AddEgde(node2, node5)
g.AddEgde(node3, node4)
g.AddEgde(node4, node5)
s := g.String()
fmt.Println(s)
fmt.Println("dfs res:")
g.DFS(node1, node4)
}
总结
广度优先搜索和深度优先搜索是图上的两种最常用、最基本的搜索算法,比起其他高级的搜索算法,比如 A*、IDA* 等,要简单粗暴,没有什么优化,所以,也被叫作暴力搜索算法。所以,这两种搜索算法仅适用于状态空间不大,也就是说图不大的搜索。
广度优先搜索,通俗的理解就是,地毯式层层推进,从起始顶点开始,依次往外遍历。广度优先搜索需要借助队列来实现,遍历得到的路径就是,起始顶点到终止顶点的最短路径。
深度优先搜索用的是回溯思想,非常适合用递归实现。换种说法,深度优先搜索是借助栈来实现的。在执行效率方面,深度优先和广度优先搜索的时间复杂度都是 O(E),空间复杂度是 O(V)。