数据结构3线性表——单链表(C)

前言:

本专栏属于数据结构相关内容,附带一些代码加深对一些内容的理解,为方便读者观看,本专栏内的所有文章会同时附带C语言和Python对应的代码,(可自行通过目录跳转到对应的部分)辅助不同主修语言的读者去更好的理解对应的内容,若是代码0基础的读者,可先去博主其他专栏学习一下基础的语法及知识点:

魔法天才的跳转链接:

C语言:C基础_Gu_shiwww的博客-CSDN博客

Python语言:python1_Gu_shiwww的博客-CSDN博客

其他数据结构内容可见:数据结构_Gu_shiwww的博客-CSDN博客

什么是单链表

单链表(Singly Linked List)是一种链式存储结构,由一系列**节点(Node)**组成,每个节点包含两部分

1 链表的特征

逻辑结构:线性结构

存储结构:链式存储结构

特点:内存不连续,通过指针实现

解决顺序表问题:顺序表长度固定和插入删除效率低的问题。

操作:增删改查

链表就是将节点用链串起来的线性表,链就是节点中的引用

链表分为带头结点链表和不带头节点的链表,两者在逻辑结构上都属于链式存储结构,只是带头节点的链表单独拿出一个节点存放首个有效节点的地址,头节点内部也存在数据域,且也有数据存储,但是在逻辑上这个数据域内部的元素无效。带头与不带头节点在代码编写上有细微差别,本文以带头节点的单向链表来进行讲解

【例子】通过一个表格给定一个链表遍历的例子(可自行理解)

(赵, 钱, 孙, 李, 周, 吴, 郑, 王)

以下是用C语言实现单链表的一些具体操作,Python的具体编程实现详见魔法天才预设的跳转路径:

2 编程实现链表

C 语言编程实现

C.a 节点的构建

在学习链表之前,我们要先定义好每个链表连接的节点,在C语言中可以用结构体完成节点的构建

cpp 复制代码
typedef struct node_t
{
    int data;            //数据域:存数据
    struct node_t *next; //指针域:存放下一个节点的地址
} link_node_t, *link_node_p;

以上定义了一个名为link_node_t的结构体(typedef是对后面的结构体定义进行重命名,link_node_t等价于struct node_t),而重定义名之后的*link_node_p是对结构体指针的名进行重定义

【练习】建立A、B、C、D四个节点,内部数据域可为空,用指针进行连接,最后通过首节点进行逐个遍历打印

1.1 遍历无头单向链表

cpp 复制代码
#include <stdio.h>

typedef struct node_t
{
    int data;            //数据域:存数据
    struct node_t *next; //指针域:存放下一个节点的地址
} link_node_t, *link_node_p;

int main(int argc, char const *argv[])
{
    //1. 定义四个节点
    link_node_t A = {1, NULL};
    link_node_t B = {2, NULL};
    link_node_t C = {3, NULL};
    link_node_t D = {4, NULL};

    //2. 连接节点
    A.next = &B;
    B.next = &C;
    C.next = &D;

    //3. 定义一个头指针指向第一个节点,用于遍历无头单向链表
    link_node_p p = &A;

    //4. 遍历无头单向链表
    while (p != NULL)
    {
        printf("%d ", p->data); //打印所指节点数据
        p = p->next; //将指针向后移动一个单位
    }
    printf("\n");
    return 0;
}

1.2 遍历有头单向链表

cpp 复制代码
#include <stdio.h>

typedef struct node_t
{
    int data;            //数据域:存数据
    struct node_t *next; //指针域:存放下一个节点的地址
} link_node_t, *link_node_p;

