零基础学算法(第4版)
上QQ阅读APP看书,第一时间看更新

2.1 最简单的结构:线性表

从本节开始将讲解各种常用的数据结构,首先介绍的是线性表。线性表是一种典型的线性结构,也是最简单、应用十分广泛的数据结构,它是其他数据结构的基础。本节介绍线性表的两种形式:顺序线性表和链式线性表。

2.1.1 线性表的概念

由若干个数据组成的集合可看做一个线性表。线性表中的数据元素之间的关系是一对一,即除了第一个和最后一个元素外,其他元素都是首尾相接。例如,某班学生学号组成的数据就是一个线性表,如图2-1a所示。在该图中每一个学号为一个数据元素。

再看看更复杂的情况,可以将一组数据看做一个单独的数据元素,如图2-1b所示,将每个学生的信息作为一个数据元素,则所有学生的信息就组成一个线性表(每个学生的信息为一行,看做一个数据元素,从上向下就组成了一个线性表)。

图2-1 线性表

由图2-1所示的数据可看出,线性表是一个线性结构,它是一个含有n个节点(数据元素)的有限序列。

注意 不同线性表的数据元素可以不同,但对于同一线性表,各数据元素必须具有相同的数据类型,即同一线性表中各数据元素具有相同的类型,每个数据元素的长度相同。

线性表数据结构具有以下特征:

·有且只有一个“首元素”,它没有直接前驱,只有一个直接后继;

·有且只有一个“末元素”,它没有直接后继,只有一个直接前驱;

·其他元素均有一个直接前驱和直接后继;

·元素之间为一对一的线性关系

在计算机中保存线性表时,一般可用顺序存储结构和链式存储结构两种方法。顺序存储结构的线性表称为顺序表,链式存储的线性表称为链表。

对于线性表,主要可进行以下操作:

·添加节点;

·插入节点;

·删除节点;

·查找节点;

·遍历节点;

·统计节点数。

下面将分别介绍在顺序表和链表中实现这些操作的代码。

提示 队列、栈是线性表的特殊情况,本章后面也将介绍这两种线性表的操作。

2.1.2 操作顺序表

采用顺序存储方式的线性表称为顺序表。顺序表是线性表的一种最简单和最常用的方式,这种方式用一组地址连续的存储单元依次保存线性表中的数据元素,如图2-2所示。

图2-2 顺序表的存储示意图

顺序结构是将表中的元素一个接一个地存入一组连续的存储单元中。由于顺序表是依次存放的,因此只要知道该顺序表的首地址及每个数据元素所占用的存储长度,就很容易计算出任何一个数据元素的位置,也可以很方便地将数据元素添加到顺序表中。

要访问第i个数据元素,可使用以下公式计算出第i个元素的存储位置L:


L = (i-1)*数据元素长度

例如,若数据元素长度为4字节,则第5个元素的存储位置(相对位置)为:


L = (5-1)*4 
L = 16

即,保存第5个数据元素的地址相对于线性表的首位置偏移16个字节,从这里开始保存第5个数据元素。

了解顺序表的存储结构后,就可编写代码来操作顺序表了。

在编写具体的C语言代码之前,首先说一下代码组织的约定,为了让操作这些数据结构的代码具有一定的通用性,每种数据结构使用3个文件来保存,分别是:

·头文件:用来保存数据结构的定义、操作函数的原型声明等。程序员可以修改其中的数据结构定义,以适应不同的需要。

·作函数文件:用来保存操作数据结构的相关函数,这个文件的内容应该固定,程序员一般不能对其进行修改,只是直接调用其中的函数即可(最好将其编译为一个库文件)。该文件中不应有main函数。

·试文件(调用文件):在该文件中应包含上面所列的两个文件,然后编写代码实现对数据的操作。在该文件中编写main函数。

1.定义顺序表的结构

由于C语言中的数组也是采用顺序存储的方式,因此,可使用数组来模拟顺序表的保存形式。在顺序表中还需定义一个变量,用来保存顺序表中已有元素的数量,因此,可使用以下结构来定义顺序表结构。

提示 操作顺序表的代码较长,为了讲解方便,将只显示需要的代码,对同一个源文件,行号的编写是连续的,以便于在文字中参照引用。完整的代码可参见源代码中对应章节的相应文件。

【程序2-1】操作顺序表的头文件“2-1 SeqList.h”


1    #include <stdio.h>
2    #include <string.h>
3    #define MAXSIZE 100                 //定义顺序表的最大长度
4    
5    typedef struct                     //定义顺序表结构
6    {
7        DATA ListData[MAXSIZE+1];        //保存顺序表的数组 
8        int ListLen;                     //顺序表已存节点的数量 
9    }SeqListType;

【程序说明】

·第3行定义了顺序表能保存的最大节点数,即顺序表的最大长度。

·第5~9行定义顺序表的结构,其中字段ListData为顺序表中的数据,是一个一维数组,为了方便操作,设置该数组序号为0的序号不使用,因此定义的数组大小须比MAXSIZE大1;字段ListLen保存顺序表中实际保存的节点数量。

提示 在很多介绍数据结构的书籍中都只是使用简单数据类型(如int、double等)来演示线性表的操作,其代码比较简单。本例中使用一个复杂的数据类型DATA(由结构体定义的多种数据)作为节点,这种线性表的操作更具有实际意义。

2.定义顺序表的操作

接着,在操作顺序表的头文件中定义操作顺序表的函数原型,具体代码如下:


