数据结构 - 动态单链表的实行(C语言)

本文详细介绍了单链表的存储结构、基本操作如读取、插入、删除,以及特殊场景下的插入与删除,包括头部和尾部的插入、整表删除等。通过具体的C语言代码示例,深入解析了每种操作的算法思路和实现过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

动态单链表的实现

1 单链表存储结构代码描述

若链表没有头结点,则头指针是指向第一个结点的指针。

若链表有头结点,则头指针是指向头结点的指针。

空链表的示意图:

img

带有头结点的单链表:

img

不带头结点的单链表的存储结果示意图:

img

在C语言中可用结构指针来描述单链表:

/* 线性表的单链表存储结构 */
typedef struct node
{
    ElemType data;
    struct node *next;
}Node;
typedef Node* LinkList; //定义单链表指针

从这个结构定义中可以看出,结点由存放数据元素的数据域和存放后继结点地址的指针域组成。


2 单链表的读取

在单链表中,由于第i个元素到底在哪?没办法一开始就知道,必须得从头开始找。

获得链表第i个数据的算法思路:

  1. 声明一个结点指针cur指向链表第一个结点,初始化j从1开始;
  2. 当 j<i 时,就遍历链表,让cur的指针向后移动,不断指向下一结点,j 累加1;
  3. 若到链表末尾cur为空,则说明第i个元素不存在;
  4. 否则查找成功,返回结点cur的数据。

注意:cur最开始指向第一个结点,而不是头结点,cur与j同步。

/******************************************
名称: 获取元素操作
功能: 获取链表第i个结点数据,赋给e
返回值: 查找成功返回true,否则返回false
******************************************/
Status GetElem(LinkList pList, int i, ElemType *e)
{
    LinkList cur;

    cur = pList->next; //让cur指向链表pList的第1个结点
    int j = 1; //j为计数器,赋值为1,对应cur指向结点
    //cur不为空或者计数器j还没有等于i时,循环继续
    while (cur!=NULL && j < i) //若i=1,则不用进入循环遍历,就可找到了
    {
        cur = cur->next;
        j++;
    }

    if (cur == NULL || j>i) //j>i,为了避免输入i小于1的情况
        return FALSE; //到链表尾,仍未找到第i个元素

    *e = cur->data; //取第i个元素的数据

    return TRUE;
}

说白了,就是从头开始找,直到第 i 个元素为止。由于这个算法的时间复杂度取决于 i 的位置,当 i=1 肘,则不用进入循环遍历,第一个就取出数据了,而当 i=n 时则遍历 n-1
次才可以。 因此最坏情况的时间复杂度是O(n)。

由于单链表的结构中没有定义表长,所以不能事先知道要循环多少次,因此也就不方便使用for来控制循环。其主要核心思想就是"工作指针后移',这其实也是很多算法的常用技术。


3 单链表的插入与删除

单链表的插入操作核心代码只有两句(在结点p后面插入结点s):

s->next = p->next; /* 将p的后继结点赋值给s的后继  */
p->next = s; /* 将s赋值给p的后继 */

解读这两句代码,也就是说让p的后继结点改成s的后继结点,再把结点s变成p的后继结点,如下图所示:

img

对于单链裴的表头和表尾的特殊情况,操作是相同的,如下图所示:

img

单链表第i个位置插入结点的算法思路:

  1. 声明一结点p指向链表头结点,初始化j从1开始;
  2. 当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j 累加1;
  3. 若到链表末尾p为空,则说明第 i 个元素不存在i;
  4. 否则查找成功,在系统中生成一个空结点s;
  5. 将数据元素e赋值给s->data;
  6. 执行单链表的插入结点语旬s->next=p->next p->next=s;
  7. 返回成功。

注意:front最开始指向头结点,而不是第一个结点,而j赋值为1,让front与j不同步,front始终指向i位置前一个结点。

实现代码算法如下:

