单链表详解(C语言版)


前言

本文介绍了单链表的定义、常用操作、优化和使用的注意点等内容。


一、单链表的定义

单链表是一种链式存取的数据结构,用一组地址任意的存储单元存放线性表中的数据元素。

二、单链表的结构

结构图
在这里插入图片描述
结构代码描述

#define ElemType int

//单链表结点
typedef struct Node
{
    ElemType data;      //数据域
    struct Node *next;  //指针域
}Node, *PNode;

//单链表管理结构
typedef struct List
{
    PNode  first; //指向头结点
    PNode  last;  //指向尾结点
    size_t size;  //记录有效结点数
}List;

三、单链表的常用操作

1.初始化

//初始化
void InitList(List *list)
{
    //创建头结点(此时头结点也是尾结点)
    list->first = list->last = (Node*)malloc(sizeof(Node));
    assert(list->first != NULL);
    //为该结点赋值
    list->first->next = NULL;
    list->size = 0;
}

2.尾插

//尾插
void push_back(List *list, ElemType x)
{
    //Node *s = _buynode(x);
    //创建尾插结点
    Node *s = (Node *)malloc(sizeof(Node));
    assert(s != NULL);
    //为尾插结点赋值
    s->data = x;
    s->next = NULL;
    
    //将尾插结点连接到链表尾部
    list->last->next = s;
    //更改管理结点中last的指向
    list->last = s;
    //更改有效结点个数
    list->size++;
}

3.头插

//头插
void push_front(List *list, ElemType x)
{
    //Node *s = _buynode(x);
    //创建头插结点
    Node *s = (Node *)malloc(sizeof(Node));
    assert(s != NULL);
    s->data = x;
    //将头插结点连接到链表中
    s->next = list->first->next;
    list->first->next = s;
    //如果这个是插入的第一个有效结点,需要更改尾指针的指向
    if(list->size == 0)
    {
        list->last = s;
    }
    //更改有效结点个数
    list->size++;
}

4.显示链表内的数据

//打印链表中的数据
void show_list(List *list)
{
    Node *p = list->first->next;
    while(p != NULL)
    {
        printf("%d-->",p->data);
        p = p->next;
    }
    printf("Nul.\n");
}

5.尾删

//尾部删除
void pop_back(List *list)
{
    //链表是否还有结点?
    if(list->size == 0)
        return;
    //寻找倒数第二个结点
    Node *p = list->first;
    while(p->next != list->last)
        p = p->next;
    //删除尾结点
    free(list->last);
    //更改尾指针的指向
    list->last = p;
    //更改现在尾结点的指针域
    list->last->next = NULL;
    //更改有效结点数
    list->size--;
}

6.头删

//头部删除
void pop_front(List *list)
{
    //链表中是否有元素?
    if(list->size == 0)
        return;
    //临时保存要删除结点的地址
    Node *p = list->first->next;
    //删除该结点与链表的连接
    list->first->next = p->next;
    //删除结点
    free(p);
    //该链表是否只有一个有效结点
    if(list->size == 1)
    {
        //更改尾指针的指向
        list->last = list->first;
    }
    //更改有效结点个数
    list->size--;
}

7.按值插入(按照链表内的数据从小到大排序的规则进行插入)

//按值插入(前提是链表内的数据从小到大排序)
void insert_val(List *list, ElemType x)
{
    
    //创建要插入的链表结点
    Node *s = (Node *)malloc(sizeof(Node));
    assert(s != NULL);
    s->data = x;
    s->next = NULL;
    
    //查找要插入的位置
    Node *p = list->first;
    while(p->next!=NULL && p->next->data<x)
        p = p->next;
    
    //这个位置是否为尾部?
    if(p->next == NULL)
    {
        //如果为尾部修改尾指针位置
        list->last = s;
    }
    //将结点插入
    s->next = p->next;
    p->next = s;
    //更改有效结点个数
    list->size++;
}

8.查找某数据在链表中第一次出现的数据结点

//查找数据
Node* find(List *list, ElemType key)
{
    Node *p = list->first->next;
    while(p!=NULL && p->data!=key) 
        p = p->next;
    return p;
}

