Go语言链表学习,从底层理解到做题!

170 阅读4分钟

链表直接硬着头皮看很头疼,但其实它是一个很实用的算法,如果把它当做算法来解释的话,仔细思考还挺有意思的,作者学了几天,踩过了一些难理解的坑,想把知识点分享给大家

这有很多我个人的理解,但是也查了不少资料,如果有误请大佬们到评论区给我指正,谢谢!

再开始前,我们需要知道几个重要的知识点。

  • 链表是由一系列节点组成的,和数组不同。数组在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,将 node1Next 字段设置为指向 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
}