C语言之链表基础

前面的话:这篇总结我不太满意,因为知识要点比较散,文字中有要点,代码中也有要点,而且二者都不可分开来看,否则不容易理解。可能这样的笔记唯一的好处就是,有代码案例能让“要点”的作用所在更直观一些。所以建议后来再看的话,莫着急,例子也要看得!
其中的函数SList_Revert_0(), 函数SList_Destroy(), 函数SList_NodeIndert()是我自己按照理解所写,也经过测试验证。
说一点自己的感想,谈不上经验:对于链表的使用,写代码前要先想好一步步地流程,最直观的是找一张白纸,画上几个节点,节点之间用一个有向箭头连接起来,其实我们修改指针域的时候,就是在修改箭头指向的对象。带给我的便利之处在于:可以清晰地把头结点的情况和中间节点的情况都能在自己大脑中展示出来,而不是凭空想象。不然很容易想漏,最后还是要回头检查流程。可是呢,我们人的大脑没有白纸”记得牢“啊......

链表的基础

链表的定义:链表是一种物理存储单元上非连续的存储结构,由一系列节点(链表中的每一个元素称为节点)组成,节点可以在运行时动态生成,节点与节点之间,通过指针链接。

每个节点包括两部分,一部分是存储数据元素的的数据域,一部分是存储下一个节点地址的指针域。

第一句话通俗一点说就是:链表存储在内存块上,是不连续存储的。从链表的构造上来看链表是什么更直观:

    1. 链表是结构体,因为它既有数据域,也有指针域;
    1. 链表的每个元素有两部分组成;
    1. 链表是引用自身的结构体,因为指针域里面要声明指针指向的数据类型;
    1. 链表的存储特点是:非顺序存储。
#include 
#include 
#include 

typedef struct Teacher
{
    int            data;
    struct Teacher *pNext;    
} Teacher;

Teacher *createTeacher()
{
    Teacher t1, t2, t3;
    
    t1.data = 1;                        
    t2.data = 2;                        
    t3.data = 3;                        

    t1.pNext = &t2;                     
    t2.pNext = &p3;                     
    t3.pNext = NULL;                    
    
    return &t1;
// 这个函数虽然返回了链表的头结点,但是它的数据域和指针域,以及后续的所有节点都被释放掉
// 所以在自定义函数里面没法使用静态链表。
}

int main()
{
    int nRet = 0;
    Teacher t1, t2, t3;
    Teacher *p = NULL;  //用于遍历链表
/*******************************************
    t1.data = 1;                        /***
    t2.data = 2;                        /***
    t3.data = 3;                        /***
                                        /***
    t1.pNext = &t2;                     /***
    t2.pNext = &p3;                     /***
    t3.pNext = NULL;                    /***
/*******************************************
/***像这样一种有固定个数元素的链表就是静态链表,它的应用受到元素个数的局限;
/***另外,静态链表的存储位置在栈区,也是它的一个缺点,见createTeacher()里面的叙述。


/***如何对链表进行遍历***/
    p = &t1; 
    while (p) {
        printf("data: %d\n", p.data);
        p = p.pNext;    //这样就可以实现把下一个元素的地址传给p
    }

    printf("hello world..\n");

    return nRet;
}

链表常用的一些辅助指针变量

  1. 指向头结点的指针叫头指针:pHead;
  2. 指向当前元素(节点)的指针叫当前指针:pCUr;
  3. 前面节点叫前趋节点,指向前趋节点的指针叫前趋指针:pPre;
  4. 后面节点叫后继节点,指向后继节点的指针叫后继指针:pNext;
  5. 指向新建的节点的指针叫pMalloc;

链表编程就两个关键点:

  1. 指针指向谁,就把谁的地址赋给指针;
  2. 辅助指针变量和操作逻辑的关系,能够看图说清楚操作。

程序案例:

    1. 建立一个带有头结点的单向链表;
    1. 顺序访问链表中各个节点的数据域。
      因为是单向链表,所以一切的操作都要注意:先处理尾巴,再理顺前趋节点。
