GO语言基础入门-数据结构基础 | 青训营笔记

110 阅读6分钟

前言

这是我参与「第五届青训营 」伴学笔记创作活动的第 13 天,其实第五届青训营已经正式开课n多天了,笔记不断更新中,都是我听完课之后的总结和平时自己的学习积累,分享出来给有需要的朋友。

本文内容

本文将涉及到数据结构稀疏数组、循环队列和链表增删改查的基本使用。

Go语言基础

1.数据结构

(1)稀疏数组

问一个问题,如果现在要让你通过编码的方式,让你将这盘棋局保存起来,你会怎么做呢?

面对行列数据的保存,我相信大多人第一时间都会想到用二维数组进行保存。但最其并不是最好的方案,因为如果棋盘上就几个黑子和白子,大部分都是空的,我们使用二维数组把这样没有价值的数据起来,不但会浪费存储空间的大小,如果写入到磁盘中,还会增大 IO 的读写量,影响性能,这就是用普通二维数组来表示棋盘数据的不足之处。

这时候稀疏数组就出场了,以下是完整代码

type ValNode struct {
    row int
    col int
    val int
}
​
func main() {
    //1.先创建一个原始数组
    var chessMap [11][11]int
    chessMap[1][2] = 1 //黑子
    chessMap[2][3] = 2 //蓝子//2.输出看看原始数组
    for _, v := range chessMap {
        for _, v2 := range v {
            fmt.Printf("%d\t", v2)
        }
        fmt.Println()
    }
    //3.转成系数数组
    //  思路
    //  (1) 遍历chessMap,如果我们发现有一个元素不为0,创建一个node结构体
    //  (2) 将其放入到其对应的切片即可
    var sparseArr []ValNode
    valNode := ValNode{
        row: 11,
        col: 11,
        val: 0,
    }
    sparseArr = append(sparseArr, valNode)
    for i, v := range chessMap {
        for j, v2 := range v {
            if v2 != 0 {
                //创建一个ValNode值结点
                valNode := ValNode{
                    row: i,
                    col: j,
                    val: v2,
                }
                sparseArr = append(sparseArr, valNode)
            }
        }
    }
​
    //输出稀疏数组
    fmt.Println("======当前的稀疏数组是=====")
    for i, v := range sparseArr {
        fmt.Printf("%d %d %d %d\n", i, v.row, v.col, v.val)
    }
​
    //1.先创建一个原始数组
    var chessMap2 [11][11]int
    for i, v := range sparseArr {
        if i != 0 {
            chessMap2[v.row][v.col] = v.val
        }
    }
    /*
        输出结果:(第一列可以省略)
        ======当前的稀疏数组是=====
​
        0 11 11 0
        1 1 2 1
        2 2 3 2
        第一行 11 11 0记录着行数、列数、和初始值
        第二行开始 记录具体值 1 2 1 代表第一行第2列值是1
​
    *///2.输出看看还原后的数组
    fmt.Println("======还原后的数组=====")
    for _, v := range chessMap2 {
        for _, v2 := range v {
            fmt.Printf("%d\t", v2)
        }
        fmt.Println()
    }
}
​
(2)数组模拟队列

完整案例

// 使用一个结构体管理队列
type Queue struct {
    maxsize int
    array   [5]int //数组->模拟队列
    front   int    //表示指向队列首
    rear    int    //表示指向队列的尾部
}
​
// 添加数据到队列
func (this *Queue) AddQueue(val int) (err error) {
    //先判断队列是否已满
    if this.rear == this.maxsize-1 { //rear是队尾(含最后元素)
        return errors.New("queue full")
    }
    this.rear++ //往后移
    this.array[this.rear] = val
    return
}
​
// 从队列取出数据
func (this *Queue) GetQueue() (int, error) {
    //先判断队列是否为空
    if this.rear == this.front { //front不含对首元素
        return -1, errors.New("queue null") //对空
    }
    this.front++
    val := this.array[this.front] //从队首出,队尾进
    return val, nil
}
​
// 显示队列数据 从对首遍历到队尾
func (this *Queue) ShowQueue() {
    for i := this.front + 1; i <= this.rear; i++ {
        fmt.Printf("array[%d]=%d\t", i, this.array[i])
    }
    fmt.Println()
​
}
​
func main() {
    queue := &Queue{
        maxsize: 5,
        front:   -1,
        rear:    -1,
    }
    var key string
    var val int
    for {
        fmt.Println("1.输入add 表示添加数据到队列")
        fmt.Println("2.输入get 表示从队列获取数据")
        fmt.Println("3.输入show 表示显示队列")
        fmt.Println("4.输入exit 表示退出队列")
​
        fmt.Print("输入你的选项:")
        fmt.Scanln(&key)
        switch key {
        case "add":
            fmt.Print("输出你要入队列的数:")
            fmt.Scanln(&val)
            err := queue.AddQueue(val)
            if err == nil {
                fmt.Println("success,加入队列成功!")
            } else {
                fmt.Println(err.Error())
            }
        case "get":
            val, err := queue.GetQueue()
            if err != nil {
                fmt.Println("队列为空,去除失败")
            } else {
                fmt.Println("取出了一个数", val)
            }
​
        case "show":
            queue.ShowQueue()
        case "exit":
            os.Exit(0)
        }
    }
}
(3)循环队列相关用法