10    void SeqListInit(SeqListType *SL);                    //初始化顺序表
11    int SeqListLength(SeqListType *SL);                  //返回顺序表的元素数量 
12    int SeqListAdd(SeqListType *SL,DATA data);            //向顺序表中添加元素 
13    int SeqListInsert(SeqListType *SL,int n,DATA data);     //向顺序表中插入元素 
14    int SeqListDelete(SeqListType *SL,int n);              //删除顺序表中的数据元素
15    DATA *SeqListFindByNum(SeqListType *SL,int n);        //根据序号返回元素
16    int SeqListFindByCont(SeqListType *SL,char *key);    //按关键字查找 
17    int SeqListAll(SeqListType *SL);                    //遍历顺序表中的内容

各函数的作用参见右侧的注释。

3.初始化顺序表

接下来就需要编写操作顺序表的各函数,这些函数的代码保存在名为“2-2 SeqList.c”的文件中。首先介绍初始化顺序表,要初始化顺序表,只需要设置该顺序表的元素长度为0即可,以后添加元素到顺序表时就从第1个位置开始。

提示 如果顺序表中原来已有数据,也会被覆盖。

顺序表初始化具体代码如下:

【程序2-2】操作顺序表的函数文件“2-2 SeqList.c”


1    void SeqListInit(SeqListType *SL)        //初始化顺序表
2    {
3        SL->ListLen=0;                     //初始化时, 设置顺序表长度为0 
4    }

4.顺序表的长度

在顺序表结构中,字段ListLen中保存着顺序表中节点的数量,要获得顺序表的长度,只需返回该字段中的值即可。具体代码如下:


5    int SeqListLength(SeqListType *SL)        //返回顺序表的元素数量 
6    {
7        return (SL->ListLen); 
8    }

5.添加节点

添加节点的操作是在顺序表的最后位置添加一个节点数据。在添加节点时,首先要判断顺序表中的空间是否已使用完,未满再将传入的数据保存到顺序表的下一位置即可。具体代码如下:


9    int SeqListAdd(SeqListType *SL,DATA data)    //增加节点到顺序表尾部
10    {
11        if(SL->ListLen>=MAXSIZE)                //顺序表已满 
12        {
13            printf("顺序表已满, 不能再添加节点了!\n");
14            return 0; 
15        }
16        SL->ListData[++SL->ListLen]=data;        //保存数据到顺序表尾部
17        return 1;
18    }

【程序说明】

·该函数使用了两个参数,其中SL是指向顺序表的指针(即需要添加节点的顺序表),而data则是需要添加到顺序表中的数据。

·第16行将传入的数据复制到顺序表中,并将保存顺序表节点数量的变量ListLen增加1。

编程陷阱 在添加节点时,首先要判断顺序表中的空间是否已使用完,若不进行判断,则可能会将数据添加到不可预知的内存空间,从而得不到正确的结果,造成不可预知的错误。

6.插入节点

在顺序表中插入节点的操作比较麻烦,因为顺序表中的所有节点都是按序号紧邻存放的,如果要在其中插入一个节点,则需要将插入点后面的节点数据向后移动一个位置,以便在空出的位置保存插入节点的数据,如图2-3所示。

提示 在新节点插入时,需要移动大量的元素,并且还需要修改顺序表的长度,即元素数量加1。

图2-3 插入节点

插入节点的代码如下:


19    int SeqListInsert(SeqListType *SL,int n,DATA data)  //向顺序表中插入元素
20    {
21        int i;
22        if(SL->ListLen>=MAXSIZE)        //顺序表节点数量已超过最大数量 
23        {
24            printf("顺序表已满, 不能插入节点!\n");
25            return 0;                    //返回0表示插入不成功 
26        }
27        if(n<1 || n>SL->ListLen-1)         //插入节点序号不正确
28        {
29            printf("插入节点序号错误, 不能插入元素!\n");
30            return 0;                     //返回0, 表示插入不成功 
31        } 
32        for(i=SL->ListLen;i>=n;i--)        //将顺序表中的数据向后移动 
33            SL->ListData[i+1]=SL->ListData[i]; 
34        SL->ListData[n]=data;            //插入节点 
35        SL->ListLen++;                    //顺序表节点数量增加1
36        return 1;                        //返回1,表示成功插入
37    }

【程序说明】

·该函数使用了3个参数,其中SL是指向顺序表的指针(即需要插入节点的顺序表),而参数n指定要插入的序号,data则是需要插入到顺序表中的数据。

·第22~26行判断顺序表的节点数量是否超过最大值。

·第27~31行判断插入的序号是否正确。

·第32、33行从顺序表的表尾开始,将元素逐个向后移动,将第n个节点移向第n+1个节点位置。

·第34行将插入的数据data复制到第n个节点位置。

·第35行将顺序表节点数量的变量ListLen增加1。

提示 如果在第1个元素处插入新节点,则需要将顺序表中的所有节点都移动一次,因此,在顺序表中插入节点操作的效率是非常低的。

7.删除节点

与插入节点的操作类似,删除节点也需要大量移动数据,若需删除序号为n的节点,则要将序号n+1至尾节点的所有节点都向前移动,将节点移动之后,还需要修改顺序表元素数量(减1)。具体代码如下:


38    int SeqListDelete(SeqListType *SL,int n)   //删除顺序表中的数据元素
39    {
40        int i;
41        if(n<1 || n>SL->ListLen+1)    //删除节点序号不正确
42        {
43            printf("删除节点序号错误, 不能删除节点!\n");
44            return 0;                //返回0, 表示删除不成功 
45        } 
46        for(i=n;i<SL->ListLen;i++)    //将顺序表中的数据向前移动 
47            SL->ListData[i]=SL->ListData[i+1]; 
48        SL->ListLen--;             //顺序表元素数量减1 
49        return 1;                     //返回1,表示成功删除  
50    }

