链表

「单向链表 + 双向循环链表」

一、基本概念

顺序表:顺序存储的线性表

链式表链式存储线性表,简称链表

既然顺序存储中的数据因为挤在一起而导致(插入、删除)需要成片移动,那很容易想到的解决方案是将数据离散地存储在不同内存块中,然后用指针将它们串起来。这种朴素的思路所形成的链式线性表,就是所谓的链表。

顺序表和链表在内存中的基本样态如下图所示:

顺序表与链表对比

二、 链表的分类

根据链表中各个节点之间使用指针的个数以及首尾节点是否相连,可以将链表细分为如下种类:

  • 单向链表(每一个节点只存储一个指针,并首尾不相连)
  • 单向循环链表(每一个节点只存储一个指针,且首尾相连)
  • 双向循环链表(每一个节点存储两个指针分别指向下一个节点以及上一个节点,并首尾相连)
  • 内核链表(把链表与数据进行抽离,可以实现同一个数据中存在于不同的链表以及同一个链表可以存储不同的数据)

这些不同链表的操作都是差不多的,只是指针数目的异同。以最简单的单向链表为例,其基本示意图如下所示:

单向链表示意图

上图中,所有的节点均保存一个指针,指向其逻辑上相邻的下一个节点(末尾节点指向空)。另外注意到,整条链表用一个所谓的头指针 head 来指向,由 head 开始可以找到链表中的任意一个节点。head 通常被称为头指针。


三、 单向链表的基本操作

① 节点设计

节点设计示意图
// 设计数据域
typedef struct DataType
{
    int Num;
    char Name[32];
} DataType_t;

// 设计节点
typedef struct Node
{
    // 数据域
    DataType_t Data;
    // 指针域
    struct Node *Next;
} Node_t, *P_Node_t;

② 初始化空链表

有头节点与无头节点的区别:

有头节点与无头节点的区别

有头节点

表中有一个专门的节点用于表示链表头,该节点不存储有效数据。有头节点的链表在使用时更为方便,因为头指针永远指向头节点,后期不管删除还是添加都不影响他们之间的关系,无需关系他们的指向问题。

有头节点
P_Node_t InitList( void )
{
    // 申请头节点的内存空间
    P_Node_t newNode = calloc(1 , sizeof(Node_t));
    if (newNode == NULL)
    {
        perror("calloc NewNode error");
        return NULL ;
    }
    
    // 设置头节点的后继指针指向NULL
    newNode->Next = NULL ;

    // 返回该节点的地址
    return newNode ;
}

无头节点

由于没有头节点, 因此头指针指向的第一个节点就是数据节点,而该数据节点可能会被删除,也可能会被新的节点替代成为新的第一个节点,因此无法保证头指针永远指向某一个固定的节点,导致操作时需要时刻关注头指针的指向。

无头节点

③ 添加节点

添加节点
void OrderlyAdd2List( P_Node_t head , P_Node_t NewNode )
{
    // 找到合适的位置
    P_Node_t tmp = NULL ;
    for ( tmp = head ; tmp->Next != NULL  ; tmp = tmp->Next )
    {
        // 使用临时指针tmp的下一个节点的数据与新节点的数据进行比较
        // 《无奈之举》单向不循环链表,如果tmp直接指向第一个比新数据大的节点
        //    我们将无法把该新数据放到tmp的前面
        if ( tmp->Next->Data.Num > NewNode->Data.Num )
        {
            break; 
        }
    }
    // 经过以上的for循环后tmp会指向第一个比当前新数据大的节点
    // 或者指向了整个链表的末尾节点


    // 插入
    // 1 . 让新节点的后继指针指向tmp的下一个节点
    NewNode->Next = tmp->Next ;

    // 2. 让tmp的后继指针指向新节点
    tmp->Next = NewNode ;

    return ;
}

④ 查找节点

为了让查找节点的返回值能被其他功能复用,本函数设计返回值为目标节点的上一个节点的地址。

P_Node_t FindNode( P_Node_t head , int Num )
{

    if (head->Next == NULL)
    {
        printf("当前链表为空..\n");
        return NULL ;
    }
    

    // 通过for循环来遍历整个链表
    for (P_Node_t tmp  = head ; tmp->Next != NULL ; tmp = tmp->Next )
    {
        // 遍历过程中判断下一个节点的数据是否为目标数据
        if (tmp->Next->Data.Num == Num)
        {
            return tmp ;
        }
    }

    // 如果遍历结束则表示找不到目标数据
    return NULL ;
}

