写给过去的自己-No.1-数据结构篇-单向链表C语言的实现

过去的自己,你好。
    今天我来教你单向链表,不知道你何时会看到这篇文章,也不知道你此刻对C语言的掌握,我会尽可能讲解细一点。
   
讲链表之前我先讲一下数组。相信你不管怎样肯定用过数组对吧,数组就是一种数据的组织形式,特点是所有的数据在逻辑上是顺序摆放的,通过数组名和数组下标
就可以找到某个元素,而且在内存中存放的物理地址也是连续的,定义数组时,一旦中括号中的数字确定了,数组中元素个数也就确定了。那么问题来了,因为数组
的大小需要提前确定,所以定义数组时必须统计好程序中用了多少元素,用多了,数组越界;用少了,浪费内存。
   
链表也是一种逻辑上顺序数据结构,但是他在物理地址上不一定是连续的,那如何确定各个元素之间的关系呢?通过指针来实现。数组中每个元素只有一个部分
---数据(单个数据或结构体),不同于数组,链表的每个元素包含两个部分:数据域和指针域。数据域的作用和数组是一样的,用于存放数据(单个数据或结构
体),而指针域则存放表征逻辑关系的相邻元素的指针,单向链表中即指向下一个节点(链表中的元素称为节点NODE)。举个例子,一个节点是一个有门牌号的
房间,房间中存放在有各种“水果”(数据域)和“下个有水果的房间的门牌号”(指针域)。假设第一个房间中放了猕猴桃,第二个房间放了火龙果,第三个房间
放了橙子,我突然想吃橙子,那我不知不觉就知道了第一个房间号,我去了那个房间,但是我看到了猕猴桃(最不喜欢吃),失望之余我就看了下这个房间中的“下
个有水果的房间的门牌号”,顺着这个门牌号我就来到了第二个房间,可是发现的是火龙果(没吃过),压抑着怒火继续看下一个房间号并去第三个房间,终于在第
三个房间找到了橙子。这样三个房间就组成了一个链表,链表的头总会提供出来便于你从第一个房间开始找。从上面的这个例子你也看出来链表的缺点是不能想数值
那样直接查找数据,必须从头开始查。但是好处就是,你随时可以在最后一个房间中留下“下一个房间的门牌号”来增加链表的长度,这个节点的增加是可以在你代
码运行的过程中进行的(数组是在编译时确定的),这个特性在你日后的模块化编程时会相当有用。链表就介绍这么多,不知道你能理解多少,下面我用C语言实现
一遍,希望你通过代码进一步理解链表的概念和相关的操作。

首先要定义一个节点,如上所说一个节点有数据域和指针域,我们假设数据是个32位无符号整型变量,而指针域则指向下一个节点。

1 typedef struct _LINK_
2 {
3     uint32_t dwData;
4     struct _LINK_ *ptNext;
5 }LINK;

第1行:typedef struct _LINK_ 是个组合,实际上是两个步骤:1)定义一个结构体struct _LINK_; 2)给这个结构体起一个好几的别名typedef struct _LINK_ LINK(用LINK代替struct_LINK_);    
第3行:定义了数据域中的数据,这里之用一个32位无符号整型变量。
第4行:定义了指针域中,这里需要说明一下,既然我已经用typedef将struct
_LINK_替换为LINK,那为什么在这里还要继续用这么麻烦的写法定义ptNext呢?原因是代码编译到这里的时候,还没有完成typedef的工
作,也就是说,编译器优先定义结构体,然后再去typedef,这时的LINK还不能被编译器识别,故这里依旧需要用struct
_LINK_来定义指向与自身类型相同的指针用于存放下一个节点的地址。

完成节点结构体的定义,就要创建一个链表,创建链表之前,需要创建一个节点。创建节点代码如下:

 1 LINK* Alloc_Node(uint32_t dwData)
 2 {
 3     LINK* ptNode = NULL;
 4
 5     ptNode = (LINK*)malloc(sizeof(LINK));
 6
 7     if(NULL != ptNode)
 8     {
 9         ptNode->dwData = dwData;
10         ptNode->ptNext = NULL;
11     }
12     return ptNode;
13 }