9.查找链表中有效结点的个数

//查找有效结点的个数
int  length(List *list)
{
    return list->size;
}

10.删除链表中某值第一次出现的结点

方法一:查找要删除结点位置来进行删除。该方法的删除技巧在于拷贝下一结点到当前结点,然后删除下一结点,这样一来可以摆脱对删除结点前驱的依赖。

//按照值来删除方法一:查找要删除结点位置来进行删除
void delete_val(List *list, ElemType key)
{
    //链表是否为空?
    if(list->size==0)
        return;
    //查找到要删除结点的位置
    Node *p = find(list,key);
    //如果没有找到
    if(p == NULL)
    {
        printf("要删除的数据不存在.\n");
        return;
    }
    //如果找到,且这个结点是尾结点
    if(p == list->last)
    {
        //对这个结点进行尾删
        pop_back(list);
    }
    else//删除方法:拷贝下一结点到当前结点,删除下一结点
    {
        //保存下一结点指针
        Node *q = p->next;
        //拷贝下一结点
        p->data = q->data;
        p->next = q->next;
        //删除下一结点
        free(q);
        //更改有效结点数
        list->size--;
    }    
}

方法二:查找要删除结点的前驱结点位置来进行删除

//按照值来删除方法二:查找要删除结点的前驱结点位置来进行删除
void delete_val(List *list, ElemType key)
{
    //链表是否为空?
    if(list->size==0)
        return;
    //查找到要删除结点的前驱位置
    Node *p = list->first;
    while(p->next!=NULL && p->next->data!=key)
        p=p->next;
    //如果没有找到
    if(p->next== NULL)
    {
        printf("要删除的数据不存在.\n");
        return;
    }
    //如果找到,且这个结点是尾结点
    if(p->next == list->last)
    {
        //对这个结点进行尾删
        pop_back(list);
    }
    else
    {
        //保存要删除结点的指针
        Node *q = p->next;
        //删除结点与链表的连接
        p->next = q->next;
        //删除结点
        free(q);
        //更改有效结点数
        list->size--;
    }    
}

11.排序

思路一:结点的位置不动,对结点中的数据进行交换排序。

思路二:将一个链表断开分成两个链表(第一个链表包含头结点和第一个有效结点,第二链表包含剩余的有效结点),从第二个链表中逐一取出数据并按照从小到大的规则逐一插入到第一个链表中。
在这里插入图片描述
以下代码对思路二进行实现

//排序(从小到大排序)
void sort(List *list)
{
    //有效的链表个数<=1?
    if(list->size==0 || list->size==1)
        return;

    /*  将链表分成两个链表s和q  */    
    Node *s = list->first->next; //保存第一个链表的指针    
    Node *q = s->next;  //保存第二个链表的指针

    /*断开两个链表的连接*/
    list->last = s;  
    list->last->next = NULL;

    //第二个链表是否还有结点?
    while(q != NULL) 
    {
        /*从第二个链表中不断取出结点,插入第一个链表*/
        s = q; //从第二个链表中取出一个结点
        q = q->next;//取出一个结点后q后移一位

        //在第一个链表中查找要插入结点的前驱结点
        Node *p = list->first;
        while(p->next!=NULL && p->next->data<s->data)
            p = p->next;
        
        //如果要插入结点的位置在第一个链表尾部
        if(p->next == NULL)
        {
            //修改尾指针
            list->last = s;
        }
        //插入
        s->next = p->next;
        p->next = s;
    }
}

12.逆置

思路一:结点的位置不变,对结点内的数据进行逆置。

思路二:将一个链表断开分成两个链表(第一个链表包含头结点和第一个有效结点,第二链表包含剩余的有效结点),从第二个链表中逐一取出数据并按照头插的方式插入到第一个链表中。
在这里插入图片描述
以下代码对思路二进行实现

