C语言实现单链表
线性表与单链表的区别
存储类别 | 顺序存储结构 | 单链表 |
---|---|---|
存储分配方式 | 用一段连续的存储单元依次存储线性表的数据元素 | 采用链式存储结构,用一组任意的存储单元存放线性表的元素 |
时间性能 | 查找O(1)、插入和删除O(n) | 查找O(n)、插入和删除O(1) |
空间性能 | 需要预分配存储空间,分大了浪费,小了容易发生上溢 | 不需要分配存储空间,只要有就可以分配,元素个数不受限制 |
通过上面的对比,可以得出一些经验性的结论:
若线性表需要频繁查找,很少进行插入和删除操作时,宜采用顺序存储结构。若需要频繁插入和删除时,宜采用单链表结构。
当线性表中的元素个数变化较大或者根本不知道有多大时,最好用单链表结构,这样可以不需要考虑存储空间的大小问题。而如果事先知道线性表的大致长度,用顺序存储结构效率会高很多。
首先,了解下什么是头指针、头结点、首元结点
链表中第一个结点的存储位置叫做头指针,那么整个链表的存取就必须是从头指针开始进行了。之后的每一个结点,其实就是上一个的后继指针指向的位置。
这里有个地方要注意,就是对头指针概念的理解,这个很重要。“链表中第一个结点的存储位置叫做头指针”,如果链表有头结点,那么头指针就是指向头结点数据域的指针。画一个图吧。
头指针就是链表的名字。头指针仅仅是个指针而已。
- 头结点是为了操作的统一与方便而设立的,放在第一个元素结点之前,其数据域一般无意义(当然有些情况下也可存放链表的长度、用做监视哨等等)。
- 有了头结点后,对在第一个元素结点前插入结点和删除第一个结点,其操作与对其它结点的操作统一了。
- 首元结点也就是第一个元素的结点,它是头结点后边的第一个结点。
- 头结点不是链表所必需的。
- 是的,对于头指针,我们也可以有相应的理解了。
- 在线性表的链式存储结构中,头指针是指链表指向第一个结点的指针,若链表有头结点,则头指针就是指向链表头结点的指针。
- 头指针具有标识作用,故常用头指针冠以链表的名字。
- 无论链表是否为空,头指针均不为空。头指针是链表的必要元素。
- 单链表也可以没有头结点。如果没有头结点的话,那么单链表就会变成这样:
然后什么是一级指针,什么是二级指针
用框图表示链表中二级指针或者一级指针的使用更加直白了。
1,二级指针创建头指针。
a.只有头指针,没有头结点
b,有头指针,也有头节点
c,而如果不用二级指针,直接传一个一级指针,相当于生成L的拷贝M,但是对M分配空间与L无关了。
2,二级指针销毁头指针
无论有没有头节点都要用二级指针或者一级指针的引用传参来销毁。
3,二级指针与一级指针方式插入结点
传二级指针就是在从链表头指针开始对链表操作,传一级指针只不过是对头结点L生成了一个拷贝M,M的next指向的仍然是L的next,因此,后面的操作仍然是在原链表上操作。
4,二级指针与一级指针方式删除结点
删除的原理与插入一样。
注意:
在没有传入头结点的情况下必须使用二级指针,使用一级指针无效。
参考文章:
https://www.cnblogs.com/wincai/p/5893475.html
https://blog.youkuaiyun.com/lidafoye/article/details/76974058
https://blog.youkuaiyun.com/u012234115/article/details/39717215
代码实现
1.1 初始化链表
这里为什么要使用二级指针,我的理解是:形参改变无法影响实参。创建链表需要改变指针的指向,即该变指针的值,函数参数为结构体二级指针*L,即为指向结构体指针的指针,如果我们只传L,子函数会为这个参数临时分配空间以存储这个参数,在子函数中,将malloc分配内存的返回使用L去接收,操作会将新的内存的地址送入这个原本为参数分配的空间中,无法做到对L的指向做出改变,对实参没影响(即,返回结果只在函数体内有效,函数结束后,这个空间就被释放了)。而后面的操作为什么只用一级指针就可以,因为无需改变头指针的指向,只是对L->next做操作,所以使用一级指针就够了。如果还无法理解可参考文章:https://www.cnblogs.com/wincai/p/5893475.html
/**
* @description: 创建顺序线性表
* @param {LinkList *L: 结构体二级指针}
* @return: FAILURE:内存分配失败
* @return: SUCCESS:内存分配成功
*/
Statue InitList(LinkList *L)
{
//分配内存空间, 产生头结点
*L = (LinkList) malloc (sizeof(node)); // 分配成功返回内存空间和首地址,分配失败返回空
//判断分配是否成功
if(*L == NULL){
printf("单链表内存分配失败\n\n");
return FAILURE; //内存空间分配失败
}
//分配成功, 将指针域置空, 空表
(*L)->next = NULL;
//返回成功
printf("单链表内存分配成功\n");
return SUCCESS;
}
其他操作代码
/**
* @description: 输出表内容
* @param {LinkList L: 结构体一级指针}
* @return: NULL
*/
void ListElemOutput(LinkList L)
{
LinkList p = L->next;
printf("当前表数据为:");
while(p){
printf("%4d", p->data);
p = p->next;
}
printf("\n");
}
/**
* @description: 获取表长度
* @param {LinkList L:结构体指针}
* @return: 表长度
*/
Statue GetLength(LinkList L)
{
int length = 0; // 保存表长度变量
LinkList p = L->next; // 获取头结点的下一个结点地址
while (p != NULL) // 结点指针域不为NULL, 说明有数据
{
length++; // 长度加1
p = p->next; // 获取下一个结点地址
}
// 返回结果
return length;
}
/**
* @description: 获取表元素
* @param {LinkLine L: 结构体指针 }
* @param {int location: 待获取元素的位置}
* @param {ElemType* data: 数据指针, 存放获取到的数据}
* @return: ERROR: 传入获取元素位置的参数错误
* @return: FAILURE: 获取失败
* @return: SUCCESS: 获取成功
*/
Statue GetElem(LinkList L, int location, ElemType* data)
{
int n = 1;
LinkList p;
// 判断传入的数据位置是否合法(大于等于head)
if(location < n){
printf("传入元素位置有误\n");
return ERROER;
}
p = L->next; //首元结点给到p
while(p != NULL && n < location){ //未到表尾和未到指定位置
p = p->next; // 指向下一个结点
++n;
}
if(p == NULL){
printf("查找%d位置的数据失败\n", location);
return FAILURE;
}
// 获取数据域数据
*data = p->data;
return SUCCESS;
}
/**
* @description: 尾插法创建链表, 数据域data存放随机数[1, 100]
* @param {LinkList *L:结构体二级指针}
* @param {int n: 插入数据的个数}
* @return: FAILUER: 内存分配失败
* @return: SUCCESS: 插入成功
*/
Statue GreateListTail(LinkList *L, int n)
{
LinkList r, s;
int i;
srand(time(0)); // 初始化随机数种子
//获取头指针
*L = (LinkList) malloc (sizeof(node));
if(*L == NULL){
printf("内存分配失败\n\n");
return FAILURE;
}
r = *L; // 将内存分配的头指针给到r
for(i = 0; i < n; i++){
s = (LinkList) malloc (sizeof(node)); // 新结点
s->data = rand() % 100 + 1; // rand()生成随机数, 对100取模, 生成[0, 99]的数, 加1生成[1, 100]
r->next = s; // r指向新结点
r = s; //将r移动到新结点, 即现在r为新结点
}
r->next = NULL; // 表尾
return SUCCESS;
}
/**
* @description: 头插法创建链表, 数据域data存放随机数[1, 100], (即插入的为首元结点)
* @param {LinkList *L:结构体二级指针}
* @param {int n: 插入数据的个数}
* @return: FAILUER: 内存分配失败
* @return: SUCCESS: 插入成功
*/
Statue GreateListHead(LinkList *L, int n)
{
//定义一个中间结构体指针, 用于保存函数体内生成的结点内容
LinkList p;
int i;
srand(time(0)); // 初始化随机数种子
//获取头指针
*L = (LinkList) malloc (sizeof(node));
//判断内存是否分配成功
if(*L == NULL){
printf("内存分配失败\n\n");
return FAILURE;
}
//将头结点的指针域置NULL
(*L)->next = NULL;
//创建首元结点, 头插法接入头结点
for(i = 0; i < n; i++){
// 为子结点分配内存
p = (LinkList) malloc (sizeof(node));
// 生成随机数, 存放于数据域中
p->data = rand() % 100 + 1; // rand()生成随机数, 对100取模, 生成[0, 99]的数, 加1生成[1, 100]
p->next = (*L)->next; //首元结点指针域指向头结点后的地址, 实现插入头结点后
(*L)->next = p; //头结点指针域指向首元结点p
}
printf("数据插入成功, 当前表长度为:%d\n\n", GetLength(*L));
return SUCCESS;
}
/**
* @description: 插入数据
* @param {LinkList L: 结构体一级指针,
* 有一些也是如下代码, 不需要动到头指针,
* 但使用二级指针, 我也不知道为什么, 感觉没必要, 望赐教}
* @param {int location: 插入数据的位置}
* @param {ElemType* data: 插入的元素}
* @return: ERROR: 传入获取元素位置的参数错误
* @return: FAILURE: 获取失败
* @return: SUCCESS: 获取成功
*/
Statue ListInsert(LinkList L, int location, ElemType* data)
{
int n = 1;
LinkList p, s;
// 判断插入的数据是否合法
if(location < n){
printf("传入元素位置有误\n");
return ERROER;
}
p = L;
while(p != NULL && n < location){
p = p->next;
++n;
}
if(p == NULL){
printf("查找%d位置的数据失败\n", location);
return FAILURE;
}
// 生成新结点
s = (LinkList) malloc ((sizeof(node)));
s->data = *data; // 放入数据
s->next = p->next;
p->next = s;
printf("数据插入成功\n");
return SUCCESS;
}
/**
* @description:
* @param {LinkList L: 结构体一级指针,
* 有一些也是如下代码, 不需要动到头指针,
* 但使用二级指针, 我也不知道为什么, 感觉没必要, 望赐教}
* @param {int location: 删除数据的位置}
* @param {ElemType* data: 删除的元素}
* @return: ERROR: 传入获取元素位置的参数错误
* @return: FAILURE: 获取失败
* @return: SUCCESS: 获取成功
*/
Statue ListDelete(LinkList L, int location, ElemType* data)
{
int n = 1;
LinkList p, s;
p = L;
// 判断插入的数据是否合法
if(location < n){
printf("传入元素位置有误\n");
return ERROER;
}
while(p->next != NULL && n < location){
p = p->next;
++n;
}
if(p == NULL){
printf("查找%d位置的数据失败\n", location);
return FAILURE;
}
s = p->next; // 把删除结点的首地址给临时结点, 这样就能把删除结点的指针域保存下来
p->next = s->next; // 删除结点的指针域 指向 删除结点后继结点的首地址
*data = s->data; //删除元素的值
free(s); // 释放资源
printf("删除结点成功\n");
return SUCCESS;
}
/**
* @description: 单链表整表删除
* @param {LInkList L: 结构体一级指针}
* @return: SUCCESS: 删除成功
*/
Statue ClearList(LinkList L)
{
LinkList p, q;
p = L->next;
while(p){
q = p->next;
free(p);
p = q;
}
L->next = NULL;
return SUCCESS;
}
主函数测试
int main(void)
{
int result;
ElemType data; // 获取某个位置的数据变量
int i;
LinkList L; //指向单链表结构体的指针
//InitList(&L);
//GreateListHead(&L, 5);
GreateListTail(&L, 5);
result = GetLength(L);
printf("表长度为:%d\n", result);
ListElemOutput(L);
printf("\n");
GetElem(L, 3, &data);
printf("第3个元素的值为:%d\n",data);
GetElem(L, 0, &data);
printf("\n");
printf("插入的元素值为%d\n", data);
ListInsert(L, 2, &data);
ListElemOutput(L);
printf("\n");
ListDelete(L, 2, &data);
printf("删除的元素值为%d\n", data);
ListElemOutput(L);
printf("\n");
ClearList(L);
printf("整表删除后, 表长度为:%d", GetLength(L));
return 0;
}
输出(输出测试中,使用的是尾插法)
新增快慢指针查看单链表中间值(2020.6.3):https://blog.youkuaiyun.com/weixin_43290957/article/details/106535857