第1行:该函数的功能是创建一个节点,创建节点的目的也是为了存放数据,所以函数入参为对应数据域的数据。节点分配成功后,将会返回节点指针,所以函数返回类型定义为LINK*。若节点分配不成功,则会返回NULL。
第2行:定义一个节点指针,用于存放新建节点的地址。
第4行:通过动态分配malloc函数从内存的堆中分配一个大小为LINK结构体大小的空间,并返回这个内存空间的首地址(malloc返回默认类型为
void*,这里需要进行强制类型转换为LINK*)。关于malloc动态分配和堆的相关知识我以后通过其他文章告诉你,你只需要知道这里是人为地从内
存中分配了一块够用的地址给你就行了。
第7行:malloc如果申请内存失败,即内存的堆中没有足够的空间的话,那就会返回NULL,为了避免对空指针的操作,在这里进行一次判断,若ptNode为控指针,则不作任何处理直接到第12行返回NULL,以表征节点创建失败。
第9行:将入参数据填写到新建节点的数据域。
第10行:将新建节点的指针域中下一个节点地址设置为NULL,以表征这个是最后一个节点。这一步非常重要,因为malloc从堆空间获得的空间中,很可
能已经有存在过其它数据,若不对指针域进行赋值,有可能会出现非NULL的情况,导致链表找不到NULL标记而持续寻找下次,在操作非法指针之后便会出
错。
第12行:返回新建节点的指针。

新建节点后,就要将节点加入链表了,单向链表一般在链表尾部增加新节点,所以需要先找到最后一个节点,代码如下:

 1 LINK* Add_Node(LINK* ptNode, uint32_t dwData)
 2 {
 3     LINK* ptNew = NULL;
 4     ptNew = Alloc_Node(dwData);
 5
 6     if(NULL != ptNew)
 7     {
 8         if(NULL != ptNode)
 9         {
10             while (ptNode->ptNext != NULL)
11             {
12                 ptNode = ptNode->ptNext;
13             }
14             ptNode->ptNext = ptNew;
15         }
16     }
17     return ptNew;
18 }

第1行:函数的入参有两个,第一个是链表的头节点地址,之前我说过,链表的访问需要从头开始找,所以提供头节点地址是必要的;第二个参数就新建节点的数据。如果在链表中新增节点成功后,会返回该节点的地址,若不成功,则返回NULL。
第3行:定义一个指针用于存放新建节点的地址。
第4行:通过Alloc_Node函数新建一个节点,并将入参数据增加到新建节点的数据域中。
第6行:判断是否成功新建节点,若不成功,ptNew的值为NULL,函数将直接返回NULL表示链表添加节点失败。
第8行:若新建节点成功,则判断头节点地址是不是NULL,如果是NULL,说明该链表是个空链表,没有任何节点,所以这里需要直接返回新建节点的指针,
而在上层则需要将存放头地址的指针变量赋值该新建节点的指针,以创建第一个节点。这里检查ptNode是否为NULL还有另一个意义,就是在后续的查找
中,需要用到ptNode的指针域,如果ptNode为NULL,则ptNode->ptNext就是个非法操作。
第10行:查询本节点是否是最后一个节点,这个while(ptNode->ptNext != NULL)在链表操作中非常关键。如果本节点的指针域不是NULL,那就不是最后一个节点。
第12行:既然不是最后一个节点,那我们就继续找,将下一个节点的地址作为下次查询的节点。只要还没有找到最后一个节点,就会不停执行该步骤。
第14行:当本节点的指针域为NULL,即本节点为最后一个节点时,第10行的while的条件将不成立,进而执行第14行操作:将新建节点的指针存入链
表最后一个节点的指针域,这样新建节点就变成了链表中最后一个节点了,上文中我也说明了新建节点的指针域必然是NULL,这样就保证了链表最后一个节点的
指针域为NULL。
第17行:如果操作成功,函数返回新建节点的指针。

就这样链表就可以顺利地建立和增加啦,想增加链表时直接使用Add_Node函数即可,想增加几个节点就执行几次。既然链表已经创建好了,我总要使用吧,一个最简单的需求就是我要查找链表中的某个具体数据,这就是链表的查找函数,代码如下:

 1      LINK* Find_Node(LINK* ptNode, uint32_t dwData)
 2     {
 3         while (ptNode != NULL)
 4         {
 5             if (ptNode->dwData == dwData)
 6             {
 7                 return ptNode;
 8             }
 9             ptNode = ptNode->ptNext;
10         }
11         return NULL;
12     }