⑤ 删除节点

删除节点
P_Node_t Del4List( P_Node_t Prev )
{

    P_Node_t Del = Prev->Next ;

    Prev->Next = Del->Next ;
    Del->Next =  NULL ;

    return Del ;
}

⑥ 链表遍历

如何判断遍历到达链表的末尾。

删除节点
void DisPlayList( P_Node_t head )
{
    // 判断链表是否为空
    if ( head->Next == NULL )
    {
        printf("当前链表为空..\n" ) ;
        return ;
    }

    for (P_Node_t tmp  = head->Next ; tmp != NULL ; tmp = tmp->Next )
    {
        printf("%d - %s\n" , tmp->Data.Num , tmp->Data.Name);
    }
    
}

⑦ 销毁链表

销毁链表可以使用循环也可以使用递归。

// 递归版本
void DestructionList ( P_Node_t head )
{
    if (head == NULL)
    {
        return ;
    }
    DestructionList ( head->Next );
    free(head);

    return ;
}   

// 循环版本
void DestructionListFor ( P_Node_t head )
{
    P_Node_t tmp;
    P_Node_t pos ;
    for ( tmp = head , pos = tmp->Next ;
        pos != NULL;  tmp = pos , pos = pos->Next  )
    {
        printf("正在销毁:%d-%s\n" , tmp->Data.Num , tmp->Data.Name);
        free(tmp);
    }

    printf("正在销毁:%d-%s\n" , tmp->Data.Num , tmp->Data.Name);
    free(tmp);
    return ;
}   

⑧ 其他相关接口

// 获取新数据
P_Node_t GetNewNode ( DataType_t * NewData )
{

    // 申请新节点
    P_Node_t NewNode = calloc(1, sizeof(Node_t));
    if (NewNode == NULL)
    {
        perror("calloc NewNode error");
        return NULL ;
    }
    
    // 初始化节点
    // 数据域
    NewNode->Data = *NewData ;
    // memcpy( &NewNode->Data , NewData , sizeof(DataType_t)  );

    // 指针域
    NewNode->Next = NULL ;

    // 返回新节点
    return NewNode ;
}

// 创建新节点
DataType_t GetNewData( void )
{

    DataType_t newData = {0};
    while (1)
    {
        if( 2 != scanf("%d %s" , &newData.Num , newData.Name ))
        {
            printf("请输入正确的数据..\n");
            while (getchar() != '\n');
            continue;
        }
        
        break; 
    }

    // 返回新数据
    return newData ;
}

⑨ 主函数测试用例

int main(int argc, char const *argv[])
{
    // 初始化空链表
    P_Node_t head = InitList( );

    for (int i = 0; i < 5 ; i++)
    {
        // 获得新数据并创建一个新的节点
        DataType_t NewData = GetNewData();
        P_Node_t NewNode =  GetNewNode ( &NewData );

        // 把新节点有序插入到链表中
        OrderlyAdd2List( head , NewNode ) ;
    }
    
    DisPlayList( head );

    // 销毁链表
    DestructionListFor ( head )  ;

#if 0

    int Num = 0 ;
    while (1)
    {
        printf("请输入需要修改的目标数据的编号:\n");
        scanf("%d" , &Num) ;
        // 更新节点 (任何数据都可以修改)
        // 先找到目标节点
        P_Node_t Prev = FindNode( head , Num );
        if (Prev == NULL)
        {
            printf("找不到目标数据..\n");
        }
        else{
            printf("Prev:%d - %s\n" , Prev->Data.Num , Prev->Data.Name) ;
            printf("Data:%d - %s\n" , Prev->Next->Data.Num , Prev->Next->Data.Name) ;
            

            // 剔除节点
            P_Node_t del = Del4List( Prev );

            // 更新数据
            printf("请输入新的数据:\n");
            scanf( "%d %s" , &del->Data.Num  , del->Data.Name );

            // 重新插入
            OrderlyAdd2List(head , del);

        }
        

        DisPlayList( head );
    }
#endif 

    return 0;
}

