前言
笔者发现关于 Go 语言方面的数据结构算法教程十分稀少,包括书籍和视频还有文章,所以决定出一系列关于 Go 语言实现的数据结构算法教程。本教程基于 Go SDK 1.20 还有 1.18 版本集成进来的新特性,例如泛型编程等。预计会出 8~10 篇文章,不间断更新。
数组列表的定义
Go 语言内置的切片其实就是数组列表,底层基于数组实现,切片会有一个指针指向底层的数组。但是它的 CRUD 的操作不太方便,所以我们可以再封装一次。
// 数组列表
type ArrayList[T any] struct {
values []T
}
// 构造函数
func NewArrayList[T any](values []T) *ArrayList[T] {
return &ArrayList[T]{values: values}
}
重写String方法
如果你直接打印链表节点,只会得到一个内存地址。或许你会想到定义一个函数打印链表的描述字符串。在 Go 语言中 fmt/print.go 文件中有一个接口名为 Stringer
:
// Stringer is implemented by any value that has a String method,
// which defines the “native” format for that value.
// The String method is used to print values passed as an operand
// to any format that accepts a string or to an unformatted printer
// such as Print.
type Stringer interface {
String() string
}
这个接口定义了一个 String
方法用于返回类型的描述字符串,所以我们可以让结构体实现这个接口:
func (a *ArrayList[T]) String() string {
var ans strings.Builder
ans.WriteString("[")
for i, value := range a.values {
if i+1 == len(a.values) {
ans.WriteString(fmt.Sprintf("%v", value))
break
}
ans.WriteString(fmt.Sprintf("%v, ", value))
}
ans.WriteString("]")
return ans.String()
}
添加元素
// 添加元素, 可以一次添加多个
func (a *ArrayList[T]) add(e ...T) {
// 自动 1.5 倍扩容
a.values = append(a.values, e...)
}
删除元素
// 删除一个元素
func (a *ArrayList[T]) remove(e T) {
for i, value := range a.values {
if value == e {
a.values = append(a.values[:i], a.values[i+1:]...)
}
}
}
// 根据索引删除一个元素
func (a *ArrayList[T]) removeIndex(i int) {
a.values = append(a.values[:i], a.values[i+1:]...)
}
更新元素
// 根据索引更新元素
func (a *ArrayList[T]) set(i int, e T) {
a.values[i] = e
}
获取元素
// 根据索引获取元素
func (a *ArrayList[T]) get(i int) T {
return a.values[i]
}
测试用例
func TestArrayList(t *testing.T) {
values := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
list := NewArrayList(values)
fmt.Println(list)
list.add(100, 200, 90)
fmt.Println(list)
list.remove(100)
fmt.Println(list)
e := list.get(3)
fmt.Println(e)
list.set(2, 999)
fmt.Println(list)
}
单链表的定义
结构体定义
首先,我们来看一下单链表的结构体定义:
// 链表节点结构体定义 加上泛型
type ListNode[T any] struct {
Val T
Next *ListNode[T] // 后继节点指针
}
T 表示单链表节点存储的数据类型,这里表示泛型。T 的类型范围是 any,表示任意类型。因为数据结构操作中经常会用到比较的操作。可以把链表就是存储数据的容器,存储的数据类型就是泛型类型。Next 是一个节点指针类型,表示指向的下一个节点的指针。简而言之,指针内部保存了下一个节点的内存地址,那么就形成了一个链条,故名链表。
重写String方法
func (head *ListNode[T]) String() string {
var ans strings.Builder
for p := head; p != nil; p = p.Next {
if p.Next == nil {
ans.WriteString(fmt.Sprintf("%v", p.Val))
break
}
ans.WriteString(fmt.Sprintf("%v->", p.Val))
}
return ans.String()
}
这里利用 strings 包的 Builder 结构体,它可以用于快速方便地拼接字符串。
构建一条单链表
单链表节点定义好了,那么如何构建一条单链表呢?最朴素的方法就是一个一个构建节点,然后使用 Next 指针指向下一个节点,最后完成构建:
func main() {
head := &ListNode[int]{Val: 1}
head.Next = &ListNode[int]{Val: 2}
head.Next.Next = &ListNode[int]{Val: 3}
fmt.Println(head)
}
这样虽然完成了构建,但是构建过程太过繁琐。所以我们需要封装一个构建单链表的函数。利用传入的切片数据构建单链表:
// 构建一条单链表
func createListNode[T any](values []T) *ListNode[T] {
head := &ListNode[T]{}
ans := head
for _, value := range values {
ans.Next = &ListNode[T]{Val: value}
ans = ans.Next
}
return head.Next
}
上述代码表示利用传入的切片数据构建链表,刚开始创建了一个哑节点,里面不存储任何数据,然后从哑节点之后的一个节点开始构建。这里我们定义了一个 ans 指针指向 head,然后利用 ans 指针构建节点并移动位置。最后返回哑结点的 Next 节点,根据需求而定,如果你需要哑节点,那么就直接返回哑节点。在本教程中不需要哑节点。
写一个测试用例打印一下单链表:
package list
import (
"fmt"
"testing"
)
func TestListNodeString(t *testing.T) {
values := []int{1, 2, 3, 4, 5}
head := createListNode(values)
fmt.Println(head)
}
结果如下:
反转链表
相信看到这里,你一定对基本的线性表比较熟悉了。接下来看一道经典例题,反转链表。数据结构与算法初学者必会的一道题。
题目要求就是把一条链表反转过来。比如 1->2->3->4->5,要反转成 5->4->3->2->1。
思路也很简单,定义两个指针分别为 pre 和 rear,它们分别用于记录 head 指针的前驱节点和后继节点的位置。看代码:
// 反转链表
// 定义两个指针初始化指向nil
// pre 保存之前的指针, rear 保存后面指针
func reverse[T any](head *ListNode[T]) *ListNode[T] {
var pre *ListNode[T]
var rear *ListNode[T]
for head != nil {
rear = head.Next // 记录head节点当前后继节点的位置
head.Next = pre // head节点的后继指针指向pre
pre = head // pre记录当前head节点位置
head = rear // 移动head指针
}
return pre
}
能把这道题看懂了,说明你已经入门了。
双链表定义
看完了单链表的定义,再来看看双链表的定义。它跟单链表的区别就是每个节点多了一个前驱指针,这样可以根据一个节点的前驱指针找到它的前驱节点。
// 双链表节点定义
type DoubleListNode[T any] struct {
Val T
Next *DoubleListNode[T]
Prev *DoubleListNode[T]
}
Val 表示存储的数据,类型是泛型 T。Next 是后继指针指向下一个节点,Prev 是前驱指针指向前一个节点。
重写String方法
我们可以模仿单链表的 String 方法,编写双链表的 String 方法:
func (head *DoubleListNode[T]) String() string {
var ans strings.Builder
for p := head; p != nil; p = p.Next {
if p.Next == nil {
ans.WriteString(fmt.Sprintf("%v", p.Val))
break
}
ans.WriteString(fmt.Sprintf("%v⇄", p.Val))
}
return ans.String()
}
构建一条双链表
和构建一条单链表一样的思路,根据切片数据构建双链表也不难:
// 构建一条双链表
func createDoubleListNode[T any](values []T) *DoubleListNode[T] {
head := &DoubleListNode[T]{}
ans := head
for _, value := range values {
p := &DoubleListNode[T]{Val: value}
ans.Next = p
p.Prev = ans
ans = ans.Next
}
return head.Next
}
编写一个测试用例打印一下:
package list
import (
"fmt"
"testing"
)
func TestPrintDoubleListNode(t *testing.T) {
values := []int{1, 2, 3, 4, 5}
head := createDoubleListNode(values)
fmt.Println(head)
}
反转双链表
其实和反转单链表思路一样,定义两个指针分别记录前驱和后继节点。只是多了一个步骤,需要调整 head.Prev 指针的指向。
func reverseDouble[T any](head *DoubleListNode[T]) *DoubleListNode[T] {
var pre *DoubleListNode[T]
var rear *DoubleListNode[T]
for head != nil {
rear = head.Next // 记录head节点当前后继节点的位置
head.Prev = rear // head节点的前驱指针指向rear
head.Next = pre // head节点的后继指针指向pre
pre = head // pre记录当前head节点位置
head = rear // 移动head
}
return pre
}