int main(int argc, char const *argv[])
{
    //1. 定义四个节点
    link_node_t A = {1, NULL};
    link_node_t B = {2, NULL};
    link_node_t C = {3, NULL};
    link_node_t D = {4, NULL};

    //2. 连接节点
    A.next = &B;
    B.next = &C;
    C.next = &D;

    //3. 定义一个头节点,数据域无效,指针域指向第一个节点
    link_node_t H;
    H.next = &A;

    //4. 定义一个头指针指向头节点,用于遍历有头单向链表
    link_node_p p = &H;

    //5. 遍历有头单向链表
#if 0
    //方法一: 循环里面先移动再打印
    while (p->next != NULL)
    {
        p = p->next; //向后移动一个单位
        printf("%d ", p->data);
    }
    printf("\n");
#else
    //方法二:
    //先跨越头节点,相当于让头指针指向了一个无头单向链表
    p = p->next;
    while (p != NULL)
    {
        printf("%d ", p->data);
        p = p->next;
    }
    printf("\n");
#endif

    return 0;
}

1.3 链表尾插法

写一个有头单向链表,用于保存输入的学生成绩,实现一输入学生成绩就创建一个新的节点,将成绩保存起来。再将该节点链接到链表的尾,直到输入-1结束。

要求:每个链表的节点由动态内存分配得到 , 也就是用malloc。

过程:

  1. malloc申请空间link_node_t大小作为头节点
  2. 将新节点放到链表尾部
cpp 复制代码
#include <stdio.h>
#include<stdlib.h>
typedef struct node_t
{
    int data;
    struct node_t *next;
} link_node_t, *link_node_p;

int main(int argc, char const *argv[])
{
    link_node_p pnew = NULL;  //用于指向新建节点
    link_node_p ptail = NULL; //用于指向尾节点
    int score;
    //1. 创建一个头节点并初始化
    link_node_p p = (link_node_p)malloc(sizeof(link_node_t));
    if (NULL == p)
    {
        perror("p malloc err");
        return -1;
    }
    p->next = NULL; //初始化头节点

    //2.让尾指针指向头节点
    ptail = p;

    //3.循环输入学生成绩-1结束,如果不是-1那就新建一个节点保存成绩尾插到链表
    while (1)
    {
        scanf("%d", &score);
        if (-1 == score)
            break;
        //(1) 开辟新节点空间,让pnew指向新节点
        pnew = (link_node_p)malloc(sizeof(link_node_t));
        if (NULL == pnew)
        {
            perror("pnew err");
            return -1;
        }
        //(2) 初始化新节点
        pnew->data = score;
        pnew->next = NULL;
        //(3) 连接新节点到链表尾部
        ptail->next = pnew;
        //(4) 移动尾指针到新节点
        ptail = pnew;
    }

    //4. 遍历有头链表
    p = p->next;
    while (p != NULL)
    {
        printf("%d ", p->data);
        p = p->next;
    }
    printf("\n");

    return 0;
}

C.b 有头链表的函数操作

C.1 C语言编程操作的函数接口

cpp 复制代码
#ifndef __LINKLIST_H__
#define __LINKLIST_H__

typedef int datatype;
typedef struct node_t
{
	datatype data;
	struct node_t *next;
}link_node_t,*link_node_p;

//1.创建一个空的有头单向链表
link_node_p createEmptyLinkList();
 //2.链表指定位置插入数据
int insertIntoPostLinkList(link_node_p p,int post, datatype data);
//3.计算链表的长度。
int lengthLinkList(link_node_p p);
//4.遍历链表
void showLinkList(link_node_p p);
//5.链表指定位置删除数据
int deletePostLinkList(link_node_p p, int post);
//6.判断链表是否为空
int isEmptyLinkList(link_node_p p);
//7.清空单向链表
void clearLinkList(link_node_p p);
//8.修改指定位置的数据 post 被修改的位置 data修改成的数据
int changePostLinkList(link_node_p p, int post, datatype data);
//9.查找指定数据出现的位置 data被查找的数据 //search 查找
int searchDataLinkList(link_node_p p, datatype data);
//10.删除单向链表中出现的指定数据,data代表将单向链表中出现的所有data数据删除
int deleteDataLinkList(link_node_p p, datatype data);
//11.转置链表
void reverseLinkList(link_node_p p);
#endif

C.2 创建一个空的有头单向链表

