定义:
链表是物理存储单元上非连续、非顺序的存储结构
特点:
数据元素的逻辑顺序是通过链表中的指针链接次序实现的
概括:
链表是一种数据存储结构,通过指针指向定义它的链接次序。有点像区块链。只不过一个链表的结构体拥有的元素比区块链的区块少
链表的每个点称为节点,一个节点包括:
-
数据域
-
指针域
链表的优缺点
优点:
免在使用数组时需要预先知道数据大小的缺点
充分利用计算机内存空间,实现灵活的内存动态管理
缺点:
失去了数组随机读取的优点
空间开销比较大
链表的种类
在之前的数据结构一章讲过,链表属于线性表的一种。链表的种类有:
-
单向链表
-
双向链表
一个完整的链表需要包含的元素
-
首元节点
-
头节点
-
指针
一个节点包括:
-
数据
-
指针
头节点在链表中的好处:
-
首元结点的地址保存在头结点的指针域中,对链表的第一个数据元素的操作与其他数据元素相同,无需进行特殊处理。
-
无论链表是否为空,头指针都是指向头结点的非空指针,若链表为空的话,那么头结点的指针域为空
Go定义单链表
使用Struct
关键字定义节点:
package main
/*
定义节点
*/
type Node struct {
// 数据域
Data int
// 指针域
Next *Node
/*
Data 用来存放结点中的有用数据
Next 是指针类型的成员,它指向 Node struct 类型数据,也就是下一个结点的数据类型
*/
}
为链表赋值并遍历节点:
package main
import "fmt"
/*
定义一个遍历链表节点的方法
*/
func ShowNode(p *Node) {
// 节点不为空就打印
for p != nil {
if p != nil {
fmt.Println(*p) // '*'符号+变量名获取到变量的指针地址
// 指针域指向下一个节点的数据域
p = p.Next
}
}
}
/*
调用该方法
*/
func main() {
// 定义头节点
var head = new(Node)
// 定义该节点的数据域和指针域
head.Data = 1
// 定义第二个节点
var node1 = new(Node)
// 定义数据域
node1.Data = 2
// 讲头节点指向第二个节点
head.Next = node1
// 定义第三个节点
var node2 = new(Node)
// 定义数据域
node2.Data = 3
// 将第二个节点与第三个节点连接
node1.Next = node2
//调用打印链表的方法
ShowNode(head)
}
插入节点
头插法
本质:
每次都在链表的头部插入数据,相当于修改新插入的节点都是头节点。
实现:
只需要构造一个头指针,该指针只指向头节点即可。每次插入节点都将该指针指向最新的节点
package main
import (
"fmt"
)
/*
定义节点
*/
type Node2 struct {
// 数据域
Data int
// 指针域
next *Node2
}
/*
定义遍历打印方法:
传入指针变量,每次修改指针变量进行读取
*/
func ShowNode2(p *Node2) {
for p != nil {
fmt.Println(*p) // 打印指针在此处的地址值
// 移动指针指向下一个节点
p = p.next
}
}
/*
头插法插入链表
*/
func main() {
// 定义头节点
var head = new(Node2)
// 定义头节点数据域
head.Data = 0
// 定义头指针,该指针只会指向头节点,这是一个指针变量,指向节点
var top *Node2
// 头指针指向头节点,并且始终指向头节点
top = head
// 循环添加节点,当每次添加节点成功了以后修改头指针的指向,让其指向头节点
for i := 1; i < 10; i++ {
// 新建节点
var node = Node2{Data: i}
// 每次添加成功都将头指针指向新增的节点
node.next = top
// 给头指针重新赋值
top = &node
}
// 调用打印节点方法
ShowNode2(top)
}
尾插法
本质:
每次插入数据直接修改头节点的指针值,将新加入的节点的指针赋值给头节点
实现:
package main
import (
"fmt"
)
/*
定义节点
*/
type Node2 struct {
// 数据域
Data int
// 指针域
next *Node2
}
/*
定义遍历打印方法:
传入指针变量,每次修改指针变量进行读取
*/
func ShowNode2(p *Node2) {
for p != nil {
fmt.Println(*p) // 打印指针在此处的地址值
// 移动指针指向下一个节点
p = p.next
}
}
/*
头插法插入链表
*/
func main() {
// 定义头节点
var head = new(Node2)
// 定义头节点数据域
head.Data = 0
// 定义头指针,该指针只会指向头节点,这是一个指针变量,指向节点
var top *Node2
// 头指针指向头节点,并且始终指向头节点
top = head
// 尾插法插入节点数据
for i := 1; i < 10; i++ {
// 新建节点
var node = Node2{Data: i}
// 每次添加成功都修改头节点的指针域的值,将新插入的节点指针值赋值给头节点
(*top).next = &node
// 重新给头节点赋值
top = &node
}
// 调用打印节点方法
ShowNode2(top)
}
访问元素,链表没有数组高效。因为数组的存储地址是连续的。
新增和删除元素,链表比数组高效很多。因为链表本身的地址就是不连续的。
循环链表
本质:
链表最后的节点的指针域指向头节点的数据域。实现的时候就需要固定一个头节点
实现:
package main
import "fmt"
/*
定义节点
*/
type Node3 struct {
// 数据域
Data int
//指针域
next *Node3
}
/*
循环打印指针节点内容方法
*/
func ShowNode3(p *Node3) {
// 如果p不为空就打印内容
for p != nil {
fmt.Println(*p)
// 移动指针指向下一个节点
p = p.next
}
}
/*
双指针方法定位循环链表结构
*/
func main() {
// 定义头节点
var head = new(Node3)
// 赋值头节点的数据域
head.Data = 0
// 定义两个指针,一个作为头指针一个作为移动指针
var headTop *Node3
var moveNode *Node3
headTop = head
moveNode = head
// 循环添加节点,使用尾插法
for i := 1; i < 10; i++ {
// 新建节点
var node = Node3{Data: i}
// 每次修改移动指针的指针域指向新建的节点的地址
(*moveNode).next = &node
// 重新给移动节点赋值
moveNode = &node
if i == 9 {
// 将移动节点的指针域指向头节点
(*moveNode).next = headTop
}
}
ShowNode3(headTop)
}
双向链表
本质:
链表的一个节点不止有后继指针,还有前驱指针。前驱指针指向前一个节点。后继指针指向下一个节点
特点:
优点:
-
可以支持双向遍历
缺点:
-
需要额外的两个空间来存储后继结点和前驱结点的地址。存储同样多的数据,双向链表要比单链表占用更多的内存空间
package main
import "fmt"
/*
定义双向链表节点信息
*/
type Node4 struct {
// 前驱节点
prev *Node4
// 数据域
Data int
// 后继节点
next *Node4
}
/*
遍历节点的方法
*/
func ShowNode4(p *Node4) {
for p != nil {
fmt.Println(*p)
// 打印前驱节点如果存在
if p.prev != nil {
fmt.Println(*p.prev)
}
// 将指针移向下一个节点
p = p.next
}
}
/*
遍历双向链表
*/
func main() {
// 定义头节点
var head = new(Node4)
// 定义头节点的数据域
head.Data = 0
// 定义头节点的指针域
head.prev = nil
// 定义头指针
var headTop *Node4
// 添加节点使用尾插法
for i := 1; i < 10; i++ {
// 新建节点
var node = Node4{Data: i}
// 修改头指针指向新建节点
(*headTop).next = &node
// 修改新建的节点的前驱指针指向头指针
node.prev = headTop
// 更新头指针
headTop = &node
}
ShowNode4(headTop)
}
// 此答案有误