2.1.1无结构体管理单链表的核心特点
首先,我们通过一个表格来对比这种实现方式与你之前有结构体管理的差异:
| 特性对比 | 无结构体管理单链表 | 有结构体管理单链表(你的前一个版本) |
|---|---|---|
| 管理方式 | 直接操作 ListNod e** (指向头指针的指针) | 封装在 LinkLis t 结构体中(包含head和cursize) |
| 大小获取 | 需要遍历链表,时间复杂度O(n) | 直接返回cursize,时间复杂度O(1) |
| 内存开销 | 仅节点本身开销,更节省 | 额外需要LinkList结构体的开销 |
| 代码安全性 | 容易产生野指针,需要更谨慎的内存管理 | 封装性好,更安全 |
| 空表判断 | 检查头指针的next是否为NULL | 检查cursize或head->next |
| 使用便利性 | 函数参数多为 ListNod e** ,稍显复杂 | 接口更清晰,使用更方便 |
无结构体管理的单链表"通常指的是不将链表本身( 如头指针和大小等信息)封装在一个单独的结构体(如你的 LinkLis t 结构体)中进行 管理的实现方式。
在这种模式下:
- 管理方式:链表的头指针通常被定义为一个全局变量,或者作为参数在各个函数间传递。链表的大小(cursize)等信息可能需要通过遍历链表才能获得。
- 与你实现的对比 :你的实现方式(使用 LinkLis t 结构体封装头指针和大小)是现代数据结构设计中更推荐的做法。它将链表作为一个完整的抽象数据类型(ADT)来管理,封装性更好,更 安全,也更易于使用和维护。例如,获取链表长度的时间复杂度从O(n)降低到了O(1)。
2.2.2设计与实现
- 数据域(vav al):存放数据元素本身的信息。
- 指针域(nex t):存放指向下一个节点的内存地址的指针。
cpp
typedef int ELEM_TYPE;
typedef struct ListNode
{
ELEM_TYPE data;//数据域
struct ListNode* next;//指针域
}ListNode;
- 初始化函数 ( InitLis t )
cpp
void InitList(ListNode** plist) {
assert(plist != NULL);
*(plist) = NULL;
}
功能 :初始化链表,将头指针设为NULL。关键点:
- 参数使用 ListNod e** 是为了修改调用方的指针变量
- 这种实现是不带头节点的,第一个节点就是存储实际数据的节点
- 空链表的状态就是头指针为NULL
- 获取链表大小 ( GetSize )
cpp
int GetSize(ListNode** plist) {
assert(plist != NULL);
int i = 0;
ListNode* p = (*plist);
while (p != NULL) {
i++;
p = p->next;
}
return i;
}
关键点:
- 时间复杂度O( n):必须遍历整个链表计数
- 这是无结构体管理的主要缺点之一
- 从第一个数据节点开始计数(没有头节点)
- 判空函数 ( Is_Empty )
cpp
bool Is_Empty(ListNode** plist) {
assert(plist != NULL);
if ((*plist) == NULL) return true;
else { return false; }
}
关键点:
- 检查头指针是否为NULL
- 时间复杂度O(1),效率很高
- 对于不带头节点的链表,这是正确的判空方式
- 按位置查找节点 ( FindPos )
cpp
ListNode* FindPos(ListNode** plist, int pos) {
assert(plist != NULL);
if (pos < 1 || pos > GetSize(plist)) {
printf("下标超出范围或者为空");
return NULL;
}
ListNode* p = (*plist);
while (pos > 1) {
pos--;
p = p->next;
}
return p;
}
- 插入操作函数群
在指定节点后插入 ( insertN ext )
cpp
bool insertNext(ListNode** plist, ListNode* prt, ELEM_TYPE val) {
assert(plist != NULL && prt != NULL);
if (*plist == NULL) { Push_Front(plist, val); return true; }
// ... 其余代码
}
关键点:
- 核心插入操作,时间复杂度O(1)
- 有特殊处理空链表的情况
- 对于不带头节点的链表,需要更多边界条件检查
头插法 ( Push_Fr ont )
cpp
bool Push_Front(ListNode** plist, ELEM_TYPE val) {
assert(plist != NULL);
ListNode* p = (ListNode*)malloc(sizeof(ListNode));
if (p == NULL) { printf("分配内存失败"); return false; }
p->data = val;
p->next = (*plist);
(*plist) = p;
return true;
}
关键点:
- 需要修改头指针:这是不带头节点链表的特点
- 时间复杂度O(1)
- 新节点的next指向原头指针,然后更新头指针指向新节点
尾插法 ( Push_Ba ck )
cpp
bool Push_Back(ListNode** plist, ELEM_TYPE val) {
assert(plist != NULL);
if (FindPos(plist, GetSize(plist)) == NULL) return Push_Front(plist, val);
return insertNext(plist, FindPos(plist, GetSize(plist)), val);
}
简化:
cpp
bool Push_Back(ListNode** plist, ELEM_TYPE val) {
assert(plist != NULL);
if (*plist == NULL) {
return Push_Front(plist, val); // 空链表直接头插
}
// 找到最后一个节点
ListNode* p = *plist;
while (p->next != NULL) {
p = p->next;
}
// 在最后一个节点后插入
ListNode* newNode = (ListNode*)malloc(sizeof(ListNode));
if (newNode == NULL) return false;
newNode->data = val;
newNode->next = NULL;
p->next = newNode;
return true;
}
- 删除操作函数群
删除头节点 ( Pop_Fro nt )
cpp
bool Pop_Front(ListNode** plist) {
assert(plist != NULL);
if (Is_Empty(plist)) {
printf("链表为空,无法删除\n");
return false;
}
ListNode* p = *plist;
*plist = (*plist)->next;
free(p);
return true;
}
关键点:
- 需要修改头指针:这是不带头节点链表删除操作的特点
- 时间复杂度O(1)
- 缺少空链表检查:如果链表为空, (*plist )->next 会访问非法内存
删除指定节点的后继 ( Pop_Nex t )
cpp
bool Pop_Next(ListNode** plist, ListNode* prt) {
assert(plist != NULL);
if (Is_Empty(plist)) { printf("没有元素"); return false; }
if (prt == NULL || prt->next == NULL) { printf("删除位置不合理"); return false; }
if (prt == (*plist)) { Pop_Front(plist); return true; }
// ... 其余代码
}
关键点:
- 包含了多种边界情况处理
- 对于头节点的特殊处理体现了不带头节点链表的复杂性
- 查找函数
按值查找 ( FindVal ue )
cpp
ListNode* FindValue(ListNode** plist, ELEM_TYPE val) {
assert(plist != NULL);
if (Is_Empty(plist)) { printf("没有元素"); return NULL; }
ListNode *n = *plist;
while (n != NULL) {
if (n->data == val) return n;
n = n->next;
}
return NULL;
}
关键点:
- 逻辑正确,时间复杂度O(n)
- 从第一个数据节点开始查找(没有头节点)
2.2.3总结与对比
无结构体管理单 链表的优缺点
优点:
- 内存效率高:没有额外的结构体开销
- 教学价值:更好地理解指针操作的本质
- 代码简洁:对于简单应用足够使用
缺点:
- 安全性差:容易产生野指针和内存泄漏
- 效率低:获取大小需要O(n)时间
- 使用复杂:需要处理更多边界条件
- 可维护性差:接口不够清晰