/******************************************
名称: 插入元素操作
功能: 在链表第i个位置插入新的数据元素e
返回值: 插入成功返回true,否则返回false
******************************************/
Status ListInsert(LinkList pList, int i, const ElemType e)
{
    LinkList front; //指向插入位置前一个结点
    LinkList pTemp;
    int j;

    front = pList; //让front指向链表pList的头结点
    j = 1; //j为计数器,赋值为1,让front能指向插入位置前一个结点
    //找到i位置所在的结点
    while (front != NULL && j < i)
    {
        front = front->next;
        j++;
    }

    if (front == NULL || j>i) //j>i,为了避免输入i小于1的情况
        return FALSE; //到链表尾,仍未找到第i个元素

    pTemp = (LinkList)malloc(sizeof(Node));
    pTemp->data = e;

    //插入结点s
    pTemp->next = front->next;
    front->next = pTemp;

    return TRUE;
}


4 单链表的删除

单链表的删除操作核心代码只有一句(删除结点p后面一个结点):

p->next = p->next->next; /* 将p的后继结点的后继赋值给p的后继 */

img

单链表第 i 个数据之后删除结点的算法思路(删除的是i+1位置的结点):

  1. 声明一结点p指向链表头结点, 初始化 j 从1开始;
  2. 当 j<i 时,就遍历链表,让front的指针向后移动,不断指向下一个结点,j 累加1;
  3. 若到链表末尾front为空,则说明第 i 个元素不存在;
  4. 否则查找成功,查找到要删除位置的前一个结点front,并赋值给q;
  5. 执行链表的删除结点语句front->next = front->next->next;
  6. 将q结点中的数据赋值给e, 作为返回;
  7. 释放q结点,并指向NULL;
  8. 返回成功。

注意:front最开始指向头结点,而不是第一个结点,而j赋值为1,让front与j不同步,front始终指向i位置前一个结点。

实现代码算法如下:

/******************************************
名称: 删除元素操作
功能: 在链表第i个位置删除结点
返回值: 删除成功返回true,否则返回false
******************************************/
Status ListDelete(LinkList pList, int i, ElemType *e)
{
    LinkList front, pTemp;

    front = pList; //让front指向链表pList的头结点
    int j = 1; //j为计数器,赋值为1,让front能指向插入位置前一个结点
    //找到i位置所在的结点
    while (front != NULL && j < i)
    {
        front = front->next;
        j++;
    }

    if (front == NULL || j > i) //j>i,为了避免输入i小于1的情况
        return FALSE; //到链表尾,仍未找到第i个元素

    LinkList q = front->next;

    //删除结点
    front->next = front->next->next;

    //使用q结点,保存已经删除的结点
    *e = q->data; //将要删除结点的数据赋给e
    free(q);
    q = NULL;

    return TRUE;
}

分析一下刚才我们讲解的单链表插入和删除算法,我们发现,它们其实都是由两部分组成;第一部分就是遍历查找第i个元素;第二部分就是插入和删除元素。从整个算法来说,我们很容易推导出:它们的时间复杂度都是O(n)。

从第i个位置插入10个元素,对于顺序存储结构意味着,每一次插入都需要移动 n-i 个元素,每次都是O(n)。而单链表,我们只需要在第一次时,找到第i个位置的指针,此时为O(n),接下来只是简单地通过赋值移动指针而已,时间复杂度都是O(1)。显然,对于插入或删除数据越频繁的操作,单链表的效率优势就越是明显。


5 单链表的头部插入与尾部插入

头部插入,就是始终让新结点在第一个结点的位置,这种算法简称为头插法,如下图所示:

img

实现代码算法如下:

/******************************************
名称: 头部后插入元素操作
功能: 在链表头部后插入存储数据元素的结点
返回值: 插入成功返回true,否则返回false
******************************************/
Status ListInsertHead(LinkList pList, const ElemType e)
{
    LinkList head, pTemp;

    //检测链表是否存在
    if (!pList)
    {
        printf("pList not exist!\n");
        return FALSE;
    }

    pTemp = (LinkList)malloc(sizeof(Node));
    if (!pTemp)
    {
        printf("malloc error!\n");
        return -1;
    }
    pTemp->data = e;

    head = pList; //让head指向链表pList的头结点
    //头结点后插入结点
    pTemp->next = head->next;
    head->next = pTemp;

    return TRUE;
}


尾部插入,将数据元素插入到尾节点后面,这种简称为尾插法。

