链表直接硬着头皮看很头疼,但其实它是一个很实用的算法,如果把它当做算法来解释的话,仔细思考还挺有意思的,作者学了几天,踩过了一些难理解的坑,想把知识点分享给大家
这有很多我个人的理解,但是也查了不少资料,如果有误请大佬们到评论区给我指正,谢谢!
再开始前,我们需要知道几个重要的知识点。
- 链表是由一系列节点组成的,和数组不同。数组在Go语言中内存地址是连续的。而链表是非连续的。因为它靠的是指针来连接每一个节点。
- 得益于指针的优势,只传递地址,并不是整个结构体,所以它比较节省内存
1.自己定义一个链表的基本结构
在Go语言中,让我们自己来定义一个链表的基本结构。(这一步,可能很多同学就蒙了,比如我。指针学的不扎实)。
Val:用来储存每个节点的值的
Next:这里的Next字段其实就是指向Node结构体的指针。
第一次看得人会很蒙,为什么在struct里面还需要用Next在指向结构体本身
(这句话先放着,等到下面的例子再解释为什么要这么指向?)
type Node struct {
Val int
Next *Node
}
这就是一个最基本链表结构,当然你可以根据情况在里面加上其他数据,比如
type MyLinkedList struct {
size int
dummyHead *Node //虚拟头节点
dummyTail *Node //虚拟尾部节点
}
2.上例子!!!
下面这个例子就分成了步骤。
定义链表结构→定义几个链表节点→链接节点→最后的把链表定义出来
创建链表节点: node1 := &ListNode{Val: 1}
- &符号获取地址值是为了创建指向结构体实例的指针,要知道链表和指针的关系密不可分。
{Val:}结构体可以指定某个值赋值,如果不加上Val:则会报错
链接节点: node1.Next = node2
- 为什么
Next要指向*ListNode?原因就在连接节点上,因为node1.Next相当于新开辟了一个内存地址, 并将node2指向这块内存。 - 然后,通过
node1.Next = node2,将node1的Next字段设置为指向node2,即node1.Next现在存储的是node2的内存地址。
遍历链表: current = current.Next
因为我们不能改变原先的头节点,所以遍历或者其他操作的时候,需要定义一个current来指向头节点,进行便利操作。
这里顾名思义就是当current一直指向下一个节点的for循环,就可以将所有的节点遍历出来
package main
import "fmt"
// 定义链表节点结构体
type ListNode struct {
Val int
Next *ListNode
}
func main() {
// 创建链表节点
node1 := &ListNode{Val: 1}
node2 := &ListNode{Val: 2}
node3 := &ListNode{Val: 3}
// 连接节点
node1.Next = node2
node2.Next = node3
// 遍历链表
current := node1
for current != nil {
fmt.Println(current.Val)
current = current.Next
}
}
3.做几道题加深印象
力扣题203(移除链表元素): leetcode.cn/problems/re…
(代码随想录)卡哥讲解:www.bilibili.com/video/BV18B…
可以先思考一下,下面揭晓答案
方法一:
就是我们正常的思路,把整体框架写出来,头节点需要单独判断。我在每一行都写上了注释,方便大家理解
- 为什么要把ListNode定义注释掉?
因为力扣贴心的不让我们输入,它已经定义过了,并不是你平时做题的时候不用定义
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func removeElements(head *ListNode, val int) *ListNode {
//移除元素的思想是把后面的元素移动到前面,头节点前面没有元素,所以头节点需要单独判断
//这个判断也可以解决连续同样的元素的问题,例如{7,7,7,7},删除7元素
for head != nil && head.Val == val {
head = head.Next
}
cur := head //cur流通,来接收头节点
for cur != nil && cur.Next != nil { //cur本身不能等于空值,cur的下一个也不能等于空值,不然会引发索引越界
if cur.Next.Val == val {
cur.Next = cur.Next.Next //头元素已经处理过了,所以默认就是头元素的下一个元素判断
} else {
cur = cur.Next //继续把cur转到下一个节点,最终完成循环
}
}
return head //head才是头结点,cur只是一个临时变量,所以要返回头节点
}
方法二:虚拟头节点
普通的方法不能直接判断头节点,因为需要头节点前面要有元素。虚拟头节点的中心思想就是在头节点前面设置一个虚拟的节点,方便判断头节点。
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func removeElements(head *ListNode, val int) *ListNode {
dummy := &ListNode{} //先定义dummy,让它引用&ListNode{}
dummy.Next = head //dummy的设计思路是在head的前面,所以让它的Next指向head
cur := dummy //定义流通cur,等于dummy
for cur != nil && cur.Next != nil {
if cur.Next.Val == val { //cur接收了dummy,所以接下来全用cur做定义
cur.Next = cur.Next.Next
} else {
cur = cur.Next
}
}
return dummy.Next //因为dummy是头节点的前一个节点,我们需要返回头节点,所以用dummy.Next
}