08 | container包中的那些容器
08 | container包中的那些容器
讲述:黄洲君
时长09:40大小13.28M
问题解析
参考阅读
切片与数组的比较
赞 29
提建议
精选留言(37)
- 李皮皮皮皮皮2018-08-291.list可以作为queue和 stack的基础数据结构 2.ring可以用来保存固定数量的元素,例如保存最近100条日志,用户最近10次操作 3.heap可以用来排序。游戏编程中是一种高效的定时器实现方案共 3 条评论122
- 陌上人 .2018-11-28老师,之后的课可不可以多加一些图形解释,原理性的知识只用文字确实有些晦涩难懂共 9 条评论106
- 传说中的成大大2020-03-04关于 container包中的链表list 和环ring的知识总结 按我的思考 1. 为什么go语言会出现list这个包 首先一个语言或者新技术的出现肯定是为了解决一些疑难杂症 在go语言中数组的特点非常鲜明 固定不可变 访问方便,但是如果不适合动态增长,所以出现了slice切片 切片是对数组的一层封装为了解决数组动态扩容问题, 但是实际上底层依赖的还是数组,但是问题来了 如果slice切片 在添加或者删除元素的时候如果没一个好的策略 扩容或者缩容过后旧的切片没有释放 则会造成内存泄漏 就是c语言的malloc 久而久之 内存越来越少 程序就会崩溃, 而List的出现就是为了解决动态扩容或者缩容的后遗症 因为依赖指针这个东西 所以删除和增加都非常方便 2. 延迟初始化机制 延迟初始化机制 主要是为了解决像数组这种 声明的时候就分配了内存空间的问题,有的时候我们只需要声明,但还不需要使用它,这个时候没有必要分配内存空间,再如文中提到的 在同一时刻声明大量的内存空间的话 那么cpu的使用和内存空间的使用将会激增 所以我们需要延迟初始化机制(设计模式中的单例模式也提到了延迟初始化问题,避免声明出来没人使用的尴尬局面) 3. list关于延迟初始化机制的一些处理 延迟初始化机制的缺点就在于在使用的时候需要判断是否已经初始化了,如果使用比较频繁的话,就会造成大量的计算浪费(cpu浪费) 所以list当中关于延迟初始化机制的处理方案如下 3.1 在插入新元素时 需要检查是否已经初始化好了 3.2 在移动 或者将已有元素再修改位置插入时 需要判断元素身上的链表是否和要操作的链表的指针相同 如果不相同说明元素不存在该链表中 如果相同则说明这个链表肯定已经初始化好了 4. ring包和list包的区别 首先从源码来看 type Ring struct { next, prev *Ring Value } type Element struct { next, prev *Element list *List //它所属的链表的指针 Value interface{} } type List struct { root element len int } 从源码的定义分析得出 ring 的元素就是ring结构体(只代表了一个元素) 而list的元素 是list+element(代表了一个链表) list在创建的时候不需要指定元素也没有必要,因为它会不停的增长 而ring的创建需要指定元素个数 且不再增长 并且list的还存在一个没有实际意义的根元素 该根元素还可以用来连接首位两端 使其成为一个循环链表 关于文中 两个结论的思考 结论1 go语言中切片实现了数组的延迟初始化机制 我的思考是 因为切片延迟初始化了, 所以他的底层数组在切片声明时也没有被初始化出来 结论2 ring 使用len方法是o(n) 而list使用len方法是o(1) 还没看讲解我就去翻看了源码(我比较喜欢的一句话是源码之下无秘密),从上面两个结构体的声明和list的insert方法可以看出 因为list的根元素这个根元素也代表了链表(突然想明白ring和list的第二点区别) 在这个根元素上存放了一个len数据表示链表的长度 insert时这个长度会执行+1,所以执行len方法时只需要取出这个长度即可从而达到了o(1)的时间复杂度 而ring结构体中却不存在这样的len所以需要遍历完整个环,所以时间复杂度为o(n) 关于思考题 1. ring包从实现来分析得出 适合用来执行长度固定的循环事件 2. heap包 则适合用来做堆排序 求第k大 第k小等问题 还有就是前面某些同学提到的优先调度问题 关于优先调度问题我觉得思路大概如下 首先维护一个堆 然后针对每个要调度的事件 分配一个优先级 然后从下到上执行堆化过程 让优先级(最低或者最高的放到堆的顶部) 当处理完成之后 再把堆尾部的事件放到堆顶部 然后执行从上往下进行堆化维护好堆的顺序 再执行逻辑展开共 4 条评论47
- louis2019-04-23郝老师,这里不太理解什么叫“自己生成的Element类型值”?把自己生成的Element类型值传给链表——这个能不能再通俗点描述?
作者回复: 比如你用 list.New 函数创建了一个 List 类型的双向链表,然后通过它的一些方法往里面塞了一些元素。可以往里面塞的元素的方法有 PushFront、PushBack、InsertAfter、InsertBefore 等。 但是你发现没有,这些方法接受的新元素的类型都是 interface{} 的。也就是说,这个 List 类型的链表只接受 interface{} 类型的新元素值。 而当新元素值进入链表之后,链表会把它们再包装成 list.Element 类型的值。你看,那些往里塞元素值的方法返回的都是被包装后的 *list.Element 类型的元素值。 当你像我这样浏览了 container/list.List 类型的相关 API 之后,就应该可以明白我问这个问题的背景了。 这个 List 类型只会接受 interface{} 类型的新元素值,并且只会吐出 *list.Element 类型的已有元素值。显然,任何移动已有元素值或者删除已有元素值的方法,都只会接受该链表自己吐出来的“Element 值”。因此,对于我们自己生成的“Element 值”,这个链表的任何方法都是不会接受的。 当然了,如果你之前完全没用过 List 类型,可能会觉得这个问题有些突兀。但是当你看完下面的详细解释之后,我相信你就会有所了解了。 我们这个专栏的一个风格就是:“先抛出问题,然后再解释前因后果”。目的就是,逼迫大家在碰到问题之后自己先去了解背景并试着找找答案,然后再回来看我的答案。这样的话,你对这些知识点的记忆会更牢固,不容易忘。 我非常希望这个专栏能成为大家的“枕边书”,而不是听听音频就放在一边的那种。所以才有了这样的结构设计。如果你们能在今后碰到问题时想起这个专栏,到这里翻一翻并能找到答案的线索,那我就太高兴了。
共 5 条评论40 - Err2018-10-23我觉得写一个实际的例子能帮助更好理解39
- melon2018-08-29list的一个典型应用场景是构造FIFO队列;ring的一个典型应用场景是构造定长环回队列,比如网页上的轮播;heap的一个典型应用场景是构造优先级队列。共 1 条评论22
- fliter2018-08-29为什么不把list像slice,map一样作为一种不需要import其他包就能使用的数据类型?是因为使用场景较后两者比较少吗
作者回复: 这需要一个过程,之前list也不是标准库中的一员。况且也没必要把太多的东西多做到语言里,这样反倒不利于后面的扩展。
共 2 条评论10 - 云学2018-08-31在内存上,和ring的区别是list多了一个特殊表头节点,充当哨兵8
- 会网络的老鼠2018-08-30现在大家写golang程序,一般用什么IDE?共 2 条评论8
- 后端进阶2018-09-06前面的网友,goland了解一下,超赞的ide共 1 条评论7
- Geek_a8be592019-06-26您好 能否出一个list 链表生成的一个图解,现在我看源码用图去模拟生成 一直搞混掉,特别是在初始化的时候prev和next都指向自身的root 这个很迷糊 比如: c.PushBack("123") c.PushFront("456") c.PushFront("789") 根据个人图解应该是789-》456-》nil,为什么能遍历出来很不清楚。能否有一个从初始化到最后生成的样例看一下 万分感谢展开
作者回复: 按照你这三行代码,应该是 789 -> 456 -> 123 啊,你那个“789-》456-》ni”是怎么出来的? 这个链表里所谓的 root 就是用来表示链表两端的尽头的。所以,这个链表的末端实际上并不是 nil ,而是 root。只是在 Element 的 Next 方法中,如果发现它的 next 字段的值等于 root,就会返回 nil 而已。 明白了吗?
共 2 条评论6 - 李斌2018-10-30用 vscode 就蛮好的,我之前是八年 vim 党,写 golang 时硬生生地被掰成 vscode
作者回复: 嗯,也算是与时俱进吧,未来有脑机接口了,也就用不着这些了。
共 2 条评论5 - Zer02019-08-06不能把自己生成的Element传给List主要是因为Element的list成员是不开放的,我们不能操作,而在List上操作Element的时候是会判断Element的list成员是否是自己。是这样吗?
作者回复: ✅
4 - 雷朝建2021-04-02老师, 我看了一下list.go的源码,发现一个疑问是:延迟初始化的含义就是调用lazyInit,它的一个判断条件是:l.root.next==nil; 但是我们在使用list时候,不是先调用New函数吗?那么不应该会出现l.root.next为nil的情况的。 什么时候回出现l.root.next==nil, 从而导致源码中每次的PushFront等操作调用lazyInit呢?
作者回复: List 是一个 struct 啊,有了 lazyInit 你就可以直接 var myList list.List 了啊。这不是就做到开箱即用了吗?
3 - jackstraw2019-07-09我尝试打印了 “var l = list.New()” 与 “var l list.List”两种方式的l类型,发现是不一样的,但是下面的操作却都是可以的 func main() { //l := list.New() var l list.List e4 := l.PushBack(4) e1 := l.PushFront(1) l.InsertBefore(3, e4) l.InsertAfter(2, e1) //travel(l) travel(&l) }展开
作者回复: 当然不一样,list.New 返回的是指针值。另外你可以再看看讲结构体和方法那篇文章。
3 - 杨震2020-08-25以后再有课的话 希望老师多加点图 虽然费点事 但应该更多为学员着想吧。文字阐述一点也不直观。
作者回复: OK。
2 - Sky2019-06-12这一讲没有实例代码
作者回复: 建议大家在阅读这一篇文章时对照着 container 包的文档看。对这几个类型的 API 有一定了解之后,使用就是水到渠成的事情了。 自己先试一试,如果有具体的问题,可以来这里问。
2 - lesserror2021-07-27对于这一讲的内容,确实要用IDE(例如Goland)打开源码文件一边阅读一边对着课中的文字,一边推敲、实验。 几个源码文件都是一两百来行,读起来不吃力。(前提是已经有了Go的基础)1
- KK2020-07-19关于list的结构,画个图会更加直观1
- lixiaofeng2019-12-20func flist(){ link := list.New() for i:=1; i< 11; i++ { link.PushBack(i) } for p:= link.Front(); p!=link.Back(); p=p.Next(){ fmt.Println("Number", p.Value) } } #链表的常用方法 func (e *Element) Next() *Element func (e *Element) Prev() *Element func (l *List) Init() *List New() *List { return new(List).Init() } func (l *List) Len() int { return l.len } func (l *List) Front() *Element func (l *List) Back() *Element func (l *List) Remove(e *Element) interface{} func (l *List) PushFront(v interface{}) *Element func (l *List) PushBack(v interface{}) *Element func (l *List) InsertBefore(v interface{}, mark *Element) *Element func (l *List) InsertAfter(v interface{}, mark *Element) *Element func (l *List) MoveToFront(e *Element) func (l *List) MoveToBack(e *Element) func (l *List) MoveBefore(e, mark *Element) func (l *List) MoveAfter(e, mark *Element) func (l *List) PushBackList(other *List) func (l *List) PushFrontList(other *List)展开1