Go语言实战_自己定义OrderedMap
一、 自己定义OrderedMap
在Go语言中。字典类型的元素值的迭代顺序是不确定的。想要实现有固定顺序的Map就须要让自己定义的 OrderedMap 实现 sort.Interface 接口类型。该接口类型中的方法 Len 、Less 和 Swap 的含义各自是获取元素的数量、比較相邻元素的大小以及交换它们的位置。
1.定义 OrderedMap
想要实现自己定义一个有序字典类型,仅仅基于字典类型是不够的,须要使用一个元素有序的数据类型值作为辅助。
例如以下声明了一个名为 OrderedMap 的结构体类型:
type OrderedMap struct {
keys []interface{}
m map[interface{}]interface{}
}
如上在 OrderedMap 类型中,除了一个字典类型的字段 m,另一个切片类型的字段。
2.实现 sort.Interface
这里须要加入为 OrderedMap 类型例如以下方法:
// 获取键值对的数量
func (omap *OrderedMap) Len() int {
return len(omap.keys)
}
func (omap *OrderedMap) Less(i, j, int) bool {
// 省略若干条语句
}
func (omap *OrderedMap) Swap(i, j int) {
omap.keys[i], omap.keys[j] = omap.keys[j], omap.keys[i];
}
如上 *OrderMap 类型(不是 OrderedMap 类型)就是一个 sort.Interface 接口类型的实现类型了。
能够看出,Len 方法中是以 keys 字段的值的长度作为结果值。而在 Swap 方法中,使用平行赋值语句交换的两个元素值也是操作的 keys 字段的值。
如今考虑 Less 方法的实现,方法 Less 的功能是比較相邻的两个元素值得大小病返回推断结果。读者假设了解Go语言值的可比性与有序性(详细的各种数据类型的比較方法能够參考本人的Go语言学习笔记6中所述),就知道在Go语言中。仅仅有当值的类型具备有序性的时候。它才可能与其它的同类型值比較大小。另外Go语言规定,字典类型的键类型的值必须是可比的(即能够判定两个该类型的值是否相等)。可是。在Go语言中具有可比性的数据类型中仅仅有一部分同一时候具备有序性,因此仅仅是依靠Go语言本身对字典类型的键类型的约束是不够的。在Go语言中。具备有序性的提前定义数据类型仅仅有整数类型、浮点数类型和字符串类型。
类型 OrderedMap 中的字段 keys 是 []interface{} 类型的。因此想要有序性。总是须要比較两个 []interface{} 类型值的大小。
因为接口类型的值仅仅具有可比性而不具备有序性,所以上面声明的OrderedMap 类型在这里显得不适用了。
假设将 keys 字段的元素类型改为某一个详细的数据类型(整数类型、浮点数类型和字符串类型)。又会使得 OrderedMap 类型的使用非常局限。
因此这里须要对 keys 字段进行详细的分析。例如以下需求列表:
- 字段keys的值中的元素值应该都是有序的。应该能够方便地比較它们之间的大小。
- 字段keys的值的元素类型不应该是一个详细的类型,应该能够在执行时再确定它的元素类型。
- 字段keys的值应该能够方便地进行加入元素值、删除元素值以及获取元素值等操作。
- 字段keys的值中的元素值应该能够被按照固定的顺序获取。
- 字段keys的值中的元素值应该能够被自己主动地排序。
- 字段keys的值总是已排序的,应该能够确定某一个元素值的详细位置。
- 字段keys既然能够在执行时决定它的值的元素类型,那么就能够在执行时获知这个元素类型。
- 字段keys的值中的不同元素值比較大小的详细方法。应该能够在执行时获知到。
3.定义Keys接口类型
为了满足上面的需求列表,能够定义例如以下的名为Keys的接口类型:
type Keys interface {
sort.Interface
Add(k interface{}) bool
Remove(k interface{}) bool
Clear()
Get(index int) interface{}
GetAll() []interface{}
Search(k interface{})(index int, contains bool)
ElemType() reflect.Type
CompareFunc() func(interface{}, interface{}) int8
}
在 Keys 接口类型中嵌入了 sort.Interface 接口类型,也就是说 Keys 类型的值一定是可排序的。Add,Remove,Clear 和 Get 这4个方法使得能够对 Keys 的值进行加入,删除,清除和获取元素值的操作。GetAll 方法能够获取一个与 Keys 类型值有着同样元素值集合和元素迭代顺序的切片值。Search 方法确定某一个元素值的详细位置。CompareFunc 返回一个比較大小的详细方法,ElemType 方法返回一个 reflect.Type 类型的结果值。
注意:实际上,reflect 包中的程序实体提供了Go语言执行时的反射机制。通过这些程序实体。能够编写出一些代码来动态的操纵随意类型的对象(如 TypeOf 函数用于获取一个 interface{类型的值的动态类型信息})。
4. Keys接口类型的实现类型
Keys 接口类型的定义并没有体现需求列表中的第1 , 2 , 5项所描写叙述的功能。既然 Keys 接口类型的值必须是 sort.Interface 接口的一个实体。通过 sort 代码包中的程序实体实现元素自己主动排序的功能应该不难。
为了能够动态地决定元素类型,须要在这个 Keys 的实现类型中声明一个 []interface{} 类型的字段。以作为存储被加入到 Keys 类型值中的元素值得底层数据结构:
Container []interface{}
另外因为Go语言本身并没有对自己定义泛型提供支持。因此须要这个字段的值存储某一个数据类型的元素值。
可是接口类型的值不具备有序性(即不能比較大小)。尽管这样,我们能够让详细使用者去实现一个比較大小的方法,还需加入例如以下字段:
compareFunc func(interface{}, interface{}) int8
这是一个函数类型的字段,这个函数返回一个 int8 类型的结果值,对结果值做出例如以下规定:
当第一个參数值小于第二个參数值时,结果值应该小于0.
当第一个參数值大于第二个參数值时。结果值应该大于0
当第一个參数值等于第二个參数值时。结果值应该等于0
如今,通过将比較两个元素值大小的问题抛给使用者,既攻克了须要动态确定元素类型的问题,又明白了比較两个元素值大小的解决方法。只是,因为 container 字段是 []interface{} 类型的。经常不能够方便地在执行时获取到它的实际元素类型(比方在它的值中还没有不论什么元素值的时候)。
这里须要一个明白 container 字段的实际元素类型的字段,这个字段的值所代表的类型也应该是当前的Keys类型值的实际元素类型。
例如以下 Keys 接口类型的实现类型的声明例如以下:
type myKeys struct {
container []interface{}
compareFunc func(interface{}, interface{}) int8
elemType reflect.Type
}
如今使用一个 *myKeys 类型的值来存储 int64 类型的元素值,应该例如以下来初始化它:
int64Keys := &myKeys{
container : make([]interface{}, 0),
compareFunc : func(e1 interface{}, e2 interface{}) int8 {
k1 := e1.(int64)
k2 := e2.(int64)
if k1 < k2 {
return -1
} else if k1 > k2 {
return 1
} else {
return 0
}
},
elemType : reflect.Typeof(int64(1))
}
注意: compareFunc 字段的值中的那两个类型断言表达式的目标类型一定要与 elemType 字段的值所代表的类型保持一致。elemType 字段的值所代表的类型事实上就是调用 reflect.TypeOf 函数时传入的那个參数值的类型。即 int64。
被用于实现 sort.Interface 接口类型的方法的声明例如以下:
func (keys *myKeys) Len() int {
return len(keys.container)
}
//该方法中,比較两个元素值的操作全权交给了compareFunc字段所代表的那个函数
func (keys *myKeys) Less(i, j int)bool {
return keys.compareFunc(keys.container[i], keys.container[j]) == -1
}
func (keys *myKeys) Swap(i, j int) {
keys.container[i], keys.container[j] = keys.container[j], keys.container[i];
}
如上这3个方法的接收者类型都是 *myKeys。所以事先 sort.Interface 接口类型的类型是 *myKeys 而不是 myKeys。
5.实现Add方法
如今考虑实现 Add 方法,但在真正向字段 container 的值加入元素值之前,须要先推断这个元素值的类型是否符合要求。
当然。这须要使用字段 elemType 的值。它代表了可接受的元素值的类型。如今使用一个独立的方法来实现这个推断。例如以下:
func (keys *myKeys) isAcceptableElem(k interface{}) bool {
if k == nil {
return false
}
if reflect.TypeOf(k) != keys.elemType {
return false
}
return true
}
在 Add 方法中。使用 isAcceptableElem 方法来判定元素值的类型是否可被接收。假设结果是否定的,直接返回 false ;假设结果是肯定的。就向 container 字段的值加入这个元素值。
在加入之后,应该对 container 的值中的元素值进行一次排序。这须要用到 sort 代码包中的排序函数 sort.Sort。它的声明例如以下:
func Sort(data Interface) {
// 省略若干语句
}
函数 sort.Sort 的签名中的參数类型 Interface 事实上就是接口类型 sort.Interface,而且这两个程序实体处在同一个代码包中。
知识点:sort.Sort 函数使用的排序算法是一种由三向切分的高速排序算法,堆排序算法和插入排序算法组成的混合算法。尽管高速排序是最快的通用排序算法,但在元素值非常少的情况下它比插入顺序要慢一些。而堆排序的空间复杂度是常数级别的,且它的时间复杂度在大多数情况下仅仅略逊于其它两种排序算法,所以在高速排序中的递归达到一定深度的时候。切换至堆排序来节约空间是值得的。这种算法组合使得sort.Sort 函数的时间复杂度在最坏的情况下是 O(N*logN) 的。而且能够有效地控制对空间的使用,可是不提供稳定性的保证(即在排序过程中不保留数组或切片值中反复元素的相对位置)。
如今实现 Add 方法,声明例如以下:
func (keys *myKeys) Add(k interface{}) bool {
ok := keys.isAcceptableElem(k)
if !ok {
return false
}
keys.container = append(keys.container, k)
// sort.Sort函数会通过对keys的值的Len、Less和Swap方法的调用来完毕排序。
// 而在Less方法中。通过compareFunc函数对相邻的元素值进行比較的。
sort.Sort(keys)
return ture
}
6.实现Search方法
如今考虑实现 Remove 方法,但在实现之前须要使用 Search 方法找到指定删除的元素所处的位置。因此先实现 Search 方法。在 Search 方法中,要搜索參数 k 代表的值在 container 中相应的索引值。因为 k 的类型是 interface{} 的。所以须要先使用 isAcceptableElem 方法对它进行判定,然后能够通过调用 sort.Search 函数来实现搜索元素值的核心逻辑。sort.Search 函数的声明例如以下:
func Search(n int, f func(int) bool) int {
//省略若干条语句
}
因为 sort.Search 函数使用二分查找算法在切片值中搜索指定的元素值。
该搜索算法有着稳定的 O(logN) 的时间复杂度。但它要求被搜索的数组或切片值必须是有序的,而这里在加入元素的时候已经保证了container 字段的值中的元素值是已被排序过的。
从上面的声明中可知,sort.Search 函数有两个參数。第一个參数接受的是欲排序的切片值得长度,而第二个參数接受的是一个函数值。这个函数值的含义是:对于一个给定的索引值,判定与之相应的元素值是否等于欲查找的元素值或者应该排在欲查找的元素值的右边。
对于參数f的值例如以下:
func(i int) bool {
return keys.compareFunc(keys.container[i], k) >= 0
}
如上这个參数 f 应该如何理解呢?这里先假设有这样一个切片值:
[]int{1, 3, 5, 7, 9, 11, 13, 15}
如今要查找的元素值是 7,根据二分查找算法,sort.Search 函数内部会在第三次折半的时候使用 7 的索引值 3 作为函数f的參数值,函数f的结果值应该是 true,sort.Search 函数的执行会结束并返回 7 的索引值 3 作为它的结果值。可是,另一种情况就是,要查找的元素根本就不在这个切片值里,比方 6 或者 8 等等,sort.Search 函数的执行也会在 f(3) 被求值之后结束,且它的结果值会是 4 或者 3,相应的元素都不是要查找的。
sort.Search 函数的结果值总会在 [0, n] 的范围内,但结果值并不一定就是欲查找的元素值所相应的索引值。因此,须要在调用 sort.Search 函数的结果值之后再进行一次推断,例如以下:
if index < keys.Len() && keys.container[index] == k {
contains = true
}
当中 index 代表了 sort.Search 函数的结果值。这里须要先检查结果值是否在有效的索引范围之内,然后还须要推断它所相应的元素值是否就是要查找的。经过这些分析之后。相信不难实现 *myKeys 类型的 Search 函数。
7.实现Remove方法
实现了 Search 函数就来看看 Remove 函数,通过调用 *myKeys 类型的 Search 函数,就能够获取欲删除的元素值相应的索引值和它是否被包括在 container 中的推断结果。假设第二个结果值是 false,就能够直接返回 false。否则就从 container 中删除掉这个元素值。
从切片值中删除一个元素值有非常多种方法。比方使用 for 语句、copy 语句或 append 函数等等。这里选择用 append 函数来实现,因为它能够在不添加时间复杂度和空间复杂度的情况下使用更少的代码来完毕功能,且不减少可读性。
例如以下实现了删除一个元素值的功能:
keys.container = append(keys.container[0: index],keys.container[index+1: ]…)
如上代码充分地使用了切片表达式和 append 函数,使用例如以下切片表达式:
keys.container[0: index]//取出container字段的值中的在欲删除元素值之前的子元素序列
keys.container[index+1: ]// 取出container字段的值中的在欲删除元素值之后的子元素序列
接着通过 append 函数将两个元素子序列拼接起来,能够在第二个參数值之后加入“…”以表示把第二个參数值中的每一个元素值都作为传给 append 函数的独立參数,这样就把第二个子序列中的全部元素值逐个追加到了第一个子序列的尾部。最后把拼接后的元素序列赋值给了 container 字段。
8.实现Clear方法
func (keys *myKeys) clear() {
keys.container = make([]interface{}, 0)
}
9.实现Get方法
func (keys *myKeys) Get(index int) interface{} {
if index >= keys.Len() {
return nil
}
return keys.container[index]
}
10.实现GetAll方法
func (keys *myKeys) GetAll() []interface{} {
initialLen := len(keys.container)
snapshot := make([]interface{}, initialLen)
actualLen := 0
for _, key := range keys.container {
if actualLen < initialLen {
snapshot[actualLen] = key
} else {
snapshot = append(snapshot, key)
}
actualLen++
}
if actualLen < initialLen {
snapshot = snapshot[:actualLen]
}
return snapshot
}
11.实现ElemType和CompareFunc方法
func (keys *myKeys) ElemType() reflect.Type {
return keys.elemType
}
func (keys *myKeys) CompareFunc() CompareFunction {
return keys.compareFunc
}
12.实现String方法
//String方法被用于生成可读性更好的接收者值的字符串表示形式。
func (keys *myKeys) String() string {
var buf bytes.Buffer
buf.WriteString("Keys<")
buf.WriteString(keys.elemType.Kind().String())
buf.WriteString(">{")
first := true
buf.WriteString("[")
for _, key := range keys.container {
if first {
first = false
} else {
buf.WriteString(" ")
}
buf.WriteString(fmt.Sprintf("%v", key))
}
buf.WriteString("]")
buf.WriteString("}")
return buf.String()
}
13.定义NewKeys函数
眼下已经完毕了 myKeys 类型以及相关方法的编写。还应该编写一个用于初始化 *myKeys 类型值的函数。这个函数名为 NewKeys,结果类型为 Keys,声明例如以下:
func NewKeys ( compareFunc func(interface{}, interface{}) int8, elemType reflect.Type) Keys {
//因为仅仅有*myKeys类型的方法集合中才包括了Keys接口类型中声明的全部方法,
//所以以下返回的是一个*myKeys类型值。而不是一个myKeys类型值。
return &myKeys {
container : make([]interface{}, 0),
compareFunc : compareFunc,
elemType : elemType,
}
}
从上能够看出。在 NewKeys 函数的參数声明列表中没有与 container 字段相相应的參数声明。原因是 container 字段的值总应该是一个长度为 0 的 []interface{} 类型值。它不必由 NewKeys 函数的调用方提供。另外。NewKeys 函数的compareFunc 參数和 elemType 參数之间的关系。也要满足之前上文提到的约束条件。
14.又一次理解OrderedMap
至此我们已经编写完毕了 OrderedMap 类型所须要用到的最核心的数据类型 Keys 和 myKeys。
因为有了 Keys 接口类型,OrderedMap 类型的声明被改动为:
type myOrderedMap struct {
keys Keys
elemType reflect.Type
m map[interface{}]interface{}
}
如上更改了该类型的名称,这里要声明一个接口类型来描写叙述有序字典类型所提供的功能,而 OrderedMap 更适合作为这个接口类型的名称。
声明例如以下:
// 泛化的Map的接口类型
type OrderedMap interface {
// 获取给定键值相应的元素值。
若没有相应元素值则返回nil。
Get(key interface{}) interface{}
// 加入键值对,并返回与给定键值相应的旧的元素值。若没有旧元素值则返回(nil, true)。
Put(key interface{}, elem interface{}) (interface{}, bool)
// 删除与给定键值相应的键值对,并返回旧的元素值。若没有旧元素值则返回nil。
Remove(key interface{}) interface{}
// 清除全部的键值对。
Clear()
// 获取键值对的数量。
Len() int
// 推断是否包括给定的键值。
Contains(key interface{}) bool
// 获取已排序的键值所组成的切片值。
Keys() []interface{}
// 获取已排序的元素值所组成的切片值。
Elems() []interface{}
// 获取已包括的键值对所组成的字典值。
ToMap() map[interface{}]interface{}
// 获取键的类型。
KeyType() reflect.Type
// 获取元素的类型。
ElemType() reflect.Type
}
这里要使 *myOrderedMap 类型成为 OrderedMap 接口类型的实现类型。
尽管方法不多,但实现起来并不难。完毕后还能够编写一个 NewOrderedMap 函数,用于将初始化好的 *myOrderedMap 类型值作为结果值返回。
完整的 myOrderedMap 类型以及相关方法的实现,例如以下链接:
https://github.com/hyper-carrot/goc2p/tree/master/src/basic/map1
Go语言实战_自己定义OrderedMap的更多相关文章
- 《Go语言实战》读书笔记
<Go语言实战>中文版pdf 百度网盘: https://pan.baidu.com/s/1kr-gMzaPAn8BFZG0P24Oiw 提取码: r6rt 书籍源码:https://gi ...
- R语言实战(三)基本图形与基本统计分析
本文对应<R语言实战>第6章:基本图形:第7章:基本统计分析 =============================================================== ...
- R语言实战(二)数据管理
本文对应<R语言实战>第4章:基本数据管理:第5章:高级数据管理 创建新变量 #建议采用transform()函数 mydata <- transform(mydata, sumx ...
- swift语言实战晋级-第9章 游戏实战-跑酷熊猫-9-10 移除平台与视差滚动
9.9 移除场景之外的平台 用为平台是源源不断的产生的,如果不注意销毁,平台就将越积越多,虽然在游戏场景中看不到.几十个还看不出问题,那几万个呢?几百万个呢? 所以我们来看看怎么移除平台,那什么样的平 ...
- swift语言实战晋级-第9章 游戏实战-跑酷熊猫-7-8 移动平台的算法
在上个小节,我们完成了平台的产生.那么我们来实现一下让平台移动.平台的移动,我们只需要在平台工厂类中写好移动的方法,然后在GameScene类中统一控制就行了. 在GameScene类中,有个upda ...
- Swift语言实战晋级-第9章 游戏实战-跑酷熊猫-5-6 踩踏平台是怎么炼成的
在游戏中,有很多分来飞去的平台,这个平台长短不一.如果每种长度都去创建一张图片那是比较繁琐的事情.实际上,我们只用到3张图.分别是平台的,平台的中间部分,平台的右边.关键是平台的中间部分,两张中间部分 ...
- R语言实战(五)方差分析与功效分析
本文对应<R语言实战>第9章:方差分析:第10章:功效分析 ================================================================ ...
- R语言实战(七)图形进阶
本文对应<R语言实战>第11章:中级绘图:第16章:高级图形进阶 基础图形一章,侧重展示单类别型或连续型变量的分布情况:中级绘图一章,侧重展示双变量间关系(二元关系)和多变量间关系(多元关 ...
- PYTHON爬虫实战_垃圾佬闲鱼爬虫转转爬虫数据整合自用二手急速响应捡垃圾平台_3(附源码持续更新)
说明 文章首发于HURUWO的博客小站,本平台做同步备份发布. 如有浏览或访问异常图片加载失败或者相关疑问可前往原博客下评论浏览. 原文链接 PYTHON爬虫实战_垃圾佬闲鱼爬虫转转爬虫数据整合自用二 ...
随机推荐
- css3--简单的加载动画
.load-container { width: 30%; height: auto; position: relative; margin: 1rem auto; } .load { width: ...
- FreeModbus RTU slave & Modbus RTU master
一.FreeModbus RTU 协议数据格式 FreeModbus RTU是开源的一个协议,并且使用FreeModbus RTU 只能当做从机Slave,RTU协议中的指令由地址码(一个字节),功能 ...
- TOJ 2233 WTommy's Trouble
2233. WTommy's Trouble Time Limit: 2.0 Seconds Memory Limit: 65536KTotal Runs: 1499 Accepted R ...
- bug 7715339 登录失败触发 ‘row cache lock’ 等待
Bug 7715339 - Logon failures causes "row cache lock" waits - Allow disable of logon delay ...
- MySQL乱码问题以及utf8mb4字符集---utf8mb4和utf8有什么区别? emoji表情与utf8mb4
utf8mb4兼容utf8,且比utf8能表示更多的字符. 关于emoji表情的话mysql的utf8是不支持,需要修改设置为utf8mb4,才能支持, 因为utf8mb4是utf8的超集
- Docker 内程序时间设置,很重要
原文:Docker 内程序时间设置,很重要 重要!!!!! 创建容器时候需要修改一个参数,设置tomcat的时区 -e TZ="Asia/Shanghai" -v /etc/loc ...
- SSO单点登录学习总结(2)——基于Cookie+fliter单点登录实例
1.使用Cookie解决单点登录 技术点: 1.设置Cookie的路径为setPath("/").即Tomcat的目录下都有效 2.设置Cookie的域setDomain(&quo ...
- mySQL函数根据经纬度计算两点距离 复制代码
http://www.cnblogs.com/lujiulong/p/6185041.html https://my.oschina.net/u/2273085/blog/505172?p={{pag ...
- Apache httpd.conf 配置文件语法验证
Apache 的 httpd.conf文件改动之后,必须重新启动server才干生效. 有时server在提供服务的时候,直接更改配置,重新启动服务.会带来非常大的危急性. 假设能在改动配置之后,先验 ...
- wap.css
wap.css 一.总结 1.官方有教程:英语的 http://www.developershome.com/wap/wcss/ 2.wap.css :就是控制页面在手机端样式的 3.DOCTYPE ...