//逆置
void resver(List *list)
{
    if(list->size==0 || list->size==1)
        return;
    /*  将链表分成两个链表s和q  */
    Node *p = list->first->next;
    Node *q = p->next;
    /*断开两个链表的连接*/
    list->last = p;
    list->last->next = NULL;

    /*从第二个链表中不断取出结点,头插到第一个链表*/
    while(q != NULL)
    {    
        //从第二个链表中取出结点
        p = q;
        q = p->next;
        //头插到第一个链表
        p->next =list->first->next;
        list->first->next = p;
    }
}

13.清除链表(释放所有的有效结点)

思路:不断进行头删,当有效结点都删除完成之后,将尾指针指向头结点,将有效结点个数设置为0

//清除单链表(释放所有的有效结点)
void clear(List *list)
{
    //是否有有效结点?
    if(list->size == 0)
        return;

    Node *p = list->first->next;
    //将有效结点全部释放
    while(p != NULL)
    {
        list->first->next = p->next;
        free(p);
        p = list->first->next;
    }
    //有效结点删除完成之后,更新尾指针和有效结点个数
    list->last = list->first;
    list->size = 0;
}

14.销毁链表(释放所有结点包括头结点和有效结点)

//销毁(释放所有结点即包括头结点和有效结点)
void destroy(List *list)
{
    //释放有效结点
    clear(list);
    //释放头结点
    free(list->first);
    //将头结点和尾结点置空
    list->first = list->last = NULL;
}

四、单链表代码的优化

1.单链表是由一个个结点连接而成的,所以在单链表中涉及很多生成结点的代码,对于这一部分的代码可以将其包装成一个生成结点的函数,让代码得到复用,从而使得代码更加简洁,可读性更强。

//生成结点
Node* _buynode(ElemType x)
{
    //为结点分配空间
    Node *s = (Node *)malloc(sizeof(Node));
    assert(s != NULL);
    //为该结点的数据域赋值
    s->data = x;
    //对应指针域,当结点插入链表时再对其进行赋值,目前先置空
    s->next = NULL;
    return s;
}

2.单链表中高频率的使用到第一个有效结点和尾部结点的空指针域,为了提高代码的简洁程度和可读性,可以对这两部分进行包装。

//返回第一个有效结点
It begin(List *list)
{
    return list->first->next;
}
//返回尾结点的下一结点(空结点)
It end(List *list)
{
    return list->last->next;
}

3.单链表中的插入操作比较多样,为了使得代码更加简洁易懂,可以书写一个往某结点的前面插入结点的函数,这样在头插、尾插等插入函数中都可以得到使用。

typedef Node* It;

//在链表的某个结点前面插入结点
void insert(List *list, It pos, ElemType x)
{
    //找到结点要插入的位置
    Node *p = list->first;
    while(p->next != pos)
    {
        p = p->next;
    }
    //获取一个结点
    Node *s = _buynode(x);
    //将该结点插入链表
    s->next = p->next;
    p->next = s;
    //如果插入结点的位置在尾部,需要更改尾指针的值
    if(pos == NULL)
        list->last = s;
    //更改有效结点个数
    list->size++;
}

优化后的尾插代码

//尾插
void push_back(List *list, ElemType x)
{
    insert(list,end(list),x);    
}

优化后的头插代码

//头插
void push_front(List *list, ElemType x)
{
    insert(list,begin(list),x);
}

五、单链表的注意点

1.正确理解前驱和后继的关系

一定要把握好前驱和后继之间的关系,只有得到前驱结点才能对后继结点进行操作,因为单链表是逐一相连的,前驱结点的指针域中存放着后继结点的地址,也就是说失去前驱也就意味着丢失后继,所以在对前驱结点的指针域做改动时一定要进行备份,不能让其丢失。

2.结点的插入过程

(1).创建一个结点:结点内存空间的分配、结点数据域的赋值。
(2).将结点插入链表:更改插入结点的指针域(此时指针域内的值来源于前驱结点指针域内的值)、更改前驱结点的指针域(此时指针域内的值为要插入结点的地址)。在插入数据时,一定要先将前驱的指针域的值先赋值给插入结的指针域,然后再更改前驱结点指针域的值,否则会造成后续结点的丢失。

3.结点删除的过程

