【数据结构与算法篇】线性表的链式存储之单链表(无哨兵头节点)

一 链表简介

1> 什么是链表

线性表的链式存储被称为链表

  • 实质是 : 一种在物理存储单元上, 非连续的, 非顺序的数据存储结构。
  • 也就是说 : 链表中的数据在存储单元中存放的位置是非连续的, 且非顺序的。
    链表在逻辑上连续, 物理是非连续。

2> 链表的分类

链表一共有八种, 分类如下:

是否带头 :

  • 头指的是 – 哨兵头节点, 这个头节点中的数值域不存放任何数据, 哨兵头节点的下一个节点的数值域才开始存储数据。链表使用者不可删除哨兵头节点。
  • 带头 : 链表中第一个节点为哨兵头节点, 这样的链表被称之为带头链表
  • 不带头 : 链表中不存在哨兵头节点, 第一个节点就是用来存储数据的。 这样的链表被称为不带头链表

单向or双向:

  • 这里的单向或双向指的是链表中的指针域。
  • 单向 : 链表中的指针域只存在一个指针, 该指针存储的是下一个节点的地址, 它指向下一个节点。 这样的链表只能从第一个节点向后遍历,因此被称为 单向链表
  • 双向 : 链表中的指针域存在两个指针, 其中一个指向下一个节点, 另一个指向前一个节点。 这样的链表既可以从前向后, 也可以从后向前遍历该链表的每一个节点。 因此被称为双向链表

是否循环

  • 循环指的是 这个链表是否构成了一个闭环, 由线性链表变成了一个环链表
  • 不循环 : 链表中尾节点的指针域为空指针, 也就是这个指针指向空。 这样的链表被称为不循环链表
  • 循环 : 链表中尾节点中的指针指向第一个节点(将链表头尾相连), 这样链表的遍历就会成为一个圆环, 从链表的任意一个节点, 向后遍历, 都能回到这个节点。这样的链表被称为循环链表

3> 链表的基本结构

1) 链表结构

  • 链表由多个节点组成, 节点与节点之间通过指针实现逻辑上的链接。
  • 新建链表不存在任何一个节点, 此时被称为空链表
  • 链表一般都需要有个指向头节点的指针, 这样的指针一般被称为头指针, 往往用 phead或 plist命名
    - 这里的 头节点 指的是链表中的第一个节点, 第一个节点并不一定是哨兵头节点

2) 节点基本结构

节点分为数值域和指针域两个区域

  • 数值域 : 存放指定类型的数据
  • 指针域 : 由指定类型的指针存放地址, 在单向链表中存放的是下一个节点的地址,指针名往往为next ; 双向链表中有两个指针,分别存放前一个节点和下一个节点的地址

该段文字引用于<<数据结构与算法>> : 对于链表中的每个数据元素, 除了存放本应存放的数据之外, 还应同时存放其后继数据元素所在的存储单元的地址。 这两部分信息组成了一个节点, 而链表就是由这样的节点组成。

二 单链表的C++实现

本文中的单链表指的是 单向 不循环, 不带头节点的链表

1> 单链表之节点的定义

  • 对于单链表, 我们用代码实现, 实现的是链表中的节点
  • (一个或多个节点构成的链式存储结构才是链表)
  • 定义节点, 实际上是自定义了一种数据类型

节点的C++实现

  • 在这里, 我们采用存放int数据的链表作为示例
  • 类似于模板, 区别只是手动指定了链表所存放的数据类型

typedef int SLTDataType;     // 为了链表后续数据类型修改方便, 采用关键字typedef 为int这个数据类型起别名 

typedef struct SListNode
{
	SLTDataType data;      // 数值域 , 用于存放数据 。  data 变量名, 存放数据
	struct SListNode* next;  // 指针域 ,存放后继节点的地址, next 变量名
}SLTNode;    // 起别名

2> 单链表之节点的创建

  • 链表中的节点都是在堆区开辟的空间, 节点的信息都是存放在堆区。
  • 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;        // 返回这个节点的地址
}

3> 单链表的增删改查

1) 向单链表中添加指定元素

指定元素, 指定的是元素的数值域中存放的数据。 而指针域中的指针根据实际情况赋值。

- 头插法
// 单链表, 头插法, 利用头指针
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;                  // 将指定节点指针域的指针指向更改为 新建节点
}

2) 单链表中删除元素

- 头删法
 // 单链表, 头删, 利用头指针
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;
	}
}

3) 修改指定节点的data中存放的值
 // 单链表, 修改指定节点的数值域data中的数据
void SListModify(SLTNode* pos, SLTDataType val)   
{
	if (pos == NULL)   // 如果 pos为空
	{
		perror("pos is NULL");  // 提醒一下
		exit(-1);   // 退出该程序, 主调函数的返回值为-1, 一般0代表程序正常退出, -1代表程序非正常退出
	}
	pos->data = val;
}
4) 查找链表中是否存在指定值的节点
// 单链表, 查找指定值在链表中是否存在, 存在返回其对应节点的地址, 不存在返回空指针
SLTNode* SListFind(SLTNode* phead, SLTDataType val)
{
	SLTNode* cur = phead;
	while (cur)
	{
		if (cur->data == val)
			return cur;     // 存在返回其地址
		cur = cur->next;
	}
	return NULL;    // 不存在返回 空指针
}

4> 遍历并打印链表中的每一个节点中的data值

// 单链表打印 每个节点的数值域 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;
}

  • 12
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值