链表——单链表

本文详细介绍了单链表的概念,特点及其在逻辑和物理上的存储结构。阐述了单链表的初始化、头插法、尾插法、查找、删除、获取长度、判断是否为空、打印数据、清空、摧毁、获取指定位置数据、逆置和去除重复值等操作,并提供了相应的代码实现。通过实例和逻辑分析帮助理解链表操作的本质。

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

一、单链表
单链表,就是线性表的链式存储结构,又称线性链表。
它的特点是:用一组任意的存储单元存储线性表的数据元素,即结点之间在逻辑上是连续的,在物理上是不连续的。
单链表是带有头结点的,头节点只起到“哨兵”的作用,它的数据域是不使用的,指针域指向第一个数据域的位置。

以下代码是单链表各个结点的逻辑表示:data是节点的数据域,next是指针域——存储下一个结点的地址。

typedef struct Node
{
    int data;
    Node *next;
}Node,*List;

见下图:

这里写图片描述

二、单链表的相关操作

一共十二种常用操作,头文件声明见下:

//1.初始化
void InitList(List plist);

//2.头插
bool Insert_head(List plist, int key);

//3.尾插
bool Insert_tail(List plist, int key); 

//4.查找
Node *Search(List plist, int key);

//5.删除数据结点
bool Delete(List plist, int key);

//6.得到单链表的长度,不包括头结点
int Getlength(List plist);

//7.判断单链表是否为空
bool IsEmpty(List plist);

//8.打印单链表的数据
void Show(List plist);

//9.清空链表
void clear(List plist);

//10.摧毁单链表的数据(释放数据结点)
void Destroy(List plist);

//11.得到pos位置的数据
bool GetElem(List plist, int pos, int key);

//12.逆置
void Reverse(List plist);

//13.取出重复的值
void Unique(List plist);

1. 初始化 void InitList(List plist);

void InitList(List plist)
{
    assert(plist!=NULL);
    if(plist==NULL)
    {
        return ;
    }

    plist->next = NULL;
}

初始化一个单链表,就是把头结点的next置空,即没有一个数据结点。

2.头插

bool Insert_head(List plist, int key);
{
    assert(plist!=NULL);
    if(plist==NULL)
    {
        return false;
    }

    Node *p = (Node*)malloc(sizeof(Node));//利用动态内存   新创建一个节点
    p->data = key;//将数据存入新结点的数据域

    p->next = plist->next;//先把后面的结点牵住
    plist->next = p;//头结点把插入的点牵住
    return true;
}

头插法,即在头结点后面插入新的节点,我们需要解决的问题是:如何将新的结点与它的前驱(头结点)、后继联系在一块?

我们不妨换个角度思考,一排的人手拉手,第一个人的后面要插进来一个人,第一个人肯定要先将后面的人交到新人的手里,再把新人牵住,才不会把后面的人丢掉。见下图:

这里写图片描述

头插法的具体过程:

这里写图片描述

三、尾插

bool Insert_tail(List plist, int key)//插入的结点变为新的尾结点
{
    assert(plist!=NULL);
    if(plist==NULL)
    {
        return false;
    }

    Node *q;//找到尾结点
    Node *p = (Node*)malloc(sizeof(Node));//创建新结点
    p->data = key;

    for(q=plist; q->next!=NULL; q=q->next);//遍历找到尾结点

    p->next = q->next;//
    q->next = p;
    return true;
}

尾插法,即在当前尾结点的后面插入新的结点,插入的结点变为新的尾结点。具体实现过程见下图。

这里写图片描述

四、查找

遍历数据结点,找到目标结点,并且返回数据结点中数据域的内容,因此for循环里p从plist->next开始,循环条件为p!=NULL

Node *Search(List plist, int key)
{
    assert(plist!=NULL);
    if(plist==NULL)
    {
        return NULL;
    }

    Node *p;
    for(p=plist->next; p!=NULL; p=p->next)
    {
        if(p->data == key)
        {
            return p;
        }
    }

    return NULL;
}

五、删除数据结点

删除数据结点q之前,必须将q->next交给q的前驱,以免丢失节点,所以通过前驱找到删除结点之后,完成交接,再删除结点。链表中的所有前驱包括头结点到尾结点前的一个结点,所以for循环中的p从plist开始遍历,循环条件为p->next!=NULL