(1).获取该结点的前驱结点。
(2).备份一份要删除结点的地址。
(3).断开结点与链表的连接:将要删除结点的指针域值赋给它前驱结点的指针域。
(4).释放要删除结点内存:这里就需要利用到备份的删除结点的地址,如果前面没有先对删除结点的地址进行备份,那么经过步骤(3)就会丢失删除结点的地址,将无法释放删除结点的内存空间。

六、附录

SList.h

#ifndef __SLIST_H__
#define __SLIST_H__

#include<stdio.h>
#include<malloc.h>
#include<assert.h>

#define ElemType int

//单链表结点
typedef struct Node
{
    ElemType data;      //数据域
    struct Node *next;  //指针域
}Node, *PNode;

//单链表管理结构
typedef struct List
{
    PNode  first; //指向头结点
    PNode  last;  //指向尾结点
    size_t size;  //记录有效结点数
}List;

void InitList(List *list);

void push_back(List *list, ElemType x);
void push_front(List *list, ElemType x);
void show_list(List *list);

void pop_back(List *list);
void pop_front(List *list);
void insert_val(List *list, ElemType x);
Node* find(List *list, ElemType key);
int  length(List *list);
void delete_val(List *list, ElemType key);
void sort(List *list);
void resver(List *list);
void clear(List *list);
void destroy(List *list);

//代码优化/
typedef Node* It;
Node* _buynode(ElemType x);
It begin(List *list);
It end(List *list);
void insert(List *list, It pos, ElemType x);
#endif //__SLIST_H__

SList.cpp

#include"SList.h"

//初始化
void InitList(List *list)
{
    //创建头结点(此时头结点也是尾结点)
    list->first = list->last = (Node*)malloc(sizeof(Node));
    assert(list->first != NULL);
    //为该结点赋值
    list->first->next = NULL;
    list->size = 0;
}
//尾插
void push_back(List *list, ElemType x)
{
    insert(list,end(list),x);    
}

/*
//尾插
void push_back(List *list, ElemType x)
{
    //Node *s = _buynode(x);
    //创建尾插结点
    Node *s = (Node *)malloc(sizeof(Node));
    assert(s != NULL);
    //为尾插结点赋值
    s->data = x;
    s->next = NULL;
    
    //将尾插结点连接到链表尾部
    list->last->next = s;
    //更改管理结点中last的指向
    list->last = s;
    //更改有效结点个数
    list->size++;
}
*/
//头插
void push_front(List *list, ElemType x)
{
    insert(list,begin(list),x);
}

/*
//头插
void push_front(List *list, ElemType x)
{
    //Node *s = _buynode(x);
    //创建头插结点
    Node *s = (Node *)malloc(sizeof(Node));
    assert(s != NULL);
    s->data = x;
    //将头插结点连接到链表中
    s->next = list->first->next;
    list->first->next = s;
    //如果这个是插入的第一个有效结点,需要更改尾指针的指向
    if(list->size == 0)
    {
        list->last = s;
    }
    //更改有效结点个数
    list->size++;
}
*/

//打印链表中的数据
void show_list(List *list)
{
    Node *p = list->first->next;
    while(p != NULL)
    {
        printf("%d-->",p->data);
        p = p->next;
    }
    printf("Nul.\n");
}

//尾部删除
void pop_back(List *list)
{
    //链表是否还有结点?
    if(list->size == 0)
        return;
    //寻找倒数第二个结点
    Node *p = list->first;
    while(p->next != list->last)
        p = p->next;
    //删除尾结点
    free(list->last);
    //更改尾指针的指向
    list->last = p;
    //更改现在尾结点的指针域
    list->last->next = NULL;
    //更改有效结点数
    list->size--;
}

//头部删除
void pop_front(List *list)
{
    //链表中是否有元素?
    if(list->size == 0)
        return;
    //临时保存要删除结点的地址
    Node *p = list->first->next;
    //删除该结点与链表的连接
    list->first->next = p->next;
    //删除结点
    free(p);
    //该链表是否只有一个有效结点
    if(list->size == 1)
    {
        //更改尾指针的指向
        list->last = list->first;
    }
    //更改有效结点个数
    list->size--;
}

