第三天 链表基础
注意
head := &ListNode{}的方式初始化头节点,值为0
如果想要头节点值为nil,需要var head *ListNode = nil
golang如何定义链表?
// 定义单向链表
type Node struct {
value int // 节点的值
next *Node // 指向下一个节点的指针
}
为什么.next可以取到下一个值?
这是因为结构体Node中定义了一个名为next的字段,它是一个指向Node类型的指针。当我们创建一个Node类型的变量时,我们可以用点号(.)来访问它的字段,例如:
node := Node{value: 10, next: nil} // 创建一个Node类型的变量
fmt.Println(node.value) // 访问value字段,输出10
fmt.Println(node.next) // 访问next字段,输出nil
当我们把一个Node类型的变量赋值给另一个Node类型的变量的next字段时,我们就建立了一个指向关系,例如:
node1 := Node{value: 10, next: nil} // 创建第一个节点
node2 := Node{value: 20, next: nil} // 创建第二个节点
node1.next = &node2 // 把第二个节点的地址赋值给第一个节点的next字段
这样,我们就可以通过第一个节点的next字段来访问第二个节点,例如:
fmt.Println(node1.next.value) // 访问第二个节点的value字段,输出20
如何创建一个链表
按题中的意思,给定一个头节点head = [1,2,6,3,4,5,6],如何生成一个链表?
package main
import "fmt"
// 定义一个节点结构体
type Node struct {
value int // 节点的值
next *Node // 指向下一个节点的指针
}
// 定义一个打印链表的函数,接受头节点作为参数
func printList(head *Node) {
// 创建一个临时变量,指向头节点
temp := head
// 遍历链表,打印每个节点的值
for temp != nil {
fmt.Print(temp.value, " ")
temp = temp.next
}
fmt.Println()
}
func main() {
// 创建一个切片,包含1, 2, 6, 3, 4, 5, 6这七个值
slice := []int{1, 2, 6, 3, 4, 5, 6}
// 创建一个空的链表
var head *Node = nil
// 使用一个for循环,遍历切片中的元素,并依次插入到链表尾部
for _, v := range slice {
// 创建一个新的节点,值为v
newNode := &Node{value: v}
// 如果头节点为空,把新节点赋值给头节点
if head == nil {
head = newNode
} else {
// 否则,找到链表的最后一个节点,并把新节点接到它后面
tail := head // 从头部开始遍历
for tail.next != nil { // 找到最后一个节点
tail = tail.next
}
tail.next = newNode // 在最后一个节点后面插入新节点
}
}
// 打印链表中的所有值
printList(head)
}
203. 移出链表元素
标准的方法,死记硬背即可
使用虚拟节点的方法,写法固定
package main
import "fmt"
// 定义单向链表
type ListNode struct {
Val int // 数据域
Next *ListNode // 指针域
}
// 定义一个删除节点的函数,接收一个链表的头节点和要删除的节点的值
func deleteNode(head *ListNode, val int) *ListNode {
dummyHead := &ListNode{}
dummyHead.Next = head
cur := dummyHead
for cur.Next != nil && cur != nil {
if cur.Next.Val == val {
cur.Next = cur.Next.Next
} else {
cur = cur.Next
}
}
// 返回头节点
return dummyHead.Next
}
// 定义一个打印链表的函数,接收一个链表的头节点
func printList(head *ListNode) {
// 创建一个临时变量指向头节点
cur := head
for cur != nil {
fmt.Print(cur.Val, " ")
cur = cur.Next
}
fmt.Println()
}
func main() {
// 创建一个切片
slice := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}
// 创建一个空链表
var head *ListNode = nil
//遍历切片中的元素,依次插入链表的尾部
for _, v := range slice {
// 创建一个新节点,值为v
newNode := &ListNode{Val: v}
//如果头节点为空,把新节点赋值给头节点
if head == nil {
head = newNode
} else {
//找到链表的最后一个节点,把新节点接在它后面
tail := head // 从头节点开始找
for tail.Next != nil {
tail = tail.Next
}
tail.Next = newNode // 把新节点接在最后一个节点后面
}
}
// 打印原始链表
fmt.Println("原始链表:")
printList(head)
// 删除链表中的元素
fmt.Println("删除链表中的元素5后: ")
head = deleteNode(head, 5)
printList(head)
}
output:
原始链表:
1 2 3 4 5 6 7 8 9 0
删除链表中的元素5后:
1 2 3 4 6 7 8 9 0
707 设计链表
设计链表
添加头节点、尾节点、中间节点;定位索引处的值、删除索引处的值。
初始化一个切片[]int{1, 2, 3, 4, 5},生成链表后,在该链表的基础上实现链表的基本操作。
package main
import "fmt"
type Node struct {
val int // 节点值
prev *Node // 前驱节点
next *Node // 后继节点
}
// 定义一个链表结构体
type MyLinkedList struct {
head *Node // 头节点
tail *Node // 尾节点
size int // 链表长度
}
// 初始化链表
func Constructor() MyLinkedList {
return MyLinkedList{
head: &Node{},
tail: &Node{},
size: 0,
}
}
// this 是方法的接受者,可以使用任何合法的标识符号命名,但是通常使用this或self来表示
// 获取链表中下标为index的节点的值,如果下标无效,则返回-1
func (this *MyLinkedList) Get(index int) int {
// 如果下标小于0或者大于等于链表长度,返回-1
if index < 0 || index >= this.size {
return -1
}
// 创建一个临时变量,指向头节点
cur := this.head
// 遍历链表,直到找到下标为index的节点或者到达链表尾部
for i := 0; i < index && cur != nil; i++ {
cur = cur.next
}
// 如果找到了下标为index的节点,返回它的值,否则返回-1
if cur != nil {
return cur.val
} else {
return -1
}
}
// 将一个值为val的节点插入到链表中第一个元素之前,在插入完成后,新节点会成为链表的第一个节点
func (this *MyLinkedList) AddAtHead(val int) {
// 创建一个新的节点,值为val,prev为nil,next指向原来的头节点
newNode := &Node{
val: val,
next: this.head,
}
// 如果头节点不为空,把它的prev指向新节点
if this.head != nil {
this.head.prev = newNode
}
// 把新节点赋值给头节点
this.head = newNode
// 如果尾节点为空,说明链表为空,把新节点也赋值给尾节点
if this.tail == nil {
this.tail = newNode
}
// 链表长度加1
this.size++
}
// 将一个值为val的节点追加到链表的最后一个元素
func (this *MyLinkedList) AddAtTail(val int) {
// 创建一个新的节点,值为val,next为nil,prev指向原来的尾节点
newNode := &Node{
val: val,
prev: this.tail,
}
// 如果尾节点不为空,把它的next指向新节点
if this.tail != nil {
this.tail.next = newNode
}
// 把新节点赋值给尾节点
this.tail = newNode
// 如果头节点为空,说明链表为空,把新节点也赋值给头节点
if this.head == nil {
this.head = newNode
}
}
// 在链表的第index个节点之前插入一个值为val的节点,如果index等于链表的长度,则说明是在链表尾部添加,如果index大于链表的长度,则不会插入节点
func (this *MyLinkedList) AddAtIndex(index int, val int) {
// 如果下标小于0,相当于在头部插入新节点
if index <= 0 {
this.AddAtHead(val)
return
}
// 如果下标等于链表长度,相当于在尾部插入新节点
if index == this.size {
this.AddAtTail(val)
return
}
// 如果下标大于链表长度,不插入新节点
if index > this.size {
return
}
// 创建一个临时变量,指向头节点
cur := this.head
// 遍历链表,直到找到下标为index的节点或者到达链表尾部
for i := 0; i < index && cur != nil; i++ {
cur = cur.next
}
// 如果找到了下标为index的节点,创建一个新的节点,值为val,prev指向原来的节点的prev,next指向原来的节点
if cur != nil {
newNode := &Node{
val: val,
prev: cur.prev,
next: cur,
}
// 如果原来的节点的prev不为空,把它的next指向新节点
if cur.prev != nil {
cur.prev.next = newNode
}
// 把原来节点的prev指向新节点
cur.prev = newNode
//如果原来的节点为头节点,将新节点赋值给头节点
if cur == this.head {
this.head = newNode
}
// 链表长度加1
this.size++
}
}
// 如果下标有效,则删除链表中下标为index的节点。
func (this *MyLinkedList) DeleteAtInedex(index int) {
// 如果下标小于0或者大于等于链表长度,不做任何操作
if index < 0 || index >= this.size {
return
}
//创建一个临时变量,指向头节点
cur := this.head
// 遍历链表,直到找到下标为index的节点或者到达链表尾部
for i := 0; i < index && cur != nil; i++ {
cur = cur.next
}
// 如果找到了下标为index的节点,把它从链表中删除
if cur != nil {
// 如果他的prev不为空,把它prev的next指向它的next
if cur.prev != nil {
cur.prev.next = cur.next
}
// 如果他的next不为空,把它next的prev指向它的prev
if cur.next != nil {
cur.next.prev = cur.prev
}
// 如果他是头节点,把它的next赋值给头节点
if cur == this.head {
this.head = cur.next
}
// 如果他是尾结点,把它的prev赋值给尾结点{
if cur == this.tail {
this.tail = cur.prev
}
// 链表长度减1
this.size--
}
}
// 打印链表中所有元素的值和前后指针地址(用于调试)
func (this *MyLinkedList) Print() {
fmt.Printf("List size: %d\n", this.size)
fmt.Printf("Head: %v\n", this.head)
fmt.Printf("Tail: %v\n", this.tail)
for e := this.head; e != nil; e = e.next {
fmt.Printf("value: %d, prev: %p, next: %p\n", e.val, e.prev, e.next)
}
}
// 定义一个初始化链表的函数,接收一个切片作为参数,返回一个链表
func InitList(slice []int) *MyLinkedList {
// 创建一个空链表
list := Constructor()
// 遍历切片,把切片中的元素依次添加到链表中
for _, v := range slice {
list.AddAtTail(v)
}
// 返回链表
return &list
}
func main() {
slice := []int{1, 2, 3, 4, 5}
// 初始化链表
list := InitList(slice)
// 打印链表
fmt.Println("初始化链表:")
list.Print()
// 在链表头部添加元素
list.AddAtHead(0)
// 打印链表
fmt.Println("在链表头部添加元素:")
list.Print()
// 在链表尾部添加元素
list.AddAtTail(6)
// 打印链表
fmt.Println("在链表尾部添加元素:")
list.Print()
// 在链表中间插入元素
list.AddAtIndex(3, 7)
// 打印链表
fmt.Println("在链表中间插入元素:")
list.Print()
//获取链表下标为2的元素
fmt.Printf("获取链表下标为2的元素:%d\n", list.Get(2))
//获取链表下标为100的元素,应该是-1,表示不存在
fmt.Println(list.Get(100))
// 删除链表下标为2的元素
list.DeleteAtInedex(2)
// 打印链表
fmt.Println("删除链表下标为2的元素:")
list.Print()
}
output:
初始化链表:
List size: 0
Head: &{0 <nil> <nil>}
Tail: &{5 0xc0000080f0 <nil>}
value: 0, prev: 0x0, next: 0x0
在链表头部添加元素:
List size: 1
Head: &{0 <nil> 0xc000008078}
Tail: &{5 0xc0000080f0 <nil>}
value: 0, prev: 0x0, next: 0xc000008078
value: 0, prev: 0xc000008150, next: 0x0
在链表尾部添加元素:
List size: 1
Head: &{0 <nil> 0xc000008078}
Tail: &{6 0xc000008108 <nil>}
value: 0, prev: 0x0, next: 0xc000008078
value: 0, prev: 0xc000008150, next: 0x0
在链表中间插入元素:
List size: 1
Head: &{0 <nil> 0xc000008078}
Tail: &{6 0xc000008108 <nil>}
value: 0, prev: 0x0, next: 0xc000008078
value: 0, prev: 0xc000008150, next: 0x0
获取链表下标为2的元素:-1
-1
删除链表下标为2的元素:
List size: 1
Head: &{0 <nil> 0xc000008078}
Tail: &{6 0xc000008108 <nil>}
value: 0, prev: 0x0, next: 0xc000008078
value: 0, prev: 0xc000008150, next: 0x0
this什么意思?
this是一个方法的接收者,也就是调用这个方法的MyLinkedList类型的指针。
在Go语言中,方法是一种特殊的函数,它可以绑定到某个类型上,这个类型称为方法的接收者。方法的接收者可以是任何类型,不一定是结构体,也可以是基本类型,切片,映射等。方法的接收者可以使用任何合法的标识符命名,但是通常使用this或者self来表示。
例如,如果有一个结构体Person,定义了一个方法SayHello,接收者是*this,那么这个方法可以这样调用:
p := Person{name: "Alice"} // 创建一个Person类型的变量
p.SayHello() // 调用SayHello方法
在SayHello方法内部,this就表示调用这个方法的Person类型的指针,也就是p。所以可以使用this来访问或修改p的字段或者其他方法。例如:
func (this *Person) SayHello() {
fmt.Println("Hello, my name is", this.name) // 访问this的name字段
this.Greet() // 调用this的Greet方法
}
206 反转链表
将链表内的指针转变方向,使链表翻转
package main
import "fmt"
type ListNode struct {
Val int
Next *ListNode
}
func reverseList(head *ListNode) *ListNode {
var pre *ListNode
cur := head
for cur != nil {
next := cur.Next //先保存下一个节点到临时变量,因为指针反转后会断开与下一个节点的链接
cur.Next = pre //指针反转,下一个节点指向pre
pre = cur //pre指向当前节点
cur = next //cur指向下一个节点
}
return pre
}
// 传入一个切片生成链表
func initList(slice []int) *ListNode {
// 创建一个空链表
head := &ListNode{}
// 遍历切片元素,依次插入链表尾部
for _, v := range slice {
// 创建一个新节点,值为v
newNode := &ListNode{Val: v}
// 如果头节点为空,赋值给头节点
if head == nil {
head = newNode
} else {
tail := head
for tail.Next != nil {
tail = tail.Next
}
tail.Next = newNode
}
}
return head
}
func PrintList(head *ListNode) {
cur := head
for cur != nil {
fmt.Print(cur.Val, "")
cur = cur.Next
}
fmt.Println()
}
func main() {
slice := []int{1, 2, 3, 4, 5}
head := initList(slice)
fmt.Println("反转前链表:")
PrintList(head)
fmt.Println("反转后链表:")
head = reverseList(head)
PrintList(head)
}
output:
反转前链表:
012345
反转后链表:
543210
注意
- 翻转指针时,需要先把下一个节点存入一个临时变量中,因为指针断开后,会与下一个节点失去联系。
- 向后传递时,应该先将当前节点cur赋予前面的节点pre,然后再将next临时变量赋予cur,即
next := cur.Next //先保存下一个节点到临时变量,因为指针反转后会断开与下一个节点的链接
cur.Next = pre //指针反转,下一个节点指向pre
pre = cur //pre指向当前节点
cur = next //cur指向下一个节点
两两交换链表中的节点
package main
import "fmt"
/*
有链表dummy,1,2,3,4,5
1. 定义虚拟头节点,初始化为当前节点cur;
2. cur指向2、2指向1、1指向3;
3. 节点2作为cur
4. cur指向4、4指向3、3指向5
当链表数量位偶数时,1,2,3,4,
指针指到4,后边位nil,所以cur.next == nil为结束条件;
当链表数量位奇数时,1,2,3,4,5,
指针指到4,cur.next.next == nil 为结束条件
for循环时,首先要进行cur.next != nil的判定,否则如果先判定cur.next.next == nil
如果cur.next为空的话,会指向空指针
两两交换节点时:
先由cur-》2,此时cur-〉1会断开,所以需要先保存节点1的地址到临时变量temp
同理,节点2-》1后,节点2-〉3会断开,也要先保存节点3的地址到临时变量temp1*/
type ListNode struct {
Val int
Next *ListNode
}
func SwapPairs(head *ListNode) *ListNode {
//定义虚拟头节点指向头节点
dummy := &ListNode{
Next: head,
}
// dummy初始化为cur,通过cur指针处理后面两个节点的交换操作:1->2 交换为 2->1
cur := dummy
// 链表内节点数如果为偶数,如1->2->3->4->5->6, cur指针会在指向第六个节点时结束,此时结束条件为cur.Next == nil;
// 链表内节点数如果为奇数,如1->2->3->4->5->6->7, cur指针会在指向第六个节点时结束,此时结束条件为cur.Next.Next == nil;
// 在结束判定时,应该先写cur.Next != nil, 否则如果先写 cur.Next.Next != nil 会导致一旦存在cur.Next为空的情况,会先指向空指针,导致报错
for cur.Next != nil && cur.Next.Next != nil {
// 首先需要将第一个节点和第三个节点的地址保存到临时变量,因为一旦将 cur.Next -> 2以及将 2.Next -> 1,节点1和节点3的就会被断开
temp1 := cur.Next //存储节点1
temp3 := cur.Next.Next.Next //存储节点3
//下面开始交换节点的操作
cur.Next = cur.Next.Next //前一个节点指向处于交换状态两节点1,2的节点2
cur.Next.Next = temp1 //处于交换状态的两节点1,2中的节点2 指向 节点1
temp1.Next = temp3 // 处于交换状态的两节点1,2中的节点1 指向后面的节点3
cur = cur.Next.Next // 当前节点像后移两位,处于下一对交换状态的两节点3,4的前一个位置
}
return dummy.Next // 交换结束后,返回头节点
}
// 定义一个函数打印链表的节点,传入头节点作为参数
func PrintList(head *ListNode) {
// 创建一个临时变量,指向头节点
temp := head
// 遍历链表,打印链表的值
for temp != nil {
fmt.Print(temp.Val, " ")
temp = temp.Next
}
fmt.Println()
}
// 定义一个函数,将一个切片转换为链表.传入slice切片,返回链表头节点
func SliceToList(slice []int) *ListNode {
// 创建一个空链表
var head *ListNode = nil
// 遍历切片中的元素
for _, v := range slice {
// 创建一个新节点
newNode := &ListNode{
Val: v,
}
// 如果头节点为空,将新节点赋值给头节点
if head == nil {
head = newNode
} else {
// 找到链表的最后一个节点,将新节点放在他后面
tail := head // 从头节点开始找
for tail.Next != nil {
tail = tail.Next
}
tail.Next = newNode
}
}
return head
}
func main() {
// 创建一个切片,将其转换为链表
slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
head := SliceToList(slice)
//打印原始链表
PrintList(head)
// 两两交换链表中的节点
head = SwapPairs(head)
//打印交换后的链表
PrintList(head)
}
output:
0 1 2 3 4 5 6 7 8 9
1 0 3 2 5 4 7 6 9 8
注:
- 链表内节点数如果为偶数,如1->2->3->4->5->6, cur指针会在指向第六个节点时结束,此时结束条件为cur.Next == nil;
- 链表内节点数如果为奇数,如1->2->3->4->5->6->7, cur指针会在指向第六个节点时结束,此时结束条件为cur.Next.Next == nil;
- 在结束判定时,应该先写cur.Next != nil, 否则如果先写 cur.Next.Next != nil 会导致一旦存在cur.Next为空的情况,会先指向空指针,导致报错
- 首先需要将第一个节点和第三个节点的地址保存到临时变量,因为一旦将 cur.Next -> 2以及将 2.Next -> 1,节点1和节点3的就会被断开
- 当前节点cur向后移两位,处于下一对交换状态的两节点3,4的前一个位置;
删除链表中倒数第N个节点
package main
import "fmt"
/* 删除第n个节点 */
type ListNode struct {
Val int
Next *ListNode
}
func RemoveNthFromEnd(head *ListNode, n int) *ListNode {
/* 1. 设置一个快指针right,一个慢指针left,初始在虚拟头节点位置;
2. 要定位倒数第n个节点,先让快指针向后走n步,然后两个指针一起向后走,知道快指针right指向nil,此时left就指向了倒数第n个节点
3. 如果要删除left指向的节点,需要将left先向前移动一个位置left-1,然后将left-1指向left+1,
4. 为了方便,在最开始的时候,先让right向后走n+1个节点,此时遍历完毕后,left指向的就是倒数第n-1个节点了 */
dummy := &ListNode{
Next: head,
}
left := dummy
right := dummy
i := 1
for right != nil {
right = right.Next
if i > n+1 {
left = left.Next
}
i++
}
left.Next = left.Next.Next
return dummy.Next
}
// 打印链表
func PrintList(head *ListNode) {
temp := head
for temp != nil {
fmt.Print(temp.Val, " ")
temp = temp.Next
}
fmt.Println()
}
// 生成链表
func SliceToList(slice []int) *ListNode {
/// 初始化头节点
head := &ListNode{}
for _, v := range slice {
newNode := &ListNode{
Val: v,
}
if head == nil {
head = newNode
} else {
//找到最后一个节点
tail := head
for tail.Next != nil {
tail = tail.Next
}
tail.Next = newNode
}
}
return head
}
func main() {
slice := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
head := SliceToList(slice)
PrintList(head)
// 删除倒数第3个节点
RemoveNthFromEnd(head, 3)
PrintList(head)
}
output:
0 1 2 3 4 5 6 7 8 9
0 1 2 3 4 5 6 8 9
注:
- 设置一个快指针right,一个慢指针left,初始在虚拟头节点位置;
- 要定位倒数第n个节点,先让快指针向后走n步,然后两个指针一起向后走,知道快指针right指向nil,此时left就指向了倒数第n个节点
- 如果要删除left指向的节点,需要将left先向前移动一个位置left-1,然后将left-1指向left+1,
- 为了方便,在最开始的时候,先让right向后走n+1个节点,此时遍历完毕后,left指向的就是倒数第n-1个节点了
链表相交
注意:
这里的相交是指针相同,不仅仅是值相同,即该相交指针后共用一个链表
即,如果一个链表A比链表B长了5个节点,那么链表A的前5个节点就是没用的,因为链表B不够长。
只有遍历到长度相等后,才有相交的可能性。
看来相交也讲究青梅竹马啊。
假如有链表A: 1,2,3,4,5,6,7,8,9
链表B: 1,2,7,8,9
两个链表相交部分为7,8,9
当将两个链表结合在一起时,不论是A+B还是B+A
1,2,3,4,5,6,7,8,9,1,2,7,8,9
1,2,7,8,9,1,2,3,4,5,6,7,8,9
假设有两个指针分别遍历上边两个链表,如果相交的话,那么肯定会在一个地方,两个指针相等,后面的就是相交的链表;
当然,如果不相交的话,那么遍历完都是nil。
这个方法的详细解释应该看官方攻略的双指针解法, 讲的比较详细
func GetIntersectionNode(headA, headB *ListNode) *ListNode {
if headA == nil || headB == nil {
return nil
}
pa, pb := headA, headB
for pa != pb {
if pa == nil {
pa = headB
} else {
pa = pa.Next
}
if pb == nil {
pb = headA
} else {
pb = pb.Next
}
}
fmt.Println(pa.Val)
return pa
}
坑:
最开始的时候依旧是定义了两个切片slice转成链表的形式生成了两个链表,寻找相交链表
但是运行了好多次都是得到的nil
为什么leetcode能运行成功,到本地就运行不成功了呢?
因为我定义了两个slice啊!!!两个slice里面元素的地址当然都不一样啦!!!只是值是相同的而已
困扰了我半个小时,可恶!!!
环形链表II
代码随想录
讲的很详细
func detectCycle(head *ListNode) *ListNode {
left, right := head, head
// 记得这个for循环的条件,不是for right.Next != nil && right.Next.Next != nil
// 可能是因为right本身就可能是nil
for right != nil && right.Next != nil {
left = left.Next
right = right.Next.Next
if left == right {
// 根据代码随想录攻略中给的公式,head走到环入口节点时,left正好走完当前环后又走了n-1圈
// 两个指针正好在环入口处相遇,所以left==head时,正好是在环入口处
for left != head {
head = head.Next
left = left.Next
}
return head
}
}
return nil
}