链表由一组节点组成(链表中的每个元素称为节点),节点可以在运行时动态生成。每个节点由两部分组成。一个是数据字段,存储数据元素,另一个是指针字段,存储下一个节点的地址。
链表结构避免了使用数组时必须提前知道数据大小的缺点,充分利用了计算机的内存空间,并允许灵活的动态内存管理。但链表失去了数组随机读取的优势,同时由于节点指针字段的增加而产生了比较大的空间开销。
链表允许在链表中的任何位置插入和删除节点,但不允许随机访问。链表有三种类型:单链表、双向链表和循环链表。
单链表
单向链表中的每个节点包含两部分:数据字段和指针字段,其中前一个节点的指针指向下一个节点,它们依次连接形成链表。
这里引入三个概念:头节点、头节点、头指针。
- 头节点:存储链表第一个元素的节点,如下图的位置a1。
- 头节点:附加在头节点之前的节点,其指针字段指向头节点。头节点的数据字段可以包含链表的长度或其他信息,也可以为空,不包含任何信息。
- 头指针:指向链表第一个节点的指针。如果链表中有头节点,则头指针指向头节点;如果链表中没有头节点,则头指针指向头节点。
链表不需要头节点,但添加一个头节点可以带来以下好处:
- 添加头节点后,头节点的地址存储在头节点的指针字段中,对链表的第一个数据元素的操作与对其他数据元素的操作相同,无需任何特殊处理。
- 添加头节点后,无论链表是否为空,头指针都变成指向头节点的非空指针。如果链表为空,则头节点的指针字段将为空。
使用 Struct 定义单链表
结构体可以容纳多种数据类型,非常适合用作链表中的节点。一个结构体可以包含多个成员,这些成员可以是基本类型、自定义类型、数组类型或指针类型。这里,可以使用指针类型成员来存储下一个节点的地址。
【例1】使用Struct定义单向链表。
type Node struct {
Data int
Next *node
} 这些成员中,成员Data用于存储节点中有用的数据,Next是指针类型成员,指向Node结构体类型的数据,即下一个节点的数据类型。
【例2】给一个链表赋值,并遍历链表中的每个节点。
package main
import "fmt"
type Node Struct {
data int
next *Node
}
func Showned(p * Node) {//トラバース
for p!=オフ {
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)
} 操作的结果将是:
{1 0xc00004c1e0}
{2 0xc00004c1f0}
{3 <nil>}
插入节点
单链表中插入节点一般采用头插入法和尾插入法。
1) 头部插入方式
每次插入都会在链表的开头插入一个节点。这是代码:
package main
import "fmt"
type Node struct { //ノードの構造体
data int //データ部分
next *Node //次のノードのアドレス
}
func Shownode(p *Node){ //ノードを巡回する関数
for p != nil{
fmt.Println(*p)
p=p.next //ポインタを移動
}
}
func main() {
var head = new(Node)
head.data = 0
var tail *Node
tail = head //tail はヘッドノードのアドレスを参照する。最初は、tail のポインタはヘッドノードを指している。
for i :=1 ;i<10;i++{
var node = Node{data:i}
node.next = tail //新しいノードのnextをヘッドノードに設定する
tail = &node //ヘッドノードを再度割り当てる
}
Shownode(tail) //出力
} 操作的结果将是:
{9 0xc000036270}
{8 0xc000036260}
{7 0xc000036250}
{6 0xc000036240}
{5 0xc000036230}
{4 0xc000036220}
{3 0xc000036210}
{2 0xc000036200}
{1 0xc0000361f0}
{0 <nil>}
2)尾插法
这也是我们经常使用的方法,每当在末尾插入一个节点时。
package main
import "fmt"
type Node struct{
data int
next *node
}
func Shownode(p * Node){ //トラバース
for p!= nil{
fmt.Println(*p)
p = p.next //ポインターを移動する
}
}
func main(){
var head = new(Node)
head.data=0
var tail *Node
tail = head // tailは最後のノードのアドレスを記録するために使用されます。 最初は、tailのポインターはヘッドノードを指します
for i:= 1; i <10; i ++ {
var node = Node{data:i}
node.next = tail
tail = &node
}
Shownode(head)//トラバース結果
} 操作的结果将是:
{0 0xc0000361f0}
{1 0xc000036200}
{2 0xc000036210}
{3 0xc000036220}
{4 0xc000036230}
{5 0xc000036240}
{6 0xc000036250}
{7 0xc000036260}
{8 0xc000036270}
{9 <nil>}
当插入或删除数组时,由于必须移动大量数据以保持内存数据连续性,因此速度相对较慢。链表本身的存储是不连续的,因此在链表中插入或删除数据不需要移动节点来保持内存连续性。因此,从链表中插入和删除数据是非常快的。
然而,有优点也有缺点。如果要随机访问第 k 个元素,链表的效率不如数组。由于链表中的数据不是连续存储的,所以不能像数组中那样根据首地址和下标通过寻址表达式直接计算出对应的内存地址,而是通过指针一次一个节点的遍历。你需要追踪它。找到对应的节点。
循环链表
循环链表是单链表的一种特殊类型。
循环链表和单链表唯一的区别是尾节点。单向链表中的结束节点指针指向一个空地址,表明这是最后一个节点。另一方面,循环链表的结束节点指针指向链表最后连接的第一个节点。它被称为“循环”,因为它以环形结束,如下图所示。 ” 链接列表。
循环链表相对于单链表的优点是从链尾到链头比较方便。当要处理的数据具有环形结构时,循环链表特别适合。例如,著名的约瑟夫问题可以用单链表实现,但用循环链表实现时代码就简单得多。

单向链表只有一个方向,一个节点只有一个指向下一个节点的尾随指针。顾名思义,双向链表支持两个方向,每个节点都有多个后继指针next指向下一个节点,多个前驱指针prev指向前一个节点。
双向链表需要两个额外的空间来存储后继节点和前驱节点的地址。因此,在存储相同数据量的情况下,双链表比单链表占用更多的内存空间。两个指针虽然浪费存储空间,但是它们可以支持双向遍历,这也为您提供了双向链表操作的灵活性。




![2021 年如何设置 Raspberry Pi Web 服务器 [指南]](https://i0.wp.com/pcmanabu.com/wp-content/uploads/2019/10/web-server-02-309x198.png?w=1200&resize=1200,0&ssl=1)