cpp 复制代码
//创建一个空的有头单向链表
link_node_p createEmptyLinkList()
{
    //1.开辟头节点空间
    link_node_p p = (link_node_p)malloc(sizeof(link_node_t));
    if (NULL == p)
    {
        perror("p err");
        return NULL;
    }
    //2. 初始化头节点
    p->next = NULL;

    //3. 返回头节点地址
    return p;
}

定义了一个名为createEmptyLinkList()的函数,返回的数据类型是结构体指针,内部用malloc函数动态开辟了一个头节点,且将头节点初始化(即将头节点的next指针指向NULL),返回开辟成功的结构体指针

C.3 计算链表长度:length

cpp 复制代码
//计算链表长度 length:长度
int lengthLinkList(link_node_p p)
{
    int len = 0;
    while (p->next != NULL)
    {
        p = p->next;
        len++;
    }
    return len;
}

通过while循环遍历整个链表,只要链表的next指针不为空,就进入while循环,将p的指针指向下一个节点,直到最后一个next域为空的节点停止循环,同时在循环外部定义一个len变量记录整个链表的长度只要while循环执行一次len就要+1,最终返回len(即链表的长度)

C.4 向单向链表的指定位置插入数据

cpp 复制代码
//向单向链表的指定位置插入数据
//p保存链表的头指针 post 插入的位置 data插入的数据
int insertIntoPostLinkList(link_node_p p, int post, datatype data)
{
    // 1. 容错判断: post<0 || post>长度
    if (post < 0 || post > lengthLinkList(p))
    {
        printf("insert err\n");
        return -1;
    }
    // 2. malloc新建一个节点
    link_node_p pnew = (link_node_p)malloc(sizeof(link_node_t));
    if (NULL == pnew)
    {
        perror("pnew err");
        return -1;
    }
    // 3. 初始化新节点
    pnew->data = data;

    // 4. 将指针移动到插入位置的前一个
    for (int i = 0; i < post; i++)
        p = p->next;

    // 5. 将新节点连接到链表(先连后面再连前面)
    pnew->next = p->next;
    p->next = pnew;
    return 0;
}

首先容错判断,判断要插入位置post的合理性,不能小于0也不能大于链表的长度

2、3步新建一个节点,并且初始化将传入的数据data初始化到节点内部

第4步通过for循环遍历链表,将指针指向要插入元素的前一个,之后第5步将该节点连入链表,区分代码中的p和pnew,先连后面再连前面(顺序交换也无所谓)

注意:为什么要将p移动到前面一个位置是因为要找到被插入位置上一个节点的next指针,因为单链表是单向遍历的,只能从前往后遍历,找到post位置的前一个位置才能与插入位置的前一个节点的next指针进行连接

C.5 遍历单向链表

cpp 复制代码
//遍历单向链表
void showLinkList(link_node_p p)
{
    while (p->next != NULL)
    {
        p = p->next;
        printf("%d ", p->data);
    }
    printf("\n");
}

移动p指针,打印p的data域,注意函数中p指针的移动并不会改变开辟的动态结构体链表的头指针

C.6 判断链表为空,为空返回1,不为空返回0

cpp 复制代码
//判断链表为空,为空返回1,不为空返回0
int isEmptyLinkList(link_node_p p)
{
    return p->next == NULL;
}

函数可以直接返回p的next域是否为NULL的判断是结果,满足为1,不满足为0

C.7 删除单向链表中指定位置的节点

cpp 复制代码
//删除单向链表中指定位置的数据 post 代表的是删除的位置
int deletePostLinkList(link_node_p p, int post)
{
    // 1.容错判断:判空 || post<0 || post>=长度
    if (isEmptyLinkList(p) || post < 0 || post >= lengthLinkList(p))
    {
        printf("delete err\n");
        return -1;
    }
    // 2.将指针移动到删除位置的前一个节点
    for (int i = 0; i < post; i++)
        p = p->next;
    // 3.设指针pdel指向要删除节点
    link_node_p pdel = p->next;
    // 4.前后跨过要删除节点
    p->next = pdel->next;
    // 5.释放要删除节点
    free(pdel);

    return 0;
}

