Container包中的那些容器
1.切片的优缺点
切片具有占用内存少和创建便捷等优点,同时切片还能够让我们通过窗口快速的定位并获取,或者修改底层数组中的元素。
不过,要删除切片中的元素就不简单了,元素复制,大量元素移动,内存泄漏都是存在的问题。而且切片频繁扩容的情况下,新的底层数组会不断产生,这时内存分配的量以及元素复制的次数就很可观了,这会对程序的性能产生影响。尤其是当我们没有合理、有效的“缩容”策略的时候,旧的底层数组无法被回收,新的底层数组中也会有大量无用的元素槽位。过度的内存浪费会降低程序性能,还有可能导致内存溢出,最终导致程序崩溃。所以我们要根据需求选择合适的存储容器。
2.container/list-双向链表-List
双向链表是链表中的一种,它的基本数据结构如下:
type Element struct {
next, prev *Element //分别指向前一个节点和后一个节点
list *List //表明属于哪一个list
Value interface{}//存储的值
}
List struct {
root Element
len int
}
这个包包含了两个公开的程序实体:List,Element,前者实现了一个双向链表,后者代表了链表中的元素结构,一个链表所占用的内存空间,往往要比包含相同元素的数组所占的内存大很多,因为其元素并不是连续存储的,所以list结构体要存储一系列额外信息来标识元素之间的关系。
结构体类型有一个特点,它们的零值都会拥有其特定的结构,但没有任何定制化内容,相当于一个空壳。广义的来说,零值只是做了声明,但未初始化的变量被赋予了缺省值。每个类型的零值都会依据该类型的特性而被设定。
例子:var a [2] int申明的变量的值将会是一个包含了两个0的整数数组。又比如,经过语句var s []int声明的变量的值将会是一个[]int类型的值为nil的切片。
var l list.List 声明的变量l的值将会是一个长度为0的链表。这个链表持有的根元素也将会是一个空壳,其中只会包含缺省的内容。
3.container/list包含的一些方法
list移动元素的方法:
func (l *List) MoveBefore(e, mark *Element)
func (l *List) MoveAfter(e, mark *Element) //用于将指定元素移动到另一个元素前面或者后面
func (l *List) MoveToFront(e *Element)
func (l *List) MoveToBack(e *Element)//用于将给顶元素移动到链表最前端和最后端
如果我们自己生成这种类型的值,链表是不会接受的,如果传入这种值,这些方法将不会对链表做出任何改动。因为我们自己生成的Element值并不在链表中。
list插入元素的方法:
func (l *List) Front() *Element
func (l *List) Back() *Element //获取链表最前端最后端的元素
func (l *List) InsertBefore(v interface{}, mark *Element) *Element
func (l *List) InsertAfter(v interface{}, mark *Element) *Element //在指定元素之前和之后插入新元素
func (l *List) PushFront(v interface{}) *Element
func (l *List) PushBack(v interface{}) *Element//在链表最前端和最后端插入新元素
这些方法只接受interface{}类型的值。这些方法在内部会使用Element值包装接受到的新元素,这样做是为了避免我们使用我们自己生成的元素,主要是为了避免链表内部关联遭到外界破坏。
通过调用这些方法,我们会获得一个Element值的指针,这个指针就是链表返回的安全接口,通过这些指针我们可以调用之前用于移动元素的方法。
4.为什么链表声明就可以使用
通过语句var l list.List声明的链表l可以直接使用。
List这个结构体类型有两个字段,一个是Element类型的字段root,另一个是int类型的字段len。顾名思义,前者代表的就是那个根元素,而后者用于存储链表的长度。注意,它们都是包级私有的,也就是说使用者无法查看和修改它们。
像前面那样声明的l,其字段root和len都会被赋予相应的零值。len的零值是0,正好可以表明该链表还未包含任何元素。由于root是Element类型的,所以它的零值就是该类型的空壳,用字面量表示的话就是Element{}。
Element类型包含了几个包级私有的字段,分别用于存储前一个元素、后一个元素以及所属链表的指针值。
另外还有一个名叫Value的公开的字段,该字段的作用就是持有元素的实际值,它是interface{}类型的。在Element类型的零值中,这些字段的值都会是nil。
其实单凭这样一个l是无法正常运作的,但关键不在这里,而在于它的“延迟初始化”机制。所谓的延迟初始化,你可以理解为把初始化操作延后,仅在实际需要的时候才进行。延迟初始化的优点在于“延后”,它可以分散初始化操作带来的计算量和存储空间消耗。
例如,如果我们需要集中声明非常多的大容量切片的话,那么那时的 CPU 和内存空间的使用量肯定都会一个激增,并且,只有设法让其中的切片及其底层数组被回收,内存使用量才会有所降低。
如果数组是可以被延迟初始化的,那么计算量和存储空间的压力就可以被分散到实际使用它们的时候。这些数组被实际使用的时间越分散,延迟初始化带来的优势就会越明显。
实际上,Go 语言的切片就起到了一定的延迟初始化其底层数组的作用,你可以想一想为什么会这么说的理由。
而且,延迟初始化的缺点恰恰也在于“延后”。你可以想象一下,如果我在调用链表的每个方法的时候,它们都需要先去判断链表是否已经被初始化,那这也会是一个计算量上的浪费。
在这些方法被非常频繁地调用的情况下,这种浪费的影响就开始显现了,程序的性能将会降低。
在这里的链表实现中,一些方法是无需对是否初始化做判断的。比如Front方法和Back方法,一旦发现链表的长度为0直接返回nil就好了。
又比如,在用于删除元素、移动元素,以及一些用于插入元素的方法中,只要判断一下传入的元素中指向所属链表的指针,是否与当前链表的指针相等就可以了。
如果不相等,就一定说明传入的元素不是这个链表中的,后续的操作就不用做了。反之,就一定说明这个链表已经被初始化了。
原因在于,链表的PushFront方法、PushBack方法、PushBackList方法以及PushFrontList方法总会先判断链表的状态,并在必要时进行初始化,这就是延迟初始化。
而且,我们在向一个空的链表中添加新元素的时候,肯定会调用这四个方法中的一个,这时新元素中指向所属链表的指针,一定会被设定为当前链表的指针。所以,指针相等是链表已经初始化的充分必要条件。
明白了吗?List利用了自身,以及Element在结构上的特点,巧妙地平衡了延迟初始化的优缺点,使得链表可以开箱即用,并且在性能上可以达到最优。
链表使用实例请见:github.com/GodShuning/…
5.container/ring
ring是一个双向循环链表,它的数据结构如下:
// A Ring is an element of a circular list, or ring.
// Rings do not have a beginning or end; a pointer to any ring element
// serves as reference to the entire ring. Empty rings are represented
// as nil Ring pointers. The zero value for a Ring is a one-element
// ring with a nil Value.//
type Ring struct {
next, prev *Ring
Value interface{}
}
环使用实例请见:github.com/GodShuning/…
6.Ring和List的区别
其实List在内部就是一个循环链表。它的根元素永远不会持有任何实际的元素值,该元素的存在,就是为了链接这个循环链表的首位端。
所以,List的零值是一个只包含了根元素,但不包含任何实际元素值的空链表。
Ring和List最主要的不同:
1.Ring类型的数据结构仅有它自身即可代表,而List类型则需要由它以及Element类型联合表示。这是表示方式的不同,也是结构复杂度上的不同
2.一个Ring类型的值严格来讲,只代表了其所属循环链表的一个元素,而一个List类型的值则代表了一个完整的链表。这是表示维度上的不同,
3.在创建并初始化一个Ring值的时候,我们可以指定它包含的元素数量,但对于一个List类型的值来说,却不能这样做。循环链表一旦被创建其长度是不可变的。这是两个代码包中的New函数在功能上的不同,也是两个类型在初始化值方面的第一个不同。
4.仅通过var r ring.Ring语句申明饿r将会是一个长度为一的循环链表,而List类型的零值将会是一个长度为0的链表。因为List的根元素不会持有实际元素值,因此计算长度时不会包括它。这是两个类型在初始化值方面的弟二个不同。
5.Ring值计算len方法的算法复杂度是O(N)而List的算法复杂的是O(1)的。
7.container/heap
暂时先不讲