博主csdn个人主页:小小unicorn
⏩专栏分类:c++
代码仓库:小小unicorn的学习足迹
关注我带你学习编程知识
前面我们讲的线性表的顺序存储结构。它是有缺点的,最大的缺点就是插入和删除时需要移动大量元素,这显然就需要耗费时间,那能不能想办法解决呢?
要解决这个问题,我们就得考虑一下导致这个问题的大致原因:
为什么当插入和制除时,就要移动大量元素?仔细分析后,发现原因就在于自元素的存储位置也具有邻居关系。它们编号是1,2,3,…,n,它们在内存中的位最的是按着的,中间没有空源,当然就无法快速插入,而删除后,当中就会留出空像,自然需要弥补。问题就出在这里。
A同学思路:让当中每个元素之间都留有一个空位置,这样要插入时,就不至于动,可一个空位置如何解决多个相同位置插入数据的问题呢?所以这个想法显然不行。
B同学思路:那就让当中每个元素之间都留足够多的位置,根据实际情况制定空像大小,比如10个,这样插入时,就不需要移动了。万一10个空位用完了,再考虑移动使得每个位置之间都有10个空位置。如果删除,就直接删掉,把位置留空即可。这样似乎蓄时解决了插入和删除的移动数据问题。可这对于超过10个同位置数据的插入,效率上还是存在问题。对于数据的遍历,也会因为空位置太多而造成判断时间上的浪费。而且显然这里空间复杂度还增加了,因为每个元素之间都有若干个空位置。
C同学思路:我们反正也是要让相邻元素间留有足够余地,那干脆所有的元素都不要考虑相邻位置了,哪有空位就到哪里,而只是让每个元素知道它下一个元素的位置在哪里,这样,我们可以在第一个元素时,就知道第二个元素的位置(内存地址),而找到它:在第二个元素时,再找到第三个元素的位置(内存地址)。这样所有的元素我们就都可以通过遍历而找到。
好!大棒了,这个想法非常好!
C同学,可惜生晚了几十年,不然,c同学的想法对于数据结构来讲就是划时代的意义。我们要的就是这个思路。
在解释这个思路之前,我们先来谈另一个话题。前几年,有一本书风靡了全世界,它叫《达·芬奇密码》,成为世界上最畅销的小说之一,书的内容集合了侦探、惊使和阴谋论等多种风格,很好看。这本书和绝大部分负小说一样,都是同一种处理办法。那就是,作者不会让你事先知道整个过程的全部,而是在一步一步地到这某个环节,才根据现场的信息,获得或推断出下一步是什么,也就是说,每一步除了对你侦破的信息进一步确认外(之前信息也不一定都是对的,有时是证明某个信息不正确),还有就是对下一步如何操作或行动的指引。
不过,这个例子也不完全与线性表相符合,因为案件侦破的线案可能是错综复杂的,有点像我们之后要讲到的树和图的数据结构。今天,我们要讲的是单线索,无分支的情况。即线性表的链式存储结构。
线性表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素,这组存储单元可以是连续的,也可以是不连续的。这就意 味着,这些数据元素可以存在内存未被占用的任意位置
(如下图所示)。
以前在顺序结构中,每个数据元素只需要存储数据 元素信息就可以了。现在链式结构中,除了要存储数据 元素信息外,还要存储它的后继元素的存储地址。
因此,为了表示每个数据元素ai与其直接后继数据 元素ai+1(i是下标)之间的逻辑关系,对数据元素ai来说,除了存储 其本身的信息之外,还需存储一个指示其直接后继的信息(即直接后继的存储位置)。我们把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域。指针域中存储的信息称作指针或链。这两部分信息组成数据元素ai的存储映像,称为结点(Node)。
n个结点(a的存储映像)链结成一个链表,即为线性表(a,ay·,a)的链式存储结构,因为此链表的每个结点中只包含一个指针域,所以叫做单链表。单链表正是通过每个结点的指针域将线性表的数据元素按其逻辑次序链接在一起。
对于线性表来说,总得有个头有个尾,链表也不例外。我们把链表中第一个结点的存储位置叫做头指针,那么整个链表的存取就必须是从头指针开始进行了。之后的每一个结点,其实就是上一个的后继指针指向的位置。想象一下,最后一个结点,它的指针指向哪里?
最后一个,当然就意味着直接后继不存在了,所以我们规定,线性链表的最后一个结点指针为“空”(通常用NULL或“^”符号表示,如下图所示)。
有时,我们为了更加方便地对链表进行操作,会在单链表的第一个结点前附设一个结点,称为头结点。头结点的数据域可以不存储任何信息,谁叫它是第一个呢,有这个特权。也可以存储如线性表的长度等附加信息,头结点的指针域存储指向第一个结点的指针,如下图所示。
这里我们大概用图示表达了内存中单链表的存储状态。看着满图的省略号“…”,就知道是有多么不方便。而我们真正关心的它在内存中的实际位置吗?不是的。这只是它所表示的线性表中的数据元素及数据元素之间色逻辑关系。所以我们改用更方便的存储示意图来表示单链表,如下图所示:
单链表中,我们在c语言中可用结构指针来描述:
//线性表的单链表的存储结构:
typedef struct Node
{
ElemType data;
struct Node*next;
}Node;
typedef struct Node*LinkList;//定义linkList
从这个结构定义中,我们也就知道,结点由存放数据元素的数据域和存放后继结点地址的指针域组成。假设p是指向线性表第i个元素的指针,则该结点ai 的数据域我们可以用p->data来表示,p->data的值是一个数据元素,结点ai的指针域可以用p->next来表示,p->next的值是一个指针。p->next指向谁呢?当然是指向第i+1个元素,即指向ai+l的指针。也就是说,如果p->data等于a,那么p>next->data等于ai+1(如下图所示)。
首先创建一个新项目,分为三个模块:SList.c用来实现接口函数;SList.h用来结构体创建与函数声明;test.c则用来进行测试我们的接口函数。
创建节点如下:
咱们首先用一个cur来存放头结点的地址;
那后面节点的数据怎么访问呢?咱们可以让cur->next赋给cur;
依次内推;当cur为空时;说明已经访问结束。 最后打印即可。
为方便测试我们可以先写一个交互性的链表测试:
写交互性测试的时候,会涉及到开辟新节点,这里我们可以抽离一个函数来专门进行开辟新节点。
那么如何实现尾插呢?尾插我们首先得先找到尾。
那么如何找呢?
先看下面这个代码。
这个代码是不是首先有三个指针变量 plist tail newnode;
随着代码运行,最后taill的地址为空,假设新节点的地址为0x0012ff00,那节点newnode存放的地址也就是0x0012ff00,最后呢,又把newnode的地址给了taill,看起来没问题,但有没有想过,这三个指针变量出了作用域呢?
出了作用域,这三个指针变量是不是都销毁了,这不仅造成了内存泄漏还没有吧链表链接起来。基于这个问题,我们在回过来思考这个问题,如何找尾?
所以我们的尾用taii找的时候,条件不是null结束,而是存放null前一个的地址。
继续思考一下,我们写完了吗?那如果本身就是个空链表呢?也就是说空链表传过来的时候。第一次尾插怎么插呢?看下面这个代码。
这个代码就会有一个明显的错误,插入不进去。为什么会这样呢?
看phead=newnode这一步,这一步是把0x0012ffcc给了phead,出了作用域,phead,newnode****销毁,那么显然我们的plist的地址并没有发生任何变化,所以插入不进去。
这就是典型的形参与实参之间的区别,形参只是实参的一份临时拷贝。那么怎们样才能改变呢,那就需要传地址了。
测试结果如下:
总结一下:
有了尾插的思想,头插就简单了。
实质也就是修改plist,所以还是传值还是用二级指针。
完整代码如下:
测试结果如下:
如果是空链表,我们可以直接使用断言。
一个节点直接释放掉plist.两个节点以上就首先需要找尾,先看下面这个代码。(**pphead就是plist)
上面这个代码找到尾后,直接把taill置空,肯定不行
free(taill)的本质是把tail指向的节点给free了,再把taill置空。出了作用域,taill销毁,那他前一个还是空。所以还需要找到taill的前一个。
以下两种解决办法都可以。
测试结果如下:
测试结果如下:
查找实质还是遍历链表:
那他还有什么特殊情况呢,头插,尾插就是其中两种,其次处理pos在中间位置。
完整代码如下:
测试结果如下:在给定数字前面插它的十倍
要是后插就会便捷很多。因为他不可能实现头插,所以就会很简单。
修改后完整代码如下:
删除部分就简单了。分情况,头删首先需要单独处理,其次尾删是不需要单独处理的,因为正常删就已经包含尾删情况了。
完整代码如下:
测试结果如下:
删后一个考虑的就是要找到前一个,处理好这个就简单了。其次有个坑,它删不了头。
简单地对单链表结构和顺序存储结构做对比:
1.若线性表需要频繁查找,很少进行插入和删除操作时,宜采用顺序存储结构。若需要频繁插入和删除时,宜采用单链表结构。比如说游戏开发中,对于用户注册的个人信息,除了注册时插入数据外,绝大多数情况都是读取,所以应该考虑用顺序存储结构。而游戏中的玩家的武器或者装备列表,随着玩家的游戏过程中,可能会随时增加或删除,此时再用顺序存储就不太合适了,单链表结构就可以大展拳脚。当然,这只是简单的类比,现实中的软件开发,要考虑的问题会复杂得多。
2.当线性表中的元素个数变化较大或者根本不知道有多大时,最好用单链表给构。这样可以不需要考虑存储空间的大小问题。而如果事先知道线性表的大致长度,比如一年12个月,一周就是星期一至星期日共七天,这种用顺序存储结构效率会高很多。
总之,线性表的顺序存储结构和单链表结构各有其优缺点,不能简单地说哪个好,哪个不好,需要根据实际情况,来综合平衡采用哪种数据结构更能满足和达到需求和性能。
#include
#include
#include
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SLTNode;
void SLTPrint(SLTNode* phead);
SLTNode* BuySListNode(SLTDataType x);
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x);
//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x);
//尾删
void SLTPopBack(SLTNode** pphead);
//头删
void SLTPopFront(SLTNode** pphead);
//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
//在pos位置之前插入x
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//在pos位置以后插入x
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
//删除pos位置
void SLTErase(SLTNode** pphead, SLTNode* pos);
//删除pos的后一个位置
void SLTEraseAfter(SLTNode* pos);
#define _CRT_SECURE_NO_WARNINGS
#include"SList.h"
//打印函数
void SLTPrint(SLTNode* phead)
{
SLTNode* cur = phead;
//while (cur != NULL)
while (cur)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
//开辟新节点
SLTNode* BuySListNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail");
exit(-1);
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
SLTNode* newnode = BuySListNode(x);
if (*pphead == NULL)
{
//改变的结构体的指针
*pphead = newnode;
}
else
{
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
//改变的结构体
tail->next = newnode;
}
}
//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
SLTNode* newnode = BuySListNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
//尾删
void SLTPopBack(SLTNode** pphead)
{
//空
assert(*pphead);
//一个节点
//一个以上节点
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SLTNode* tail = *pphead;
while (tail->next->next)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
/*SLTNode* tailprev = NULL;
SLTNode* tail = *pphead;
while (tail->next)
{
tailprev = tail;
tail = tail->next;
}
free(tail);
tail = NULL;
tailprev->next = NULL;*/
}
}
//头删
void SLTPopFront(SLTNode** pphead)
{
//空
assert(*pphead);
//非空
SLTNode* newnode = (*pphead)->next;
free(*pphead);
*pphead = newnode;
}
//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
SLTNode* cur = phead;
while (cur)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
//在pos位置之前插入x
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead);
assert(pos);
if (pos == *pphead)
{
SLTPushFront(pphead, x);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
SLTNode* newnode = BuySListNode(x);
prev->next = newnode;
newnode->next = pos;
}
}
//在pos位置以后插入x
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = BuySListNode(x);
/*pos->next = newnode;*/
newnode->next = pos->next;
pos->next = newnode;
}
//删除pos位置
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead);
assert(pos);
if (pos == *pphead)
{
SLTPopFront(pphead);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
//pos = NULL;
}
}
//删除pos的后一个位置
void SLTEraseAfter(SLTNode* pos)
{
assert(pos);
//检查Pos是否为尾节点
assert(pos->next);
SLTNode* posNext = pos->next;
pos->next = posNext = posNext->next;
free(posNext);
posNext = NULL;
}
#define _CRT_SECURE_NO_WARNINGS
#include"SList.h"
//交互链表测试
void TestSList1()
{
int n;
printf("请输入链表的长度:\n");
scanf("%d", &n);
printf("请依次输入每个节点的值:\n");
SLTNode* plist = NULL;
for (size_t i = 0; i < n; i++)
{
int val;
scanf("%d", &val);
SLTNode* newnode = BuySListNode(val);
//头插
newnode->next = plist;
plist = newnode;
}
SLTPrint(plist);
}
//测试尾插头插
void TestSList2()
{
SLTNode* plist = NULL;
SLTPushBack(&plist, 1);
SLTPushBack(&plist, 2);
SLTPushBack(&plist, 3);
SLTPushBack(&plist, 4);
SLTPushBack(&plist, 5);
SLTPrint(plist);
SLTPushFront(&plist, 10);
SLTPushFront(&plist, 20);
SLTPushFront(&plist, 30);
SLTPushFront(&plist, 40);
SLTPrint(plist);
}
//测试尾删
void TestSList3()
{
SLTNode* plist = NULL;
SLTPushBack(&plist, 1);
SLTPushBack(&plist, 2);
SLTPushBack(&plist, 3);
SLTPushBack(&plist, 4);
SLTPushBack(&plist, 5);
SLTPrint(plist);
SLTPopBack(&plist);
SLTPrint(plist);
SLTPopBack(&plist);
SLTPrint(plist);
SLTPopBack(&plist);
SLTPrint(plist);
SLTPopBack(&plist);
SLTPrint(plist);
SLTPopBack(&plist);
SLTPrint(plist);
/*SLTPopBack(&plist);
SLTPrint(plist);*/
}
//测试头删
void TestSList4()
{
SLTNode* plist = NULL;
SLTPushBack(&plist, 1);
SLTPushBack(&plist, 2);
SLTPushBack(&plist, 3);
SLTPushBack(&plist, 4);
SLTPushBack(&plist, 5);
SLTPrint(plist);
SLTPopFront(&plist);
SLTPrint(plist);
SLTPopFront(&plist);
SLTPrint(plist);
SLTPopFront(&plist);
SLTPrint(plist);
SLTPopFront(&plist);
SLTPrint(plist);
SLTPopFront(&plist);
SLTPrint(plist);
/*SLTPopFromt(&plist);
SLTPrint(plist);*/
}
void TestSList5()
{
SLTNode* plist = NULL;
SLTPushBack(&plist, 1);
SLTPushBack(&plist, 2);
SLTPushBack(&plist, 3);
SLTPushBack(&plist, 4);
SLTPushBack(&plist, 5);
SLTPrint(plist);
SLTNode* pos = SLTFind(plist, 40);
if (pos)
{
pos->data *= 10;
}
SLTPrint(plist);
int x;
scanf("%d", &x);
pos = SLTFind(plist, x);
if (pos)
{
SLTInsert(&plist, pos, x * 10);
}
SLTPrint(plist);
}
void TestSList6()
{
SLTNode* plist = NULL;
SLTPushBack(&plist, 1);
SLTPushBack(&plist, 2);
SLTPushBack(&plist, 3);
SLTPushBack(&plist, 4);
SLTPushBack(&plist, 5);
SLTPrint(plist);
int x;
scanf("%d", &x);
SLTNode* pos = SLTFind(plist, x);
if (pos)
{
SLTErase(&plist, pos);
}
SLTPrint(plist);
}
void TestSList7()
{
SLTNode* plist = NULL;
SLTPushBack(&plist, 1);
SLTPushBack(&plist, 2);
SLTPushBack(&plist, 3);
SLTPushBack(&plist, 4);
SLTPushBack(&plist, 5);
SLTPrint(plist);
int x;
scanf("%d", &x);
SLTNode* pos = SLTFind(plist, x);
if (pos)
{
SLTEraseAfter(pos);
pos = NULL;
}
SLTPrint(plist);
//SLTPopFront(&plist);
//SLTPrint(plist);
//SLTPopFront(&plist);
//SLTPrint(plist);
//SLTPopFront(&plist);
//SLTPrint(plist);
//SLTPopFront(&plist);
//SLTPrint(plist);
}
int main()
{
TestSList7();
}