C语言高级篇(数据结构) --- 链表

C语言高级篇(数据结构) — 链表

==========================================================================

链表的引入

1. 数组的缺陷与解决方案

 数组的2个缺陷

 (1) 数组中所有元素的类型必须一致;
 (2) 数组元素个数定义后不能再做修改;

  数组缺陷的解决方案

 (1) 定义结构体时可以定义不同类型的元素;
 (2) 链表就是一个元素个数可以实时改变的数组;

2. 数组拓展的方案

 “搬迁”

  先在另外一个空白的内存创建一个更大空间的数组,然后将原来数组的元素复制到新数组的头部,最后释放原来数组的内存空间,并且把新的数组代替原数组。这种可变数组在C语言上是不支持的,需要在高级语言c++,Java上支持。

  “外部拓展”

  思路:在原来内存空间不动的基础上,到另外的内存空间进行外部拓展。链表就是采取这种方式。

3. 什么是链表?

   链表就是用锁链连接起来的表。这里的表指的是一个一个的节点,节点中有一些内存可以用来存储数据;这里的锁链指的是链接各个表的方法,C语言中用来连接2个表(其实就是2块内存)的方法就是指针。
   链表是由若干个节点组成的(链表的各个节点结构是完全类似的),节点是由有效数据和指针组成的。有效数据区域用来存储信息完成任务的,指针区域用于指向链表的下一个节点从而构成链表。

4. 链表的作用

   链表就是用来存储数据的。链表用来存数据相对于数组来说优点就是灵活性,需要多少个动态分配多少个,不占用额外的内存。



单链表的实现

1.单链表的节点构成

  (1) 链表是由多个节点构成的,每个节点包括:有效数据以及指针。
  (2) 节点的定义由结构体构成,struct node只是一个结构体,本身没有变量生成,也没有占用内存空间。实际创建链表时需要用这个结构体模板进行创建。

    struct node
    {
        int data;
        struct node *pNext;
    };

2. 链表的构建

  链表的内存空间

   链表内存的使用较为灵活,不能用栈(局部变量),也不能使用.data数据段(程序一开始就分配空间,程序结束时释放),只能用堆内存(申请使用开始,free释放结束或者程序跑完就自动释放)。

  使用对内存创建一个节点的步骤

  (1)申请堆内存,大小为一个节点的大小(检查申请结果是否正确);
  (2)清理申请到的堆内存;
  (3)把申请到的堆内存当作一个新节点;
  (4)填充你的新节点的有效数据和指针区域。

    struct node * create_node(int data)
    {
        /* 申请堆内存空间 */
        struct node *p = (struct node *)malloc(sizeof(struct node));

        /* 检查申请内存是否成功 */
        if(NULL == P)
        {
            printf("malloc error.\n");
            return NULL;
        }

        /* 清理对内存空间 */
        bzero(p.sizeof(struct node));

        /* 填充节点 */
        p -> pNext = NULL;
        P -> data = data;

        return 0;
    }
  链表的头指针

   头指针并不是节点,而是一个普通指针,只占4字节。头指针的类型是struct node *类型的,所以它才能指向链表的第一个节点。
   头指针的作用在于利用本身的连接第一个节点,通过指针向下访问整个链表的所有元素。
   一个典型的链表的实现就是:头指针指向链表的第1个节点,然后第1个节点中的指针指向下一个节点,然后依次类推一直到最后一个节点。这样就构成了一个链。

    // 定义头指针
    struct node *pHeader = NULL;

    /** 创建第一个节点 **/ 
    pHeader = create_node(1);   
    /** 创建第2个节点 **/ 
    pHeader ->pNext = create_node(2);   
    /** 创建第3个节点 **/ 
    pHeader ->pNext ->pNext = create_n


附录

