在学习了顺序表的基本原理后,我们自然会思考:既然顺序表已经能够实现线性结构,为何还需要引入单链表呢?关键在于空间利用效率。回顾顺序表的实现,每次扩容都需要申请原空间两倍的新内存,这种策略在实际应用中可能造成显著的空间浪费。正因如此,我们引入单链表这一更加灵活的数据结构。
单链表同样属于线性表,在逻辑结构上保持线性特性,但在物理存储上则不一定连续。它由一系列节点通过指针链接而成,每个节点包含数据域和指针域。
下面是节点结构的基础定义:
c
typedef int SLTDataType;
typedef struct SListNode {
SLTDataType data;
struct SListNode* next;
} SLNode;
我们将此定义置于头文件中,并创建对应的源文件进行实现。
1. 链表打印功能
首先在头文件中声明打印函数:
c
void SLTPrint(SLNode* phead);
实现原理:接收链表头指针,通过遍历依次访问每个节点并输出数据值,直到遇到空指针为止。
2. 链表插入操作
尾插实现:
c
void SLTPushBack(SLNode** pphead, SLTDataType x);
关键点:需要处理链表为空和非空两种情形。为空时直接设置头节点;非空时需要遍历至末尾节点再链接新节点。
头插实现:
c
void SLTPushFront(SLNode** pphead, SLTDataType x);
实现逻辑:创建新节点并将其next指针指向原头节点,然后更新头节点指针指向新节点。
3. 链表删除操作
尾删实现:
c
void SLTPopBack(SLNode** pphead);
核心逻辑:遍历找到倒数第二个节点,释放尾节点并将前驱节点的next置空。需要特别注意单节点链表的特殊处理。
头删实现:
c
void SLTPopFront(SLNode** pphead);
实现方式:保存原头节点的后继节点地址,释放原头节点,更新头节点指针。
4. 查找与指定位置操作
查找功能:
c
SLNode* SLTFind(SLNode* phead, SLTDataType x);
通过遍历比对数据值,返回匹配节点的地址。
指定位置前插入:
c
void SLTInsert(SLNode** pphead, SLNode* pos, SLTDataType x);
分两种情况处理:在头节点前插入相当于头插;在其他位置需要找到前驱节点再进行链接操作。
指定位置后插入:
c
void SLTInsertAfter(SLNode* pos, SLTDataType x);
实现相对简单,直接修改当前节点和后继节点的链接关系。
5. 指定位置删除
删除当前节点:
c
void SLTErase(SLNode** pphead, SLNode* pos);
需要处理头节点删除的特殊情况,其他情况下需要找到前驱节点来维持链表连续性。
删除后继节点:
c
void SLTEraseAfter(SLNode* pos);
直接修改当前节点的next指针,跳过后继节点。
6. 链表销毁
c
void SLTDestroy(SLNode** pphead);
通过遍历逐个释放所有节点内存,最终将头指针置空,防止野指针问题。
通过上述实现,单链表展现了其动态内存管理的优势:每个节点按需申请,避免了顺序表扩容带来的空间浪费。虽然单链表在随机访问性能上不如顺序表,但在频繁插入删除的场景下表现更加高效。
这种空间与时间的权衡正是不同数据结构各有适用场景的根本原因,理解这些特性有助于我们在实际开发中选择最合适的数据结构解决方案。