【程序说明】

·该函数使用了两个参数,其中SL是指向顺序表的指针(即需要删除节点的顺序表),而参数n指定删除节点的序号。

·第46、47行将n+1后面的所有节点数据向前移动,完成删除操作。第48行将节点数量减1,表示删除节点后的节点总数。

编程陷阱 在删除节点时,首先要判断要删除的节点序号是否在指定范围之内,若不进行判断,则可能会将错误的数据移到顺序表中。

8.按序号查找节点

创建好顺序表之后,最常用的操作就是在顺序表中进行查找,以获取相应节点的数据。按序号查找节点就是返回指定序号的节点数据,其代码如下:


51    DATA *SeqListFindByNum(SeqListType *SL,int n)   //根据序号返回数据元素
52    {
53        if(n<1 || n>SL->ListLen+1)    //元素序号不正确
54        {
55            printf("节点序号错误, 不能返回节点!\n");
56            return NULL;            //返回NULL, 表示不成功 
57        } 
58        return &(SL->ListData[n]);    //返回找到节点的指针
59    }

【程序说明】

·该函数使用了两个参数,其中SL是指向顺序表的指针(即需要查找节点的顺序表),而参数n指定要查找节点的序号。

·第58行返回序号为n的节点指针。

提示 该函数的返回值是一个指向节点数据类型的指针,通过该指针就可以访问到所查找序号的节点数据。

9.按关键字查找节点

在实际的查找操作中,可能更多的是按关键字查找。例如,在保存学生信息的顺序表中,按学号查询是比较常用的。因此,在定义顺序表的查找操作时,应提供按关键字查找的功能。具体代码如下:


60    int SeqListFindByCont(SeqListType *SL,char *key) //按关键字查询结点
61    {
62        int i;
63        for(i=1;i<=SL->ListLen;i++)
64            if(strcmp(SL->ListData[i].key,key)==0)    //如果找到所需节点 
65                return i;                            //返回节点序号 
66        return 0;                                    //遍历后仍没有找到, 则返回0 
67    }

【程序说明】

·该函数使用了两个参数,其中SL是指向顺序表的指针(即需要查找节点的顺序表),而参数key是要查找节点的关键字。

·第63~65行遍历顺序表,逐个比较关键字。若节点的关键字与查找关键字相同,则返回节点的序号;若遍历后未找到指定关键字的节点,则返回0。

提示 该函数的返回值是节点的序号,而不是一个指向节点的指针。因为已经有按序号返回节点指针的函数,所以这里返回序号即可。设置为返回序号,还可方便插入操作,如需要将新增节点插入某个节点处,则可先按关键字查询返回序号,再调用SeqListInsert函数即可。

10.测试顺序表操作

定义好顺序表的操作函数后,接下来编写测试这些功能的程序。具体代码如下:

【程序2-3】测试顺序表操作的程序“2-3 SeqListTest.c”


1    #include <stdio.h>
2    typedef struct
3    {
4        char key[15];                     //节点的关键字 
5        char name[20];
6        int age;
7    } DATA;                            //定义节点类型
8    #include "2-1 SeqList.h"             //引入自定义的头文件
9    #include "2-2 SeqList.c"             //引入自定义的作函数文件
10    int SeqListAll(SeqListType *SL)    //遍历顺序表中的节点 
11    {
12       int i;
13       for(i=1;i<=SL->ListLen;i++)
14          rintf("(%s,%s,%d)\n",SL->ListData[i].key,SL->ListData[i].name,SL->ListData[i].age);
15    }

【程序说明】

·第2~7行定义节点的数据类型,除了保存常用数据外,还增加了一个关键字字段,方便对节点的内容进行查询。对于关键字段,这里采用一个字符串来保存,因为这样具有更大的灵活性,既可以保存数字,也可以保存字符。

·第8、9行包含操作顺序表的头文件和函数。

·第10~15行为遍历顺序表各节点的函数。

技巧 由于DATA类型的定义不同,需要修改本函数第14行中输出的内容,因此将该函数编写在测试文件中,这样可保证操作函数的文件“2-2 SeqList.c”不被修改。

提示 以上代码中,第8~9行又是一个包含文件的指令,看起来有点另类。也可将以上内容单独保存,作为测试文件的头文件。

接下来就是测试操作顺序表的主函数main的代码:


16    int main()
17    {
18        int i;
19        SeqListType SL;                 //定义顺序表变量 
20        DATA data,*data1;                //定义节点保存数据类型变量和指针变量 
21        char key[15];                    //保存关键字 
22    
23        SeqListInit(&SL);                //初始化顺序表 
24    
25        do {                            //循环添加节点数据 
26            printf("输入添加的节点(学号 姓名 年龄):"); 
27            fflush(stdin);                //清空输入缓冲区 
28            scanf("%s%s%d",&data.key,&data.name,&data.age); 
29            if(data.age)                //若年龄不为0 
30            {
31                if(!SeqListAdd(&SL,data))//若添加节点失败 
32                    break;                //退出死循环 
33            }else                        //若年龄为0 
34                break;                    //退出死循环 
35        }while(1);
36        printf("\n顺序表中的节点顺序为:\n");
37        SeqListAll(&SL);                //显示所有节点数据 
38        
39        fflush(stdin);                 //清空输入缓冲区 
40        printf("\n要取出节点的序号:");
41        scanf("%d",&i);                 //输入节点序号
42        data1=SeqListFindByNum(&SL,i);    //按序号查找节点 
43        if(data1)                        //若返回的节点指针不为NULL 
44            printf("第%d个节点为:(%s,%s,%d)\n",i,data1->key,data1->name,data1->age);
45        
46        fflush(stdin);                 //清空输入缓冲区 
47        printf("\n要查找节点的关键字:");
48        scanf("%s",key);                //输入关键字
49        i=SeqListFindByCont(&SL,key);    //按关键字查找, 返回节点序号 
50        data1=SeqListFindByNum(&SL,i);    //按序号查询, 返回节点指针 
51        if(data1)                //若节点指针不为NULL 
52            printf("第%d个节点为:(%s,%s,%d)\n",i,data1->key,data1->name,data1->age);
53        getch();
54        return 0;
55    }