//按值插入(前提是链表内的数据从小到大排序)
void insert_val(List *list, ElemType x)
{
    Node *s = _buynode(x);
    /*
    //创建要插入的链表结点
    Node *s = (Node *)malloc(sizeof(Node));
    assert(s != NULL);
    s->data = x;
    s->next = NULL;
    */
    //查找要插入的位置
    Node *p = list->first;
    while(p->next!=NULL && p->next->data<x)
        p = p->next;
    
    //这个位置是否为尾部?
    if(p->next == NULL)
    {
        //如果为尾部修改尾指针位置
        list->last = s;
    }
    //将结点插入
    s->next = p->next;
    p->next = s;
    //更改有效结点个数
    list->size++;
}

//查找数据
Node* find(List *list, ElemType key)
{
    Node *p = list->first->next;
    while(p!=NULL && p->data!=key) 
        p = p->next;
    return p;
}

//查找有效结点的个数
int  length(List *list)
{
    return list->size;
}
/*
//按照值来删除方法一:查找要删除结点位置来进行删除
void delete_val(List *list, ElemType key)
{
    //链表是否为空?
    if(list->size==0)
        return;
    //查找到要删除结点的位置
    Node *p = find(list,key);
    //如果没有找到
    if(p == NULL)
    {
        printf("要删除的数据不存在.\n");
        return;
    }
    //如果找到,且这个结点是尾结点
    if(p == list->last)
    {
        //对这个结点进行尾删
        pop_back(list);
    }
    else//删除方法:拷贝下一结点到当前结点,删除下一结点
    {
        //保存下一结点指针
        Node *q = p->next;
        //拷贝下一结点
        p->data = q->data;
        p->next = q->next;
        //删除下一结点
        free(q);
        //更改有效结点数
        list->size--;
    }    
}
*/


//按照值来删除方法二:查找要删除结点的前驱结点位置来进行删除
void delete_val(List *list, ElemType key)
{
    //链表是否为空?
    if(list->size==0)
        return;
    //查找到要删除结点的前驱位置
    Node *p = list->first;
    while(p->next!=NULL && p->next->data!=key)
        p=p->next;
    //如果没有找到
    if(p->next== NULL)
    {
        printf("要删除的数据不存在.\n");
        return;
    }
    //如果找到,且这个结点是尾结点
    if(p->next == list->last)
    {
        //对这个结点进行尾删
        pop_back(list);
    }
    else
    {
        //保存要删除结点的指针
        Node *q = p->next;
        //删除结点与链表的连接
        p->next = q->next;
        //删除结点
        free(q);
        //更改有效结点数
        list->size--;
    }    
}


//排序(从小到大排序)
void sort(List *list)
{
    //有效的链表个数<=1?
    if(list->size==0 || list->size==1)
        return;

    /*  将链表分成两个链表s和q  */    
    Node *s = list->first->next; //保存第一个链表的指针    
    Node *q = s->next;  //保存第二个链表的指针

    /*断开两个链表的连接*/
    list->last = s;  
    list->last->next = NULL;

    //第二个链表是否还有结点?
    while(q != NULL) 
    {
        /*从第二个链表中不断取出结点,插入第一个链表*/
        s = q; //从第二个链表中取出一个结点
        q = q->next;//取出一个结点后q后移一位

        //在第一个链表中查找要插入结点的前驱结点
        Node *p = list->first;
        while(p->next!=NULL && p->next->data<s->data)
            p = p->next;
        
        //如果要插入结点的位置在第一个链表尾部
        if(p->next == NULL)
        {
            //修改尾指针
            list->last = s;
        }
        //插入
        s->next = p->next;
        p->next = s;
    }
}

//逆置
void resver(List *list)
{
    if(list->size==0 || list->size==1)
        return;
    /*  将链表分成两个链表s和q  */
    Node *p = list->first->next;
    Node *q = p->next;
    /*断开两个链表的连接*/
    list->last = p;
    list->last->next = NULL;

    /*从第二个链表中不断取出结点,头插到第一个链表*/
    while(q != NULL)
    {    
        //从第二个链表中取出结点
        p = q;
        q = p->next;
        //头插到第一个链表
        p->next =list->first->next;
        list->first->next = p;
    }
}

