C语言实现单链表(及部分概念解析)

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.csdn.net/lidafoye/article/details/76974058

https://blog.csdn.net/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.csdn.net/weixin_43290957/article/details/106535857

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值