第二课 golang数据结构和算法-链表
tags:
- golang
- 2019尚硅谷
categories:
- golang
- 链表
- 约瑟夫问题
文章目录
第一节 链表
1.1 链表介绍
- 链表是有序的列表,但是它在内存中是存储(不连续)如下:
1.2 单向链表
- 单链表的示意图:
- 说明:一般来说,为了比较好的对单链表进行增删改查的操作,我们都会给他设置一个头结点,头结点的作用主要是用来标识链表头,本身这个结点不存放数据。
1.3 单向链表的应用实例
- 使用带head头的单向链表实现–水浒英雄排行榜管理 案例的说明:
- 完成对英雄人物的增删改查操作,删除和修改,查找
- 第一种方法在添加英雄时,直接添加到链表的尾部
- 第二种方式在添加英雄时,根据排名将英雄插入到指定位置(如果有这个排名,则添加失败并给出提示。
- 第一种方法代码实现:
package main
import (
"fmt"
)
// 定义一个HeroNode
type HeroNode struct{
no int // 编号
name string // 名称
nickname string // 花名
next *HeroNode //这个表示指向下一个节点
}
// 给链表插入一个节点
// 编写第一种插入方法, 在单链表的最后加入
func InsertHeroNode(head *HeroNode, newHeroNode *HeroNode){
// 思路 1. 先找到该链表的最后这个节点
// 2. 创建一个辅助节点 【用来帮忙的】
temp := head
for {
if temp.next == nil{ //表示找到最后了
break
}
temp = temp.next // 让temp不断的指向下一个节点
}
// 3. 加入到链表的最后 (现在temp已经位于链表末尾了)
temp.next = newHeroNode
}
// 显示链表的所有节点信息
func ListHeroNode(head *HeroNode){
// 1. 创建一个辅助节点 来帮忙 头节点不要动
temp := head
if temp.next == nil{
fmt.Println("链表为空链表!")
return
}
// 2. 至少有一个节点 遍历这个链表
for {
fmt.Printf("[%d, %s, %s]==>", temp.next.no,
temp.next.name, temp.next.nickname)
// 判读是否到链表尾部
temp = temp.next
if temp.next == nil{
break
}
}
}
// 删除节点
func DelHeroNode(head *HeroNode, id int){
temp := head
flag := false
for {
if temp.next == nil{
break
}else if temp.next.no > id{
break
}else if temp.next.no == id{
flag = true
break
}
temp = temp.next
}
if !flag{
fmt.Println("对不起,不存在no=", id)
return
}else{
temp.next = temp.next.next
}
}
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: "豹子头",
}
// 3. 加入一个节点
InsertHeroNode(head, hero1)
InsertHeroNode(head, hero2)
InsertHeroNode(head, hero3)
// 4. 显示
ListHeroNode(head)
fmt.Println()
// 删除
DelHeroNode(head, 2)
ListHeroNode(head)
}
- 第二种方法实现:根据no 的编号从小到大插入…
// 给链表插入一个节点 拍好之后插入数据库 会节省很多order_by内存
// 编写第二种插入方法, 根据no 的编号从小到大插入..
func InsertHeroNode(head *HeroNode, newHeroNode *HeroNode){
// 1. 找到适当的节点
// 2. 创建一个辅助节点 【用来帮忙的】
temp := head
flag := true
for {
// 让插入节点的no 和temp的下一个节点的no 进行比较
if temp.next == nil{ // 到链表的最后了
break
}else if temp.next.no > newHeroNode.no{
// 说明newHeroNode 就应该插入到temp后面
break
}else if temp.next.no == newHeroNode.no{
// 链表中已经有这个排名 不让插入
flag = false
break
}
temp = temp.next
}
if !flag{
fmt.Println("对不起,已经存在no=", newHeroNode.no)
return
}else{
// 3. 插入节点
newHeroNode.next = temp.next
temp.next = newHeroNode
}
}
第二节 双向链表
2.1 双向链表介绍
- 单向链表的缺点分析
- 单向链表,查找的方向只能是一个方向,而双向链表可以向前或者向后查找
- 单向链表不能自我删除,需要靠辅助节点,而双向链表,则可以自我删除,所以前面我们单链表删除时节点,总是找到temp的下一个节点来删除的。
- 双向链表示意图
2.2 双向链表应用实例
package main
import (
"fmt"
)
// 定义一个HeroNode
type HeroNode struct{
no int // 编号
name string // 名称
nickname string // 花名
pre *HeroNode // 这个表示指向前面一个节点
next *HeroNode //这个表示指向下一个节点
}
// 给双向链表插入一个节点
// 编写第一种插入方法, 在单链表的最后加入
func InsertHeroNode(head *HeroNode, newHeroNode *HeroNode){
// 思路 1. 先找到该链表的最后这个节点
// 2. 创建一个辅助节点 【用来帮忙的】
temp := head
for {
if temp.next == nil{ //表示找到最后了
break
}
temp = temp.next // 让temp不断的指向下一个节点
}
// 3. 加入到链表的最后 (现在temp已经位于链表末尾了)
temp.next = newHeroNode
newHeroNode.pre = temp
}
// 显示链表的所有节点信息
func ListHeroNode(head *HeroNode){
// 1. 创建一个辅助节点 来帮忙 头节点不要动
temp := head
if temp.next == nil{
fmt.Println("链表为空链表!")
return
}
// 2. 至少有一个节点 遍历这个链表
for {
fmt.Printf("[%d, %s, %s]==>", temp.next.no,
temp.next.name, temp.next.nickname)
// 判读是否到链表尾部
temp = temp.next
if temp.next == nil{
break
}
}
}
// 逆序打印链表的所有节点信息
func ListHeroNode1(head *HeroNode){
temp := head
if temp.next == nil{
fmt.Println("链表为空链表!")
return
}
for {
if temp.next == nil{
break
}
temp = temp.next
}
for {
fmt.Printf("[%d, %s, %s]==>", temp.no,
temp.name, temp.nickname)
// 判读是否到链表尾部
temp = temp.pre
if temp.pre == nil{
break
}
}
}
// 删除节点
func DelHeroNode(head *HeroNode, id int){
temp := head
flag := false
for {
if temp.next == nil{
break
}else if temp.next.no > id{
break
}else if temp.next.no == id{
flag = true
break
}
temp = temp.next
}
if !flag{
fmt.Println("对不起,不存在no=", id)
return
}else{
temp.next = temp.next.next
if temp.next != nil{
// 这里需要注意 上面除了连线 还移动了一下
temp.next.pre = temp.next
}
}
}
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: "豹子头",
}
// 3. 加入一个节点
InsertHeroNode(head, hero1)
InsertHeroNode(head, hero2)
InsertHeroNode(head, hero3)
// 4. 显示
ListHeroNode(head)
fmt.Println()
fmt.Println("逆序打印")
ListHeroNode1(head)
fmt.Println()
// 删除
DelHeroNode(head, 2)
ListHeroNode(head)
}
2.3 双向链表添加优化
func InsertHeroNode(head *HeroNode, newHeroNode *HeroNode){
// 1. 找到适当的节点
// 2. 创建一个辅助节点 【用来帮忙的】
temp := head
flag := true
for {
// 让插入节点的no 和temp的下一个节点的no 进行比较
if temp.next == nil{ // 到链表的最后了
break
}else if temp.next.no > newHeroNode.no{
// 说明newHeroNode 就应该插入到temp后面
break
}else if temp.next.no == newHeroNode.no{
// 链表中已经有这个排名 不让插入
flag = false
break
}
temp = temp.next
}
if !flag{
fmt.Println("对不起,已经存在no=", newHeroNode.no)
return
}else{
// 3. 插入节点
newHeroNode.next = temp.next
newHeroNode.pre = temp
if temp.next != nil{
temp.next.pre = newHeroNode
}
temp.next = newHeroNode
}
}
第三节 单向环形链表
3.1 单向环形链表介绍
3.2 单向环形链表实现
- 删除一个环形单向链表的思路如下:
- 先让temp指向head
- 先让helper指向head遍历后,让helper指向环形链表最后.
- 让temp和要删除的id进行比较,如果相同,则同helepr完成删除[这里必须考虑如果删除就是头结点]
- 返回一个新的head, 防止head指向删除节点
package main
import (
"fmt"
)
// 定义猫的结构体
type CatNode struct{
no int // 猫猫编号
name string
next *CatNode
}
// 单向链表和双向链表的头节点都是空的 但是环形链表的头节点要有数据
func InsertCatNode(head *CatNode, newCatNode *CatNode){
// 判断是不是添加的第一只猫
if head.next == nil{
head.no = newCatNode.no
head.name = newCatNode.name
head.next = head // 形成环形
return
}
// 定义一个临时变量,帮忙找到环形最后的节点
temp := head
for {
if temp.next == head{
break
}
temp = temp.next
}
// 加入到链表中
temp.next = newCatNode
newCatNode.next = head
}
// 输出这个环形的链表
func ListCircleLink(head *CatNode){
fmt.Println("环形链表输出如下")
temp := head
if temp.next == nil{
fmt.Println("空的环形列表")
}
for{
fmt.Printf("猫的信息为=[id=%d name=%s]->\n", temp.no, temp.name)
if temp.next == nil{
break
}
if temp.next == head{
break
}
temp = temp.next
}
}
// 删除一个节点
func DeleteCatNode(head *CatNode, id int) *CatNode{
temp := head
helper := head
// 空链表
if temp.next == nil{
fmt.Println("这是一个空的环形链表 删不了")
return head
}
// 如果只有一个节点
if temp.next == head{
if temp.no == id{
temp.next = nil
fmt.Printf("删除了no=%d的猫猫", id)
return head
}
fmt.Printf("没有no是%d猫", id)
return head
}
// 将helper 定位到链表的最后
for {
if helper.next == head{
break
}
helper = helper.next
}
// 如果有两个以上的节点
flag := true
for{
if temp.next == head{ // 找了一圈还没找到 [最后一个还没比较]
break
}
if temp.no == id{
// 删除的刚好是头节点
if temp == head {
head = head.next
}
// 找到要删除的节点
helper.next = temp.next
fmt.Printf("删除了no=%d的猫猫", id)
flag = false
break
}
temp = temp.next // 移动 用于比较
helper = helper.next // 移动 找到helper能帮我们删除它
}
// 这里是上面的最后一个
if flag { //上面没有删除过flag为真
if temp.no == id{
helper.next = temp.next
fmt.Printf("删除了no=%d的猫猫", id)
}else{
fmt.Printf("没有no是%d猫", id)
}
}
return head
}
func main(){
// 这里我们初始化一个环形链表的头节点
head := &CatNode{}
// 创建一只猫
cat1 := &CatNode{
no : 1,
name : "tom",
}
// cat2 := &CatNode{
// no : 2,
// name : "Som",
// }
// cat3 := &CatNode{
// no : 3,
// name : "pom",
// }
InsertCatNode(head, cat1)
// InsertCatNode(head, cat2)
// InsertCatNode(head, cat3)
ListCircleLink(head)
// 这里必须给head重新赋值
head = DeleteCatNode(head, 1)
ListCircleLink(head)
}
第四节 约瑟夫问题
4.1 约瑟夫问题介绍
- Josephu问题为:设编号为1,2,… n的n个人围坐一圈,约定编号为k(l<=k<=n)的人从1开始报数,数到m的那个人出列,它的下一位又从1开始报数,数到m的那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号的序列。
- 方案:用一个不带头结点的循环链表来处理Josephu问题:先构成一个有n个结点的单循环链表,然后由k结点起从1开始计数,计到m时,对应结点从链表中删除,然后再从被删除结点的下一个结点又从1开始计数,直到最后一个结点从链表中删除算法结束。
4.2 约瑟夫问题解决
package main
import(
"fmt"
)
// 小孩的结构体
type Boy struct{
No int // 编号
Next *Boy // 指向下一个小孩的指针
}
// 编写一个函数,构成单向的环形链表
// num:表示小孩的个数
func AddBoy(num int) *Boy {
first := &Boy{} //空节点
curBoy := &Boy{} //空节点 辅助节点
// 判断
if num < 1{
fmt.Println("num的值不对")
return first
}
// 循环的构建这个环形链表
for i := 1; i <= num; i++{
boy := &Boy{
No : i,
}
// 分析构成循环链表,需要一个辅助指针[帮忙的]
//1. 第一个小孩比较特殊
if i == 1{ //构建第一个小孩
first = boy
curBoy = boy
curBoy.Next = first
}else{
curBoy.Next = boy
curBoy = boy
curBoy.Next = first //构造环形链表
}
}
return first
}
// 显示单向的环形链表[遍历]
func ShowBoy(first *Boy){
// 处理一下 如果环形链表为空
if first.Next == nil{
fmt.Println("链表为空")
return
}
//创建一个指针 帮助遍历
curBoy := first
for {
fmt.Printf("小孩编号=%d ->", curBoy.No)
// 退出
if curBoy.Next == first{
break
}
curBoy = curBoy.Next
}
}
func GetNumBoy(first *Boy) int {
curBoy := first
sum := 0
if first.Next == nil{
fmt.Println("链表为空")
return sum
}
for {
sum++
if curBoy.Next == first{
break
}
curBoy = curBoy.Next
}
return sum
}
//分析思路
//1.编写一个函数,PlayGame(first*Boy,startNo int,countNum int) 头, 开始位置, 计数长度
//2.最后我们使用一个算法,按照要求,在环形链表中留下最后一个人
func PlayGame(first *Boy, startNo int, countNum int){
//1. 空的链表我们单独处理
if first.Next == nil{
fmt.Println("空的链表, 没有小孩")
return
}
fmt.Println(GetNumBoy(first))
if startNo > GetNumBoy(first){
fmt.Println("参数输入错误")
return
}
// 2. 创建一个辅助节点, 帮我们删除小孩
tail := first
// 3. 让tail执行环形链表的最后一个小孩 tail在删除时需要用到
for{
if tail.Next == first{ //说明tail到了最后一个小孩
break
}
tail = tail.Next
}
// 4. 让fisrt移动到startNo [后面我们删除小孩 就用first做标准]
for i := 0; i < startNo -1; i++{
first = first.Next
tail = tail.Next
}
// 5. 开始数countNum 然后就删除first 指向的小孩
for{
// 开始数countNum-1次
for i := 0; i < countNum - 1; i++{
first = first.Next
tail = tail.Next
}
fmt.Printf("小孩编号为%d出圈->", first.No)
// 删除first 指向的小孩
first = first.Next
tail.Next = first
// 如果tail==first,圈中只有一个小孩
if tail == first{
break
}
}
fmt.Printf("小孩编号为%d出圈->", first.No)
}
func main(){
first := AddBoy(5)
// 显示
ShowBoy(first)
PlayGame(first, 2, 3)
}