函数Slist_Create_1:创建链表
#include 
#include 
#include 

#include "itcastlog.h"

#define ERR_LEVEL   (4)
#define LOG_LEVEL   (2)
typedef struct Node
{
    int         data;
    struct Node *pNext;
} List;

// 建立带有头结点的单向链表。循环创建节点。
// 节点数据域中的数值从键盘输入,以-1作为输入结束的标志。
// 链表的头结点地址由函数值返回。
int SList_Create_1(List **pHead)    //创建链表
{
    // 1.不断地malloc新节点(pM) :pM = malloc(...)
    // 2.新节点入链表:pCur->pNext = pM
    // 3.让新节点变成当前节点(pCur):pCur = pM   
    // 上述三步持续循环就可以不停地让链表增长起来
    List *pHeadTmp, *pCur, *pM;
    int data = 0;
    int nRet = 0;

    pHeadTmp = (List *)malloc(sizeof(List));
    if (pHeadTmp == NULL) 
    {
        nRet = -1;
        ITCAST_LOG(__FILE__, __LINE__, ERR_LEVEL, nRet, "func SList_Create():: detects that (pHeadTmp == NULL), error code: %d\n", nRet);
        /* printf("func SList_Create():: detects that (pHeadTmp == NULL), error code: %d\n", nRet); */
        goto END;
    }
    pHeadTmp->data = 0;
    pHeadTmp->pNext = NULL;

    printf("\nPlease enter your data: ");
    scanf("%d", &data);

    pCur = pHeadTmp;

    while (data != -1) {
        // 1. 只要用户输入的数据不为结束标志,则创建业务节点并初始化
        pM = (List *)malloc(sizeof(List));
        if (pM == NULL) {
            pHeadTmp = NULL;
            nRet = -2;
            ITCAST_LOG(__FILE__, __LINE__, ERR_LEVEL, nRet, "func SList_Create():: detects that (pM == NULL), error code: %d\n", nRet);
            /* printf("func SList_Create():: detects that (pM == NULL), error code: %d\n", nRet); */
            goto END;
        }
        pM->data = data;
        pM->pNext = NULL;
        
        // 2. 新节点入链表
        pCur->pNext = pM;

        // 3. 新节点变为当前节点
        pCur = pM;  // 完成链表节点的尾部追加
        
        printf("\nPlease enter your data: ");
        scanf("%d", &data);
    }
    *pHead = pHeadTmp;

    goto END;
END:
    if (nRet) {
        SList_Destroy(pHeadTmp);
    }
    return pHead;
}