程序的代码很简单,关键部分都添加了注释,就不再一一解释了。

提示 由于本书篇幅所限,顺序表中其他各操作函数未在上面的例子中进行测试,读者可将相关测试功能添加到上面的代码中。

编译执行以上代码,将首先显示输入数据的提示,按要求输入相应的数据后,即可显示、查询顺序表中各节点的数据,具体过程如图2-4所示。

图2-4 【程序2-3】执行结果

在图2-4中共输入了5个节点数据,最后输入三个0结束输入(从main函数中第29~34行的代码可看出,只需要第3个数据为0即可退出输入状态),接着在下方将显示所输入节点的所有数据,然后要求用户输入一个序号,即可显示该序号对应节点的数据,最后要求用户输入关键字,即可显示关键字所对应的节点数据。

2.1.3 操作链表

顺序表是将数据按顺序保存到内存中,因此在顺序表中存取数据很方便,但同时也带来一些问题,例如,向顺序表中插入元素时,需要移动大量的数据。最坏的情况下,如果每次都是要在顺序表的首位置插入元素,则需要将顺序表中所有元素都向后移,效率非常低。

编程经验 对于需要经常插入元素的线性表,可采用链式存储的链表。链表是采用动态存储分配的一种结构。可以根据需要申请内存单元。

链表的结构如图2-5所示,链表中每个节点都应包括两个部分:一部分是实际数据,另一部分是下一个节点的地址。操作链表时,需定义一个“头指针”变量(一般以head表示),该指针变量指向链表的第一个节点,第一个节点又指向第二个节点,直到最后一个节点。最后一个节点不再指向其他节点,称为“表尾”,在表尾的地址部分放一个“NULL”(∧),表示“空地址”,链表到此结束。

图2-5 链表结构

图2-5所示的链表中,每个节点中只包含一个指针,一般称为单向链表。若每个节点包含两个指针,一个指向下一个节点,另一个指向上一个节点,称为双向链表。

在链表中,逻辑上相邻的节点在内存中并不一定相邻,逻辑相邻关系通过指针来实现。由于节点之间不要求连续存放,就可以通过malloc函数动态分配节点的存储空间。

编程陷阱 由于C语言程序不能自动回收动态分配的空间,因此,当删除某个节点时,应该使用free函数释放其占用的内存空间。

对于链表的访问只能从表头逐个查找,即通过head头指针找到第一个节点,再从第一个节点中的地址找到第二个节点……这样逐个比较,一直到找到需要的节点为止,而不能像顺序表那样进行随机访问,因此在链表中查找获取某个节点的数据时需要较长的过程。

下面介绍在C语言中操作单链表的代码。

1.定义链表的结构

图2-5所示的节点可使用以下结构来定义。

提示 操作链表的代码较长,为了讲解方便,将只显示需要的代码,对同一个源文件,行号的编写是连续的,以便于在文字中参照引用。完整的代码可参见源代码中对应章节的相应文件。

【程序2-4】操作链表的头文件“2-4 ChainList.h”


1    #include <stdlib.h>               //引入内存分配头文件
2    typedef struct Node              //定义链表节点结构
3    {
4        DATA data;                      //节点数据
5        struct Node *next;             //节点指针
6    }ChainListType;
7    ChainListType *ChainListAddEnd(ChainListType *head,DATA data);
        //添加节点到链表末尾 
8    ChainListType *ChainListAddFirst(ChainListType *head,DATA data);
        //添加节点到链表首部 
9    ChainListType *ChainListFind(ChainListType *head,char *key);
        //按关键字在链表中查找内容 
10    ChainListType *ChainListInsert(ChainListType *head,char *findkey,DATA data);
        //插入节点到链表指定位置 
11    int ChainListDelete(ChainListType *head,char *key);
        //删除指定关键字的节点 
12    int ChainListLength(ChainListType *head);
        //获取链表节点数量

【程序说明】

·因为要使用动态分配内存的函数malloc,因此在第1行包含头文件stdlib.h。

·第3~6行定义一个结构体作为链表节点的数据类型,节点的具体数据保存在一个结构DATA中,而指针next用来指向下一个节点。只有一个指向下一节点的指针,因此,这是一个单向链表,如果构建双向链表,则还需要增加一个指针,用来指向上一个节点。

·第7~12行为操作链表的函数原型。

提示 在定义链表的结构体中使用了一个名为DATA的类型,为了使操作链表的头文件和函数文件在使用时不进行修改,因此需将DATA类型定义在测试文件中。

2.添加节点至尾部

向链表添加节点有3种方式,分别为:

·在链表的后面(最后一个节点)添加一个节点;