/******************************************
名称: 尾部后插入元素操作
功能: 在链表尾部后插入存储数据元素的结点
返回值: 插入成功返回true,否则返回false
******************************************/
Status ListInsertTail(LinkList pList, const ElemType e)
{
    LinkList cur, pTemp;

    //检测链表是否存在
    if (!pList)
    {
        printf("pList not exist!\n");
        return FALSE;
    }

    pTemp = (LinkList)malloc(sizeof(Node));
    if (!pTemp)
    {
        printf("malloc error!\n");
        return -1;
    }
    pTemp->data = e;

    cur = pList; //让head指向链表pList的头结点
    //找到链表尾节点
    while (cur->next)
    {
        cur = cur->next;
    }

    //尾结点后插入结点
    pTemp->next = cur->next;
    cur->next = pTemp;

    return TRUE;
}


6 单链表的整表删除

单链表整表删除的算法思路如下:

  1. 声明一结点p和q;
  2. 将第一个结点赋值给p;
  3. 循环:
  • 将下一结点赋值给q;
  • 释放p;
  • 将q赋值给p。

实现代码算法如下:

/******************************************
名称: 整表删除操作
功能: 销毁整个线性表,即释放所有结点申请的内存
返回值: 删除成功返回true,否则返回false
******************************************/
Status ClearList(LinkList pList)
{
    LinkList p; //当前结点
    LinkList q; //用来保存下一结点,防止释放当前结点后导致“掉链”

    //检测链表是否存在
    if (!pList)
    {
        printf("pList not exist!\n");
        return FALSE;
    }

    p = pList; //q指向头结点
    while (p)
    {
        q = p->next; //事先保存下一结点,防止释放当前结点后导致“掉链”
        free(p); //释放当前结点
        p = q; //将下一结点赋给当前结点p
    }
    pList->next = NULL; //头结点指针域指向空

    return TRUE;
}


7 单链表的完整实现

#include "stdafx.h"
#include <stdlib.h>

#define TRUE 1
#define FALSE 0
typedef int Status; //Status是函数结果状态,成功返回TRUE,失败返回FALSE

typedef int ElemType;
/* 线性表的单链表存储结构 */
typedef struct node
{
    ElemType data;
    struct node *next;
}Node;
typedef Node* LinkList; //定义单链表指针

/******************************************
名称: 初始化单链表操作
******************************************/
void InitList(LinkList *pList) //必须使用双重指针,一重指针申请会出错
{
    *pList = (LinkList)malloc(sizeof(Node));

    (*pList)->data = 0;
    (*pList)->next = NULL;
}

/******************************************
名称: 获取元素操作
功能: 获取链表第i个结点数据,赋给e
返回值: 查找成功返回true,否则返回false
******************************************/
Status GetElem(LinkList pList, int i, ElemType *e)
{
    LinkList cur;

    cur = pList->next; //让cur指向链表pList的第1个结点
    int j = 1; //j为计数器,赋值为1,对应cur指向结点
    //cur不为空或者计数器j还没有等于i时,循环继续
    while (cur != NULL && j < i) //若i=1,则不用进入循环遍历,就可找到了
    {
        cur = cur->next;
        j++;
    }

    if (cur == NULL || j>i) //j>i,为了避免输入i小于1的情况
        return FALSE; //到链表尾,仍未找到第i个元素

    *e = cur->data; //取第i个元素的数据

    return TRUE;
}

/******************************************
名称: 插入元素操作
功能: 在链表第i个位置插入新的数据元素e
返回值: 插入成功返回true,否则返回false
******************************************/
Status ListInsert(LinkList pList, int i, const ElemType e)
{
    LinkList front; //指向插入位置前一个结点
    LinkList pTemp;
    int j;

    front = pList; //让front指向链表pList的头结点
    j = 1; //j为计数器,赋值为1,让front能指向插入位置前一个结点
    //找到i位置所在的结点
    while (front != NULL && j < i)
    {
        front = front->next;
        j++;
    }

    if (front == NULL || j>i) //j>i,为了避免输入i小于1的情况
        return FALSE; //到链表尾,仍未找到第i个元素

    pTemp = (LinkList)malloc(sizeof(Node));
    pTemp->data = e;

    //插入结点s
    pTemp->next = front->next;
    front->next = pTemp;

    return TRUE;
}