函数SList_Print:顺序输出单向链表各项节点数据域的内容。
int SList_Print(List *pHead)    //遍历链表
{
    int nRet = 0;
    List *pTmp = NULL;

    if (pHead == NULL) {
        nRet = -1;
        ITCAST_LOG(__FILE__, __LINE__, ERR_LEVEL, nRet, "func SList_Print():: detects that (pHead == NULL), error code: %d\n", nRet);
        /* printf("func SList_Print():: detects that (pHead == NULL), error code: %d\n", nRet); */
        goto END;
    }
    
    pTmp = pHead->pNext;  
    ITCAST_LOG(__FILE__, __LINE__, LOG_LEVEL, nRet, "\nPrint Begin.\n");
    /* printf("\nPrint Begin.\n"); */
    while (pTmp) {
        ITCAST_LOG(__FILE__, __LINE__, LOG_LEVEL, nRet, "%d ", pTmp->data);
        /* printf("%d ", pTmp->data); */
        pTmp = pTmp->pNext;
    }
    ITCAST_LOG(__FILE__, __LINE__, LOG_LEVEL, nRet, "\nPrint End.\n");
    /* printf("\nPrint End.\n"); */


END:
    return nRet;
}
函数SList_NodeIndert:插入新节点到链表
// 在值为x的节点前,插入值为y的节点;
// 若值为x的节点不存在,则插在链表尾部。
int SList_NodeInsert(List *pHead, int x, int y)
{
    // 这个函数是在已有的节点后面添加,而课程的代码是在已有节点的前面添加
    int nRet = 0;
    int nValid = 0; //是否存在x
    List *pTmp = NULL;
    List *pM = NULL;

    if (pHead == NULL) {
        nRet = -1;
        ITCAST_LOG(__FILE__, __LINE__, ERR_LEVEL, nRet, "func SList_NodeInsert():: detects that (pHead == NULL), error code: %d\n", nRet);
        /* printf("func SList_NodeInsert():: detects that (pHead == NULL), error code: %d\n", nRet); */
        goto END;
    }

    pTmp = pHead;
    while (pTmp) {
        if (pTmp->data == x) {
            // 若找到值为x的节点,则开车插入值为y的节点
            pM = (List *)malloc(sizeof(List));
            if (pM == NULL) {
                nRet = -2;
                ITCAST_LOG(__FILE__, __LINE__, ERR_LEVEL, nRet, "func SList_NodeInsert():: detects that (pM == NULL), error code: %d\n", nRet);
                goto END;
            }
            pM->data = y;
            pM->pNext = pTmp->pNext;    //新节点pM的指针域指向pTmp的下一个节点 
            pTmp->pNext = pM;           //老节点pTmp的指针域修改为pM,则链表就被再次连接上了
            nValid = 1;                 //值为x的节点存在
            ITCAST_LOG(__FILE__, __LINE__, LOG_LEVEL, nRet, "func SList_NodeInsert()::找到值为%d的节点, 并且在后面插入%d的新节点\n", x, y);
            break;
        }

        if (pTmp->pNext != NULL) {
            pTmp = pTmp->pNext;     //若不是值为x则继续查找
        }
        else {
            break; 
        }
    }

    if (!nValid) {
        // 链表中没有找到值为x的节点,则插在链表的尾部
        pM = (List *)malloc(sizeof(List));
        if (pM == NULL) {
            nRet = -3;
            ITCAST_LOG(__FILE__, __LINE__, ERR_LEVEL, nRet, "func SList_NodeInsert():: detects that (pM == NULL), error code: %d\n", nRet);
            goto END;
        }
        pM->data = y;       // 新节点赋值
        pM->pNext = NULL;   // 新节点指向链表尾部
        pTmp->pNext = pM;
        ITCAST_LOG(__FILE__, __LINE__, LOG_LEVEL, nRet, "func SList_NodeInsert()::未找到值为%d的节点, 在链表尾部插入%d的新节点\n", x, y);
    }
    goto END;

END:
    return nRet;
}

