线性表的实现分顺序存储结构和链式存储结构
上一节我们主要介绍了顺序存储结构,在最后我们还分别总结了顺序存储结构的优缺点,
对于顺序结构的缺点,我们有没有什么好的解决方法呢?
我们今天要介绍的线性表的链式存储结构就可以很好的解决顺序结构的缺点,一起来看。
顺序结构最大的缺点就是在进行插入和删除操作的时候,如果插入位置不理想,那么我们需要移动大量的元素,那产生这一问题的原因是什么呢?
仔细分析后,我们可以发现在顺序存储结构中,他们相邻的元素的存储位置也是相邻的,我们在申请内存的的时候,是一次性申请一块连续的内存,中间是没有空隙的,这样我们也就没办法进行快速的插入,如果进行删除操作,就需要进行位置的填充。
链式存储结构之所以能很好地解决这个问题,原因就在于它不考虑存储位置的相邻关系了,哪里有空位就存到哪里,我们只需要让每个元素都知道下一个元素的位置在哪里就可以了。
这样就是我们在定义链式存储结构的时候,除了定义它本身需要存储的信息之外,还需要存储一个能够指示它直接后继的一个信息,这个信息我们可以用指针来表示。
这也就是我们课本上讲的数据域和指针域。
我们将这种只带有一个指针域的线性表称为单链表。
链表中第一个结点的存储位置叫做头指针。
单链表的第一个结点钱附设一个结点,称为头结点。
上一节我们提到过,线性表的最后一个元素是没有直接后继的,所以在链式存储中,我们将最后一个结点的指针域设置为null.
下面我们来看单链表的具体代码实现
链式存储结构
很多同学分不清头指针,头结点,以及第一个结点之间的关系和区别,我们下面简单地区分下。
头指针:是指向链表的指针,如果链表有头结点,它会指向头结点
头结点:第一个结点之前的一个辅助结点,其next指向第一个结点
第一个结点:就是一个结点,data变量存放第一个数据,next指针变量指向第二个结点
(画图渣渣,大家将就着看)
这里要注意的就是头指针是一个链表的必要元素,而头结点却不是,那么头结点存在的意义是什么呢?
个人的理解就是使第一个结点的插入操作和删除操作和后面结点的操作一致,否则我们在修改第一个结点的时候,就需要修改头指针。
如果没有头结点,头指针直接指向第一个结点。
下面我们来看链式存储结构的具体操作
在进行操作之前我们需要建立一个连式存储结构的链表
下面看插入操作
创建链表
我觉得这个创建链表的方法可以叫插队法,它就是无限在插队,哈哈
插入操作
写完代码之后,我们一起来整理下插入结点的思路:
- 声明一个指针p指向链表的头结点
- 循环遍历,找到插入的位置,让p的指针不断向后移动,不断指向下一个结点,计数变量j累加
- 判断插入位置的合理性
- 生成新结点
- 将要插入的值赋给新结点的数据域
- 单链表插入的重点:newNode->next = p->next; p->next = newNode;
- 成功
接下来看链式存储的删除操作
删除操作
整理一下删除结点的思路:
- 声明指针p指向链表的头结点
- 循环遍历查找要删除的位置,计算变量累加
- 判断删除位置的合理性,如果要删除的结点的后继为空,则位置不合理
- 将要删除的结点p->next 赋值给q
- 单链表删除语句 p->next = q->next
- 赋值
- 释放内存
- 成功
写完插入和删除操作,我们便可以看出,链式存储结构对于插入和删除的优势是明显的,不需要进行大量的元素的移动。
增删改查,增和删给大家说完了,下面来说改和查,个人感觉这两个其实差不多,改就是比查多了个修改元素,所以这里我只给大家贴一个查的代码了
查找操作
观察查找操作,大家就会发现,链式存储结构也是有其缺点的,其不便于进行查找和修改
这里再给大家说下另外两种其他形式的链表
1、循环链表:
这个就是在单链表的基础上头尾相接了,将最后一个结点的指针指向了L->next;这里我们也不多做赘述,它的大部分操作和单链表是相似的。
还有一点要注意的就是判断一个循环链表是否为空条件是看头结点是否指向其本身。
2、双向链表:
就是在单链表的基础上多了一个指针域,用来指向其前驱,这个主要是解决了单链表只能顺指针往后选差其他的结点的问题。
双线链表
然后简单说说插入操作,我画个简图帮助大家理解,然后写下关键代码
插入关键代码
下面是删除,老规矩,先上图
删除关键代码
对于双向离岸边来说,它要稍微复杂些,因为了一个前驱指针,这样,在插入和删除的时候尤其小心指针变换的顺序。
但是由于前后指针都有,这样一定程度上给我们的操作带来了方便。
到这里,我们的链式存储就算是讲完了,最后将顺序存储结构和链表存储结构进行一下对比
我们主要从两个方面进行对比,一个是时间,一个是空间
时间:
- 查找
顺序存储结构 O{1}
链式存储结构O{n}
- 插入和删除
顺序存储结构 O{n}
链式存储结构O{1}
空间:
- 顺序存储结构:需要提前分配空间大小,分配打了,产生碎片,浪费,分配小了,容易导致溢出
- 连式存储结构:不需要预分配,只要有就可以分配,并且数量不受变量限制
从以上比较不难看出
1、若线性表需要频繁的查找,很少进行插入和删除操作的时候,应该采用顺序存储。相反,则应采用链式存储结构
2、当线性表不知道到底有多大的时候,建议采用链式存储结构,如果我们已经提前知道其大小,可采用顺序存储结构
3、顺序存储结构和链式存储结构各有各的优缺点,不能一概而论说就是哪种更好。我们需要根据实际情况来选择我们需要的结构
代码实例(带有头指针):
package main
import (
"math/rand"
"time"
"fmt"
"errors"
)
//元素类型:
type ElemType1 int
//数据模块:存储结构
type Node struct {
data ElemType1
next *Node
}
//数据模块地址别名(模块指针类型)
type LinkList *Node
/*
fun:单链表的创建,先添加的元素在后面,随机生成n个数据进行添加
带有头结点的链表,头结点没有数据
*/
func CreateListHead(L *LinkList, n int) {
var p LinkList
rand.Seed(time.Now().UnixNano()) //初始化种子
//L = new(LinkList)
*L = LinkList(new(Node))
(*L).next = nil
//添加10个数据
for i := 0; i < n; i++ {
p = LinkList(new(Node))//分配内存
p.data = ElemType1(rand.Intn(100) + 10)//添加数据:10-110之间的随机数
p.next = (*L).next
(*L).next = p
}
}
/*
fun:单链表的创建,先添加的元素在前面,随机生成n个数据进行添加
*/
func CreateListTail(L *LinkList, n int) {
rand.Seed(time.Now().UnixNano())
var p LinkList
var r LinkList //遍历节点
*L = LinkList(new(Node))
r = *L
for i := 0; i < n; i++ {
p = LinkList(new(Node)) //新增加的节点
m := ElemType1(rand.Intn(100))
p.data = m
r.next = p
r = p
//fmt.Println(m)
}
r.next = nil
}
/*
fun:获取链表中的某个元素
*/
func getElem(list LinkList, i int) (err error, res ElemType1) {
var p LinkList
p = list //移动的目标节点
for {
for j := 0; j <= i; j++ {
p = p.next
if p == nil { //如果移动到 i,节点为nil,说明没有找到对应的数据
err = errors.New("没有找到!")
return
}
}
res = p.data
return
}
}
/*
插入数据:i:位置,e:元素
*/
func listInsert(L *LinkList, i int, e ElemType1) (err error) {
var p, r LinkList //p:插入的节点 r 遍历寻找位置
r = *L
for {
for j := 0; j < i; j++ {
r = r.next //先移动到下一个位置,再进行判断
if r == nil {
err = errors.New("没有找到插入的位置")
return
}
}
p = LinkList(new(Node))
p.data = ElemType1(e)
p.next = r.next
r.next = p
return
}
}
//遍历出链表的所有元素
func showLinkList(L *LinkList) (n int, e []ElemType1) {
var p LinkList
p = *L
for {
p = p.next //先移动到下一个位置,再进行判断
if p == nil {
return
}
n++
e = append(e, p.data)
}
}
/*
删除位置为 i的元素
*/
func listDelete(L *LinkList, i int) (err error) {
var p LinkList
p = *L
for {
//找到i的前一个位置
for j := 0; j < i; j++ {
p = p.next
if p == nil {
err = errors.New("删除的位置不正确!") //数据不存在,则返回
return
}
}
//判断第i个元素是否存在,不存在则返回
if p.next == nil {
err = errors.New("删除的位置不正确!")
return
}
r := p.next
p.next = r.next
return
}
return
}
func ClearList(L *LinkList) {
var p, r LinkList
p = (*L).next
for {
if p == nil {
return
}
r = p.next
p = nil
p = r
}
(*L) = nil
}
func main() {
//N:=Node{}
var l LinkList
/*CreateListHead(&l,10)
fmt.Println(*l)*/
CreateListTail(&l, 3)
//fmt.Println(*l)
/*_, res := getElem(l, 2)
fmt.Println(res)*/
listInsert(&l, 3, 888)
/*_, res = getElem(l, 2)
fmt.Println(res)*/
_, arr := showLinkList(&l)
//fmt.Println(n)
for _, v := range arr {
fmt.Print(v, "\t")
}
listDelete(&l, 3)
_, arr = showLinkList(&l)
fmt.Println("删除元素之后:")
for _, v := range arr {
fmt.Print(v, "\t")
}
ClearList(&l)
_, arr = showLinkList(&l)
fmt.Println("清除整个链表之后:")
for t, v := range arr {
fmt.Print(t, v, "\t")
}
}
注:链表操作比较抽象,通过画图使得理解简单化。
➢了解更多Go语言知识:https://study.163.com/course/introduction/1210620804.htm