Go语言常用的数据结构


链表

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有几点需要注意:

  1. 因为list节点element中的value是一个any也就是空接口,这也就意味着list可以插入任意类型的数据比如这样:
    在这里插入图片描述

  2. 既然是一个空接口,如果需要获取传入的类型,可以使用反射完成,或者用switch case判断一下类型

在这里插入图片描述

  1. 与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() intLess(i, j int) boolSwap(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缓存

146. 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
	}
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

今天也要写bug、

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值