·在链表的前面(第一个节点)添加一个节点;

·在链表中间指定处(可指定关键字)添加一个节点。

当需要在链表末尾增加节点时,由于一般情况下,链表只有一个头指针head,要在末尾添加节点就需要从头指针head开始逐个检查,直到找到最后一个节点(即表尾)。表尾节点的地址部分原来保存的是NULL,此时需将其设置为新增节点的地址(即原表尾节点指向新增节点),然后将新增节点的地址部分设置为NULL,即新增节点成为表尾。例如,在图2-5所示的链表末尾添加节点的过程如图2-6所示。

图2-6 在表尾添加节点

链表操作函数都保存在文件“2-5 ChainList.c”中,下面首先列出在链表末尾添加节点的操作代码:

【程序2-5】操作链表的函数文件“2-5 ChainList.c”


1    #include <string.h>
2    ChainListType *ChainListAddEnd(ChainListType *head,DATA data)    //添加节点到链表结尾 
3    {
4        ChainListType *node,*h;
5        if(!(node=(ChainListType *)malloc(sizeof(ChainListType))))    //为链表分配内存空间
6        {
7            printf("为保存节点数据申请内存失败!\n");
8            return NULL;                            //分配内存失败 
9        }
10        node->data=data;                             //保存数据 
11        node->next=NULL;                            //设置节点指针为空, 即为表尾 
12        if(head==NULL)                                //是头指针 
13        {
14            head=node;                             //链表头指针指向当前节点
15            return head;                             //返回头指针
16        }
17        h=head;                                     //保存头指针
18        while(h->next!=NULL)                         //当指向下一节点的地址不为NULL时
19            h=h->next ;                             //指向下一个节点
20        h->next=node;                                 //原末节点地址指向新增节点
21        return head;                                 //返回头指针
22    }

【程序说明】

·该函数共有两个参数,其中head为链表头指针,data为节点保存的数据。

·第5~9行使用malloc函数申请保存节点数据的内存空间,如果分配内存成功,node中将保存指向该内存区域的指针。

·第10、11行将传入的data保存到申请的内存区域,并设置该节点中指向下一节点的指针值为NULL。

·第12~16行判断链表的头指针head是否为空,如为空,则使头指针指向当前节点。

·若头指针head不为NULL,则执行第17~19行,从第一个节点开始向后查找,直至找到最后一个节点,第20行使最后一个节点指向新增的节点即可。

提示 将节点添加到链表尾部时,如果只知道链表的头指针head,则需要执行上面程序中的第18、19行查找链表尾部。如果需要经常进行这种操作,可定义一个尾指针,指向链表尾部。这样,在进行尾部添加节点操作时,就可以省去第18、19行的查找过程,直接针对尾指针操作即可。

3.添加节点至首部

在链表首部添加节点的操作过程如图2-7所示。由于数据结构中定义了头指针head,因此,只需使新增节点指向头指针head所指向的节点,然后使头指针head指向新增节点即可。

在链表首部添加节点的操作代码如下:


23    ChainListType *ChainListAddFirst(ChainListType *head,DATA data)//添加结点到链表首部
24    {
25        ChainListType *node,*h;
26        if(!(node=(ChainListType *)malloc(sizeof(ChainListType))))    //为链表分配内存空间
27        {
28            printf("为保存节点数据申请内存失败!\n"); 
29            return NULL;            //分配内存失败 
30        }
31        node->data=data;             //保存数据 
32        node->next=head;            //指向头指针所指节点 
33        head=node;                //头指针指向新增节点
34        return head; 
35    }

图2-7 在表首添加节点

【程序说明】

·该函数共有两个参数,其中head为链表头指针,data为节点保存的数据。

·第26~30行使用malloc函数申请保存节点数据的内存空间,如果分配内存成功,node中将保存指向该内存区域的指针。

·第31~33行将传入的data保存到申请的内存区域,并使新增节点指向头指针head所指向的节点,然后设置头指针head指向新增节点。

提示 在第26行使用malloc函数申请内存时,如果内存分配失败,没有地方保存节点数据,只有退出该函数,并返回一个错误信息(本函数中返回NULL)给调用函数。

4.插入节点

插入节点就是在链表的中间部分增加节点,这时首先需要找到要插入的位置(这里是指逻辑位置,而不是物理位置),然后修改插入位置节点的指针,使其指向新增节点,而使新增节点指向原插入位置所指向的节点,具体过程如图2-8所示。

图2-8 插入节点到链表

插入节点到链表的操作代码如下:


36    ChainListType *ChainListInsert(ChainListType *head,char *findkey,DATA data)
                                        //插入结点到链表指定位置
37    {
38        ChainListType *node,*node1;
39        if(!(node=(ChainListType *)malloc(sizeof(ChainListType)))) //申请保存节点的内存
40        {
41            printf("为保存节点数据申请内存失败!\n"); 
42            return 0;                        //分配内存失败 
43        }
44        node->data=data;                    //保存节点中的数据 
45        node1=ChainListFind(head,findkey);
46        if(node1)                            //若找到要插入的节点 
47        {
48            node->next=node1->next;             //新插入节点指向关键节点的下一节点 
49            node1->next=node;                //设置关键节点指向新插入节点 
50        }else{
51            free(node);                    //释放内存
52            printf("未找到插入位置!\n"); 
53        }
54        return head;                        //返回头指针
55    }

【程序说明】

·该函数共有3个参数,其中head为链表头指针,参数findkey是用来在链表中进行查找的节点关键字,找到该节点后将在该节点后面添加节点数据,data为新增节点的数据。

