前言
本篇要求掌握的C语言基础知识:指针、结构体
目录
前言
单链表
概念
对比链表和顺序表
创建链表
实现单链表
准备工作
打印链表
创建节点并初始化
尾插
二级指针的调用
尾插代码
头插
尾删
头删
查找(返回节点)
在指定位置(pos)之前插入数据
在指定位置(pos)之后插入数据
删除pos节点
删除pos之后的节点
销毁链表
单链表
概念
链表是⼀种物理存储结构上⾮连续、⾮顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。
对比链表和顺序表
顺序表:
1) 占用一大片连续内存空间
2) 不需要额外空间存储逻辑关系,总空间需求最少
4) 可顺序访问,支持随机访问
5) 在C语言中,通过数组实现
6) 数据元素的插入和删除操作通过移动元素完成
链表:
1) 不要求占用连续内存空间
2) 不仅要存储数据,还要存储数据之间的关系,故总空间需求较大
3) 通过指针反映逻辑关系
4) 逻辑连续,物理可不连续
5) 只可顺序访问,不支持随机访问
6) 存在标记:头指针
7) 数据元素的插入和删除操作通过修改指针完成:定位插入点/删除点的直接前驱/后
从上文可以得知与顺序表不同的是,链表⾥的每节"⻋厢"都是独⽴申请下来的空间,我们称之为“结点/节点” ,节点的组成主要有两个部分:当前节点要保存的数据和保存下⼀个节点的地址(指针变量)。
创建链表
//创建节点
typedef int SLTDataType;
typedef struct SLNode
{
SLTDataType data;//数据域
struct SLNode* next;//指针域
}SLTNode;
//创建节点
SLTNode* node1 = (SLTNode*)malloc(sizeof(SLTNode));
node1->data = 1;
SLTNode* node2 = (SLTNode*)malloc(sizeof(SLTNode));
node2->data = 2;
SLTNode* node3 = (SLTNode*)malloc(sizeof(SLTNode));
node3->data = 3;
SLTNode* node4 = (SLTNode*)malloc(sizeof(SLTNode));
node4->data = 4;
//链接节点
node1->next = node2;
node2->next = node3;
node3->next = node4;
node4->next = NULL;//尾指针置空
其中数据域用于存放数据,指针域用于存放下一个结点的地址。上面的链表是手动创建节点,只是为了展示链表的形成,后续创建和链接单链表可以通过函数实现。
实现单链表
准备工作
在工程中一共包含三个文件
- 定义文件SLNode.h:定义函数和结构体,头文件:stdio.h、stdlib.h、assert.h
- 实现文件SLNode.c:实现函数具体功能,头文件:SLNode.h
- 测试文件test.c:测试每一部分代码的正确性,头文件:SLNode.h
在开始之前我们需要定义一个指向为空的结构体类型的节点(SLNode*)plist,作为链表的头节点。
SLNode* plist = NULL;
打印链表
//打印 void SLTprint(SLTNode* phead) { SLNode* pcur = phead; while (pcur != NULL) { printf("%d ", pcur->data); pcur = pcur->next; } printf("\n"); }
创建节点并初始化
//创建节点并初始化 SLNode* SLTbuyNode(SLTDataType x) { SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));//创建新节点 if (newnode == NULL) { perror("malloc fail!"); exit(1);//表示非正常退出 } newnode->data = x; newnode->next = NULL; return newnode; }
尾插
二级指针的调用
从这一部分开始就涉及到了二级指针传参的问题,在对单链表进行尾插时,如果此时头节点plist指向为空(即该单链表为空),就需要在函数内部改变头指针的指向,指向新插入的节点。
这里举一个简单的例子,假如我要实现一个交换两个整形数据的函数,应该如何实现?
void Exchange(int a,int b) { int tmp=a; a=b; tmp=b; }
如果仅仅将两个整形作为参数是无法成功的,因为在主函数中调用Exchange时在栈帧中又开辟了一块地址不同于主函数的函数栈帧,以上"传值调用"仅仅将形参里的内容进行交换,在函数执行结束时所占据的空间会被释放,同时形参也会因为被销毁而无法对实参产生影响。
如果想要"形参影响实参",就要把"传值调用"改为"传址调用",即将变量的地址作为参数传给函数,对应的函数参数应为指针类型。
void Exchange(int* a,int* b) { int* tmp=*a; *a=*b; *tmp=*b; }
这样就实现了交换两个数据的操作。
同理,想要在函数内部改变一级头指针plist的指向,应该把plist的地址传入,用二级指针接收,也就是"传址调用",如果只传递一级指针(即链表的头指针),无法直接修改它所指向的地址,因为在函数内部对指针的修改不会影响到函数外部,最终只是将形参指针的指向改变而无法对实参造成影响。为了实现对链表头指针的修改,需要传递指向指针的指针,这样在函数内部就可以修改指针所指向的地址,从而改变链表的头指针。
来一张图解释二级指针
总结:只要头指针发生改变就需要用到二级指针
尾插代码
void SLTpushBack(SLTNode** pphead, SLTDataType x) { assert(pphead); SLTNode* newnode = SLTbuyNode(x); if (*pphead == NULL)//链表为空 { *pphead = newnode; } else { SLNode* ptail = *pphead; while (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是指向第一个节点的指针 *pphead = newnode; }
尾删
void SLTpopBack(SLTNode** pphead) { assert(pphead && *pphead);//*pphead为空说明整个链表为空 if ((*pphead)->next == NULL)//链表中只有一个节点 { free(*pphead); *pphead = NULL; } else { SLTNode* ptail = *pphead; SLTNode* prev = *pphead; while (ptail->next != NULL) { prev = ptail;//prev指向的是尾节点的前一个节点 ptail = ptail->next; } free(ptail); prev->next = NULL;//prev成为新的尾节点 ptail = NULL; } }
头删
void SLTpopFront(SLTNode** pphead) { assert(pphead && *pphead); if ((*pphead) == NULL) { free(*pphead); *pphead = NULL; } else { SLTNode* p = *pphead;//此时p指向的是头节点 *pphead = (*pphead)->next; free(p); p = NULL; } }
查找(返回节点)
SLNode* SLTfind(SLTNode* phead, SLTDataType x) { assert(phead); SLNode* pcur = phead; while (pcur != NULL) { if (pcur->data == x) { return pcur; } pcur = pcur->next; } return NULL;//没有找到返回NULL }
在指定位置(pos)之前插入数据
void SLTinsert(SLTNode** pphead, SLTNode* pos, SLTDataType x) { assert(pphead && *pphead && pos); SLTNode* pcur = *pphead; SLNode* newnode = SLTbuyNode(x); if (pos == *pphead) { SLTpushFront(pphead, x); } else { while (pcur->next != pos)//遍历到pos节点的前驱节点 { pcur = pcur->next; } newnode->next = pos; pcur->next = newnode; } }
在指定位置(pos)之后插入数据
void SLTinsertAfter(SLTNode* pos, SLTDataType x) { assert(pos); SLNode* newnode = SLTbuyNode(x); if (pos->next == NULL)//如果pos是尾节点 { pos->next = newnode; newnode->next = NULL; } else { SLNode* pafter = pos->next;//pcur是pos的后继节点 newnode->next = pafter; pos->next = newnode; } }
在这里不调用二级指针的原因是头指针无需改变,需要改变的时pos节点内部next指针的指向,而对于next指针来说,pos指向的时next所在的节点,所以pos可以直接访问这个黑点,从而改变next的指向,换句话pos相对于next来说就是二级指针。
删除pos节点
void SLTerase(SLTNode** pphead, SLTNode* pos) { assert(*pphead && pos && pphead); if (pos->next == NULL)//如果pos是尾节点 { SLTNode* prev = *pphead; while (prev->next != pos) { prev = prev->next; } prev->next = NULL; free(pos); pos = NULL; } else if (*pphead == pos)//如果pos是头节点 { SLTNode* next = (*pphead)->next; free(*pphead); (*pphead) = next; } else { SLTNode* prev = *pphead; while (prev->next != pos) { prev = prev->next; } prev->next = pos->next; free(pos); pos = NULL; } }
删除pos之后的节点
void SLTeraseAfter(SLTNode* pos) { assert(pos->next && pos); SLTNode* next = pos->next; pos->next = pos->next->next; free(next); next = NULL; }
销毁链表
void SLTdestroy(SLTNode** pphead) { assert(*pphead && pphead); SLTNode* pcur = *pphead; while (pcur != NULL) { SLTNode* next = pcur->next; free(pcur); pcur = next; } *pphead = NULL; }