第1行:和增加链表节点的函数一样,查找函数的入参有两个:一个是头节点指针(很关键,又被使用了),另一个是需要查找的数据。
第3行:又见while,这里是检查本节点是否为空指针,如果是NULL,有两种情况:1)头节点的地址就是NULL,这是个空链表,必须狠狠返回个NULL;2)已经找遍了链表,没有找到有相同数据的节点,此时的NULL实际上是最后一个节点的指针域的地址。
第5行:只要本节点不是NULL,就进行数据判断。
第7行:如果本节点的数据域的数据等于要查找的数据,则返回本节点的指针。
第9行:如果本节点的数据域的数据不等于要查找的数据,则继续找下一个节点。
第11行:如第3行所述,没有找到指定节点后,返回NULL。

写到这里有点累了,已经半夜11点多了,要不要吃完面呢??不过既然这篇文章的本意是为了教育你这个过去的我,那我就做个好榜样吧,避免增肥,抵制诱惑,你也一样要多多锻炼,不要长膘。

好了,我们继续吧,反正你读这篇文章应该没有我写着累,我们就继续吧。上面我们已经完成了链表的查找函数,查找的对象是某一个节点,下面我再写一个统计节点总数的函数,代码如下:

 1      uint8_t Count_Node(LINK* ptNode)
 2     {
 3         uint8_t i = 0;
 4         while(ptNode != NULL)
 5         {
 6             i++;
 7             ptNode = ptNode->ptNext;
 8         }
 9         printf("We have %d node(s)\n", i);
10         return i;
11     }

第1行:函数的入参一如既往地是那个链表的头节点地址,函数返回值为链表所有节点的个数。
第3行:定义临时变量用于统计节点个数。
第4行:老面孔while再次出现,如果本节点为NULL,则说明最后一个节点已经统计过了,对于空链表而言,本身也是NULL,直接返回0个节点。
第6行:既然本节点不是NULL,那么i就自增加。
第7行:既然本节点不是NULL,那么就继续看下一个节点。
第9行:当找到了最后一个节点后,串口打印有多少个节点。
第10行:返回统计的节点数。

笔记本电池快要坚持不住了,今晚就先写到这吧,明天继续讲最后三个链表操作函数。

第二天。。。。
     睡了一觉神清气爽,我们接着讲,昨天我讲了查找函数,实际上就是把链表历遍了一遍,今天我再升级一下--打印整个链表的数据,代码如下:

 1      uint8_t Print_Link(LINK* ptNode)
 2     {
 3         uint8_t i = 0;
 4         while(ptNode != NULL)
 5         {
 6             i++;
 7             printf("Node address is 0x%x, Node data is %d, Next node address is 0x%x.\n", ptNode, ptNode->dwData,ptNode->ptNext);
 8             ptNode = ptNode->ptNext;
 9         }
10         printf("We have %d node(s)\n", i);
11         return i;
12     }

第1行:依旧是需要头指针地址作为参数,我们依旧会统计节点个数并通过函数返回。
第3行:定义临时变量i用于统计节点数量。
第4行:又见while,如果本节点为NULL,则说明最后一个节点已经统计过了,对于空链表而言,本身也是NULL,直接返回0个节点。
第6行:如果本节点不会NULL,则i自增。
第7行:在这里打印当前节点地址,当前节点数据和下一个节点的地址。
第8行:继续找下一个节点。
第10行:打印统计的总节点数。
第11行:返回节点数量。

现在已经完成了节点的创建、增加节点到链表中、查找某个节点、统计链表节点数、打印整个链表,下面讲个稍微复杂点的操作,删除某个节点,代码如下:

 1      void Delet_Node(LINK** ptHd,LINK *ptDel)
 2     {
 3         LINK *ptNode = NULL;
 4         if((ptHd == NULL)||(*ptHd == NULL)||(ptDel == NULL))
 5         {
 6             return;
 7         }
 8         if(*ptHd == ptDel)
 9         {
10             *ptHd = (*ptHd)->ptNext;
11             ptDel->ptNext = NULL;
12             free(ptDel);
13         }
14         else
15         {
16             ptNode = *ptHd;
17             while(ptNode->ptNext != NULL)
18             {
19                 if(ptNode->ptNext == ptDel)
20                 {
21                     ptNode->ptNext = ptNode->ptNext->ptNext;
22                     ptDel->ptNext = NULL;
23                     free(ptDel);
24                     break;
25                 }
26                 ptNode = ptNode->ptNext;
27             }
28         }
29         return;
30     }