·第39~43行使用malloc函数申请保存节点数据的内存空间,如果分配内存成功,node中将保存指向该内存区域的指针。

·第45行调用函数ChainListFind查找指定关键字的节点,并返回节点的指针(暂称其为目标节点),如果找到的节点有效,执行第48~49行,设置新增节点指向目标节点所指向的节点(这里读起来很绕口,可参见图2-8所示),再使目标节点指向新增节点。如果未找到目标节点,将不执行插入操作,执行第51行释放申请的内存空间。

5.查找节点

在链表中访问节点不如顺序表方便。对于保存在链表中的数据,可通过关键字进行查询,找到关键字相同的节点后,返回该节点的指针,方便调用程序处理。

提示 在上面的插入节点函数的第45行调用过查找节点函数ChainListFind,以找到需要添加节点的位置。

按关键字在链表中查找内容的代码如下:


56    ChainListType *ChainListFind(ChainListType *head,char *key) //按关键字在链表中查找内容
57    {
58        ChainListType *h;
59        h=head;                            //保存链表头指针 
60        while(h)                            //若节点有效, 则进行查找 
61        {
62            if(strcmp(h->data.key,key)==0)    //若节点关键字与传入关键字相同 
63                return h;                    //返回该节点指针 
64            h=h->next;                        //处理下一节点 
65        }
66        return NULL;                         //返回空指针 
67    }

【程序说明】

·该函数共有两个参数,其中head为链表头指针,参数key是用来在链表中进行查找的节点关键字。

·第60~65行完成查找操作。首先从链表头指针开始,如果指针h的值不为NULL(即未到链表结尾),则执行第62行判断节点关键字与传入的关键字是否相等,若相等,则表示找到目标节点了,第63行返回该节点的指针;若关键字不相等,则使指针指向当前节点的下一节点,继续进行比较。

提示 因为关键字使用的是字符串,因此比较大小时调用字符串比较函数strcmp。

6.删除节点

在设计删除节点时,首先想到的是使用前面编写的ChainListFind函数找到要删除的节点,然后释放该节点所占用的内存空间即可。但是这样操作是有问题的,因为链表需要通过指针来进行链接,如果直接删除其中的某一个节点,则该节点的后续节点将丢失,导致无法进行操作。因此,删除节点不能只使用查找节点的方法来获取要删除的节点,还需要知道被删除节点的上一个节点,然后使被删除节点的上一个节点指向被删除节点的下一个节点。

也就是说,在查找需要删除的节点时,还需定义一个指针来指向前一节点。如图2-9所示,若需要删除指针h所指向的节点,可设置node指针所指节点的地址部分,使其指向h指针所指节点的下一节点,也就是使node指向的节点的地址部分指向指针h的下一节点(文字读起来很绕口,看图更清楚)。

图2-9 删除节点

删除节点的代码如下:


68    int ChainListDelete(ChainListType *head,char *key) //删除指定关键字的结点
69    {
70        ChainListType *node,*h;             //node保存删除节点的前一节点 
71        node=h=head;
72        while(h)
73        {
74            if(strcmp(h->data.key,key)==0)     //找到关键字, 执行删除操作 
75            {
76                node->next=h->next;          //使前一节点指向当前节点的下一节点
77                free(h);                      //释放内存 
78                return 1;
79            }else{
80                node=h;                      //指向当前节点 
81                h=h->next;                     //指向下一节点 
82            }
83         }
84         return 0;                        //未删除 
85    }

【程序说明】

·该函数共有2个参数,其中head为链表头指针,参数key表示一个关键字,是链表中需要删除节点的关键字。

·第72~83行为一个循环,按关键字在整个链表中查找要删除的节点,若找到被删除节点,则执行第76行,设置上一节点(node指针所指节点)指向当前节点(h指针所指节点)的下一节点,即可完成链表中节点的逻辑删除,但这时被删除节点仍然保存在内存中,还需执行第77行的free函数,用来释放被删除节点所占用的内存空间。

·若当前节点不是要删除的节点,则执行第80行保存当前节点指针在node变量中,而让h指针指向下一节点,这样,在下一次循环时,h指针指向的节点就变为了当前节点,而node指针所指向的节点就成了上一节点。

提示 与查找函数ChainListFind相同,因关键字设置为字符串类型(即char数组),因此调用字符串比较函数strcmp来判断关键字是否相等。

7.链表的长度

与顺序表不同,链表在物理结构上没有相邻的要求,因此不能方便地得到其长度。要得到链表的长度,就必须遍历链表,逐个统计节点的个数。下面的代码可完成该功能:


 86    int ChainListLength(ChainListType *head)    //获取链表结点数量
87    {
88        ChainListType *h;
89        int i=0;
90        h=head;
91        while(h)                                //遍历整个链表 
92        {
93            i++;                                 //累加节点数量 
94            h=h->next;                            //处理下一节点 
95        }
96        return i;                                //返回节点数量 
97    }

【程序说明】

·该函数只有一个参数head,表示链表的头指针。

·第91~95行统计节点的数量,第94行使指针h指向下一个节点。

8.测试链表操作

定义好链表的操作函数后,接下来编写测试这些功能的程序。具体代码如下。

【程序2-6】测试链表操作的程序“2-6 ChainListTest.c”


