链表
go语言中的链表有官方实现的包"container/list"
,这是一个双向链表。
它的结构如下:
- 初始化一个双向链表
mylist := list.New()
- 插入链表尾部
PushBack(v interface{}) *Element
- 插入链表头部
PushFront(v interface{}) *Element
- 指定位置插入
InsertBefore(v interface{}, mark *Element) *Element
InsertAfter(v interface{}, mark *Element) *Element
- 删除节点
Remove(e *Element) interface{}
- 读取头节点
Front() *Element
- 读取末节点
Back() *Element
- 读取整张链表
for e:=mylist.Front();e!=nil;e=e.Next(){
fmt.Printf("%s->",e.Value)
}
测试代码:
// 遍历整个链表
func TravsList(lst *list.List) {
head := lst.Front()
for head != nil {
fmt.Println(head.Value)
head = head.Next()
}
}
func main() {
lis := list.New()
lis.PushBack(1)
lis.InsertBefore(2, lis.Back()) //在尾部(1)前面插入2
lis.InsertAfter(3, lis.Front()) //在头部(2)后面插入3
lis.PushBack(4)
lis.PushFront(5)
lis.PushFront(6)
lis1 := list.New()
lis1.PushBack(8)
lis.PushBackList(lis1) //尾插一个链表
TravsList(lis)//6 5 2 3 1 4 8
}
list需要注意的问题
使用list有几点需要注意:
-
因为list节点element中的value是一个any也就是空接口,这也就意味着list可以插入任意类型的数据比如这样:
-
既然是一个空接口,如果需要获取传入的类型,可以使用反射完成,或者用switch case判断一下类型
- 与C++不同,C++的list在调用erase删除一个节点后,会销毁整个节点,并且返回该节点下一个节点的迭代器。go中的remove函数会把节点返回,Remove函数则是把节点的Value返回,也就是说该节点并不会被销毁。但同样,该节点的next和prev指针都会被置为空。所以在循环删除的时候会出现问题:
func main() {
l := list.New()
l.PushBack(0)
l.PushBack(1)
l.PushBack(2)
l.PushBack(3)
fmt.Println("original list:")
prtList(l)
fmt.Println("deleted list:")
for e := l.Front(); e != nil; e = e.Next() {
l.Remove(e)//e的next变为nil,所以e = e.Next()会变成nil
}
prtList(l)
}
func prtList(l *list.List) {
for e := l.Front(); e != nil; e = e.Next() {
fmt.Printf("%v ", e.Value)
}
fmt.Printf("\n")
}
因此在删除前需要提前保存下一个节点的指针:
栈和队列
go语言没有官方的栈和队列的包。但是我们可以用数组、切片或者上面的链表来模拟实现栈和队列。
栈只需要尾插和尾删的接口即可。而栈则只需要尾插和头删的接口即可。
因为数组的优势在于能够随机访问,但是栈和队列也不需要随机访问,而且用数组实现队列会涉及到元素的移动。
所以用链表和切片来实现栈和队列效率会比数组更高一些。
//创建栈
stack := make([]int, 0)
//push压入栈
stack = append(stack, 10)
//pop弹出
v := stack[len(stack)-1]
stack = stack[:len(stack)-1]
//检查栈空
len(stack) == 0
//创建队列
queue := make([]int, 0)
//enqueue入队
queue = append(queue, 10)
//dequeue出队
v := queue[0]
queue = queue[1:]
//检查队列为空
len(queue) == 0
sort排序
sort通常用来给切片排序,对于自定义排序,也和C++中的sort如出一辙:
func main() {
arr := [...]int{8, 5, 6, 3, 2, 4, 1}
// int排序
sort.Ints(arr) //排升序
fmt.Println(arr)
sort.Slice(arr, func(i, j int) bool { return arr[i] > arr[j] }) //自定义排降序
fmt.Println(arr)
}
如果我们想排序一个数组,那么我们就需要将数组转化成切片,然后对切片排序:
func main() {
arr1 := [...]int{8, 5, 6, 3, 2, 4, 1}
arr := arr1[:] //将数组转化成切片
// int排序
fmt.Printf("排序前数组首元素地址%p\n", &arr1[0])
fmt.Printf("排序前切片首元素地址%p\n", &arr[0])
sort.Ints(arr) //排升序
fmt.Println(arr1)
sort.Slice(arr, func(i, j int) bool { return arr[i] > arr[j] }) //自定义排降序
fmt.Println(arr1)
fmt.Printf("排序后数组首元素地址%p\n", &arr1[0])
fmt.Printf("排序后切片首元素地址%p\n", &arr[0])
}
可以看到数组和切片的首元素地址是相同的,也就是说切片的指针是指向数组,并没有给切片额外开辟一段空间,所以效率上并不会差很多。
环ring
func main() {
ring := ring2.New(3) //ring的初始容量是3,后续我们无法修改这个初识容量
for i := 0; i < 10; i++ { //将1~9放到ring中,但我们实际上只放入了7,8,9,因为前面的数都被覆盖了
ring.Value = i
ring = ring.Next()
}
sum := 0
ring.Do(func(i interface{}) { //遍历整个环 i就是当前位置的值
//对每个元素的处理措施有func这个回调函数来定义
fmt.Println(i)
sum += i.(int) //i是一个空接口,所以需要强转成int
})
fmt.Println(sum)
}
优先级队列
优先级队列通常使用大小堆来实现,在go中heap包对任意实现了heap接口的类型提供堆操作。堆结构继承自sort.Interface, 而sort.Interface,需要实现三个方法:Len() int
、Less(i, j int) bool
、Swap(i, j int)
再加上堆接口定义的两个方法:Push(x interface{})
、Pop() interface{}
。
故只要实现了这五个方法,便定义了一个堆。
type IntHeap []int
func (h IntHeap) Len() int { return len(h) }
func (h IntHeap) Less(i, j int) bool { return h[i] < h[j] }//小根堆
func (h IntHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *IntHeap) Push(x interface{}) {
*h = append(*h, x.(int))
}
func (h *IntHeap) Pop() interface{} {
old := *h
n := len(old)
x := old[n-1]
*h = old[0 : n-1]
return x
}
func main() {
h := &IntHeap{5, 21, 5, 55, 3333, 46, 54, 57}
heap.Init(h)
heap.Push(h, 3)
for h.Len() > 0 {
fmt.Printf("%d ", heap.Pop(h))
}
}
set
go语言没有实现set,但是set和map很像,我们可以用map来实现set。
// Set 利用泛型,定义一个泛型类型(set), 泛型的写法使用中括号,comparable是新增的一个内置类型
type Set[T comparable] map[T]struct{}
// Add 向set中添加元素
func (s Set[T]) Add(v T) {
s[v] = struct{}{}
}
// Remove 移除set中某个元素
func (s Set[T]) Remove(v T) {
delete(s, v)
}
// Len 获取set的长度
func (s Set[T]) Len() int {
return len(s)
}
// Contains 查询某个元素v是否在set中
func (s Set[T]) Contains(v T) bool {
_, ok := s[v]
return ok
}
// Clear 清空set
func (s Set[T]) Clear() Set[T] {
return make(Set[T])
}
// ToSlice set转slice
func (s Set[T]) ToSlice() []T {
return s.getAll()
}
func (s Set[T]) getAll() []T {
go语言实现LRU缓存
type Node struct{ key, value int }
type LRUCache struct {
cap int //容量
cache map[int]*list.Element //key和list中节点的指针
lst *list.List //一个list链表的指针
}
func Constructor(capacity int) LRUCache {
return LRUCache{capacity, map[int]*list.Element{}, list.New()}
}
func (this *LRUCache) Get(key int) int {
pNode := this.cache[key] //拿到key对应节点的指针
if pNode == nil {
return -1
}
// 将该节点移动到链表的头部,此时该节点的地址是不变的,所以不需要修改cache
//在C++中由于没有MoveToFront这个接口,所以通常采用删除+再创建的方法将节点放到链表头部
//所以在C++中需要修改cache
this.lst.MoveToFront(pNode)
return pNode.Value.(Node).value
}
func (this *LRUCache) Put(key, value int) {
//key对应的节点存在,修改该节点的key和value,但不会修改该节点的地址
if pNode := this.cache[key]; pNode != nil {
pNode.Value = Node{key, value}
this.lst.MoveToFront(pNode) // 刷新缓存使用时间
return
}
//key对应的节点不存在,头插一个节点
this.cache[key] = this.lst.PushFront(Node{key, value})
if len(this.cache) > this.cap { //长度超过cap,删掉尾部的节点
temp := this.lst.Back() //拿到尾部节点的指针
this.lst.Remove(temp) //删掉这个节点,temp此时并不会成为野指针
delete(this.cache, temp.Value.(Node).key)//删掉cache中的key
}
}