[Go数据结构与算法]1. 线性表的结构与实现(基于泛型实现),码农一定要学好算法。

58 阅读6分钟

前言

笔者发现关于 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)  
}

image.png

单链表的定义

结构体定义

首先,我们来看一下单链表的结构体定义:

// 链表节点结构体定义 加上泛型
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)
}

结果如下:

image.png

反转链表

相信看到这里,你一定对基本的线性表比较熟悉了。接下来看一道经典例题,反转链表。数据结构与算法初学者必会的一道题。

题目要求就是把一条链表反转过来。比如 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)  
}

image.png

反转双链表

其实和反转单链表思路一样,定义两个指针分别记录前驱和后继节点。只是多了一个步骤,需要调整 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  
}

参考源码地址

algorithm-go: algorithm in go (gitee.com)