链表
链表是线性表的一种。链表⼀种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
逻辑结构线性,物理结构(绝大多数情况下)非线性。
结构 :链表是由结点组成的,而结点=存储数据+指向下一数据的指针
单链表的构建
- 思考的角度:前提条件、基本实现方法、需要特殊考虑的情况、什么代码可以复用
- 测试时调用函数关注一下参数有没有传错!
结点
c
typedef int SLTDataType;
typedef struct SListNode {
SLTDataType data;
struct SListNode* next;
}SLTNode;
注意:C语言是向上编译的,所以在取别名SLTNode前就使用这个别名会导致报错,因为使用之前并没有取好别名!所以定义next指针还是要写struct SListNode* next
遍历与打印单链表
c
void SLTPrint(SLTNode* phead) {
SLTNode* pcur = phead;
while (pcur != NULL) {
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("NULL\n");
}
初始化新结点
c
//向操作系统申请一个新结点并初始化
SLTNode* SLTBuyNode(SLTDataType x) {
//创建新结点
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL) {
perror("Malloc fail!");
exit(1);
}
//将数据存入新节点
newnode->data = x;
newnode->next = NULL;
return newnode;
}
尾插
注意要点:
- 当头节点是空指针时,需要修改指针本身的值 ,使其指向创建的新结点,所以注意要进行传址传参,即参数是二级指针SLTNode** pphead,同时因为代码中要用到*pphead,注意到NULL不可以解引用的原则,增加断言assert(pphead)
3. 要对尾结点进行操作,需要找到的是ptail->next的这个点,而不是ptail=NULL的点,这个点已经越过了。
c
void SLTPushBack(SLTNode** pphead, SLTDataType x) {
SLTNode* newnode = SLTBuyNode(x);
if (*pphead == NULL) {
*pphead = newnode;
}
else {
SLTNode* ptail = *pphead;
while (ptail->next) {//等价于ptail->next!=NULL,即遍历到尾节点
ptail = ptail->next;
}
ptail->next = newnode;
}
}
为什么我们遍历的时候,总是重新定义一个新指针SLTNode* ptail = *pphead; ``SLTNode* pcur = phead
来进行遍历?
因为我们要保留好头节点的地址,防止后续还要使用到前面的数据时出现困难。
头插
看参数传的是一级还是二级指针,就思考方法是否要改变指针本身的指向,即其本身的值
注意:
- phead永远指向头结点
- 初始链表为空时让phead直接指向新结点
在单链表中,头插的时间复杂度仅为O(1)
c
//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x) {
assert(pphead);
SLTNode* newnode=SLTBuyNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
尾删
- 当链表为空时(*pphead=NULL),删除操作就没有意义,所以把这个条件加入assert中判断,同时pphead仍然不能为NULL。
- 当链表中只有一个结点时,即删除的是头节点时,要改变phead的指向,所以函数参数仍然是二级指针pphead。
->操作符优先级高于*操作符,所以条件注意写成(*pphead)->next==NULL - 当链表中有多个结点时,首先要找到尾结点的前一个结点指针prev,将prev->next=NULL(直接释放尾节点,prev->next会成为野指针)才能释放尾节点。
- 删除操作需要注意的点:关注野指针,free+置空
c
//尾删
void SLTPopBack(SLTNode** pphead) {
assert(pphead && *pphead);
//只有一个结点的情况
if ((*pphead)->next == NULL) {
free(*pphead);
*pphead = NULL;
}
else {
SLTNode* prev = NULL;
SLTNode* ptail = *pphead;
while (ptail->next) {
prev = ptail;
ptail = ptail->next;
}
prev->next = NULL;
free(ptail);
ptail = NULL;//free完任何指针以后都要置空!
}
}
头删
- 每一个结点一一相连,所以头结点不能直接释放掉,而是要先找到第二个结点存起来,再释放掉第一个结点。
- 检验当链表中只有一个结点时,和有多个结点的方法可以通用。
c
//头删
void SLTPopFront(SLTNode** pphead) {
assert(pphead && *pphead);
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
查找
c
//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x) {
SLTNode* pcur = phead;
//注意这里是遍历链表,而不是找到尾结点!
//所以条件是pcur而不是pcur->next!!!
while (pcur) {
if (pcur->data == x) {
return pcur;
}
pcur = pcur->next;
}
//未找到
return NULL;
}
在指定位置之前插入数据
- 链表为空或pos为NULL时功能没有意义
- 链表的位置用结点的指针表示
- 分只有一个结点和有多个结点两种情况
- 有多个结点时,先找到pos的前一个结点,用prev存储
c
//在pos前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x) {
//传入的结点位置为空是没有意义的
assert(pphead&&pos);
SLTNode* newnode = SLTBuyNode(x);
if (pos == *pphead) {
//pos是头结点时,等价于头插
SLTPushFront(pphead, x);
}
else{
SLTNode* prev = *pphead;
while (prev->next != pos) {
prev = prev->next;
}
prev->next = newnode;
newnode->next = pos;
}
}
在指定位置之后插入数据
先让newnode接上pos->next,再让pos->next指向newnode,直接调换顺序是不行的
c
//在pos后插入
void SLTInsertAfter(SLTNode* pos, SLTDataType x) {
assert(pos);
SLTNode* newnode = SLTBuyNode(x);
newnode->next = pos->next;
pos->next = newnode;
//注意这两步不能直接调换!
}
删除指定位置的结点
- 已知指定位置结点时,什么时候要传入头结点?
当需要找指定位置之前的结点时要传入,因为单链表只能从前往后找,而无法从后往前回找。 - 可复用的代码尽量复用
c
//删除pos结点
void SLTErase(SLTNode** pphead, SLTNode* pos) {
//pphead为空、pos为空都没意义(不用特地强调链表为空,因为pos不为空显然链表就不会为空了)
assert(pphead && pos);
//如果删除的是头结点要单独讨论,否则下面代码会出错
if (pos == *pphead) {
SLTPopFront(pphead);
}
else {
SLTNode* prev = *pphead;
while (prev->next != pos) {
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
删除指定位置之后的结点
- 前提条件:pos之后的结点不能为空(删除一个空结点没有意义)
- 不需要传头结点了,因为这里只涉及pos及它之后的结点操作
c
//删除pos之后的结点
void SLTEraseAfter(SLTNode* pos) {
//pos之后的结点若为空,这个功能也没有意义
assert(pos && pos->next);
//先储存要删除的pos的下一个结点,否则一会pos改变指向了找不到
SLTNode* del = pos->next;
pos->next = del->next;
free(del);
del = NULL;
}
销毁链表
c
//销毁链表
void SListDestroy(SLTNode** pphead) {
assert(pphead);
SLTNode* pcur = *pphead;
while (pcur) {
SLTNode* next = pcur->next;
free(pcur);
pcur = next;
}
*pphead = NULL;//这是让函数外的头指针本身置空
}
完整代码
SList.h
c
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int SLTDataType;
typedef struct SListNode {
SLTDataType data;
struct SListNode* next;
}SLTNode;
//遍历并打印链表
void SLTPrint(SLTNode* phead);
//向操作系统申请一个新结点并初始化
SLTNode* SLTBuyNode(SLTDataType x);
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x);
//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x);
//尾删
void SLTPopBack(SLTNode** pphead);
//头删
void SLTPopFront(SLTNode** pphead);
//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
//在pos前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//在pos后插入
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
//删除pos结点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//删除pos之后的结点
void SLTEraseAfter(SLTNode* pos);
//销毁链表
void SListDestroy(SLTNode** pphead);
SList.c
c
#define _CRT_SECURE_NO_WARNINGS 1
#include "SList.h"
void SLTPrint(SLTNode* phead) {
SLTNode* pcur = phead;
while (pcur != NULL) {
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("NULL\n");
}
//向操作系统申请一个新结点并初始化
SLTNode* SLTBuyNode(SLTDataType x) {
//创建新结点
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL) {
perror("Malloc fail!");
exit(1);
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
void SLTPushBack(SLTNode** pphead, SLTDataType x) {
assert(pphead);
SLTNode* newnode = SLTBuyNode(x);
if (*pphead == NULL) {
*pphead = newnode;
}
else {
SLTNode* ptail = *pphead;
while (ptail->next) {//等价于ptail->next!=NULL,即遍历到尾节点
ptail = ptail->next;
}
ptail->next = newnode;
}
}
//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x) {
assert(pphead);
SLTNode* newnode=SLTBuyNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
//尾删
void SLTPopBack(SLTNode** pphead) {
assert(pphead && *pphead);
//只有一个结点的情况
if ((*pphead)->next == NULL) {
free(*pphead);
*pphead = NULL;
}
else {
SLTNode* prev = NULL;
SLTNode* ptail = *pphead;
while (ptail->next) {
prev = ptail;
ptail = ptail->next;
}
prev->next = NULL;
free(ptail);
ptail = NULL;//free完任何指针以后都要置空!
}
}
//头删
void SLTPopFront(SLTNode** pphead) {
assert(pphead && *pphead);
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x) {
SLTNode* pcur = phead;
//注意这里是遍历链表,而不是找到尾结点!
//所以条件是pcur而不是pcur->next!!!
while (pcur) {
if (pcur->data == x) {
return pcur;
}
pcur = pcur->next;
}
//未找到
return NULL;
}
//在pos前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x) {
//传入的结点位置为空是没有意义的
assert(pphead&&pos);
SLTNode* newnode = SLTBuyNode(x);
if (pos == *pphead) {
//pos是头结点时,等价于头插
SLTPushFront(pphead, x);
}
else{
SLTNode* prev = *pphead;
while (prev->next != pos) {
prev = prev->next;
}
prev->next = newnode;
newnode->next = pos;
}
}
//在pos后插入
void SLTInsertAfter(SLTNode* pos, SLTDataType x) {
assert(pos);
SLTNode* newnode = SLTBuyNode(x);
newnode->next = pos->next;
pos->next = newnode;
//注意这两步不能直接调换!
}
//删除pos结点
void SLTErase(SLTNode** pphead, SLTNode* pos) {
//pphead为空、pos为空都没意义(不用特地强调链表为空,因为pos不为空显然链表就不会为空了)
assert(pphead && pos);
//如果删除的是头结点要单独讨论,否则下面代码会出错
if (pos == *pphead) {
SLTPopFront(pphead);
}
else {
SLTNode* prev = *pphead;
while (prev->next != pos) {
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
//删除pos之后的结点
void SLTEraseAfter(SLTNode* pos) {
//pos之后的结点若为空,这个功能也没有意义
assert(pos && pos->next);
//先储存要删除的pos的下一个结点,否则一会pos改变指向了找不到
SLTNode* del = pos->next;
pos->next = del->next;
free(del);
del = NULL;
}
//销毁链表
void SListDestroy(SLTNode** pphead) {
assert(pphead);
SLTNode* pcur = *pphead;
while (pcur) {
SLTNode* next = pcur->next;
free(pcur);
pcur = next;
}
*pphead = NULL;//这是让函数外的头指针本身置空
}
test.c
c
#define _CRT_SECURE_NO_WARNINGS 1
#include "SList.h"
void test01() {
SLTNode* plist=NULL;//创建头节点->创建空链表
SLTPushBack(&plist, 1);
SLTPushBack(&plist, 2);
SLTPushBack(&plist, 3);
SLTPushBack(&plist, 4);
SLTPrint(plist);
}
void test02() {
SLTNode* plist = NULL;//创建头节点->创建空链表
SLTPushFront(&plist, 1);
SLTPushFront(&plist, 2);
SLTPushFront(&plist, 3);
SLTPushFront(&plist, 4);
SLTPrint(plist);
SLTPopFront(&plist);
SLTPopFront(&plist);
SLTPopBack(&plist);
SLTPopBack(&plist);
SLTPrint(plist);
}
void test03() {
SLTNode* plist = NULL;
SLTPushFront(&plist, 1);
SLTPushFront(&plist, 2);
SLTPushFront(&plist, 3);
SLTPushFront(&plist, 4);
SLTPrint(plist);
//SLTNode* ret=SLTFind(plist, 3);
SLTNode* ret = SLTFind(plist, 333);
if (ret!=NULL) {
printf("找到了\n");
}
else {
printf("没找到\n");
}
}
void test04() {
SLTNode* plist = NULL;
SLTPushBack(&plist, 1);
SLTPushBack(&plist, 2);
SLTPushBack(&plist, 3);
SLTPushBack(&plist, 4);
SLTPrint(plist);
SLTNode* pos1 = SLTFind(plist, 2);
SLTInsert(&plist, pos1, 100);
SLTPrint(plist);
SLTNode* pos2 = SLTFind(plist, 4);
SLTInsertAfter(pos2, 200);
SLTPrint(plist);
pos1 = SLTFind(plist, 2);
SLTErase(&plist, pos1);
SLTPrint(plist);
//C 语言所有参数都是"按值传递"
// 你传进去的是指针变量的值(也就是那块内存的地址),函数内部拿到的只是一个 副本。
// 因此在函数里:
// c
// 复制
// free(pos); // 释放的是 p 指向的那块内存
// pos = NULL; // 只把副本改成 NULL,外面的原指针依旧保留原来的地址值
//结果:
// • 内存确实被释放了;
// • 外部变量pos1仍保存着 已释放的地址(野指针);
// • 外部再解引用就是 未定义行为(可能崩溃、可能脏数据)。
pos2 = SLTFind(plist, 4);
SLTEraseAfter(pos2);
SLTPrint(plist);
}
int main() {
test04();
return 0;
}
总结
在完整实现顺序表和链表之后,我们可以对他们进行对比分析。
-
链表在头部(O(1))和中间的插入删除时间复杂度小于顺序表
-
链表不需要进行增容,开一个用一个,没有额外的空间浪费