因为顺序队列的假溢出的缺点,我们可以使用循环队列(逻辑上的环状空间)

原理:当队首指针Q.front=MaxSize-1后,再前进一个位置就自动到0,

可以通过除法取余运算(%)来实现

提醒(real front等价于tail head)

(1)尾索引的下一个为头索引时表示队列满,即将队列容量空出一个作为约定

(2)判断队列满:(real+1)%maxsize=front

(3)判断队列空:real=front

(4)初始化队列:real=0,front=0

(5)统计队列元素个数:(real-front+maxsize) % maxsize

完整代码:

package main
​
import (
    "errors"
    "fmt"
    "os"
)
// 使用一个结构体管理循环队列
type CircleQueue struct {
    maxSize int    //5
    array   [5]int //数组
    head    int    //指向队列队首
    tail    int    //指向队尾
}
​
// 入队列AddQueue(push) GetQueue(pop)
func (q *CircleQueue) Push(val int) (err error) {
    if q.IsFull() {
        return errors.New("queue full")
    }
    //分析出q.tail 在队列尾部,但是包含最后的元素
    q.array[q.tail] = val
    q.tail = (q.tail + 1) % q.maxSize
    return
}
​
// 出队列
func (q *CircleQueue) Pop() (val int, err error) {
    if q.IsEmpty() {
        return 0, errors.New("queue empty")
    }
    //取出,head是指向队首,并且包含队首元素
    val = q.array[q.head]
    q.head = (q.head + 1) % q.maxSize
    return
}
​
// 判断环形队列为满
func (q *CircleQueue) IsFull() bool {
    return (q.tail+1)%q.maxSize == q.head
}
​
// 判断环形队列为空
func (q *CircleQueue) IsEmpty() bool {
    return q.tail == q.head
}
​
// 去除环形队列有多少个元素
func (q *CircleQueue) Size() int {
    //这是一个关键的算法
    return (q.tail + q.maxSize - q.head) % q.maxSize
}
​
// 显示队列
func (q *CircleQueue) ListQueue() {
    fmt.Println("环形队列情况如下:")
    //取出当前队列中有多少个元素
    size := q.Size()
    if size == 0 {
        fmt.Println("队列为空")
    }
    //设计一个辅助的变量,指向head
    tempHead := q.head
    //因为
    for i := 0; i < size; i++ {
        fmt.Printf("arr[%d]=%d\t", tempHead, q.array[tempHead])
        tempHead = (tempHead + 1) % q.maxSize
    }
    fmt.Println()
}
​
func main() {
    //初始化环形队列
    queue := &CircleQueue{
        maxSize: 5,
        head:    0,
        tail:    0,
    }
​
    var key string
    var val int
    for {
        fmt.Println("1.输入add 表示添加数据到队列")
        fmt.Println("2.输入get 表示从队列获取数据")
        fmt.Println("3.输入show 表示显示队列")
        fmt.Println("4.输入exit 表示退出队列")
​
        fmt.Print("输入你的选项:")
        fmt.Scanln(&key)
        switch key {
        case "add":
            fmt.Print("输出你要入队列的数:")
            fmt.Scanln(&val)
            err := queue.Push(val)
            if err == nil {
                fmt.Println("success,加入队列成功!")
            } else {
                fmt.Println(err.Error())
            }
        case "get":
            val, err := queue.Pop()
            if err != nil {
                fmt.Println("队列为空,去除失败")
            } else {
                fmt.Println("取出了一个数", val)
            }
​
        case "show":
            queue.ListQueue()
        case "exit":
            os.Exit(0)
        }
    }
}
(4)单向链表和双向链表操作
(1)插入节点