第1行:留心一下这里的第一个入参LINK** ptHd,因为你花了好久才搞明白LINK** 的含义(或许现在的我也没有搞明白)。首先LINK* 定义的变量时个指针,这个指针指向的是一个节点,那LINK**定义的变量也是个指针,这个指针指向一个变量,这个变量里存放着一个指向节点的指针(有点 绕,多读几遍)。简单说来,LINK* ptHd,ptHd里的数据是头节点的地址,而LINK**  ptHd,ptHd里的数据就是存放头节点地址的地址(还是有点绕),用上面的例子来讲,LINK* ptHd中的ptHd是第一个房间的门牌号,而LINK** ptHd中的ptHd就是盒子的位置,这个盒子里只有第一个房间的门牌号,只要你知道这个盒子在哪里,你就能知道第一个房间在哪里。也就是说,你在使用链 表的时候会知道两个地址:一个是头节点的地址,另一个是存放头节点地址的地址。那为什么要这样做呢,不是一般有头节点地址就可以了吗?是的,如果你不对头 节点进行删除,是可以这么做,但如果你要删除的节点就是头节点的话,你需要将第二个节点作为头节点进行保存,即需要更新存放头节点地址的变量的值,所以你 需要将这个变量的地址作为入参传入函数;那么第二个入参就是需要删除的节点的地址。
第3行:定义一个节点指针,方便后面节点查找。
第4行:对入参合法性进行判断。
第6行:如果ptHd为NULL,即盒子的地址就是没有的,显然没有办法继续操作,直接return;如果*ptHd为NULL,即链表时空链表,不能继续操作,直接return;如果ptDel为NULL,即没有给你需要删除的节点地址,就是在耍你,果断return。
第8行:判断需要删除的节点是不是头节点,头节点的删除操作和其他节点的删除操作有区别。
第10行:如果需要删除的是头节点,那需要将头节点的下一个节点的地址存放在存储头节点的变量里。
第11行:既然原来的第二个节点已经变成头节点了,这是就可以放心操作原来的头节点了,将原头节点的指针域修改为NULL,这样这个节点就和链表再无瓜
葛。这里注意第10行和第11行操作顺序,如果反过来先将原来头节点的指针域修改为NULL,那就再也找不到之后的节点了,所以要“先过河,再拆桥”(你
三国杀玩的还是不错的)。
第12行:既然这个节点已经准备删除了,那么这个节点所用的内存也应该释放,因为节点是malloc动态分配出来的,所以这里需要通过free来主动释
放。这样这个节点的堆空间可以被重新使用,很有可能会投胎转世变成末尾节点,这个你可以自己试试,创建个3个节点的链表,删除头节点,再增加个节点,很有
可能会发现被删除的头结点投胎做末尾节点了。
第14行:如果需要删除的节点不是头结点,那需要另一个方式进行操作。
第16行:获取头节点地址。
第17行:又又见while,如果本节点的下一个节点的地址为NULL,说明本节点已经是最后一个节点了,在上一次对比检测中已经确认最后一个节点也不是需要删除的节点,及链表中没有找到要删除的节点,直接退出。
第19行:判断本节点的下一个节点是否为需要删除的节点。
第21行:如果本节点的下一个节点是需要删除的节点,那就先过河:将下个节点的下个节点地址给本节点的指针域。这里多讲一下,为什么用
ptNode->ptNext,而不是用ptNode呢。这里涉及到ptNode的性质,ptNode只是一个临时变量,用于保持当前节点的地址,
如果要删除本节点,那必须修改上一个节点的指针域为下一个节点的地址,如果用ptNode的方式就无法找到上一个节点,这个就是单向链表的缺点。故这里用
ptNode->ptNext来进行查找,这样当找到需要删除的节点时,其实找到的是需要删除节点的上一个节点,那么就可以对
ptNode->ptNext进行操作了(留个小问题给你,如果这行改为ptNode =
ptNode->ptNext会怎样)。用这种方法进行查找会跳过头节点,但是我们已经在开始的时候就判断是否为头节点,故放心继续吧。
第22行:上面已经过河了,那么就可以拆桥了:将需要删除的节点的指针域改为NULL,此生不相见。
第23行:彻底消失,这辈子没机会再见了,下辈子吧。
第24行:既然已经删除成功,那么就不要有所留恋,直接break再返回吧。
第26行:如果我们查找的节点不是需要删除的节点,那么继续查找下一个。

