数据结构和几个的基本API
package Bag
type Bag struct {
first *node
n int
}
type node struct {
item interface{}
next *node
}
func NewBag() Bag {
return Bag{}
}
func (b Bag) IsEmpty() bool {
return b.n == 0
}
func (b Bag) Size() int {
return b.n
}
func (b *Bag) Add(item interface{}) {
oldfirst := b.first
b.first = &node{}
b.first.item = item
b.first.next = oldfirst
b.n++
}
简单的背包迭代接口
由于go语言没有提供原生的迭代接口支持,所以想使用迭代就需要自己实现,恰巧迭代是背包这个数据结构的重要方法。以下是一个简单的迭代实现:
//导出背包中的全部元素为切片
func (b Bag) Iterator() []interface{} {
s := []interface{}{}
p := b.first
for i := 0; i < b.n; i++ {
s = append(s, p.item)
p = p.next
}
return s
}
这种简单的迭代实现原理是遍历链表,将其中所有的元素都导出为一个切片,然后返回,这样就可以通过go的for range迭代元素。
该方案会在迭代开始时将背包中的所有数据在内存中复制一份,这样做实现起来简单、迭代的速度很快,但是生成切片需要时间并且会占用双倍的内存,不适合数据量较大的情况。
改进后的迭代器
这一版迭代器的设计思路:在背包类中存储一个变量index,用来记录当前迭代到达的位置。
type Bag struct {
first *node
n int
index *node
}
//省略中间的方法
//初始化迭代器
func (b *Bag) InitIterator() {
b.index = b.first
}
//检查下一个元素
func (b Bag) HasNext() bool {
return b.index != nil
}
//获取下一个元素
func (b *Bag) Next() interface{} {
item := b.index.item
b.index = b.index.next
return item
}
下面是测试迭代器的代码:
func main() {
b := Bag.NewBag()
b.Add(1)
b.Add(2)
b.Add(3)
for b.InitIterator(); b.HasNext(); {
fmt.Println(b.Next())
}
}
控制台输出:
[Running] go run "/Users/bbfat/Desktop/Learn/数据结构/背包/main.go"
3
2
1
[Done] exited with code=0 in 0.231 seconds
这种迭代方式在时间效率和空间效率上表现良好,但是每次想在某个类中实现迭代功能都需要定义这三个接口,抽象成都不高。
运用协程和接口实现抽象迭代器类
go语言的一大特色就是并发性,这得益于它的协程机制,我现在就来使用协程和通道构建一个迭代器类。
package Iterator
// 协程迭代接口
// 可迭代对象需实现StartIterate方法,要求如下:
// 该方法在迭代完成前通过通道持续返回数据
// 返回nil代表迭代完成
type IteratorObj interface {
StartIterate(ch chan interface{})
}
type Iterator struct {
it IteratorObj
ch chan interface{}
}
//创建一个Iterator对象
func InitIterator(it IteratorObj) Iterator {
ch := make(chan interface{})
go it.StartIterate(ch)
return Iterator{it, ch}
}
//迭代时接收协程发来的数据并返回
func (it Iterator) Next() interface{} {
return <-it.ch
}
StartIterate 接口方法
可迭代类需要实现这个接口才能用Iterator进行迭代,这个接口方法将作为一个协程启动,可迭代类运用自身的逻辑进行迭代,将每次迭代的结果传入无缓冲通道ch。
InitIterator 函数
此函数接收一个IteratorObj接口类型作为参数,创建一个接口类型通道,然后启动协程,返回一个Iterator对象。
Next 方法
此方法作用在Iterator对象上,该方法负责接收ch通道传回的数据并返回。
背包实现IteratorObj接口
在Bag类下面增加一个方法
//实现迭代类接口
func (b Bag) StartIterate(ch chan interface{}) {
index := b.first
for {
if index == nil {
ch <- nil
break
} else {
ch <- index.item
index = index.next
}
}
}
就是普通的链表遍历,在其中增加了通道传输数据。
通过背包测试协程迭代
func main() {
b := Bag.NewBag()
for i := 0; i < 100; i++ {
b.Add(i)
}
//以下为迭代的具体使用方法
it := Iterator.InitIterator(b)
for v := it.Next(); v != nil; v = it.Next() {
fmt.Println(v)
}
}
测试输出:
[Running] go run "/Users/bbfat/Desktop/Learn/数据结构/背包/main.go"
99
98
//中间省略
1
0
[Done] exited with code=0 in 0.26 seconds
测试成功!
测试比较不同迭代方式的性能差异
1.生成切片法
func main() {
b := Bag.NewBag()
for i := 0; i < 10000000; i++ {
b.Add(i)
}
for _, i := range b.Iterator() {
fmt.Print(i)
}
}
测试编号 | 用时(s) |
---|---|
1 | 3.091 |
2 | 2.606 |
3 | 2.486 |
4 | 2.478 |
5 | 2.538 |
6 | 3.031 |
平均 | 2.075 |
2.直接迭代法
func main() {
b := Bag.NewBag()
for i := 0; i < 10000000; i++ {
b.Add(i)
}
for b.InitIterator(); b.HasNext(); {
fmt.Print(b.Next())
}
}
测试编号 | 用时(s) |
---|---|
1 | 1.183 |
2 | 1.131 |
3 | 1.426 |
4 | 1.584 |
5 | 1.139 |
6 | 1.085 |
平均 | 1.258 |
3.协程迭代法
func main() {
b := Bag.NewBag()
for i := 0; i < 10000000; i++ {
b.Add(i)
}
it := Iterator.InitIterator(b)
for v := it.Next(); v != nil; v = it.Next() {
fmt.Print(v)
}
}
测试编号 | 用时(s) |
---|---|
1 | 1.154 |
2 | 1.555 |
3 | 1.491 |
4 | 1.541 |
5 | 1.526 |
6 | 1.544 |
平均 | 1.468 |
综述
生成切片的迭代方法不是一个好的选择,它不光占用内存大,而且消耗时间也长,直接迭代法效率最高,但是实现起来比较麻烦,协程迭代法效率和直接迭代仅差一点点,但是实现起来非常舒适,所以我的选择是使用协程迭代法,这也是go这门语言的魅力所在。
协程迭代法的改进
当前的协程迭代方法有一个缺陷——无法优雅地退出协程,如果迭代进行到一半无法强制结束,负责迭代的协程会一直在堆栈中保持阻塞状态,这非常不爽,接下来我就尝试改进这个问题。
给IteratorObj升级
type IteratorObj interface {
StartIterate(ch chan interface{}, stop chan bool)
}
增加了一个可以控制协程终止的通道,所以背包类的接口实现也需要进行调整:
//实现迭代类接口
func (b Bag) StartIterate(ch chan interface{}, stop chan bool) {
index := b.first
for {
select {
case <-stop:
return
default:
if index == nil {
ch <- nil
return
} else {
ch <- index.item
index = index.next
}
}
}
}
select语句
这是go语言为了更好地支持协程的特有的语句,它类似常见的switch,但是每个case后必须是通信相关的表达式,go的switch于其他语言一个不同点就是自带break,select也是一样。
当收到stop通道传来的消息的时候函数返回,协程优雅退出,其他情况下继续执行之前的迭代逻辑。
Finish函数
此函数用于向迭代协程发出退出信号。
//停止迭代协程
func (it Iterator) Finish() {
mutex := sync.Mutex{}
mutex.Lock()
<-it.ch
it.stop <- true
mutex.Unlock()
}
这其中使用了互斥锁,它可以保证在Lock()和Unlock()之间的代码不会被其他协程中断。
这样我们先上锁,然后拿出ch通道阻塞的数据,再将信号发送等到stop通道,最后解锁,让迭代协程继续执行,此时迭代协程就会监测到stop通道传来的信号,这样它就会执行return退出。
测试
func main() {
b := Bag.NewBag()
for i := 0; i < 10; i++ {
b.Add(i)
}
it := Iterator.InitIterator(b)
for v := it.Next(); v != nil; v = it.Next() {
fmt.Print(v)
if v.(int) == 5 {
it.Finish()
return
}
}
}
控制台输出:
API server listening at: 127.0.0.1:6347
98765
测试成功!