zoukankan      html  css  js  c++  java
  • 指针应用-----链表一

    在于C语言指针的相关知识点算是已经学得差不多了,当然,语言的学习是一个终生的,所以还需慢慢去学习,今天就以一个非常经典,也是体现指针应用的一个例子,来操作练一下所学的指针相关的知识点-----链表

    对于链表,我想学过编程的应该都对它有比较清楚的了解,下面简单对它进行回顾一下:

    链表的基本操作:

    下面自己动手利用指针的知识一点一点来实现链表,同时学习一下C语言多文件的编译风格:

    第一步:搭建好基础开发框架:

    首先需要定义一个结构体,来代表一个结点,结点里面的数据域指针域两个构成,将其定义放到头文件(.h)中【至于放到.h头中的好处,请参考http://www.cnblogs.com/webor2006/p/3460345.html博文】如下:

    list.h:

    #ifndef _LIST_H_
    #define _LIST_H_
    
    typedef struct node
    {
        int data;
        struct node* next;
    } node_t;
    
    #endif /* _LIST_H_ */

    然后定义一个它的实现list:c,这里只去包含list.h文件,目前啥都不做,之后会慢慢去填充的:

    list:c:

    最后,再定义一个主入口文件,它会去包含list.h,也就是把具体实现放在list.c中,main.c只关心主干流程,目前也啥都不做:

    main.c:

    对于这个程序,由两个.c文件和一个.h文件组成,为了更方便去编译程序,这时需要用到Makefile【关于它的编写,会有专门篇幅来学习它,目前先简单理解下】如下:

    Makefile:

    好了,框架已经搭建完毕,下面进行编译,看能否正常生成可执行文件main

    第二步:实现链表的插入方法:

    首先定义一个头节点:

    main.c:

    然后定义一个插入方法:

    list.h:

    #ifndef _LIST_H_
    #define _LIST_H_
    
    typedef struct node
    {
        int data;
        struct node* next;
    } node_t;
    
    node_t* list_insert_front(node_t* head, int data);
    
    #endif /* _LIST_H_ */

    提示:对于这个函数,其实还有另外一种实现方法,可以不返回指针,直接用指针的指针去改为head的指针地址,这个之后会有实现。

    插入方法具体实现【关于链表的插入的基本概念这里就不多说了,就是将新的元素链接到前一个元素的next上】:

    list.c:

    #include "list.h"
    #include <stdlib.h>
    
    node_t* list_insert_front(node_t* head, int data)
    {
        node_t* n = (node_t*)malloc(sizeof(node_t));
        assert(n != NULL);
        n->data = data;
        n->next = NULL;
    
        if (head == NULL)
            head = n;
        else
        {
            n->next = head;
            head = n;
        }
    
        return head;
    }

    注意:这里是采用的头插法。

    然后这时多插入几个节点:

    main.c:

    #include "list.h"
    #include <stdio.h>
    
    int main(void){
        
        node_t* head = NULL;
        head = list_insert_front(head, 30);
        head = list_insert_front(head, 20);
        head = list_insert_front(head, 10);
    
        return 0;
    }

    对于上面的插入流程,用一个图例来解释一下这个插入方法的实现原理:

    第一次插入:head = list_insert_front(head, 30);

    第二次插入:head = list_insert_front(head, 20);

    第三次插入:head = list_insert_front(head, 10);

    接下来,为了验证结点是否插入正常,再实现第三步的方法。

    第三步:实现链表的遍历方法:

    首先定义遍历的方法,这里为了更好的实现,采用函数指针来实现,如下:

    list.h:

    #ifndef _LIST_H_
    #define _LIST_H_
    
    typedef struct node
    {
        int data;
        struct node* next;
    } node_t;
    
    typedef void (*FUNC)(node_t*);//函数指针,它专门是打印结点的
    
    node_t* list_insert_front(node_t* head, int data);
    
    void list_for_each(node_t* head, FUNC f);//最终这里面遍历到结点之后,回调打印函数,而不用将打印实现也放到这个遍历函数中,代码上更加整洁
    
    #endif /* _LIST_H_ */

    接着,我们来实现这个遍历的方法

    list.c:

    #include "list.h"
    #include <stdlib.h>
    
    node_t* list_insert_front(node_t* head, int data)
    {
        node_t* n = (node_t*)malloc(sizeof(node_t));
        assert(n != NULL);
        n->data = data;
        n->next = NULL;
    
        if (head == NULL)
            head = n;
        else
        {
            n->next = head;
            head = n;
        }
    
        return head;
    }
    //遍历链表
    void list_for_each(node_t* head, FUNC f)
    {
        while (head)
        {
            f(head);
            head = head->next;
        }
    }

    遍历方法中可能我们会这样来写:

    void list_for_each(node_t* head, FUNC f)
    {
      node_t* tempPoint = head;//定义一个临时变量去遍历,我们知道指针作为参数传递实际上是值传递,所以不用担心直接赋值会修改实参指针的指向,完全不需要这个临时变量
    while (tempPoint) { f(head); tempPoint = tempPoint->next; } }

    这种写法虽然也是可以的,但是有点多此一举,从另外一面来讲,是指针理解得不够透,所以避勉这样的写法!

    注意:我们将具体的打印函数放到main.c中,而不用写在list.c中,因为,这个函数最终是在main调用传递过去的。

    main.c:

    #include "list.h"
    #include <stdio.h>
    
    void print_node(node_t* n)
    {
        printf("data=%d ", n->data);
    }
    
    int main(void){
        
        node_t* head = NULL;
        head = list_insert_front(head, 30);
        head = list_insert_front(head, 20);
        head = list_insert_front(head, 10);
    
        list_for_each(head, print_node);//开始遍历
        putchar('
    ');
    
        return 0;
    }

    好了,遍历方法也已经写好了,接着编译运行来验证一下我们插入的结点是否生效了:

    于是在list.h中加入头文件:

    list.h:

    再次make:

    第四步:实现链表的销毁方法:

    接着,我们来实现链表的销毁方法,由于每个链表都是在堆上申请的,所以最后用完了肯定是需要销毁的,还是老规距,在头文件中定义接口:

    list.h:

    #ifndef _LIST_H_
    #define _LIST_H_
    
    typedef struct node
    {
        int data;
        struct node* next;
    } node_t;
    
    typedef void (*FUNC)(node_t*);
    
    node_t* list_insert_front(node_t* head, int data);
    
    void list_for_each(node_t* head, FUNC f);
    
    void list_free(node_t* head);//销毁链表
    
    #endif /* _LIST_H_ */

    具体实现,当然还是在list.c文件中:

    #include "list.h"
    #include <stdlib.h>
    #include <assert.h>
    
    node_t* list_insert_front(node_t* head, int data)
    {
        node_t* n = (node_t*)malloc(sizeof(node_t));
        assert(n != NULL);
        n->data = data;
        n->next = NULL;
    
        if (head == NULL)
            head = n;
        else
        {
            n->next = head;
            head = n;
        }
    
        return head;
    }
    
    void list_for_each(node_t* head, FUNC f)
    {
        while (head)
        {
            f(head);
            head = head->next;
        }
    }
    
    void list_free(node_t* head)
    {
        node_t* tmp = head;
        while (head)
        {
            head = head->next;
            free(tmp);
            tmp = head;
        }
    }

    释放方法也是需要遍历,但这次需要借助临时变量,其实现过程用简单的图来描述如下:

    这时,main.c调用之:

    #include "list.h"
    #include <stdio.h>
    
    void print_node(node_t* n)
    {
        printf("data=%d ", n->data);
    }
    
    int main(void){
        
        node_t* head = NULL;
        head = list_insert_front(head, 30);
        head = list_insert_front(head, 20);
        head = list_insert_front(head, 10);
    
        list_for_each(head, print_node);
        putchar('
    ');
    
        list_free(head);
        assert(head == NULL);//这里断言一下,看是否真正释放了
    
        return 0;
    }

    编译:

    如图上所示,在main.c中用到了assert,需要包含assert.h,我们知道main.c中包含了list.h文件,而list.c中已经包含了assert.h:

    这时,我们不应该在main.c中又再次包含assert.h,而应该将这个头文件由list.c中的包含放到list.h中,这样main.c又包含了list.h,所以就可以共用了:

    list.h:

    #ifndef _LIST_H_
    #define _LIST_H_
    
    #include <assert.h>//将list.c中的移到头文件中来,以便在main.c中可以共用
    
    typedef struct node
    {
        int data;
        struct node* next;
    } node_t;
    
    typedef void (*FUNC)(node_t*);
    
    node_t* list_insert_front(node_t* head, int data);
    
    void list_for_each(node_t* head, FUNC f);
    
    void list_free(node_t* head);
    
    #endif /* _LIST_H_ */

    这时,再次make:

    其实原因还是出在:指针作为参数传递是值传递

    看main.c,将head传递到list_free之后,由于list_free不会改变head的指向(当然如果是二级指针,那就没这个问题了),因为它是一级指针,所以,应该list_free最后需将head传回给main.c,然后再赋值给main.c中的head既可:

    list.h:

    #ifndef _LIST_H_
    #define _LIST_H_
    
    #include <assert.h>
    
    typedef struct node
    {
        int data;
        struct node* next;
    } node_t;
    
    typedef void (*FUNC)(node_t*);
    
    node_t* list_insert_front(node_t* head, int data);
    
    void list_for_each(node_t* head, FUNC f);
    
    node_t* list_free(node_t* head);//添加一个返回值
    
    #endif /* _LIST_H_ */

    list.c:

    node_t* list_free(node_t* head)
    {
        node_t* tmp = head;
        while (head)
        {
            head = head->next;
            free(tmp);
            tmp = head;
        }
        return head;//最终遍历完之后head会指向NULL
    }

    main.c:

    #include "list.h"
    #include <stdio.h>
    
    void print_node(node_t* n)
    {
        printf("data=%d ", n->data);
    }
    
    int main(void)
    {
        node_t* head = NULL;
        head = list_insert_front(head, 30);
        head = list_insert_front(head, 20);
        head = list_insert_front(head, 10);
    
        list_for_each(head, print_node);
        putchar('
    ');
    
        head = list_free(head);//由于一级指针的原因,需将head重新赋值才能改变它的指向,之后可用二级指针解决
        assert(head == NULL);
    
    
        return 0;
    }

    再次编译,运行:

    实际上,对于销毁方法的实现,还可以更精简,如下:

    node_t* list_free(node_t* head)
    {
        node_t* tmp;//这里不需要初始化
        while (head)
        {
            tmp = head;//里面的赋值也只要一句话既可
            head = head->next;
            free(tmp);
        }
        return head;
    }

    对于一个功能的实现,能用最精简的方法实现是最好的,能不多一行就不多一行代码,这也是我写代码一直追求的,好了,关于链表其它的操作,下回再分解,再见!

  • 相关阅读:
    关于pandas里面的合并
    关于动态规划的一丢丢研究
    结巴分词详细讲解
    k折交叉验证
    Boosting和Bagging的异同
    批量归一化的原理
    深度学习模型参数初始化的方法
    NLP 装桶(Bucketing)和填充(padding)
    facebook 摘要生成阅读笔记(二) Abstractive Sentence Summarization with Attentive Recurrent Neural Networks
    facebook 摘要生成阅读笔记(一) A Neural Attention Model for Sentence Summarization
  • 原文地址:https://www.cnblogs.com/webor2006/p/3481080.html
Copyright © 2011-2022 走看看