一个删除节点的操作占了这么大篇幅,正所谓请神容易送神难嘛。在坚持一下,既然我们已经成功删除某个节点了,那么要不删除这个链表试试?下面有请现场代码的报道。“代码你好,请问现场是什么样的?”, “主持人你好,现场是我这个样的”:

 1      void Delet_Link(LINK** ptHd)
 2     {
 3         LINK* ptNode = NULL;
 4         LINK* ptDel = NULL;
 5         if((ptHd == NULL)||(*ptHd == NULL))
 6         {
 7             return;
 8         }
 9         ptNode = *ptHd;
10         while(ptNode != NULL)
11         {
12             ptDel = ptNode;
13             ptNode = ptNode->ptNext;
14             ptDel->ptNext = NULL;
15             free(ptDel);
16         }
17         *ptHd = NULL;
18         return;
19     }

第1行:一回生,二回熟,三回手牵手,再次见到LINK**,不多解释,看上删除某个节点函数的解释。
第3行:定义临时变量ptNode便于查找节点。
第4行:定义临时变量ptDel用于保存需要删除的节点。
第5行:入参合法性检查。
第7行:如果ptHd是NULL,说明上层调用者忘记提供参数;如果*ptHd为NULL,说明是个空链表,果断return。
第9行:将头节点地址给ptNode。
第10行:Hi, WHILE, how old are you!(怎么老是你),判断本节点是否为NULL,如果为NULL说明链表已经历遍完成。
第12行:备份当前节点的地址给ptDel
第13行:既然已经有backup了,就义无反顾地迈向下一个节点吧。
第14行:“报告ptDel书记,群众ptHd已经成功转移,请ptDel书记指示”,“好,乡亲们安全我也就放心了,切断后路,让鬼子永远找到乡亲们!!”。
第15行:“报告ptDel书记,后路已经切断,接下来的任务请指示!”,“和鬼子同归于尽!!同志们,我们来世继续为祖国效力!!”。
第17行:尘归尘、土归土,英雄们留下了未来给后人,缺没有留下名字,即使那个当初领导大家的节点,也毅然地将自己的名字从历史上划去,留下了个充满希望的NULL给后人成长 -剧终-。

关于单向链表就写这么多吧,感觉越写越不正经了,再写下去就少儿不宜了,呵呵。最后再说一下,你之前看的ZigBee协议栈Z-stack中的时间功能就
是用链表实现的,还有很多小型OS或协议栈都用到了链表,所以我建议你先好好把链表掌握再去啃这些OS/协议栈。对了,你日后会自己写一个硬件无相关的软
件定时器模块,就是用的链表,不过有些不完善的地方,希望你看完这篇文章后再认真修改一下。好了,就这样吧。

时间: 2024-10-03 19:32:34

写给过去的自己-No.1-数据结构篇-单向链表C语言的实现的相关文章

c语言:写一个函数建立一个有3名学生数据的单向动态链表

写一个函数建立一个有3名学生数据的单向动态链表. 解:程序: #include<stdio.h> #include<stdlib.h> #define LEN sizeof(struct Student) struct Student { long num; float score; struct Student *next; }; int n; struct Student *creat(void)//定义函数返回一个指向链表头的指针 { struct Student *head

JAVA写个东西读取TXT中的数据 且要计算出平均值和总值 最后还要按总值排序

AVA写个东西读取TXT中的数据 且要计算出平均值和总值 最后还要按总值排序 例如:要计算a.txt文档中内容可如下: 学号 姓名    语文 数学 英语 平均值 总值 排序 1    肯德基   90   98   97 2    经典款   98   97   92 3    肯德的   93   92   97 import java.io.*; import java.io.File; import java.util.ArrayList; import java.util.Iterat

