线性表的链式存储被称为链表
- 实质是 : 一种在物理存储单元上, 非连续的, 非顺序的数据存储结构。
- 也就是说 : 链表中的数据在存储单元中存放的位置是非连续的, 且非顺序的。
链表在逻辑上连续, 物理是非连续。
链表一共有八种, 分类如下:
是否带头 :
- 头指的是 – 哨兵头节点, 这个头节点中的数值域不存放任何数据, 哨兵头节点的下一个节点的数值域才开始存储数据。链表使用者不可删除哨兵头节点。
- 带头 : 链表中第一个节点为哨兵头节点, 这样的链表被称之为带头链表
- 不带头 : 链表中不存在哨兵头节点, 第一个节点就是用来存储数据的。 这样的链表被称为不带头链表
单向or双向:
- 这里的单向或双向指的是链表中的指针域。
- 单向 : 链表中的指针域只存在一个指针, 该指针存储的是下一个节点的地址, 它指向下一个节点。 这样的链表只能从第一个节点向后遍历,因此被称为 单向链表
- 双向 : 链表中的指针域存在两个指针, 其中一个指向下一个节点, 另一个指向前一个节点。 这样的链表既可以从前向后, 也可以从后向前遍历该链表的每一个节点。 因此被称为双向链表
是否循环
- 循环指的是 这个链表是否构成了一个闭环, 由线性链表变成了一个环链表
- 不循环 : 链表中尾节点的指针域为空指针, 也就是这个指针指向空。 这样的链表被称为不循环链表
- 循环 : 链表中尾节点中的指针指向第一个节点(将链表头尾相连), 这样链表的遍历就会成为一个圆环, 从链表的任意一个节点, 向后遍历, 都能回到这个节点。这样的链表被称为循环链表
- 链表由多个节点组成, 节点与节点之间通过指针实现逻辑上的链接。
- 新建链表不存在任何一个节点, 此时被称为空链表
- 链表一般都需要有个指向头节点的指针, 这样的指针一般被称为头指针, 往往用 phead或 plist命名
- 这里的 头节点 指的是链表中的第一个节点, 第一个节点并不一定是哨兵头节点
节点分为数值域和指针域两个区域
- 数值域 : 存放指定类型的数据
- 指针域 : 由指定类型的指针存放地址, 在单向链表中存放的是下一个节点的地址,指针名往往为next ; 双向链表中有两个指针,分别存放前一个节点和下一个节点的地址
该段文字引用于<<数据结构与算法>> : 对于链表中的每个数据元素, 除了存放本应存放的数据之外, 还应同时存放其后继数据元素所在的存储单元的地址。 这两部分信息组成了一个节点, 而链表就是由这样的节点组成。
本文中的单链表指的是 单向 不循环, 不带头节点的链表
- 对于单链表, 我们用代码实现, 实现的是链表中的节点
- (一个或多个节点构成的链式存储结构才是链表)
- 定义节点, 实际上是自定义了一种数据类型
节点的C++实现
- 在这里, 我们采用存放int数据的链表作为示例
- 类似于模板, 区别只是手动指定了链表所存放的数据类型
typedef int SLTDataType; // 为了链表后续数据类型修改方便, 采用关键字typedef 为int这个数据类型起别名
typedef struct SListNode
{
SLTDataType data; // 数值域 , 用于存放数据 。 data 变量名, 存放数据
struct SListNode* next; // 指针域 ,存放后继节点的地址, next 变量名
}SLTNode; // 起别名
- 链表中的节点都是在堆区开辟的空间, 节点的信息都是存放在堆区。
- C语言中通常由函数 malloc在堆区申请内存,创建节点
- C++中则由关键字new在堆区申请内存, 创建节点
节点创建之 C++的代码实现:
// 单链表, 根据指定值创建新节点
SLTNode* BuySListNode(SLTDataType val) // 传值, 创建节点
{
SLTNode* newNode = new SLTNode; // 堆区申请内存, 创建空节点
if (newNode == NULL) // 如果创建失败, 给出提示
{
perror("newNode failed");
exit(-1); // 并且强制退出程序, 退出标志为 -1
}
newNode->data = val; // data 初始化为 val
newNode->next = NULL; // 指针next初始化为空
return newNode; // 返回这个节点的地址
}
指定元素, 指定的是元素的数值域中存放的数据。 而指针域中的指针根据实际情况赋值。
// 单链表, 头插法, 利用头指针
void SListPushFront(SLTNode** p_phead, SLTDataType val)
{
SLTNode* newNode = BuySListNode(val); // 根据值 new创建一个节点, 并记录该节点的地址
newNode->next = *p_phead; // 头部插入, 因此需要更改新头节点next成员的指向, 使其指向原头节点
*p_phead = newNode; // 更改头指针的指向
}
// 注意 : 头插法需要更改头指针的指向,也就是需要更改实参,那么就要传址调用, 传址调用就需要形参是个指针来接受这个地址。 而实参是个指针, 将指针的地址传递过去,因此形参就需要是一个二级指针。
void SListPushBack(SLTNode** p_phead, SLTDataType val) // 单链表, 尾插, 利用头指针
{
if (*p_phead == NULL) // 如果头指针的指向为空,
{
*p_phead = BuySListNode(val); // 那么更改头指针的指向,改为指向新建节点
return; // 退出该函数。
}
SLTNode* cur = *p_phead; // 当头指针指向不为空时, 将头指针指向的地址赋给临时指针 cur
while (cur->next != NULL) // 循环赋值, 直到 cur->next == NULL, 也就是说直到cur指向的节点为该链表的尾节点
{
cur = cur->next;
}
cur->next = BuySListNode(val); // 更改尾节点的指针域的指向为新建立节点
}
// 单链表 , 在指定位置之前根据值val创建新节点
void SListInsert(SLTNode** p_phead, SLTNode* pos, SLTDataType val)
{
if (*p_phead == pos) // 判断是否是否需要修改头指针指向
{ // pos = phead时, 插入元素就会修改头指针指向, 相当于头插
SLTNode* newNode = BuySListNode(val);
newNode->next = *p_phead;
*p_phead = newNode;
// SListPushFront(p_phead, val); //如果已经定义头插法,可以直接调用头插函数
return;
}
SLTNode* cur = *p_phead; // 创建临时指针变量 cur
SLTNode* tail = NULL; // 创建临时指针变量 tail
while (cur!=pos) // 采取双指针的形式,循环遍历该链表中的节点
{ // 直到 cur = pos
tail = cur; // 此时 tail指向的节点,是cur指向节点的前一个节点
cur = cur->next;
}
SLTNode* newNode = BuySListNode(val); // 建立新节点并记录地址
newNode->next = cur; // 新节点的指针成员next赋值为cur的值, 也就是此时next 指向 cur指向的节点
tail->next = newNode; // tail指向节点的指针成员next赋值为 newNode的值
}
// 单链表, 在指定位置之后根据值val创建新节点
void SListInsertAfter(SLTNode* pos, SLTDataType val)
{
if (pos)
{
perror("P is NULL");
exit(-1);
}
SLTNode* newNode = BuySListNode(val); // 先根据指定值, 创建一个新节点
newNode->next = pos->next; // 将新节点指针域的指针指向指定节点的下一个节点 // 指定位置之后不存在节点, 就是将NULL 赋给 空指针
pos->next = newNode; // 将指定节点指针域的指针指向更改为 新建节点
}
// 单链表, 头删, 利用头指针
void SListPopFront(SLTNode** p_phead)
{ // 头删始终是要修改头指针指向的
if (*p_phead == NULL) // 判断是否为空表
{
perror("P is NULL");
exit(-1);
}
SLTNode* cur = *p_phead;
*p_phead = (*p_phead)->next;
delete cur;
}
void SListPopBack(SLTNode** p_phead) // 单链表, 尾删, 利用头指针
{ // 尾删分为三种情况,
if (*p_phead == NULL) // 空链表, 无法删除
{
perror("P is NULL");
exit(-1);
}
SLTNode* cur = *p_phead;
if (cur->next == NULL) // 链表中只有一个节点, 删除后需要修改头指针,需要修改头指针指向
{
*p_phead = NULL;
delete cur;
return; // 让函数运行到此处就结束
}
while ((cur->next)->next != NULL) // 多个节点, 不需要修改头指针指向
{
cur = cur->next;
}
delete cur->next; // 释放该指针指向的尾节点所在的那块内存
cur->next = NULL; // 将该指针的指向更改为空
}
// 单链表 删除指定节点
void SListErase(SLTNode** p_phead, SLTNode* pos)
{
if (*p_phead == pos) // 判断 pos 是否等于 phead
{
SLTNode* cur = *p_phead;
*p_phead = cur->next;
delete cur;
//SListPopFront(p_phead); // 如果已经定义头删法, 那么直接调用头删函数
return;
}
SLTNode* cur = *p_phead;
SLTNode* tail = NULL;
while (cur != pos)
{
tail = cur;
cur = cur->next;
}
tail->next = cur->next;
delete cur;
}
// 单链表 删除指定节点之后的节点
void SListEraseAfter(SLTNode* pos)
{
if (pos->next == NULL || pos == NULL)
{
perror("P/P->next is NULL");
exit(-1);
}
SLTNode* cur = (pos->next)->next;
delete pos->next;
pos->next = cur;
}
- 在单链表中, 不使用头指针是无法删除指定节点的, 因为无法更改其前一个节点指针域中指针的指向。
- 这种删除方式, 采取的是替换, 是一种伪删除
- 缺点是, 指定节点为尾节点的时候, 无法伪删除该节点
- 替换 – 就是将后续节点的data依次赋值给前一个节点。
// 单链表, 不使用头指针, 伪删除指定节点
void SListModify(SLTNode* pos, SLTDataType val)
{
if (pos->next == NULL)
{
perror("pos->next is NULL"); // 此时无法修改指定节点的值
exit(-1);
}
while (pos->next)
{
pos->data = pos->next->data;
pos = pos->next;
}
}
// 单链表, 修改指定节点的数值域data中的数据
void SListModify(SLTNode* pos, SLTDataType val)
{
if (pos == NULL) // 如果 pos为空
{
perror("pos is NULL"); // 提醒一下
exit(-1); // 退出该程序, 主调函数的返回值为-1, 一般0代表程序正常退出, -1代表程序非正常退出
}
pos->data = val;
}
// 单链表, 查找指定值在链表中是否存在, 存在返回其对应节点的地址, 不存在返回空指针
SLTNode* SListFind(SLTNode* phead, SLTDataType val)
{
SLTNode* cur = phead;
while (cur)
{
if (cur->data == val)
return cur; // 存在返回其地址
cur = cur->next;
}
return NULL; // 不存在返回 空指针
}
// 单链表打印 每个节点的数值域 data存储的值
void SListPrint(SLTNode* phead)
{
SLTNode* cur = phead;
if (!cur)
cout << "这是一个空链表";
// while(cur != NULL)
while (cur) // 二者是等价的, 对于整型 0为假, 非零为真; 对于指针, 空指针为假, 非空为真。
{
cout << cur->data;
cur = cur->next;
if (cur != NULL)
cout << " -> ";
else
cout << " -> NULL";
}
cout << endl;
}