/linux_share/ singlelinkedlist.c

    #include <stdio.h>
    #include <strings.h>
    #include <stdlib.h>

    /* 构建链表的节点 */
    struct node 
    {
        int data;
        struct node *pNext;
    };

    /* 
     * 函数功能:创建一个节点
     * 输入参数:data:有效数据
     * 输出参数:新节点指针
    */
    struct node * create_node(int data)
    {
        /* 申请堆内存空间 */
        struct node *p = (struct node *)malloc(sizeof(struct node));

        /* 检查申请内存是否成功 */
        if(NULL == P)
        {
            printf("malloc error.\n");
            return NULL;
        }

        /* 清理对内存空间 */
        bzero(p.sizeof(struct node));

        /* 填充节点 */
        p -> pNext = NULL;
        P -> data = data;

        return p  ;
    }

    int main(void)
    {
        // 定义头指针
        struct node *pHeader = NULL;

        /** 创建第一个节点 **/ 
        pHeader = create_node(1);   
        /** 创建第2个节点 **/     
        pHeader ->pNext = create_node(2);       
        /** 创建第3个节点 **/     
        pHeader ->pNext ->pNext = create_node(3); 

        printf("node1 = %d.\n",pHeader -> data);    
        printf("node2 = %d.\n",pHeader -> pNext-> data);    
        printf("node3 = %d.\n",pHeader -> pNext-> pNext ->data); 
        printf("node4 = %d.\n",pHeader -> pNext-> pNext-> pNext ->data); 

        return 0;
    }



单链表算法之插入节点

1.从链表的尾部插入节点函数

  尾部插入节点设计思路

  函数传入链表的头指针以及新节点。已知链表的最后一个节点指针指向NULL,通过头指针寻找可以找到最后一个节点。找到最后一个节点后将最后节点的指针pNext指向新节点new,新节点的指针pNext指向NULL,完成填充新节点。

    void insert_tail(struct node *pHeader,struct node *new)
    {
        /* 指针向后一去不复返,定义此指针 */
        struct node *p = pHeader;

        if (NULL != p-> pNext)
        {
            p = p -> pNext;
        }

        p -> pNext = new;  
    }

2. 从链表头部插入新节点

  头结点问题由来:

   没有头结点的情况下,假设现在的链表是没有节点的,此时的头指针指向了NULL,而我们直接操作pNext的时候回引发段错误。

  头结点的定义与特点:

   头结点就是头指针指向的第一个节点。
   头结点的数据可以使控也可以是代表这个链表的节点个数。头结点的创建与头指针是一起的,并将头指针与其关联。头指针的实质就是头节点。

    struct node *pHeader = create_node(0);
  从链表的头节点插入新节点

   思路:头节点的插入,程序的顺序是关键。第一步需要先把新节点的指针指向与头节点的指针指向的原第一个的节点。第二步把头节点的指针指向新节点。

    void insert_top(struct node *pH,struct node *new)
    {
        /* 第1步:新节点的next指向原来的第一个节点 */
        new -> pNext = pH -> pNext;

        /* 第2步: 头节点指向新节点 */
        pH -> pNext -> new;

        /* 第3步: 节点个数计数值加一 */
        pH -> data += 1;
    }
  链表的头插与尾插的联系与区别:

   链表可以从头部插入,也可以从尾部插入。也可以两头插入。头部插入和尾部插入对链表来说几乎没有差别。对链表本身无差别,但是有时候对业务逻辑有差别。

  



单链表的算法之遍历节点

什么是链表的遍历?

   遍历就是把单链表中的各个节点挨个拿出来。遍历的要点:一是不能遗漏、二是不能重复、追求效率。

链表的遍历的实现?

  1. 分析单链表的特点:

    单链表的特点就是由很多个节点组成,头指针+头节点为整个链表的起始,最后一个节点的特征是它内部的pNext指针值为NULL。从起始到结尾中间由各个节点内部的pNext指针来挂接。由起始到结尾的路径有且只有一条。单链表的这些特点就决定了它的遍历算法。

  2. 遍历的方法:

    从头指针+头节点开始,顺着链表挂接指针依次访问链表的各个节点,取出这个节点的数据,然后再往下一个节点,直到最后一个节点,结束返回。

  3. 编程实战
    void bianli(struct node *pH)
    {
        static int cnt = 1;
        struct node *p = pH;  

        /* 没有头节点的情况处理 */

        /* 考虑了有头节点的情况 */
        if(NULL != p -> pNext)
        {
            p = p -> pNext;
            printf("node data %d = %d",p -> data);
            cnt++;
        }
    }



