数据结构 单向链表进阶

一、链表基础定义

首先定义链表节点的数据类型和结构体,这是所有操作的基础,头文件linklist.h内容如下:

复制代码
#ifndef __LINKLIST_H__
#define __LINKLIST_H__

// 链表节点存储的数据类型(可根据需求修改)
typedef int DataType;

// 链表节点结构体
typedef struct node 
{
    DataType Data;          // 节点数据域
    struct node *pNext;     // 节点指针域(指向下一个节点)
}Node_t;

// 进阶操作函数声明
extern Node_t *FindMidNode(Node_t *pHead);                  // 找中间节点
extern Node_t *FindLastNthNode(Node_t *pHead, int Nth);     // 找倒数第N个节点
extern int ReveiseLinkList(Node_t *pHead);                  // 反转链表
extern int BubbleSortLinkList(Node_t *pHead);               // 冒泡排序链表
extern int SelectSortLinkList(Node_t *pHead);              // 选择排序链表
extern int IsCircleList(Node_t *pHead, int *plen, Node_t **ppTmpNode); // 判断链表是否有环

// 基础操作(依赖)
extern Node_t *CreateEmptyLinkList(void);                   // 创建空链表
extern int InsertHeadLinkNode(Node_t *pHead, DataType TmpData); // 头插法
extern int InsertTailLinkNode(Node_t *pHead, DataType TmpData); // 尾插法
extern int ShowLinkList(Node_t *pHead);                     // 打印链表
extern int DestroyLinkList(Node_t **ppHead);                // 销毁链表

#endif

二、核心进阶操作实现(linklist.c)

1. 找链表中间节点(快慢指针法)

思路 :定义两个指针pFast(快指针)和pSlow(慢指针),快指针每次走 2 步,慢指针每次走 1 步。当快指针走到链表末尾时,慢指针恰好指向链表中间节点(偶数个节点时指向靠后的中间节点)。

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

// 找链表中间节点
Node_t *FindMidNode(Node_t *pHead)
{
    Node_t *pFast = NULL;
    Node_t *pSlow = NULL;

    pFast = pSlow = pHead->pNext;
    while (pFast != NULL)
    {
        pFast = pFast->pNext;
        if (NULL == pFast)
        {
            return pSlow;
        }
        pFast = pFast->pNext;
        pSlow = pSlow->pNext;
    }

    return pSlow;
}

2. 找链表倒数第 N 个节点(快慢指针法)

思路:让快指针先提前走 N 步,然后快慢指针同时走,当快指针走到链表末尾时,慢指针恰好指向倒数第 N 个节点。

复制代码
// 找链表倒数第N个节点
Node_t *FindLastNthNode(Node_t *pHead, int Nth)
{
    Node_t *pFast = NULL;
    Node_t *pSlow = NULL;
    int i = 0;

    pFast = pSlow = pHead;
    // 快指针先移动N步
    for (i = 0; pFast != NULL && i < Nth; i++)
    {
        pFast = pFast->pNext;
    }

    // N超过链表长度,返回NULL
    if (NULL == pFast)
    {
        return NULL;
    }

    // 快慢指针同步移动,直到快指针到末尾
    while (pFast != NULL)
    {
        pFast = pFast->pNext;
        pSlow = pSlow->pNext;
    }
    
    return pSlow;
}

3. 反转链表(头插法)

思路:将原链表的节点逐个 "摘下来",用头插法插入到新的链表头部,最终实现链表反转。

复制代码
// 反转链表
int ReveiseLinkList(Node_t *pHead)
{
    Node_t *pTmpNode = NULL;
    Node_t *pInsertNode = NULL;

    pTmpNode = pInsertNode = pHead->pNext;
    pHead->pNext = NULL; // 清空原链表头节点的指针

    while (pTmpNode != NULL)
    {
        pTmpNode = pTmpNode->pNext;       // 保存下一个节点
        pInsertNode->pNext = pHead->pNext;// 待插入节点指向新链表头部
        pHead->pNext = pInsertNode;       // 头节点指向待插入节点
        pInsertNode = pTmpNode;           // 更新待插入节点
    }

    return 0;
}

4. 链表冒泡排序

思路 :和数组冒泡排序逻辑一致,通过相邻节点的比较和数据交换,将最大的元素逐步 "冒泡" 到链表末尾;引入pEnd标记排序边界,优化循环次数。