/******************************************
名称: 删除元素操作
功能: 在链表第i个位置删除结点
返回值: 删除成功返回true,否则返回false
******************************************/
Status ListDelete(LinkList pList, int i, ElemType *e)
{
    LinkList front, pTemp;

    front = pList; //让front指向链表pList的头结点
    int j = 1; //j为计数器,赋值为1,让front能指向插入位置前一个结点
    //找到i位置所在的结点
    while (front != NULL && j < i)
    {
        front = front->next;
        j++;
    }

    if (front == NULL || j > i) //j>i,为了避免输入i小于1的情况
        return FALSE; //到链表尾,仍未找到第i个元素

    LinkList q = front->next;

    //删除结点
    front->next = front->next->next;

    //使用q结点,保存已经删除的结点
    *e = q->data; //将要删除结点的数据赋给e
    free(q);
    q = NULL;

    return TRUE;
}

/******************************************
名称: 头部后插入元素操作
功能: 在链表头部后插入存储数据元素的结点
返回值: 插入成功返回true,否则返回false
******************************************/
Status ListInsertHead(LinkList pList, const ElemType e)
{
    LinkList head, pTemp;

    //检测链表是否存在
    if (!pList)
    {
        printf("pList not exist!\n");
        return FALSE;
    }

    pTemp = (LinkList)malloc(sizeof(Node));
    if (!pTemp)
    {
        printf("malloc error!\n");
        return -1;
    }
    pTemp->data = e;

    head = pList; //让head指向链表pList的头结点
    //头结点后插入结点
    pTemp->next = head->next;
    head->next = pTemp;

    return TRUE;
}

/******************************************
名称: 尾部后插入元素操作
功能: 在链表尾部后插入存储数据元素的结点
返回值: 插入成功返回true,否则返回false
******************************************/
Status ListInsertTail(LinkList pList, const ElemType e)
{
    LinkList cur, pTemp;

    //检测链表是否存在
    if (!pList)
    {
        printf("pList not exist!\n");
        return FALSE;
    }

    pTemp = (LinkList)malloc(sizeof(Node));
    if (!pTemp)
    {
        printf("malloc error!\n");
        return -1;
    }
    pTemp->data = e;

    cur = pList; //让head指向链表pList的头结点
    //找到链表尾节点
    while (cur->next)
    {
        cur = cur->next;
    }

    //尾结点后插入结点
    pTemp->next = cur->next;
    cur->next = pTemp;

    return TRUE;
}

/******************************************
名称: 整表删除操作
功能: 销毁整个线性表,即释放所有结点申请的内存
返回值: 删除成功返回true,否则返回false
******************************************/
Status ClearList(LinkList pList)
{
    LinkList p; //当前结点
    LinkList q; //用来保存下一结点,防止释放当前结点后导致“掉链”

    //检测链表是否存在
    if (!pList)
    {
        printf("pList not exist!\n");
        return FALSE;
    }

    p = pList; //q指向头结点
    while (p)
    {
        q = p->next; //事先保存下一结点,防止释放当前结点后导致“掉链”
        free(p); //释放当前结点
        p = q; //将下一结点赋给当前结点p
    }
    pList->next = NULL; //头结点指针域指向空

    return TRUE;
}

/******************************************
名称: 遍历链表并显示元素操作
******************************************/
void ListShow(LinkList pList)
{
    LinkList cur = pList->next;
    while (cur != NULL)
    {
        printf("%d ", cur->data);
        cur = cur->next;
    }
    printf("\n");
}

int main()
{
    LinkList pList;

    //初始化单链表
    InitList(&pList);

    //插入结点
    ListInsert(pList, 1, 0);
    ListInsert(pList, 2, 1);
    ListInsert(pList, 3, 2);

    //删除结点
    int val;
    ListDelete(pList, 2, &val);
    printf("删除结点的数据: %d\n", val);

    //头部后插入元素
    ListInsertHead(pList, 5);
    ListInsertHead(pList, 4);

    //尾部后插入元素
    ListInsertTail(pList, 8);
    ListInsertTail(pList, 9);

    //遍历链表并显示元素操作
    ListShow(pList);

    //删除整个单链表
    ClearList(pList);

    return 0;
}

/*
输出结果:

删除结点的数据: 1
4 5 0 2 8 9
*/

转载于:https://www.cnblogs.com/linuxAndMcu/p/10306664.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值