首先容错判断,判断post值的合法性以及链表是否为空,若链表为空则无元素可删

第2步同插入步骤,找到要删除节点的前一个位置(因为要断开post位置的前一个节点的next指针)然后定义一个pdel指针指向要删除的节点,也可以通过p->next = p->next->next直接删除post位置的节点,最后不要忘记释放新定义的节点

C.8 删除指定数据的所有节点

cpp 复制代码
//删除单向链表中出现的指定数据,data代表将单向链表中出现的所有data数据删除
void deleteDataLinkList(link_node_p p, datatype data)
{
    link_node_p t = p->next; //让t指向头节点的后一个节点
    while (t != NULL)        //相当于让t遍历无头链表
    {
        if (t->data == data) //判断成功删除节点并向后继续遍历
        {
            //(1)跨过要删除节点
            p->next = t->next;
            //(2)释放要删除节点
            free(t);
            //(3)让t指向p的下一个继续向后遍历
            t = p->next;
        }
        else //p和t继续向后遍历
        {
            p = p->next;
            t = t->next;
        }
    }
}

只需要从头开始遍历,如果发现某一个节点的数据与传参的数据相同,则进入if判断,断开当前节点就好,请注意此时t指针和p指针指向的并不是同一个节点,p指针在t指针指向的前一个结点,于是可以通过p指针访问到该元素的前一个节点。还需注意要删除所有与data相同的数据,所以并不是找到一个数据就结束了,要循环遍历到链表尾。

C.9 清空链表

cpp 复制代码
//清空链表数据
//思想:一直删除头节点的后一个,直到为空链表为止
void clearLinkList(link_node_p p)
{
    while (p->next != NULL)
    {
        link_node_p pdel = p->next;
        p->next = pdel->next;
        free(pdel);
    }
}

从头开始遍历,遍历到一个断开一个,记得将被断开的节点free掉

C.10 修改指定位置的数据

cpp 复制代码
//修改指定位置的数据 post 被修改的位置 data修改成的数据
int changePostLinkList(link_node_p p, int post, datatype data)
{
    // 1.容错判断:判空 || post<0 || post>=长度
    if (isEmptyLinkList(p) || post < 0 || post >= lengthLinkList(p))
    {
        printf("change err\n");
        return -1;
    }
    //2.将指针遍历到修改节点位置
    for (int i = 0; i <= post; i++)
        p = p->next;
    //3.修改节点中数据
    p->data = data;

    return 0;
}

修改指定位置,首先也要进行容错判断,看post值是否合法,以及判断链表是否为空,空链表无内容可改。

接着通过for循环遍历到要修改的节点,直接进行data域的内容修改

C.11 查找指定数据出现的位置

cpp 复制代码
//查找指定数据出现的位置 data被查找的数据 //search 查找
int searchDataLinkList(link_node_p p, datatype data)
{
    int post = 0; //记录查找的位置
    while (p->next != NULL)
    {
        p = p->next;
        if (p->data == data)
            return post;
        post++;
    }
    return -1; //说明数据不存在
}

首先循环遍历整个链表,当遍历到与传入的参数data值相同的数据时,直接return,函数内部循环不再执行,同时定义一个post变量记录下表,若查询到结果时直接返回post

C.12 转置链表

cpp 复制代码
//转置链表
//解题思想:
//(1) 将头节点与当前链表断开,断开前保存下头节点的下一个节点,保证后面链表能找得到,定义一个q保存头节点的下一个节点,断开后前面相当于一个空的链表,后面是一个无头的单向链表
//(2) 遍历无头链表的所有节点,将每一个节点当做新节点插入空链表头节点的下一个节点(每次插入的头节点的下一个节点位置)
void reverseLinkList(link_node_p p)
{
    link_node_p t = NULL;    //让t在循环里一直记录q的下一个,防止头插q之后链表找不到了
    link_node_p q = p->next; //让q记录一下头节点的后一个节点
    p->next = NULL;          //断开头节点
    while (q != NULL)        //相当于遍历无头单向链表
    {
        // 让t记录q的下一个,不然头插以后链表找不到了
        t = q->next;
        //头插:将q插入到p后面,先连后面再连前面
        q->next = p->next;
        p->next = q;
        //让q去找t
        q = t;
    }
}