函数SList_NodeInsert_1:老师写的插入新节点到链表
int SList_NodeInsert_1(List *pHead, int x, int y)
{
    int nRet = 0;
    int nValid = 0;
    List *pM, *pCur, *pPre;

    if (pHead == NULL) {
        nRet = -1;
        ITCAST_LOG(__FILE__, __LINE__, LOG_LEVEL, nRet, "func SList_NodeInsert_1()::detects that (pHead == NULL), error code: %d\n", nRet);
        goto END;     
    }

    // 创建新的业务节点
    pM = (List *)malloc(sizeof(List));
    if (pM == NULL) {
        nRet = -2;
        ITCAST_LOG(__FILE__, __LINE__, LOG_LEVEL, nRet, "func SList_NodeInsert_1()::detects that (pM == NULL), error code: %d\n", nRet);
        goto END;          
    }
    pM->data = y;
    pM->pNext = NULL;

    // 遍历链表,查找节点
    pPre = pHead;   // pHead上是没有值的
    pCur = pPre->pNext;

    while (pCur) {
        if (pCur->data == x) {
            nValid = 1;
            ITCAST_LOG(__FILE__, __LINE__, LOG_LEVEL, nRet, "func SList_NodeInsert()::找到值为%d的节点, 并且在后面插入%d的新节点\n", x, y);
            break;
        }
        pPre = pCur;
        pCur = pCur->pNext;
    }
    
    if (!nValid) {
        ITCAST_LOG(__FILE__, __LINE__, LOG_LEVEL, nRet, "func SList_NodeInsert()::未找到值为%d的节点, 在链表尾部插入%d的新节点\n", x, y);
    }
    /************************************************************************************************
    ****让新节点连接后续节点  /                                                       ****
    ****这段代码可以复用,因为当循环条件不符跳出时,pPre刚好指向链表最后一个节点,pCur指向NULL   ****
    ****不管是中间添加pM还是链表尾部添加pM都可以使用这段代码                                ****
    ********************************************************************************************/
    pM->pNext = pPre->pNext;
    pPre->pNext = pM;
    
    goto END;

END:
    return nRet;    
}
函数SList_NodeDel:删除值为x的节点
int SList_NodeDel(List *pHead, int y)
{
    int nRet = 0;
    List *pCur = NULL;
    List *pPre = NULL;

    if (pHead == NULL) {
        nRet = -1;
        ITCAST_LOG(__FILE__, __LINE__, ERR_LEVEL, nRet, "func SList_NodeDel():: detects that (pHead == NULL), error code: %d\n", nRet);
        /* printf("func SList_NodeDel():: detects that (pHead == NULL), error code: %d\n", nRet); */
        goto END;
    }

    pPre = pHead;
    pCur = pPre->pNext;
    while (pCur) {
        if (pCur->data == y) {
            break;
        }
        pPre = pCur;
        pCur = pCur->pNext;
    }

    // 准备删除操作
    if (pCur != NULL) {
        pPre->pNext = pCur->pNext;
        free(pCur);
        goto END;
    }
    else {
        nRet = -2;
        ITCAST_LOG(__FILE__, __LINE__, ERR_LEVEL, nRet, "func SList_NodeDel():: 没有找到值为%d的节点, error code: %d\n", y, nRet);
        goto END;
    }

END:
    return nRet;
}
函数SList_Destroy:删除值为x的节点
int SList_Destroy(List **pHead)
{
    int nRet = 0;
    List *pTmp = NULL;
    List *pHeadTmp = NULL;

    if (pHead == NULL) {
        nRet = -1;
        ITCAST_LOG(__FILE__, __LINE__, ERR_LEVEL, nRet, "func SList_Destroy():: detects that (pHead == NULL), error code: %d/n", nRet);
        /* printf("func SList_Destroy():: detects that (pHead == NULL), error code: %d/n", nRet); */
        goto END;
    }
    
    pHeadTmp = *pHead;
    while (pHeadTmp != NULL) {
        pTmp = pHeadTmp->pNext;
        free(pHeadTmp);
        pHeadTmp = pTmp;
    }
    *pHead = NULL; 

    goto END; 

END:
    return nRet;
} 