//清除单链表(释放所有的有效结点)
void clear(List *list)
{
    //是否有有效结点?
    if(list->size == 0)
        return;

    Node *p = list->first->next;
    //将有效结点全部释放
    while(p != NULL)
    {
        list->first->next = p->next;
        free(p);
        p = list->first->next;
    }
    //有效结点删除完成之后,更新尾指针和有效结点个数
    list->last = list->first;
    list->size = 0;
}
//销毁(释放所有结点即包括头结点和有效结点)
void destroy(List *list)
{
    //释放有效结点
    clear(list);
    //释放头结点
    free(list->first);
    //将头结点和尾结点置空
    list->first = list->last = NULL;
}

//代码优化
//生成结点
Node* _buynode(ElemType x)
{
    //为结点分配空间
    Node *s = (Node *)malloc(sizeof(Node));
    assert(s != NULL);
    //为该结点的数据域赋值
    s->data = x;
    //对应指针域,当结点插入链表时再对其进行赋值,目前先置空
    s->next = NULL;
    return s;
}
//返回第一个有效结点
It begin(List *list)
{
    return list->first->next;
}
//返回尾结点的下一结点(空结点)
It end(List *list)
{
    return list->last->next;
}
//在链表的某个结点前面插入结点
void insert(List *list, It pos, ElemType x)
{
    //找到结点要插入的位置
    Node *p = list->first;
    while(p->next != pos)
    {
        p = p->next;
    }
    //获取一个结点
    Node *s = _buynode(x);
    //将该结点插入链表
    s->next = p->next;
    p->next = s;
    //如果插入结点的位置在尾部,需要更改尾指针的值
    if(pos == NULL)
        list->last = s;
    //更改有效结点个数
    list->size++;
}

Main.cpp

#include"SList.h"

void main()
{
    List mylist;
    InitList(&mylist);

    ElemType Item;
    Node *p = NULL;
    int select = 1;
    while(select)
    {
        printf("**************************************\n");
        printf("* [1] push_back      [2] push_front  *\n");
        printf("* [3] show_list      [4] pop_back    *\n");
        printf("* [5] pop_front      [6] insert_val  *\n");
        printf("* [7] find           [8] length      *\n");
        printf("* [9] delete_val     [10] sort       *\n");
        printf("* [11] resver        [12] clear      *\n");
        printf("* [13*] destroy       [0] quit_system*\n");
        printf("**************************************\n");
        printf("请选择:>");
        scanf("%d",&select);
        if(select == 0)
            break;
        switch(select)
        {
        case 1:
            printf("请输入要插入的数据(-1结束):>");
            while(scanf("%d",&Item),Item != -1)
            {
                push_back(&mylist,Item);
            }
            break;
        case 2:
            printf("请输入要插入的数据(-1结束):>");
            while(scanf("%d",&Item),Item != -1)
            {
                push_front(&mylist,Item);
            }
            break;
        case 3:
            show_list(&mylist);
            break;
        case 4:
            pop_back(&mylist);
            break;
        case 5:
            pop_front(&mylist);
            break;
        case 6:
            printf("请输入要插入的数据:>");
            scanf("%d",&Item);
            insert_val(&mylist,Item);
            break;
        case 7:
            printf("请输入要查找的数据:>");
            scanf("%d",&Item);
            p = find(&mylist,Item);
            if(p == NULL)
            {
                printf("要查找的数据在链表中不存在.\n");
            }
            break;
        case 8:
            printf("单链表的长度为:> %d \n",length(&mylist));
            break;
        case 9:
            printf("请输入要删除的值:>");
            scanf("%d",&Item);
            delete_val(&mylist,Item);
            break;
        case 10:
            sort(&mylist);
            break;
        case 11:
            resver(&mylist);
            break;
        case 12:
            clear(&mylist);
            break;
        //case 13:
        //    destroy(&mylist);
        //    break;
        default:
            printf("输入的命令错误,请重新输入.\n");
            break;
        }
    }
    destroy(&mylist);
}
  • 64
    点赞
  • 301
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值