四、单向循环链表

循环链表与单向链表唯一的差别就是他的末尾节点会指向第一个头节点。因此与单向链表相比只有判断结束的条件不一样。
原本单向链表的末尾条件:

tmp->next == NULL //表示当前tmp为末尾节点

循环链表的末尾条件:

tmp->next == head //表示当前tmp为末尾节点

五、双向循环链表

概念:对链表而言,双向均可遍历是最方便的,另外首尾相连循环遍历也可大大增加链表操作的便捷性。因此,双向循环链表,是在实际运用中是最常见的链表形态

双向链表图解

① 节点设计

节点设计
// 节点设计
typedef struct Node
{
    dataType Data ;

    // 指针域
    struct Node * Next , * Prev ;
}Node_t , *P_Node_t ;

② 初始化空链表

初始化空链表
P_Node_t InitNewNode(Data_t *NewData)
{
    P_Node_t newNode = calloc(1, sizeof(Node_t));
    if (newNode == NULL)
    {
        perror("calloc newNode error");
        return NULL;
    }

    if (NewData != NULL)
    {
        newNode->Data = *NewData;
    }

    newNode->Next = newNode->Prev = newNode;
    return newNode;
}

③ 添加节点

头插

头插
void Add2ListHead( P_Node_t head ,  P_Node_t newNode )
{

    /////////////////先操作新节点再操作链表////////////////
    /////////////////////防止链表断裂////////////////////
    // 1 让新节点的后继指针先指向第一个有效数据
    newNode->Next = head->Next ;

    // 2. 让新节点的前驱指针,先指向头节点
    newNode->Prev = head ;

    // 3. 让链表的头节点的后继指向新节点
    head->Next = newNode ;

    // 4. 让链表的第一个有效节点的前驱指向新节点
    newNode->Next->Prev = newNode ;

    return ;
}

尾插

尾插
void Add2ListTail( P_Node_t head , P_Node_t newNode )
{
    newNode->Next = head ;
    newNode->Prev = head->Prev ;

    head->Prev = newNode ;
    newNode->Prev->Next = newNode ;

}

实际上头插和尾插的差别是在于新节点的前后两个节点的差异而已,因此可以设计一个函数,让该函数直接接受目标位置的左右两个节点,因此通过控制左右节点便可实现头插或尾插的操作。

综合头尾插
void Add2List( P_Node_t Prev , P_Node_t New  , P_Node_t Next )
{
    Prev->Next = New ;
    New->Next = Next ;

    Next->Prev = New ;
    New->Prev = Prev ;
}

④ 查找节点

P_Node_t FindData(P_Node_t head)
{
    int Num;
    while (1)
    {
        printf("请输入要查找的编号:");
        if (1 != scanf("%d", &Num))
        {
            printf("输入有误!\n");
            while (getchar() != '\n')
                ;
            continue;
        }
        P_Node_t tmp = NULL;
        for (tmp = head->Next; tmp != head; tmp = tmp->Next)
        {
            if (tmp->Data.Num == Num)
            {
                printf("找到了 Num:%d Name:%s\n", tmp->Data.Num, tmp->Data.Name);
                return tmp;
            }
        }
        printf("未找到!\n");
        return NULL;
    }
}

⑤ 删除节点

删除节点
void DelData(P_Node_t Prev, P_Node_t del, P_Node_t Next)
{
    Prev->Next = Next;
    Next->Prev = Prev;
    del->Next = del->Prev = del;
}

⑥ 链表遍历

void displayList( P_Node_t head)
{

    // tmp 初始化指向链表的第一个有效数据
    for (P_Node_t tmp = head->Next ; tmp != head ; tmp = tmp->Next )
    {
        printf("Num:%d\t" , tmp->Data) ;
    }
    printf("\n");

}

⑦ 销毁链表

/// @brief 释放内存
/// @param head 链表头地址
/// @param tmp 接收head->next
void Destruction(P_Node_t head, P_Node_t tmp)
{
    if (head == tmp)
    {
        free(head);
        return;
    }
    Destruction(head, tmp->Next);
    free(tmp);
}
posted @ 2025-10-29 08:48  林明杰  阅读(18)  评论(0)    收藏  举报