总结:先断开头节点与后面数据的连接,然后再定义一个指针去记录要头插节点的后一个节点,一次向后移动一个一个头插如原来链表头节点的后面,实现整个链表的逆置

C.13 完整代码(可运行)

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

typedef int datatype;
typedef struct node_t
{
	datatype data;
	struct node_t *next;
}link_node_t,*link_node_p;


//创建一个空的有头单向链表
link_node_p createEmptyLinkList()
{
    //1.开辟头节点空间
    link_node_p p = (link_node_p)malloc(sizeof(link_node_t));
    if (NULL == p)
    {
        perror("p err");
        return NULL;
    }
    //2. 初始化头节点
    p->next = NULL;

    //3. 返回头节点地址
    return p;
}

//计算链表长度 length:长度
int lengthLinkList(link_node_p p)
{
    int len = 0;
    while (p->next != NULL)
    {
        p = p->next;
        len++;
    }
    return len;
}

//向单向链表的指定位置插入数据
//p保存链表的头指针 post 插入的位置 data插入的数据
int insertIntoPostLinkList(link_node_p p, int post, datatype data)
{
    // 1. 容错判断: post<0 || post>长度
    if (post < 0 || post > lengthLinkList(p))
    {
        printf("insert err\n");
        return -1;
    }
    // 2. malloc新建一个节点
    link_node_p pnew = (link_node_p)malloc(sizeof(link_node_t));
    if (NULL == pnew)
    {
        perror("pnew err");
        return -1;
    }
    // 3. 初始化新节点
    pnew->data = data;

    // 4. 将指针移动到插入位置的前一个
    for (int i = 0; i < post; i++)
        p = p->next;

    // 5. 将新节点连接到链表(先连后面再连前面)
    pnew->next = p->next;
    p->next = pnew;
    return 0;
}

//遍历单向链表
void showLinkList(link_node_p p)
{
    while (p->next != NULL)
    {
        p = p->next;
        printf("%d ", p->data);
    }
    printf("\n");
}

//判断链表为空,为空返回1,不为空返回0
int isEmptyLinkList(link_node_p p)
{
    return p->next == NULL;
}

//删除单向链表中指定位置的数据 post 代表的是删除的位置
int deletePostLinkList(link_node_p p, int post)
{
    // 1.容错判断:判空 || post<0 || post>=长度
    if (isEmptyLinkList(p) || post < 0 || post >= lengthLinkList(p))
    {
        printf("delete err\n");
        return -1;
    }
    // 2.将指针移动到删除位置的前一个节点
    for (int i = 0; i < post; i++)
        p = p->next;
    // 3.设指针pdel指向要删除节点
    link_node_p pdel = p->next;
    // 4.前后跨过要删除节点
    p->next = pdel->next;
    // 5.释放要删除节点
    free(pdel);

    return 0;
}

//思想:一直删除头节点的后一个,直到为空链表为止
void clearLinkList(link_node_p p)
{
    while (p->next != NULL)
    {
        link_node_p pdel = p->next;
        p->next = pdel->next;
        free(pdel);
    }
}

//修改指定位置的数据 post 被修改的位置 data修改成的数据
int changePostLinkList(link_node_p p, int post, datatype data)
{
    // 1.容错判断:判空 || post<0 || post>=长度
    if (isEmptyLinkList(p) || post < 0 || post >= lengthLinkList(p))
    {
        printf("change err\n");
        return -1;
    }
    //2.将指针遍历到修改节点位置
    for (int i = 0; i <= post; i++)
        p = p->next;
    //3.修改节点中数据
    p->data = data;

    return 0;
}