单链表算法之删除节点

删除节点的步骤


  • 第一步:找到要删除的节点;

  • 第二步:删除这个节点

如何找到要删除的节点?

   通过遍历算法查找节点。
   假设链表带有头节点,通过头节点的指针指向非控制,表示链表的节点不止一个的情况下,进入遍历环节。先通过头节点p1进入下一个节点p2,通过判断节点p2的数据是否属于要删除的节点,假设不属于的情况,保存本节点p2后进入下一个节点p3。以此循环知道找到要删除的节点。

如何删除一个节点?

   节点的删除有两种情况:删除的节点是 普通节点 或者 尾节点。
   1. 删除的节点是尾节点,将尾节点的前一个节点的指针指向NULL,释放尾节点的空间;
   2. 删除的节点是普通节点,将普通节点p的前一个节点pPrev指向普通节点的下一个节点p->pNext,同时释放普通节点的空间。

删除节点实战:

   函数输入参数包括头节点pHeard(头指针)、要删除节点的数据。函数中成功删除节点后,函数返回 0,没能找到节点,函数返回 -1。

    int delete_node(struct node *pH, int data)
    {
        struct node *pPrev = NULL;
        struct node *p = pH;

        while(NULL != p->pNext)
        {
            pPrev = p;
            p = p->pNext;
            if(p->data == data)
            {
                if(p->pNext == NULL)
                {
                    pPrev->pNext = NULL;
                    free(p);
                }
                else
                {
                    pPrev->pNext = p->pNext;
                    free(p);
                }
                return 0;
            }
        }
        return -1;
    }



单链表算法之单链表逆序

单链表逆序的定义

   链表的逆序又叫反向,意思就是把链表中所有的有效节点在链表中的顺序给反过来。

如何实现单链表的逆序

   首先遍历原链表,然后将原链表中的头指针和头节点作为新链表的头指针和头节点,原链表中的有效节点挨个依次取出来,采用头插入的方法插入新链表中即可。
   链表逆序 = 遍历 + 头插入

单链表逆序实战

1. 有头节点的情况

   函数的输入参数为链表的头指针。

    void reverse_linkedlist(struct node *pH)
    {
        struct node *p = pH->pNext;
        struct node *pBack = NULL;

        /* 判断输入的链表是否是有效链表或者节点的个数是否为1 */
        if(NULL == p || NULL == p->pNext)
        {
            return -1;
        }

        while(NULL != p->pNext)
        {
            /* 保存当前节点的下一个节点 */
            pBack = p->pNext;

            /* 链表的第一个有效节点指向NULL,非第一个有效节点执行头插入操作 */
            if(p == pH->pNext)
            {
                p->pNext = NULL;
            }
            else
            {
                 p->pNext = pH->pNext;
            }

            pH->pNext = p->pNext;
            p = pBack;      
        }
        /* 循环结束后,最后一个节点仍然缺失 */
        insert_head(pH,p);
        return 0;
    }
2. 无头节点的情况
  • 迭代循环法
              
    迭代循环法初始状态
      初始状态下,想定义一个新链表的头指针prev,头指针指向头节点A,再定义一个指向下一个节点B的指针next。从首节点A开始逆序,将头节点A的指针head->next指向新链表的头指针prev,即头节点A的指针指向NULL,这时头节点A从链表中分离出来。新链表的指针prev前移指向新节点A,即又成为新链表的头指针。
              
    迭代循环法状态2
              
    迭代循环法状态3
      经过第一次的迭代后,原链表中的头指针指向下一个节点B,作为下一个节点的储存指针next指向下下个节点C。透过节点B的指针指向新链表的头节点(也就是与新链表头指针指向相同),此时将节点B分离出来。新链表的指针前移指向新节点B(head),完成第二步逆序,链表后面的逆序一次迭代。
              
    迭代循环法状态4
      迭代循环的终止条件是完成最后一个节点的逆序,此时原链表中的头节点为NULL。