1.先定义一个HeroNode结构体

type HeroNode struct {
    no       int
    name     string
    nickname string
    // pre      *HeroNode //双向链表的前结点
​
    next *HeroNode //这个表示指向下一个结点
}

2.给链表插入一个结点[直接在尾部插入]

// 编写第一种插入方法,在单链表的最后加入
func InserHeroNode(head *HeroNode, newHeroNode *HeroNode) {
    //思路
    //1.先找到该链表的最后这个结点
    //2.创建一个辅助结点
    temp := head //辅助结点
    for {
        if temp.next == nil {
            break
        }
        temp = temp.next //让temp不断指向下一个结点
    }
    //3.将newHeroNode插入到最后
    temp.next = newHeroNode
}

3.给链表插入一个结点[根据id顺序插入]

思路 1.先找到该链表的最后这个结点 2.创建一个辅助结点

// 编写第二种插入方法,在单链表的最后加入
func InserHeroNode2(head *HeroNode, newHeroNode *HeroNode) {
    temp := head
    flag := true
    //让插入的结点的no序号和temp的下一个结点的no序号比较
    for {
        if temp.next == nil { //说明到链表的最后
            break
        } else if temp.next.no > newHeroNode.no {
            //说明newHeroNode就应该插入到temp后面
            break
        } else if temp.next.no == newHeroNode.no {
            //说明我们链表中已经有这个no序号了,不插入
            flag = false
            break
        }
        temp = temp.next //让temp不断指向下一个结点
    }
    if !flag {
        fmt.Println("对不起,已经存在该序号", newHeroNode.no)
        return
    } else {
        newHeroNode.next = temp.next
        temp.next = newHeroNode
    }
}
(2)修改节点

思路

1.先找到要删除的这个结点

2.创建一个辅助结点

func AlterHeroNode(head *HeroNode, newHeroNode *HeroNode) {
    temp := head
    flag := false
    for {
        if temp.next == nil {
            return
        } else if newHeroNode.no == temp.next.no {
            flag = true //找到要删除的结点赋值true
            break
        }
        temp = temp.next //让temp不断指向下一个结点
    }
    if flag {
        newHeroNode.next = temp.next.next //让新结点指向被修改节点的下一个结点
        temp.next = newHeroNode           //让被修改的结点的前一个结点指向新结点
    }
}
(3)删除结点

思路 1.先找到要删除的这个结点 2.创建一个辅助结点