bool Delete(List plist, int key)
{
    assert(plist!=NULL);//注意什么时候可能出现空指针,使得程序崩溃
    if(plist==NULL)
    {
        return false;
    }

    Node *p;//动态查找删除结点

    for(p=plist; p->next!=NULL; p=p->next)
    {
        if(p->next->data==key)//找到删除结点
        {
            Node *q = p->next;//q为要删除的结点
            p->next = q->next;
            free(q);
            return true;
        }
    }
    return false;
}

六、得到单链表的长度

单链表的长度指的是数据结点的个数,并不包括头结点。遍历所有的数据结点,返回数据结点的个数,for循环里p从plist->next开始,循环条件为p!=NULL

int Getlength(List plist)
{
    int count = 0;
    Node *p;
    for(p=plist->next; p!=NULL; p=p->next)
    {
        count++;
    }

    return count;

七、判断单链表是否为空

单链表的尾结点中,指针域存放的是NULL,所以当链表为空时,头结点就是尾结点,即plist->next==NULL。

bool IsEmpty(List plist)
{
    return plist->next==NULL;//看头结点的指针next的指向,是否为空
}

八、打印单链表的数据

遍历所有的数据结点,打印他们的数据域。for循环里p从plist->next开始,循环条件为p!=NULL

void Show(List plist)
{
    Node *p;
    for(p=plist->next; p!=NULL; p=p->next)
    {
        printf("%d ",p->data);
    }
    printf("\n");
}

九、清空单链表

引用了摧毁方法。

void clear(List plist)
{
    Destroy(plist);
}

十、摧毁单链表(释放数据结点)

摧毁单链表,即释放所有的数据结点,见下图。

摧毁单链表一共有两种方法。
方法一:动态删除各个结点

这里写图片描述

其中,指针q保证p可以一直向后移动,来销毁各个结点

具体实现代码:


void Destroy(List plist)
{
    assert(plist!=NULL);
    if(plist==NULL)
    {
        return ;
    }

    Node *p = plist->next;//从第一个数据结点开始
    Node *q;

    while(p!=NULL)
    {
        q = p->next;//保证结点一直向后移动
        free(p);
        p = q;
    }
    plist->next = NULL;//头结点一定要置空,否则二次摧毁的时候,程序会出问题
}

方法二:总是删除第二个结点(第一个数据结点)

这里写图片描述

其中,p初始化为plist->next,在销毁p之前,先将再下一个结点的位置信息p->next交给plist->next,这时p与结点的联系断开,销毁。然后,p=plist->next,重复上述操作,即总是在删除第一个数据结点。

具体实现代码:

void Destroy2(List plist)
{
    Node *p = plist->next;//从第一个结点开始

    while(plist->next!=NULL)//plist->next==NULL时,已经释放完了所有的数据结点
    {
        p = plist->next;
        plist->next = p->next;
        free(p);
    }
    plist->next = NULL;//头节点一定要置空,否则二次摧毁的时候,程序会出问题
}

十一、得到pos位置的数据

线性表中pos需要判断非法值,单链表中同样需要排除非法值。

具体实现代码:

bool GetElem(List plist, int pos, int* rtval)//rtval:输出参数,用于返回多个值的情况
{
    assert(plist!=NULL);
    if(plist==NULL)
    {
        return false;
    }

    if(pos<0 || pos>=Getlength(plist))
    {
        return false;
    }

    Node *p;
    int i=0;
    for(p=plist->next; p!=NULL; p=p->next)
    {
        if(i==pos)//找到目标
        {
            *rtval = p->data;//将数据输出
            return true;
        }
        i++;
    }

    return false;
}

十二、逆置

方法一:类似数组,头尾两个指针向中靠拢,交换数据
该方法较容易想到,实现过程中的一个难点就是——如何让尾指针向前移动,这个问题的解决方法就在于找到这个结点的前驱,这就需要遍历这个尾指针前面的所有数据结点。

具体思路见下图:

这里写图片描述

1.得到结点的前驱

Node *GetPre(List plist, Node *p)
{
    for(Node *q=plist; q->next!=NULL; q=q->next)
    {
        if(q->next == p)
        {
            return q;
        }
    }

    return NULL;
}

2.逆置

void Reverse(List plist)
{
    if(plist==NULL || plist->next==NULL || plist->next->next==NULL)
    {
        return ;
    }

    Node *p = plist->next;
    Node *q;
    int tmp;

    for(q=plist; q->next!=NULL; q=q->next);//找到尾结点

    for(int i=0; i<Getlength(plist)/2; i++)//交换次数由链表长度决定
    {
        tmp = p->data; 
        p->data = q->data;
        q->data = tmp;

        p = p->next;//头指针向后移动
        q = GetPre(plist,q);//尾指针向前移动
    }

}

if(plist==NULL || plist->next==NULL || plist->next->next==NULL),防止程序崩溃。

方法二:改变结点之间的指向
该方法较第一种思路上更简明,不用再交换两个结点的数据了,只需要改变结点之间的指向就可以了。见下图:

这里写图片描述

这里写图片描述

创建三个指针p、q、s,p和q用来改变指向,s保证三个指针能正确向后移动,当s==NULL时,p和q正在改变尾结点和他的前驱之间的指向,q=s(NULL)后,指向操作结束,再将头结点plist->next置为p之后,逆置完成。

具体实现代码:

void Reverse2(List plist)
{
    if(plist==NULL || plist->next==NULL || plist->next->next==NULL)
    {
        return ;
    }

    Node *p = plist->next;
    Node *q = p->next;
    Node *s;
    p->next = NULL;

    while(q!=NULL)
    {
        s = q->next;
        q->next = p;//反向

        p = q;
        q = s;
        //s = q->next;//如果放在这里,q的指向已经发生了改变,s并不能移到它之后的那个结点
    }
    plist->next = p;

}

方法三:头插法的思想

头插法不符合常识,就像插队一样让人无法理解,但是在逆置中它却起到了很大的作用。

这里写图片描述

先将头节点的next置空,相当于重新做一个链表,只不过所有的数据结点不需要再动态申请,都已经存在了。

void Reverse3(List plist)
{
    Node *p = plist->next;
    Node *q;//不用先赋值
    plist->next = NULL;

    while(p!=NULL)
    {
        q = p->next;

        p->next = plist->next;
        plist->next = p;    

        p = q;
    }

}

十三、去除重复的值

链表的唯一性,即数据不能重复。

void Unique(List plist)
{
    if(plist==NULL || plist->next==NULL || plist->next->next==NULL)
    {
        return ;
    }

    Node *p;
    Node *q;
    Node *s;

    for(p=plist->next; p!=NULL; p=p->next)
    {
        for(q=p; q->next!=NULL; q=q->next)//对节点后面的结点进行遍历,试图寻找到重复的结点(利用前驱)
        {
            if(q->next->data == p->data)//找到重复的结点
            {
                s = q->next;//要删除的结点
                q->next = s->next;
                free(s);
            }
        }
    }

}

四、测试用例

先创建一个头结点,进行初始化之后,再插入各个数据结点,之后分别验证这些方法。

int main()
{
    Node head;//创建头结点

    InitList(&head);

    /*for(int i=0; i<10; i++)
    {
        Insert_head(&head, i);//头插法
    }
    Show(&head);*/

    for(int i=0; i<10; i++)
    {
        Insert_tail(&head, i);//尾插法
    }
    Show(&head);

    printf("\n");
    //Node *p = Search(&head, 8);//查找结点
    //printf("%d\n",p->data);

    //int length = Getlength(&head);//求链表的长度
    //printf("%d\n",length);

    //Delete(&head, 8);//删除结点
    //Show(&head);

    /*Destroy2(&head);
    Destroy2(&head);*/

    int rtval = 2;
    if(GetElem(&head,1,&rtval))
    {
        printf("%d\n",rtval);
    }

    //Reverse(&head);//逆置
    //Reverse2(&head);
    /*Reverse3(&head);
    Show(&head);*/

    Insert_head(&head, 3);
    Insert_head(&head, 3);
    Show(&head);//链表中有三个重复的结点

    Unique(&head);//删除重复值
    Show(&head);

    return 0;
}
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值