代码实现

    struct node *reverse_linkedlist_diedai(struct node *pH)
    {
        struct node *next = NULL;
        struct node *prev = NULL;

        while(pH != NULL)
        {
            /* 第1步:储存下一个节点 */
            next = pH->pNext;   
            /* 第2步:当前节点的指针指向新链表的头节点 */
            pH->pNext = prev;
            /* 第3步:头节点移动到新的头节点出 */
            prev = pH;
            /* 第4步:取出下一个节点 */
            pH = next;
        }
        return prev;
    }
  • 递归法
      递归的目的是遍历到链表的尾节点,然后通过逐级回朔将节点的next指针翻转过来。递归终止条件就是链表只剩一个节点时直接返回这个节点的指针。
              
    递归法1
              
    递归法2
              
    递归法3

代码实现

    struct node *reverse_linkedlist_digui(struct node *pH)
    {
        struct node *newHead = NULL;

        if((pH == NULL) || (pH->pNext) == NULL)
        {
            return pH;
        }

        newHead = reverse_linkedlist2(pH->pNext);
        pH -> pNext ->pNext = pH;
        pH -> pNext = NULL;

        return newHead;
    }



双链表的实现

1.双链表的节点构造

  
双向链表的节点 = 有效数据 + 2个指针(一个指向后一个节点,另一个指向前一个节点)

    struct node 
    {
        int data;
        struct node *pNext;
        struct node *pPrev;
    };

2.双链表的封装和编程实现

    /* 创建双联表的节点 */
    struct node *create_double_node(int data)
    {
        /* 1.申请栈空间 */
        struct node *p = (struct node *)malloc(sizeof(struct node));
        /* 2.判断申请的堆空间是否成功 */
        if(NULL == p)
        {
            printf("malloc errror! \r\n");
            return NULL;
        }
        /* 3.清理堆空间 */
        bzero(p, sizeof(struct node));
        /* 4.填充节点 */
        p -> data = data;
        p -> pNext = NULL;
        p -> pPrev = NULL;

        return p;   
    }

3.双链表的算法之插入节点

  尾插
    /* 尾部插入节点 */
    void insert_tail_node(struct node *pH, struct node *new)
    {
        struct node *p = pH;
        /* 找到最后一个节点*/
        if(NULL != p -> pNext)
        {
            p = p -> pNext;
        }
        /* 将新节点插入到最后一个节点尾部 */
        new -> pPrev = p;
        p -> pNext = new;   
    }
  头插
    /* 头部插入节点 */
    void insert_head_node(struct node *pH, struct node *new)
    {

        /* 1.新节点指针直线下一个节点 */
        new -> pNext = pH -> pNext;
        /* 2.下一个节点的pPrev指向新节点 */
        if(NULL != pH -> pNext)
        { 
        /* 必须考虑链表中有没有节点 */
            pH -> pNext -> pPrev = new;
        }
        /* 3.头节点指向新的节点 */
        pH -> pNext = new;
        /* 4:新节点的pPrev指向头节点 */
        new->pPrev = pH;
    } 

4.双链表算法之遍历

  正向遍历
    /* 正向遍历 */
    void bianli(struct node *pH)
    {
        struct node *p = pH;
        if(NULL != p -> pNext)
        {
            p = p -> pNext;
            printf("node is %d",p -> data);
        }
    }
  反向遍历(前向遍历)
    /* 前向遍历 */
    void bianli_front(struct node *pT)
    {
        struct node *p = pT;
        if(NULL != p ->pPrev)
        {
            printf("node is %d",p -> data);
            p = p ->pPrev;
        }
    }

5.双链表算法之删除节点

    /* 删除节点 */
    void delete_node(struct node *pH, int data)
    {
        struct node *p = pH;
        while(NULL != p -> pNext)
        {
            p = p -> pNext;
            if(p -> data == data)
            {
                if(p -> pNext = NULL)
                {
                    p -> pPrev -> pNext = NULL;
                }
                else
                {
                    p - pPrev -> pNext = p -> pNext; 
                    p -> pNext -> pPrev = p -> pPrev;
                }
                free(p);
                break;
            }
        }
        printf("no node\n");
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值