c语言实现一个链表

1、基础研究

咱们在这里要理解和实现一种最基本的数据结构:链表。首先看看实现的程序代码:java

List .h编程

 

 

 

 

 

 

 

 

 

 

 

 

事实上咱们观察list.h发现前面一部分是数据结构的定义和函数的声明,后面一部分是函数的实现。咱们仅仅观察前面一部分就能够知道这个链表的结构是怎么实现的了。数组

程序将处理的对象分红了三类:线性表、结点和元素,分别定义了它们的数据类型和操做函数,对线性表有建立、撤销、清空操做,对元素有追加、加入、删除、取操做,对结点有取、遍历、建立操做,每个操做都用一个子函数来实现。它们所有被封装进了头文件list.h,这是对共性的封装。安全

咱们用m.clist.h进行测试:数据结构

 

执行结果以下:函数

 

m.c首先建立了一个字符数组来装载要存入线性表中的元素,再定义了显示线性表的函数showlist和显示单个元素的函数putelement。在主函数中首先调用CreateList函数建立一个线性表,若是建立失败会提示错误并返回,若是成功则调用ListAppend函数将字符数组里的内容放进线性表中,再调用showlist函数显示字符串。以后咱们调用ListInsert函数向链表中插入一个元素结点并显示,再调用ListDelete函数删除以前插入的元素,并显示字符串。其中CreateList函数、ListInsert函数、ListDelete函数都是在list.h中的函数,是有关链表自己的操做,是共性,而showlist函数和putelement函数是在c文件中实现的,它们的功能是个性,是需求。showlist函数是调用TraverseList函数遍历链表,并对每一个元素用putelement函数进行处理,而putelement函数是将该元素打印出来。为何在TraverseList函数里要将遍历链表和处理函数分开呢?这里也是将共性和个性分离开,不少时候咱们都须要遍历链表,可是不必定每一次都要用同一个函数来处理。那么就把个性也用函数封装起来。测试

list.h的第一个语句typedef char EleType改成typedef int EleType,再用m1.c测试:spa

 

运行结果为:设计

 

这里把链表元素由字符型改为整形,只须要再在m.c里进行极小的改动,就能够实现相关功能。指针

再将list.h的第一个语句typedef char EleType改成typedef struct{char a;int b;} EleType,再用m2.c测试:

 

执行结果为:

 

这里要处理的链表元素为结构体,因此咱们要定义一个结构体变量,并进行初始化,以后再插入链表中,而后作一些修改,则能够实现相关功能。

咱们能够发现List里只有一个数据项“ChainNode *head”,为何还要定义这个数据类型?一样地,咱们用typedef char EleType定义了线性表存储的元素类型,其实只是将char取名为EleType而已,为何要取这个别名而不是直接用char呢?咱们在编写程序的过程当中,须要一些符号来帮助咱们认识、理解、记忆变量的名字,这些符号最好是有特殊含义、能让咱们联想起它的功能的,若是元素的类型就用char表示,那么在定义和使用元素时很容易把它与别的变量弄混,会形成程序的可读性下降。并且若是链表的元素变成了int型,咱们只须要将typedef char EleType改为typedef int EleType就能够了,这样使程序易于修改和扩展。一样地,List里只有一个数据项“ChainNode *head”,可是咱们还要将它封装在一个List数据类型中,也是考虑到了程序的扩展性和可读性。并且若是咱们在这里只定义一个头指针的话,表达不出定义线性表的意思,是线性表里面包括头结点,这个结点能够用一个头指针指向,因此头指针能够表明一个线性表,可是它们不是一个层次的东西,咱们要将线性表的属性都封装起来才能更好的对它进行操做,这个属性是咱们抽象出来的,咱们一样能够抽象出更多的线性表的属性添加进来以方便实现更多功能。如今咱们向线性表中添加一个tail指针,使它指向链表的最后一个结点,那么首先要修改线性表的定义:

 

 

修改建立线性表的函数CreateList,由于建立线性表后只有一个头结点,因此headtail指针都指向这个结点:

 

撤销线性表时要将头尾两个指针都释放:

 

由于咱们要提升ListAppend的速度,而加入元素是在线性表尾端加入的,因此咱们用tail指针加入会更快:

 