1    #include <stdio.h>
2    typedef struct                                //定义数据结构体
3    {
4        char key[15];                        //关键字
5        char name[20];
6        int age;
7    }DATA;                                 //数据节点类型 
8    #include "2-4 ChainList.h"                  //引入自定义的头文件
9    #include "2-5 ChainList.c"                  //引入自定义的作函数文件
10    void ChainListAll(ChainListType *head)    //遍历链表 
11    {
12        ChainListType *h;
13        DATA data;
14        h=head;
15        printf("链表所有数据如下:\n"); 
16        while(h)                             //循环处理链表每个节点 
17        {
18            data=h->data;                    //获取节点数据 
19            printf("(%s,%s,%d)\n",data.key,data.name,data.age); 
20            h=h->next;                        //处理下一节点 
21        }
22        return;
23    }

【程序说明】

·第2~7行定义链表中的数据类型DATA,在该结构体中必须包含一个字符串类型的key,用来作为查询节点的依据,而结构体中的其他字段可根据需要进行设置。

·第8、9行包含操作链表的头文件和函数。

·第10~23行为遍历链表各节点的函数。

技巧 由于DATA类型的定义不同,需要修改本函数第19行中输出的内容,因此将该函数编写在测试文件中,这样可保证操作函数的文件“2-5 ChainList.c”不被修改。

提示 以上代码中,第8~9行又是一个包含文件的指令,看起来有点另类。也可将以上内容单独保存,作为测试文件的头文件。

接下来就是测试操作链表的主函数main的代码。


24    int main()
25    {
26        ChainListType *node, *head=NULL;
27        DATA data;
28        char key[15],findkey[15];
29        
30        printf("输入链表中的数据, 包括关键字、姓名、年龄, 关键字输入0, 则退出:\n"); 
31        do{
32            fflush(stdin);                        //清空输入缓冲区 
33            scanf("%s",data.key);
34            if(strcmp(data.key,"0")==0) break;            //若输入0, 则退出
35            scanf("%s%d",data.name,&data.age);
36            head=ChainListAddEnd(head,data);                //在链表尾部添加节点数据 
37        }while(1);
38        
39        printf("该链表共有%d个节点。\n",ChainListLength(head));    //返回节点数量 
40        ChainListAll(head);                            //显示所有节点
41     
42        printf("\n插入节点, 输入插入位置的关键字:") ;
43        scanf("%s",&findkey);//输入插入位置关键字 
44        printf("输入插入节点的数据(关键字 姓名 年龄):");
45        scanf("%s%s%d",data.key,data.name,&data.age);    //输入插入节点数据 
46        head=ChainListInsert(head,findkey,data);            //调用插入函数 
47        
48        ChainListAll(head);                             //显示所有节点
49        
50        printf("\n在链表中查找, 输入查找关键字:");
51        fflush(stdin);                                    //清空输入缓冲区 
52        scanf("%s",key);                                //输入查找关键字 
53        node=ChainListFind(head,key);                    //调用查找函数, 返回节点指针 
54        if(node)                                        //若返回节点指针有效 
55        {
56             data=node->data;                            //获取节点的数据 
57              printf("关键字%s对应的节点数据为(%s,%s,%d)\n" ,key,data.key,data.name,data.age);
58        }else                                            //若节点指针无效 
59            printf("在链表中未找到关键字为%s的节点!\n",key); 
60        
61        printf("\n在链表中删除节点, 输入要删除的关键字:");
62        fflush(stdin);                                    //清空输入缓冲区
63        scanf("%s",key);                                //输入删除节点关键字
64        ChainListDelete(head,key);                         //调用删除节点函数
65        ChainListAll(head);                             //显示所有节点
66        getch();
67        return 0;
68    }

程序的代码很简单,关键部分都添加了注释,就不再一一解释了。

提示 由于本书篇幅所限,链表中其他各操作函数未在上面的例子中进行测试,读者可将相关测试功能添加到上面的代码中。

编译执行以上代码,将首先显示输入数据的提示,按要求输入相应的数据后,即可显示出添加到链表中的各节点的数据,具体过程如图2-10所示。

图2-10 【程序2-4】执行结果

在图2-10中分别进行了以下4个操作:

(1)输入了5个节点数据,接着在下方显示所输入节点的所有数据;

(2)要求用户插入节点的关键字,再接收用户输入的节点数据,下方即可看到插入节点后的所有数据;

(3)要求用户输入查找关键字,下方即可显示该关键字所对应的节点数据;

(4)最后要求输入删除节点的关键字,删除该节点后将显示链表中的所有数据。

2.1.4 实例:用链表制作通讯录

前面介绍了操作链表的相关函数,下面使用这些函数编写一个简单的通讯录管理程序。

【程序分析】

通常,在一个通讯录中可管理多人的联系信息,由于不能预知系统中需要管理多少联系人信息,因此使用链表来保存联系人将是一种好的方案。由于本书篇幅所限,本例只编写通讯录管理的主要代码。制作如图2-11所示的4个模块功能,包括增加联系人、查找联系人、删除联系人、显示联系人这4个模块。

图2-11 程序模块

为了使主函数简洁,将只在主函数中编写调用4个模块的代码,而4个模块分别使用4个函数来完成。下面列出通讯录程序的代码。

1.定义通讯录结构

【程序2-7】通讯录程序“2-7 AddressList.c”

在程序的首部定义通讯录的数据结构如下:


1    typedef struct               //定义通信录结构体
2    {
3        char key[15];            //关键字(设置姓名为关键字)
4        char addr[20];
5        char telephone[15];
6        char mobile[12];
7    }DATA;                     //数据节点类型 
8    #include <stdio.h>
9    #include "2-4 ChainList.h"  //引入自定义头文件
10    #include "2-5 ChainList.c"  //引入自定义的作函数文件

