S2--单链表
2.1单链表
2.1.1单链表定义:
单链表是一种基础且重要的链式数据结构,它通过指针将一组零散的内存块(节点)串联起来,用于存储逻辑关系为“一对一”的数据。理解单链表是掌握更复杂链表结构(如双向链表、循环链表)和许多高级算法的基础。
特性分类 | 核心要点 |
---|---|
基本结构 | 每个节点包含数据域(存储数据)和指针域(存储下一个节点的地址)。最后一个节点的指针指向 NULL 。 |
关键指针 | 头指针:永远指向链表的第一个节点(可能是头节点或首元节点),是链表的“入口”。 |
头节点 | 可选的、不存储实际数据的节点,位于首元节点之前。其引入可以简化插入、删除等操作的代码逻辑。 |
核心优点 | 动态扩容:内存按需分配,无需预先申请大块连续空间。高效增删:在已知节点位置时,插入或删除操作仅需修改指针,时间复杂度为 O(1)。 |
主要缺点 | 随机访问低效:要访问第 i 个元素,必须从头节点开始顺序遍历,时间复杂度为 O(n)。额外空间开销:每个节点都需要额外的空间来存放指针。 |
常见操作时间复杂度 | 访问第 i 个元素:O(n) • 在指定节点后插入:O(1) • 搜索特定值:O(n) • 删除指定节点:O(1)(需先找到前驱节点则为 O(n)) |
2.1.2单链表设计与实现
节点结构:每个节点由两部分组成:
- 数据域(vaval):存放数据元素本身的信息。
- 指针域(next):存放指向下一个节点的内存地址的指针
- 头指针(Head):这是一个关键变量,它存储着链表第一个节点的地址。无论链表如何变化,头指针是访问和标识整个链表的唯一起点。
- 头节点(Head->Node):这是一个可选节点,位于链表的真正第一个数据节点(首元节点)之前。头节点的数据域通常不存储信息或存储无关信息,其指针域指向首元节点。
- 容量大小(cursize):记录链表中的元素个数。
- 引入头节点的好处:最大的优势在于统一了空表和非空表的操作。例如,在空链表中插入第一个元素,或在链表头部进行插入/删除操作时,因为有头节点作为不变的“前驱”,操作逻辑可以保持一致,无需特殊处理头指针本身,从而简化了代码。
#define ELEM_TYPE inttypedef struct ListNode {ELEM_TYPE val;ListNode* next;}ListNode;typedef struct Linklist {ListNode* head;size_t cursize;}LinkList;
链表初始化:
InitList : 正确创建了头节点(哨兵节点),这确实能简化插入和删除操作。注意检查 plist 为 NULL 。
void InitList(LinkList* plist){assert(plist != NULL);ListNode* p1 = (ListNode*)malloc(sizeof(ListNode));if (p1 == NULL)return;p1->next = NULL;plist->head = p1;plist->cursize = 0;}
获取元素大小:
GetSize 直接返回 cursize ,效率很高
int GetSize(const LinkList* plist) {assert(plist != NULL);return plist->cursize; // O(1)时间复杂度}
链表判空:
Is_Empty 判断链表是否为空
bool Is_Empty(const LinkList* plist) {assert(plist != NULL);return GetSize(plist) == 0;// 或者也可以判断头节点的next是否为空:return plist->head->next == NULL;}
按位置查找结点:
FindPos 函数返回指向第 pos 个节点的指针。这里的位置约定是:头节点(哨兵节点)的位置是0,第一个数据节点(首元节点)的位置是1。函数通过循环 pos 次移动指针来定位。
PrevFindPos 通过调用 FindPos(pos - 1) 来获得第 pos 个节点的前驱节点。
ListNode* FindPos(const LinkList* plist, int pos) {assert(plist != NULL);if (pos < 0 || pos > GetSize(plist)) { // 注意:pos可以等于GetSize(plist),此时返回的是最后一个节点的下一个位置(NULL)printf("pos位置不符\n");return NULL;}ListNode* p = plist->head; // p指向头节点,位置0if (pos == 0) return p;while (pos--) {p = p->next;}return p;}ListNode* PrevFindPos(const LinkList* plist, int pos) {assert(plist != NULL);return FindPos(plist, pos - 1); // 直接复用FindPos函数}
创建新节点 ( buynode ):
ListNode* buynode(ELEM_TYPE val) {ListNode* p = (ListNode*)malloc(sizeof(ListNode));if (p == NULL) return NULL; // 内存分配失败检查p->val = val;p->next = NULL;return p;}
插入如新节点:
在指定节点后插入 ( InsertNext )。
bool InsertNext(LinkList* plist, ListNode* ptr, ELEM_TYPE val) {assert(plist != NULL && ptr != NULL); // 修改了条件,使用&&ListNode* p = buynode(val);if (p == NULL) return false;p->next = ptr->next;ptr->next = p;plist->cursize++;return true;}
代码解读:这是最核心的插入操作,时间复杂度为O(1)。关键在于先连接新节点与后继节点,再连接前驱节点与新节点。
按位置插入 ( InsertPos )。
bool InsertPos(LinkList* plist, int pos, ELEM_TYPE val) {assert(plist != NULL);ListNode* ptr = PrevFindPos(plist, pos); // 找到第pos个节点的前驱节点if (ptr == NULL) return false;return InsertNext(plist, ptr, val); // 复用InsertNext}
代码解读:
- 先找到第 pos 个位置的前驱节点,然后调用 InsertNext 插入。
- 查找前驱节点的时间复杂度是O(n),因此按位置插入的整体复杂度是O(n)。
头插法 ( Push_Front ) 与尾插法 ( Push_Back )
bool Push_Front(LinkList* plist, ELEM_TYPE val) {assert(plist != NULL);// 直接在头节点后插入,即位置1的前驱就是头节点return InsertNext(plist, plist->head, val); // 复用InsertNext}bool Push_Back(LinkList* plist, ELEM_TYPE val) {assert(plist != NULL);ListNode* p = plist->head;while (p->next != NULL) p = p->next; // 遍历找到最后一个节点return InsertNext(plist, p, val); // 在最后一个节点后插入}
代码解读:
- Push_Front 时间复杂度为O(1)。
- Push_Back 需要遍历找到尾节点,时间复杂度为O(n)。如果频繁进行尾插,可以考虑维护一个尾指针 tail 成员。
遍历打印 ( PrintInfo ):
void PrintInfo(const LinkList* plist) {assert(plist != NULL);for (ListNode* p = plist->head->next; p != NULL; p = p->next) {printf("%d ", p->val);}}
代码解读:从首元节点开始打印,直到 NULL 。
删除节点:
删除指定节点的后继节点 ( DelNext )。
bool DelNext(LinkList* plist, ListNode* ptr) {assert(plist != NULL && ptr != NULL && ptr->next != NULL);if (Is_Empty(plist)) return false;ListNode* p = ptr->next;ptr->next = ptr->next->next;free(p);p = NULL; // 注意:这里应该是 p = NULL; 而不是 p == NULL;plist->cursize--;return true;}
代码解读:这是核心的删除操作,时间复杂度为O(1)。关键在于先保存要删除的节点,修改指针连接,然后释放内存。
按位置删除 ( DelPos )。
bool DelPos(LinkList* plist, int pos) {assert(plist != NULL);if (Is_Empty(plist)) return false;ListNode* ptr = plist->head;while (--pos) { // 循环pos-1次,找到第pos个节点的前驱ptr = ptr->next;}ListNode* p = ptr->next;ptr->next = p->next;free(p);p = NULL; // 同样,这里应该是 p = NULL;plist->cursize--;return true;}
代码解读:先找到要删除节点的前驱,然后执行删除。查找过程O(n),删除本身O(1),整体O(n)。
头删 ( Pop_Front ) 与尾删 ( Pop_Back )。
bool Pop_Front(LinkList* plist) {assert(plist != NULL);if (Is_Empty(plist)) return false;// 删除头节点后的第一个节点ListNode* ptr = plist->head->next;plist->head->next = ptr->next;free(ptr);ptr = NULL;plist->cursize--;return true;}bool Pop_Back(LinkList* plist) {assert(plist != NULL);if (Is_Empty(plist)) return false;ListNode* ptr = plist->head;while (ptr->next->next != NULL) ptr = ptr->next; // 找到倒数第二个节点ListNode* p = ptr->next;ptr->next = NULL; // 或者 ptr->next = p->next; 但此时p->next为NULLfree(p);p = NULL;plist->cursize--;return true;}
代码解读:
- Pop_Front 时间复杂度为O(1)。
- Pop_Back 需要找到倒数第二个节点,时间复杂度为O(n)。
按值查找:
查找节点 ( FindValue )。
ListNode* FindValue(const LinkList* plist, ELEM_TYPE val) {assert(plist != NULL);ListNode* p = plist->head->next; // 从第一个数据节点开始找while (p != NULL) {if (p->val == val) return p;p = p->next;}return NULL;}
代码解读:顺序遍历,时间复杂度O(n)。查找节点的前驱 ( PreFindValue )。
ListNode* PreFindValue(const LinkList* plist, ELEM_TYPE val) {assert(plist != NULL);ListNode* p = plist->head;while (p->next != NULL) {if (p->next->val == val) return p;p = p->next;}return NULL;}
代码解读:通过判断 p->next->val 来定位,返回的是目标节点的前驱节点。这在删除操作中很有用。
清空与销毁:
清空链表 ( ClearList )。
void ClearList(LinkList* plist) {assert(plist != NULL);ListNode* p = plist->head->next;while (p != NULL) {ListNode* n = p;p = p->next;free(n);n = NULL;}plist->head->next = NULL; // 重要:清空后头节点的next应指向NULLplist->cursize = 0;}
代码解读:释放所有数据节点,但保留头节点,链表可再次使用。
销毁链表 ( DestroyList )。
void DestroyList(LinkList* plist) {assert(plist != NULL);ListNode* p = plist->head;while (p != NULL) {ListNode* n = p;p = p->next;free(n);n = NULL;}// 重要:应将plist->head置为NULL,避免成为野指针。// 但plist本身是外部变量,通常由调用者管理。// 可以在函数内添加:plist->head = NULL; plist->cursize = 0;}
代码解读:释放所有节点,包括头节点。链表结构不再可用。
2.1.3总结与关键点回顾
操作 | 时间复杂度 | 关键要点 |
---|---|---|
初始化 | O(1) | 创建头节点,初始化大小 |
获取大小 | O(1) | 直接返回 cursize |
按位置查找 | O(n) | 顺序遍历 |
指定节点后插入 | O(1) | 修改指针顺序:新节点->后继,前驱->新节点 |
按位置插入 | O(n) | 查找O(n)+插入O(1) |
头插/头删 | O(1) | 操作头节点后 |
尾插/尾删 | O(n) | 需遍历找尾 |
按值查找 | O(n) | 顺序遍历比较 |
清空/销毁 | O(n) | 逐个节点释放 |