复制代码
// 链表冒泡排序
int BubbleSortLinkList(Node_t *pHead)
{
    Node_t *pTmpNode1 = NULL;
    Node_t *pTmpNode2 = NULL;
    Node_t *pEnd = NULL;
    DataType TmpData;

    // 空链表或只有一个节点,无需排序
    if (NULL == pHead->pNext || NULL == pHead->pNext->pNext)
    {
        return 0;
    }

    while (1)
    {
        pTmpNode1 = pHead->pNext;
        pTmpNode2 = pHead->pNext->pNext;
        
        // 排序边界重合,结束排序
        if (pEnd == pTmpNode2)
        {
            break;
        }

        // 未到排序边界,继续比较交换
        while (pTmpNode2 != pEnd)
        {
            if (pTmpNode1->Data > pTmpNode2->Data)
            {
                // 交换节点数据(无需移动节点,仅交换数据)
                TmpData = pTmpNode1->Data;
                pTmpNode1->Data = pTmpNode2->Data;
                pTmpNode2->Data = TmpData;
            }

            pTmpNode1 = pTmpNode1->pNext;
            pTmpNode2 = pTmpNode2->pNext;
        }
        pEnd = pTmpNode1; // 更新排序边界
    }

    return 0;
}

5. 链表选择排序

思路:遍历链表,找到当前未排序区间的最小节点,将其数据与未排序区间的第一个节点数据交换;逐步缩小未排序区间,直到整个链表有序。

复制代码
// 链表选择排序
int SelectSortLinkList(Node_t *pHead)
{
    Node_t *pSwapNode = NULL;
    Node_t *pTmpNode = NULL;
    Node_t *pMinNode = NULL;
    DataType TmpData;

    // 空链表或只有一个节点,无需排序
    if (NULL == pHead->pNext || NULL == pHead->pNext->pNext)
    {
        return 0;
    }

    pSwapNode = pHead->pNext; // 未排序区间的第一个节点

    while (pSwapNode->pNext != NULL)
    {
        pMinNode = pSwapNode;  // 初始化最小节点为未排序区间第一个节点
        pTmpNode = pSwapNode->pNext;

        // 找未排序区间的最小节点
        while (pTmpNode != NULL)
        {
            if (pTmpNode->Data < pMinNode->Data)
            {
                pMinNode = pTmpNode;
            }
            pTmpNode = pTmpNode->pNext;
        }

        // 交换最小节点和未排序区间第一个节点的数据
        if (pSwapNode != pMinNode)
        {
            TmpData = pSwapNode->Data;
            pSwapNode->Data = pMinNode->Data;
            pMinNode->Data = TmpData;
        }

        pSwapNode = pSwapNode->pNext; // 缩小未排序区间
    }

    return 0;
}

6. 判断链表是否有环(并找环入口 / 环长度)

思路

  • 判环:快慢指针法,快指针每次走 2 步,慢指针每次走 1 步,若两指针相遇则链表有环;

  • 找环长度:相遇后,固定一个指针,另一个指针继续走,再次相遇时走过的步数即为环长度;

  • 找环入口:相遇后,将其中一个指针重置到链表头,两指针以相同步长走,相遇点即为环入口。

    // 判断链表是否有环,返回1有环/0无环;plen返回环长度;ppTmpNode返回环入口节点
    int IsCircleList(Node_t *pHead, int *plen, Node_t **ppTmpNode)
    {
    Node_t *pFast = NULL;
    Node_t *pSlow = NULL;
    Node_t *pMeetNode = NULL;
    int len = 0;

    复制代码
      pSlow = pFast = pHead->pNext;
      while (1)
      {
          pFast = pFast->pNext;
          if (NULL == pFast) return 0;
          pFast = pFast->pNext;
          if (NULL == pFast) return 0;
    
          pSlow = pSlow->pNext;
          if (pSlow == pFast) // 快慢指针相遇,存在环
          {
              pMeetNode = pSlow;
              break;
          }
      }
      
      // 计算环长度
      pSlow = pMeetNode->pNext;
      len++;
      while (pSlow != pMeetNode)
      {
          len++;
          pSlow = pSlow->pNext;
      }
      *plen = len;
    
      // 找环入口
      pFast = pMeetNode;
      pSlow = pHead->pNext;
      while (pFast != pSlow)
      {
          pFast = pFast->pNext;
          pSlow = pSlow->pNext;
      }
      *ppTmpNode = pSlow;
    
      return 1;

    }