函数SList_Revert:链表的逆置(这是我写的逆置函数,从左往右)
int SList_Revert_0(List **pHead)
{
    int nRet = 0;
    List *pPre = NULL;
    List *pCur = NULL;
    // pCurOld适用于缓存当前节点的后继节点的位置
    List *pCurOld = NULL;

    /* if (pHead == NULL) { */
    if (pHead == NULL || (*pHead)->pNext == NULL || (*pHead)->pNext->pNext == NULL) {
        // 当无效的时候不用逆置;当只有头结点的时候不用逆置;当只有一个元素的时候不用逆置
        nRet = -1;
        ITCAST_LOG(__FILE__, __LINE__, ERR_LEVEL, nRet, "func SList_Revert():: detects that (pHead == NULL), error code: %d/n", nRet);
        goto END;
    }
   
    // 初始化并定义指向关系 
    pPre = *pHead;
    pCur = pPre->pNext;
    
    // 保留原始的链表连接关系
    pCurOld = pCur->pNext;

    // 修改指针域
    pCur->pNext = NULL;
    
    // 恢复连接关系
    pPre = pCur;
    pCur = pCurOld;
    while (pCur) {
        // 保留原始的链表连接关系
        pCurOld = pCur->pNext;
        // 修改指针域
        pCur->pNext = pPre;
        // 恢复连接关系 
        pPre = pCur;
        pCur = pCurOld;
    }
    (*pHead)->pNext = pPre;
    goto END;

END:
    return nRet;    
}
函数SList_Revert_1:链表的逆置
int SList_Revert_1(List *pHead)
{
    int nRet = 0;
    List *p = NULL; // 前驱指针
    List *q = NULL; // 当前指针
    // pCurOld适用于缓存当前节点的后继节点的位置
    List *t = NULL; // 缓存的一个节点

    if (pHead == NULL || pHead->pNext == NULL || pHead->pNext->pNext == NULL) {
        // 当无效的时候不用逆置;当只有头结点的时候不用逆置;当只有一个元素的时候不用逆置
        nRet = -1;
        ITCAST_LOG(__FILE__, __LINE__, ERR_LEVEL, nRet, "func SList_Revert():: detects that (pHead == NULL), error code: %d/n", nRet);
        goto END;
    }

    // 初始化
    p = pHead->pNext;
    q = pHead->pNext->pNext;

    // 一个节点一个节点的逆置
    while (q) {
        t = q->pNext;   // 缓冲后面的链表
        q->pNext = p;   // 逆置
        p = q;          // 让p下移一个节点
        q = t;
    }

    // 头结点变成尾部节点后,置为NULL
    pHead->pNext->pNext = NULL;
    pHead->pNext = p;

END:    
    return nRet;
} 
测试框架
int main()
{
    int nRet = 0;
    List *pList = NULL;
    /* printf("111111111111111111111\n"); */
    /* pList = SList_Create(); */

    nRet = SList_Create(&pList);


    nRet = SList_Print(pList); 

    /* nRet = SList_NodeInsert(pList, 20, 19); */
    nRet = SList_NodeInsert_1(pList, 20, 19);
    nRet = SList_Print(pList); 

    nRet = SList_NodeDel(pList, 19);
    nRet = SList_Print(pList);

    /* nRet = SList_Revert_0(&pList); */
    nRet = SList_Revert_1(pList);
    nRet = SList_Print(pList);

    nRet = SList_Destroy(&pList);
    nRet = SList_Print(pList);

    printf("hello world...\n");
    return nRet;
}

链表是一种组织世界的思想

链表的后面的课程里,老师把他写的创建链表的代码优化了一下:主要是使用了二级指针作为函数的输入参数,这个其实我已经意识到了,在我后面自己写的链表资源释放和逆置的函数中,我就发现这两个操作肯定要改变链表的头的指向,所以就果断使用二级指针来传入参数了。这样其实是更贴近实际工程应用的,因为功能模块就尽可能完备地实现功能就好,尽可能地和外界解耦。这个就是工程经验之谈,还是要在实干上落实,暂且不表。

链表的思想:链表就是想把对象串联起来,这是一种建立关系的机制,或者说,链表就代表着一种制度!没有链表,代码中的处理目标都是琐碎的,可是有了链表,就有了可以把代码世界链接起来的工具!这绝对是一个发明!那么链表就应该把世间万物都连接起来,可是在代码中我们也发现,世间万物的特征纷繁复杂,而且每个人对事物的理解不一样,抽象出来的特征也不一样。所以一个链表是无法把同类型的实物表达出来的,更别提世间万物了。这样反而倒逼着“创造着门”产生的一个伟大的想法:既然我无法包容万物,那我成为空的,让世间万物都能接纳我包括我!我身在万物,亦能连接万物!

所以在Linux内核的源码中,我们可以看到作者创造一个“空链表”,它只有指针域,即后继节点的地址。然后让其他的结构体嵌套它,再通过“地址相对的偏移量”就可以起到连接的作用。通过链表获得下一个对象的地址,再通过“地址偏移量”即可做到“管中窥视到一切”的效果。

更精彩的在后面,因为结构体的名称是其内存块的首地址,也是结构体中第一个数据类型的地址。那么,如果我们把链表嵌套进一个结构体,并且把链表就放在结构体的第一个位置,这时就出现了一个很大的便利!我们获得的结构体的第一个对象就是后继节点的地址。这时链表似乎有点“反客为主”的意思,走出了一条漂亮的“从农村包围城市”路线。

你可能感兴趣的:(C语言之链表基础)