菜鸟nginx源码剖析数据结构篇(十一) 共享内存ngx_shm_t[转]

菜鸟nginx源码剖析数据结构篇(十一) 共享内存ngx_shm_t Author:Echo Chen(陈斌) Email:[email protected] Blog:Blog.csdn.net/chen19870707 Date:Nov 14th, 2014 1.共享内存 共享内存是Linux下提供的最基本的进程通信方法,它通过mmap或者shmget系统调用在内存中创建了一块连续的线性地址空间,而通过munmap或者shmdt系统调用释放这块内存,使用共享内存的好处是多个进程使用同一块内存

单向链表实现多项式加和乘--自己写数据结构

用单项链表实现多项式数据结构和代码如下(由于时间原因多项式乘法的函数没用实现,读者可以在自己完善): 存放结构体的头文件polylist.h #ifndef _H_POLYLIST_ #define _H_POLYLIST_ typedef struct _Poly_Node { int ratio; //系数 int power; //幂 struct _Poly_Node* next; }Node,*pNode; typedef struct _Poly_List { struct _Pol

C语言:创建动态单向链表,创建完成后,输出每一个节点的数据信息。

// //  main.c //  dynamic_link_list // //  Created by ma c on 15/8/5. //  Copyright (c) 2015年 bjsxt. All rights reserved. //  要求:写一个函数建立有3名学生数据的动态单向链表,并输出链表中每个结点的所有内容. /* 建立动态链表的思想: 1.开辟一个新结点,并使p1,p2指向它: 2.读入一个学生数据给p1所指的结点: 3.head = NULL,n = 0; 4.判断读

一步一步写算法(之单向链表)

原文:一步一步写算法(之单向链表) [ 声明:版权所有,欢迎转载,请勿用于商业用途.  联系信箱:feixiaoxing @163.com] 有的时候,处于内存中的数据并不是连续的.那么这时候,我们就需要在数据结构中添加一个属性,这个属性会记录下面一个数据的地址.有了这个地址之后,所有的数据就像一条链子一样串起来了,那么这个地址属性就起到了穿线连结的作用. 相比较普通的线性结构,链表结构的优势是什么呢?我们可以总结一下: (1)单个节点创建非常方便,普通的线性内存通常在创建的时候就需要设定数据的

一步一步写算法(之循环单向链表)

原文:一步一步写算法(之循环单向链表) [ 声明:版权所有,欢迎转载,请勿用于商业用途.  联系信箱:feixiaoxing @163.com] 前面的博客中,我们曾经有一篇专门讲到单向链表的内容.那么今天讨论的链表和上次讨论的链表有什么不同呢?重点就在这个"循环"上面.有了循环,意味着我们可以从任何一个链表节点开始工作,可以把root定在任何链表节点上面,可以从任意一个链表节点访问数据,这就是循环的优势. 那么在实现过程中,循环单向链表有什么不同? 1)打印链表数据 void pri

C/C++笔试忍法帖05——数据结构篇

1.写出下列算法的时间复杂度. (1)冒泡排序: O(n^2) (2)选择排序: 直接选择排序是O(n^2) (3)插入排序:直接插入排序是 O(n^2) (4)快速排序: O(nlog2 n) (5)堆排序:   O(nlog2 n) (6)归并排序: O(nlog2 n) 2.编程,请实现一个c语言中类似atoi的函数功能(输入可能包含非数字和空格) #include <stdio.h> int isspace(int x) { if ((x == 0)||(x == '\t') || (

菜鸟nginx源代码剖析数据结构篇(八) 缓冲区链表ngx_chain_t

菜鸟nginx源代码剖析数据结构篇(八) 缓冲区链表 ngx_chain_t Author:Echo Chen(陈斌) Email:[email protected]mail.com Blog:Blog.csdn.net/chen19870707 Date:Nov 6th, 2014 1.缓冲区链表结构ngx_chain_t和ngx_buf_t nginx的缓冲区链表例如以下图所看到的.ngx_chain_t为链表.ngx_buf_t为缓冲区结点: 2.源码位置 头文件:http://trac.