三、测试代码(main.c)

以下测试代码覆盖所有进阶操作,可直接编译运行:

复制代码
#include "linklist.h"
#include <stdio.h>

int main(void)
{
    Node_t *plinklist = NULL;
    Node_t *pTmpNode = NULL;
    int len = 0;

    // 1. 创建空链表
    plinklist = CreateEmptyLinkList();

    // 2. 头插法插入节点
    InsertHeadLinkNode(plinklist, 1);
    InsertHeadLinkNode(plinklist, 2);
    InsertHeadLinkNode(plinklist, 3);
    InsertHeadLinkNode(plinklist, 4);
    InsertHeadLinkNode(plinklist, 5);
    printf("初始链表(头插):");
    ShowLinkList(plinklist);

    // 3. 尾插法扩展链表
    InsertTailLinkNode(plinklist, 6);
    InsertTailLinkNode(plinklist, 7);
    InsertTailLinkNode(plinklist, 8);
    InsertTailLinkNode(plinklist, 9);
    InsertTailLinkNode(plinklist, 10);
    printf("尾插后链表:");
    ShowLinkList(plinklist);

    // 4. 找中间节点
    pTmpNode = FindMidNode(plinklist);
    printf("链表中间节点值:%d\n", pTmpNode->Data);

    // 5. 找倒数第3个节点
    pTmpNode = FindLastNthNode(plinklist, 3);
    printf("链表倒数第3个节点值:%d\n", pTmpNode->Data);

    // 6. 反转链表
    ReveiseLinkList(plinklist);
    printf("反转后链表:");
    ShowLinkList(plinklist);

    // 7. 选择排序(注释掉可切换为冒泡排序)
    // BubbleSortLinkList(plinklist);
    SelectSortLinkList(plinklist);
    printf("排序后链表:");
    ShowLinkList(plinklist);

    // 8. 测试链表判环(可选)
#if 1
    // 手动制造环:尾节点指向第5个节点
    pTmpNode = plinklist->pNext;
    while (pTmpNode->pNext != NULL)
    {
        pTmpNode = pTmpNode->pNext;
    }
    pTmpNode->pNext = plinklist->pNext->pNext->pNext->pNext->pNext;
    
    if (IsCircleList(plinklist, &len, &pTmpNode))
    {
        printf("链表有环 | 环长度:%d | 环入口节点值:%d\n", len, pTmpNode->Data);
    }
    else 
    {
        printf("链表无环\n");
    }
#endif

    // 9. 销毁链表
    DestroyLinkList(&plinklist);
    printf("链表销毁后头指针:%p\n", plinklist);

    return 0;
}

四、编译运行结果

复制代码
初始链表(头插):5 4 3 2 1 
尾插后链表:5 4 3 2 1 6 7 8 9 10 
链表中间节点值:1
链表倒数第3个节点值:8
反转后链表:10 9 8 7 6 1 2 3 4 5 
排序后链表:1 2 3 4 5 6 7 8 9 10 
链表有环 | 环长度:6 | 环入口节点值:6
链表销毁后头指针:(nil)
相关推荐
秋深枫叶红2 小时前
嵌入式C语言阶段复习——函数
c语言·数据结构·算法
生命不息战斗不止(王子晗)2 小时前
2026面试大纲 - java数据结构与集合专题
java·数据结构·面试
青桔柠薯片2 小时前
数据结构:排序与算法
数据结构·排序算法
执着2592 小时前
力扣hot100 - 226、翻转二叉树
数据结构·算法·leetcode
-Try hard-2 小时前
排序和查找算法:插入排序、希尔排序、快速排序以及二分查找
数据结构·算法·排序算法
鲨辣椒100862 小时前
算法也能降低时间复杂度???—————算法延伸
数据结构·算法·排序算法
嵌入小生0072 小时前
数据结构 | 常用排序算法大全及二分查找
linux·数据结构·算法·vim·排序算法·嵌入式
小龙报3 小时前
【数据结构与算法】指针美学与链表思维:单链表核心操作全实现与深度精讲
c语言·开发语言·数据结构·c++·物联网·算法·链表
!停4 小时前
数据结构二叉树——堆
java·数据结构·算法