// 删除节点
func DeleteHeroNode(head *HeroNode, id int) {
​
    temp := head
    flag := false
    for {
        if temp.next == nil {
            return
        } else if id == temp.next.no {
            flag = true //找到要删除的结点赋值true
            break
        }
        temp = temp.next //让temp不断指向下一个结点
    }
    if flag {
        temp.next = temp.next.next //让temp的下一个结点指向删除节点的下一个结点
    }
}
(4)显示链表的所有信息
func ListHeroNode(head *HeroNode) {
    //1.创建一个辅助结点
    temp := head
    //2.判断是否为空链表
    if temp.next == nil {
        fmt.Println("链表是空的")
        return
    }
    //3.遍历链表
    for {
        fmt.Printf("[%d,%v,%v]==>", temp.next.no, temp.next.name, temp.next.nickname)
        temp = temp.next
        if temp.next == nil {
            break
        }
    }
}
(5)完整代码
package main
​
import (
    "fmt"
)
​
/*
《数据结构-单向链表和双向链表操作》
*/
type HeroNode struct {
    no       int
    name     string
    nickname string
    // pre      *HeroNode //双向链表的前结点
​
    next *HeroNode //这个表示指向下一个结点
}
​
​
// 编写第一种插入方法,在单链表的最后加入
func InserHeroNode(head *HeroNode, newHeroNode *HeroNode) {
    //思路
    //1.先找到该链表的最后这个结点
    //2.创建一个辅助结点
    temp := head //辅助结点
    for {
        if temp.next == nil {
            break
        }
        temp = temp.next //让temp不断指向下一个结点
    }
    //3.将newHeroNode插入到最后
    temp.next = newHeroNode
}
​
// 编写第二种插入方法,在单链表的最后加入
func InserHeroNode2(head *HeroNode, newHeroNode *HeroNode) {
    temp := head
    flag := true
    //让插入的结点的no序号和temp的下一个结点的no序号比较
    for {
        if temp.next == nil { //说明到链表的最后
            break
        } else if temp.next.no > newHeroNode.no {
            //说明newHeroNode就应该插入到temp后面
            break
        } else if temp.next.no == newHeroNode.no {
            //说明我们链表中已经有这个no序号了,不插入
            flag = false
            break
        }
        temp = temp.next //让temp不断指向下一个结点
    }
    if !flag {
        fmt.Println("对不起,已经存在该序号", newHeroNode.no)
        return
    } else {
        newHeroNode.next = temp.next
        temp.next = newHeroNode
    }
}
​
// 修改节点
func AlterHeroNode(head *HeroNode, newHeroNode *HeroNode) {
    temp := head
    flag := false
    for {
        if temp.next == nil {
            return
        } else if newHeroNode.no == temp.next.no {
            flag = true //找到要删除的结点赋值true
            break
        }
        temp = temp.next //让temp不断指向下一个结点
    }
    if flag {
        newHeroNode.next = temp.next.next //让新结点指向被修改节点的下一个结点
        temp.next = newHeroNode           //让被修改的结点的前一个结点指向新结点
    }
}
​
// 删除节点
func DeleteHeroNode(head *HeroNode, id int) {
    temp := head
    flag := false
    for {
        if temp.next == nil {
            return
        } else if id == temp.next.no {
            flag = true //找到要删除的结点赋值true
            break
        }
        temp = temp.next //让temp不断指向下一个结点
    }
    if flag {
        temp.next = temp.next.next //让temp的下一个结点指向删除节点的下一个结点
    }
}
​
// 显示链表的所有信息
func ListHeroNode(head *HeroNode) {
    //1.创建一个辅助结点
    temp := head
    //2.判断是否为空链表
    if temp.next == nil {
        fmt.Println("链表是空的")
        return
    }
    //3.遍历链表
    for {
        fmt.Printf("[%d,%v,%v]==>", temp.next.no, temp.next.name, temp.next.nickname)
        temp = temp.next
        if temp.next == nil {
            break
        }
​
    }
}
​
func main() {
    //1.先创建一个头结点
    head := &HeroNode{}
​
    //2.创建一个新的HeroNode
​
    hero1 := &HeroNode{
        no:       1,
        name:     "猪八戒",
        nickname: "天蓬元帅",
    }
    hero2 := &HeroNode{
        no:       2,
        name:     "孙悟空",
        nickname: "齐天大圣",
    }
    hero3 := &HeroNode{
        no:       3,
        name:     "小玉",
        nickname: "玉面狐狸",
    }
    hero4 := &HeroNode{
        no:       2,
        name:     "唐僧",
        nickname: "金蝉子",
    }
    fmt.Printf("原来的链表:")
    ListHeroNode(head)
​
    //3.加入
    //[InserHeroNode 只在尾部插入 按插入顺序显示]
    //[InserHeroNode2 从小到大排序]
    InserHeroNode2(head, hero1)
    InserHeroNode2(head, hero3)
    InserHeroNode2(head, hero2)
​
    //4.显示
    ListHeroNode(head)
​
    fmt.Printf("\n\n修改节点后的链表:\n")
    //修改节点
    AlterHeroNode(head, hero4) //修改第2个节点
    ListHeroNode(head)
​
    //删除节点
    fmt.Printf("\n\n删除节点后的链表:\n")
    DeleteHeroNode(head, 2) //删除第二个hero
    ListHeroNode(head)
​
    //双向链表中,插入时先创建新结点的联系,再修改原结点直接的联系
    /*
        a->b->新结点(newHeroNode)->d
        插入示例:
        newHeroNode.next = temp.next
        newHeroNode.pre = temp
        temp.next.pre = newHeroNode
        temp.next = newHeroNode
​
        a->b->删除结点(deleteHeroNode)->d
        删除示例:
        temp.next=temp.next.next
        if temp.next !=nil{
            temp.next.pre=temp
        }
    */
}

总结

本文主要为您介绍了数据结构的相关基础知识。

写在最后

本文是我的日常学习笔记,如果哪里有写错,麻烦请指出来,感谢。这里我也推荐大家多写笔记,写笔记是一个很好的习惯,可以帮助我们更好的吸收和理解学习的新知识,新的一年大家一起加油!