【程序说明】

·在第1~7行的DATA结构中,必须定义一个名为key的字符串,作为查找的关键字,其他字段可根据需要进行增减,如可增加QQ、E-mail地址等联系信息。

·第9、10行包含操作链表的头文件和函数文件。

2.编写显示联系人信息模块

要显示所有联系人的信息,其实就是遍历链表,逐个显示各节点的数据。因此,编写以下函数,以显示链表中所有联系人的信息。


11    void ChainListAll(ChainListType *head)    //遍历链表 
12    {
13        ChainListType *h;
14        DATA data;
15        h=head;
16        printf("链表所有数据如下:\n"); 
17        while(h)                         //循环处理链表每个节点 
18        {
19            data=h->data;                //获取节点数据 
20            printf("姓名:%s\n",data.key);
21            printf("地址:%s\n",data.addr);
22            printf("电话:%s\n",data.telephone);
23            printf("手机:%s\n",data.mobile);
24            h=h->next;                    //处理下一节点 
25        }
26        return;
27    }

以上代码将遍历链表,并显示每一个节点(联系人)的相关信息。

3.编写添加联系人模块

添加联系人就是向链表中添加节点的过程,最简单的方式就是将新联系人(新节点)直接添加到链表的头部。当在主函数中选择“输入联系人”时,将调用以下函数,用来向链表中添加新的联系人信息。


28    ChainListType *input(ChainListType *head)     //向通讯录中输入的信息
29    {
30        DATA data;
31        printf("请输入联系人信息\n");
32        printf("姓名:");
33        scanf("%s",data.key);
34        printf("地址:");
35        scanf("%s",data.addr);
36        printf("电话:");
37        scanf("%s",data.telephone);
38        printf("手机:");
39        scanf("%s",data.mobile);
40        return ChainListAddFirst(head,data);        //调用添加函数 
41    }

【程序说明】

以上代码首先在第31~39行要求用户输入联系人的相关信息,然后执行第40行调用Chain-ListAddFirst函数将输入的信息添加到链表的首部。

提示 也可调用ChainListAddEnd函数将输入的联系人信息添加到链表的尾部。

4.编写查找联系人模块

查找联系人的过程可遍历链表,并逐个比较关键字,当找到符合要求的节点时,结束遍历过程。当在主函数中选择“查找联系人”时,将调用以下函数,让用户输入联系人姓名进行查找,然后显示出查找到的联系人信息,具体代码如下:


42    void find(ChainListType *head)    //查找联系人
43    {
44        ChainListType *h;
45        DATA data;
46        char name[15];
47        printf("请输入查找姓名:");
48        scanf("%s",name);
49        h=ChainListFind(head,name);        //调用查找函数
50        if(h)                            //查找节点指针有效
51        { 
52            data=h->data;                //获取节点数据 
53            printf("姓名:%s\n",data.key);
54            printf("地址:%s\n",data.addr);
55            printf("电话:%s\n",data.telephone);
56            printf("手机:%s\n",data.mobile);
57        }
58    }

【程序说明】

以上代码第48行让用户输入查找联系人的姓名,第49行调用ChainListFind函数查找该联系人的姓名,若找到该联系人,则显示该联系人的信息。

5.编写删除联系人模块

删除联系人的过程就是删除链表中节点的过程。当在主函数中选择“删除联系人”时,将调用以下函数,让用户输入联系人姓名进行删除,具体代码如下:


59    void delete(ChainListType *head)     //删除联系人
60    {
61        ChainListType *h=head;
62        char name[15];
63        printf("请输入要删除的姓名:");
64        scanf("%s",name);
65        ChainListDelete(head,name); 
66    }

【程序说明】

以上代码第64行接收用户输入的联系人姓名,第65行调用ChainListDelete函数删除该联系人的信息。

6.编写主模块

将各子模块代码编写完成后,接着编写主模块main函数。在main函数中,主要用来显示菜单供用户进行选择,具体代码如下:


67    int main()
68    {
69        ChainListType *head=NULL;
70        int select;            //选择菜单的序号 
71        do{
72            printf("\n_____________________\n");
73            printf("1.添加联系人\n");
74            printf("2.查找联系人\n");
75            printf("3.删除联系人\n"); 
76            printf("4.显示所有联系人\n"); 
77            printf("0.退出\n");
78            printf("_____________________\n");
79            select=getch();      //把程序暂停下来,等待用户的输入
80            switch(select) 
81            {
82                case '1':
83                    printf("\n添加联系人\n"); 
84                    head=input(head);
85                    break;
86                case '2':
87                     printf("\n查找联系人\n"); 
88                     find(head);
89                     break;
90               case '3':
91                    printf("\n删除联系人\n"); 
92                    delete(head);
93                    break;
94               case '4':
95                    printf("\n显示联系人\n"); 
96                    ChainListAll(head);
97                    break;
98               case '0':
99                    return 0;
100            }
101        }while(select != '0');
102    }

【程序说明】

·以上代码中,第69行定义一个链表头指针,首先初始化为NULL。

·第71~101行为一个循环,用来显示菜单,并根据用户选择的菜单调用相应的函数进行处理。第72~78行显示系统菜单,第79行接收用户选择的菜单序号,第80~100行根据用户选择的菜单分别调用不同的子模块。

由于本书篇幅所限,这里不列出程序的运行效果,读者可上机运行。

提示 作为实际应用的程序,应该将数据保存到磁盘文件中,供用户随时使用。有关文件操作方面的代码,读者可在本程序的基础上进行扩展。