这样咱们不用改动线性表的程序m.c就能够实现了,由于这里咱们把共性和个性分离开了,使每个函数的功能单一,独立性高,与外部的隔绝性好。也就是咱们从外部看,不用管一个函数的功能是怎么实现的,而只须要知道它的参数是什么,功能是什么,返回值是什么,这样就保证了咱们要改动程序只须要改动较小的部分。

为何要使用一个头结点呢?由于线性表有为空的状况,这时若是没有头结点,咱们加入元素就没有地方存放结点的地址,并且咱们写函数时还要专门对第一个元素进行处理。这样容易出错,也会使程序变得更加复杂。

程序中实现的链表里的元素类型都是固定的,怎么实现一个链表使它的元素类型为任意类型呢?要在链表里结点的数据空间存听任意类型的数据是不可能的,由于每一个节点定义时的大小都是固定的。咱们能够这样实现:在结点里的数据空间存放指针,指针指向每个元素处的空间,这个空间的大小能够是任意的,根据咱们定义的数据类型而改变,用malloc函数动态分配内存。

可是如今的问题是咱们不知道用户传入的数据大小是多少,像printf函数同样用类型说明符只能实现基本数据类型而不能实现用户自定义类型,而用户用结构体定义的自定义大小能够为任意大小,甚至理论上是无穷大的。以前我觉得要实现链表的每个元素的类型均可以是不同的,可是后来发现应该实现的是元素类型都是同样的,可是这个类型是由用户决定的而不是提早先规定好的。

由于不知道数据大小,因此咱们要在线性表中加入一个数据项int datasize以表示数据大小,并在main函数中建立线性表时用sizeof计算数据大小并传给datasize;

 

咱们将ListAppend函数、ListInsert函数实现为不定函数,这样它们接受的参数类型就没有限制了:

 

由于咱们传入ListAppend函数的链表数据是一个局部变量,保存在栈段中,而且在函数返回后会被释放,因此要另外开辟空间来存储它。这里&lp表示传入的线性表lp在栈中的地址,&lp+1表示下一个参数,即咱们要添加的数据在栈中的地址。咱们用malloc函数建立一个传入数据大小的空间并将它的地址赋给指针target。而后用memcpy函数将数据从战中转移到target指向的咱们动态开辟的空间中。Memcpy函数的原型为:void *memcpy( void *dest, const void *src, size_t count);即从指针src指向的空间拷贝count个字节到指针dest指向的空间里。

 

以后修改NewChainCode函数、GetElement函数、CreateList函数就能够了,这也体现了各个函数的独立性,不然咱们可能就要修改整个程序了。

这里必定要注意的是,咱们在一个指针进行赋值以后,必定要对它进行判断,若是是0则返回,这样可使程序更安全、更容易调试。

如今咱们就能够在c文件里定义数据结构而不用更改头文件的内容了。咱们用m1.c进行测试:

 

结果是正确的,注意在用CreateList建立线性表时必定要先用sizeof计算传入的数据大小。

2、扩展研究

一、这个程序有什么特点?表现了一种什么样的程序设计思想?

答:这个程序将共性抽象开并封装到头文件里,咱们能够很清楚地看到头文件list.h里封装的都是链表的数据结构和方法,咱们在c文件里只须要将数据传入并用自定义的方法(好比输出)来进行操做就能够了。这个程序的结构很是清楚:共性的抽象、个性的实现,每个函数实现一个功能,函数与函数之间没有联系,这样就能够保证一个函数出问题不会影响到其它函数。这个list.h头文件彻底能够当作一个模块,调用它就能实现链表的相关功能,这是结构化的思想。

3、研究总结

程序设计须要综合的能力和视野。这个程序头文件里的函数其实和java里的类很像,每个需求都是由专门的函数来实现的,函数与函数之间没有联系,只与调用的函数传递数据,这样咱们只须要考虑单个函数的功能怎么实现就够了。而这样首先要把问题细化为一个个小需求来实现,这须要咱们在程序设计时先对问题有清楚的认识和深度的思考分析。肯定每个函数的功能、参数、返回值,而后再来实现函数,这时就是编程的细节问题了,相对程序设计要简单得多。咱们要更多地思考怎么来进行程序设计,而不是具体的技术细节。

相关文章
相关标签/搜索