//查找指定数据出现的位置 data被查找的数据 //search 查找
int searchDataLinkList(link_node_p p, datatype data)
{
    int post = 0; //记录查找的位置
    while (p->next != NULL)
    {
        p = p->next;
        if (p->data == data)
            return post;
        post++;
    }
    return -1; //说明数据不存在
}

//删除单向链表中出现的指定数据,data代表将单向链表中出现的所有data数据删除
void deleteDataLinkList(link_node_p p, datatype data)
{
    link_node_p t = p->next; //让t指向头节点的后一个节点
    while (t != NULL)        //相当于让t遍历无头链表
    {
        if (t->data == data) //判断成功删除节点并向后继续遍历
        {
            //(1)跨过要删除节点
            p->next = t->next;
            //(2)释放要删除节点
            free(t);
            //(3)让t指向p的下一个继续向后遍历
            t = p->next;
        }
        else //p和t继续向后遍历
        {
            p = p->next;
            t = t->next;
        }
    }
}

//转置链表
//解题思想:
//(1) 将头节点与当前链表断开,断开前保存下头节点的下一个节点,保证后面链表能找得到,定义一个q保存头节点的下一个节点,断开后前面相当于一个空的链表,后面是一个无头的单向链表
//(2) 遍历无头链表的所有节点,将每一个节点当做新节点插入空链表头节点的下一个节点(每次插入的头节点的下一个节点位置)
void reverseLinkList(link_node_p p)
{
    link_node_p t = NULL;    //让t在循环里一直记录q的下一个,防止头插q之后链表找不到了
    link_node_p q = p->next; //让q继续一下头节点的后一个节点
    p->next = NULL;          //断开头节点
    while (q != NULL)        //相当于遍历无头单向链表
    {
        // 让t记录q的下一个,不然头插以后链表找不到了
        t = q->next;
        //头插:将q插入到p后面,先连后面再连前面
        q->next = p->next;
        p->next = q;
        //让q去找t
        q = t;
    }
}


int main(int argc, char const *argv[])
{
    link_node_p p = createEmptyLinkList();
    insertIntoPostLinkList(p, 0, 10);
    insertIntoPostLinkList(p, 1, 20);
    insertIntoPostLinkList(p, 2, 30);
    insertIntoPostLinkList(p, 3, 40);
    insertIntoPostLinkList(p, 4, 60);
    showLinkList(p);
    deletePostLinkList(p, 2);
    showLinkList(p);
    changePostLinkList(p, 1, 50);
    showLinkList(p);
    printf("50 post is: %d\n", searchDataLinkList(p, 50));

    insertIntoPostLinkList(p, 2, 50);
    showLinkList(p);
    deleteDataLinkList(p, 50);
    showLinkList(p);

    reverseLinkList(p);  //转置链表
    showLinkList(p);

    // clearLinkList(p);
    // if (isEmptyLinkList(p))
    //     printf("empty!\n");
    return 0;
}
相关推荐
啃火龙果的兔子6 分钟前
Form.Item中判断其他Form.Item的值
开发语言·前端·javascript
学习编程的gas16 分钟前
C++多态:理解面向对象的“一个接口,多种实现”
开发语言·c++
John.Lewis28 分钟前
数据结构初阶(11)排序的概念与运用
c语言·数据结构·排序算法
EndingCoder33 分钟前
Next.js 中间件:自定义请求处理
开发语言·前端·javascript·react.js·中间件·全栈·next.js
FirstFrost --sy33 分钟前
C++ stack and queue
开发语言·c++·queue·stack·priority_queue
墨城之左34 分钟前
低版本 IntelliJ IDEA 使用高版本 JDK 语言特性的问题
java·开发语言·intellij-idea·jdk21
别来无恙1491 小时前
Java Web开发:Session与Cookie详细入门指南
java·开发语言
FPGA1 小时前
曼彻斯特编解码:数字世界的“摩斯密码”与FPGA高效实现
数据结构
数据智能老司机1 小时前
图算法趣味学——图遍历
数据结构·算法·云计算
华阙之梦1 小时前
QT环境搭建
开发语言·qt