STM32 HAL开发完全指南

写文章

一步一步教你从零开始写C语言链表(超详细)

杨源鑫

嵌入式系统工程师、物联网创业合伙人,业务经理兼产品经理

285 人赞同了该文章

为什么要学习链表?

链表主要有以下几大特性:

1、解决数组无法存储多种数据类型的问题。

2、解决数组中,元素个数无法改变的限制(C99的变长数组,C++也有变长数组可以实现)。

3、数组移动元素的过程中,要对元素进行大范围的移动,很耗时间,效率也不高。

先来感性的认识一下链表,我们先来认识下简单的链表:

从这幅图我们得出以下信息:

这个简单链表的构成:

头指针(Header),若干个节点(节点包括了数据域和指针域),最后一个节点要指向空。

实现原理:头指针指向链表的第一个节点,然后第一个节点中的指针指向下一个节点,然后依次指到最后一个节点,这样就构成了一条链表。

接下来看看链表的数据结构:

struct  list_node
{int data ; //数据域,用于存储数据struct list_node *next ; //指针,可以用来访问节点数据,也可以遍历,指向下一个节点
};

那么如何来创建一个链表的一个节点呢?我们写个程序演示一下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct  list_node
{int data ; struct list_node *next ;
};typedef struct list_node list_single ;    int main(void)
{list_single *node = NULL ;          //1、首先,当然是定义一个头指针node = (list_single *)malloc(sizeof(list_single)); //2、然后分配内存空间if(node == NULL){printf("malloc fair!\n");}memset(node,0,sizeof(list_single)); //3、清一下node->data = 100 ;                  //4、给链表节点的数据赋值node->next = NULL ;                 //5、将链表的指针域指向空printf("%d\n",node->data);free(node);return 0 ;
}

那么,这仅仅只是创建一个链表中的一个节点,为了好看,我们把创建节点封装成函数,以后想创建多少个节点,我们就可以反复调用一个函数来创建,会很方便。

list_single *create_list_node(int data)
{list_single *node = NULL ;node = (list_single *)malloc(sizeof(list_single));if(node == NULL){printf("malloc fair!\n");}memset(node,0,sizeof(list_single));node->data = 100 ;node->next = NULL ;return node ;
}

接下来完善第一个程序:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct  list_node
{int data ; struct list_node *next ;
};typedef struct list_node list_single ;
list_single *create_list_node(int data)
{list_single *node = NULL ;node = (list_single *)malloc(sizeof(list_single));if(node == NULL){printf("malloc fair!\n");}memset(node,0,sizeof(list_single));node->data = data;node->next = NULL ;return node ;
}
int main(void)
{int data = 100 ;list_single *node_ptr = create_list_node(data); //创建一个节点printf("node_ptr->data=%d\n",node_ptr->data);   //打印节点里的数据printf("node_ptr->next=%d\n",node_ptr->next);  free(node_ptr);return 0 ;
}

执行结果 :

这样我们就完成一个链表节点的创建了,那么它现在的样子如下图:

链表的结构里,数据存储了100,因为这个链表只有一个节点,所以它的指针域指向了NULL。

上面只是建立一个单链表的基本雏形,接下来咱们再来增加一点难度。如果创建多个单链表节点,实现单链表的增删改查?把单链表应用起来。

1、首先定义一个单链表的数据结构

创建节点函数原型可定义如下:

struct list *create_node(int data) ;

如何创建单链表的节点,主要分以下步骤:

(1)给当前的每个节点的数据结构配置定量的空间大小

ep : struct list *node = malloc(sizeof(struct list));

(2)清节点数据(由于结构体变量在未初始化的时候,数据是脏的)

ep : memset(node,0,sizeof(struct list));

(3)给节点初始化数据

ep : node->id = data ; 

(4)将该节点的指针域设置为NULL

ep : node->next = NULL ;

2、单链表的尾插

尾插节点函数原型可定义如下:

如何将当前链表和新的节点相连接?只要实现:

header->next = new;

尾插流程如下:

(1)获取当前节点的位置,也就是访问头节点

ep : struct list *p = header ;

(2)判断是否为最后一个节点,如果不是,移动到下一个节点,如果是,将数据插入尾部。

ep : while(NULL != p->next) p = p->next ;p->next = new ;

3、单链表的头插

很好理解,头插就是把新的节点插在原来的节点和原来节点的下一个节点之间的一个节点。如图,新的节点插在头节点和节点1。

所以可以推出头插流程如下:

(1)获取当前节点的位置,也就是访问头节点

ep : struct list *p = header ;

(2)新的节点的下一个节点设置为原来头节点的下一个节点(第一个节点)

ep : new->next = p->next ;

(3)原来的头节点的下一个节点设置为现在新插入的头节点

ep : p->next = new ;

4、单链表的遍历

如图为一条单向链表的模型,看图知道该链表由头节点和若干个节点组成,最后一个节点(尾节点)为NULL 。

从图中可以得出信息,如果我们要打印出各个节点的数据,要考虑以下问题:

(1)需要打印头节点吗?(头节点肯定是不用打印的,因为这是我们为了操作方便而设置的一个节点)。

(2)这条链表有多少个节点我们怎么知道?(通过判断该链表是否已经到达了尾节点,标志就是NULL)

那么可以得到流程如下:

(1)获取当前节点的位置,也就是访问头节点

ep : struct list *p = header ;

(2)由于头节点我们不需要去打印它,这时候,初始化打印的节点需要从第一个节点开始。

ep : p = p->next ;  

(3)判断是否为最后一个节点,如果不是,先打印第一个节点的数据(1),然后移动到下一个节点(2),重复这两个步骤。

如果是最后一个节点,直接打印数据即可。

while(NULL != p->next){ printf("node:%d\n",p->data) ; p = p->next ;}printf("node:%d\n",p->data);

当然还可以一句代码解决,这样就达到了先偏移,后取数据。

while(NULL != p->next)
{ p = p->next ; printf("node:%d\n",p->data) ;
}

5、单链表的删除

删除节点的函数原型可定义如下:

int detele_list_node(struct list *pH , int data);

单向链表的删除要考虑两种情况,一种的普通节点的删除(当然,头节点不能算)

还有一种是尾节点的前一个节点的删除情况,注意,删除完节点还需要释放对应节点的内存空间。

删除节点的设计流程:

(1)先定义两个指针,一个表示当前的节点,另一个表示当前节点的上一个节点。

ep : struct list *p = header ;  //当前节点struct list *prev = NULL ; //当前节点的上一个节点

(2)遍历整个链表,同时保存当前节点的前一个节点

 ep : while(NULL != p->next){ //保存了当前的节点的前一个节点prev = p ;  //保存当前偏移的节点p = p->next ; return 0 ;}

(3)在遍历的过程中查找要删除的数据

ep : while(NULL != p->next){ //保存了当前的节点的前一个节点prev = p ;  //保存当前偏移的节点p = p->next ; //查找到了数据if(p->id == data){ }return 0 ;}

(4)查找到了数据后,分两种情况删除

ep : 普通节点的删除if(p->id == data){prev->next = p->next ;free(p);}
ep : 考虑尾节点的下一个节点为NULL的节点删除if(p->id == data){if(p->next == NULL){prev->next = NULL ;free(p);}}

6、单链表的逆序

逆序步骤:

单链表的基本操作流程咱们基本搞懂了,下面写一个程序,这将会变得非常非常的简单。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct slist
{int id ;struct slist *next ;
}L;//创建一个节点
L *create_node(int data)
{//给每个节点分配结构体一样的空间大小 L *p = malloc(sizeof(L));if(NULL == p){printf("malloc error!\n");return NULL ;}//由于结构体在未初始化的时候一样是脏数据,所以要清 memset(p,0,sizeof(L));//初始化第一个节点 p->id = data ; //将节点的后继指针设置为NULL p->next = NULL ;
}//链表的尾插
void tail_insert(L *pH , L *new)
{//获取当前的位置 L *p = pH ; //如果当前位置的下一个节点不为空 while(NULL != p->next){//移动到下一个节点 p = p->next ;}//如果跳出以上循环,所以已经到了NULL的这个位置//此时直接把新插入的节点赋值给NULL这个位置 p->next = new ;
}//链表的头插
void top_insert(L *pH , L *new)
{L *p = pH ;new->next = p->next ;p->next = new ;
}//链表的遍历
void Print_node(L *pH)
{//获取当前的位置 L *p = pH ;//获取第一个节点的位置 p = p->next ;//如果当前位置的下一个节点不为空 while(NULL != p->next){//(1)打印节点的数据 printf("id:%d\n",p->id);//(2)移动到下一个节点,如果条件仍为真,则重复(1),再(2) p = p->next ;}//如果当前位置的下一个节点为空,则打印数据//说明只有一个节点 printf("id:%d\n",p->id);
}//删除链表中的节点
int detele_list_node(L * pH , int data)
{//获取当前头节点的位置 L *p = pH ;L *prev = NULL;while(NULL != p->next){//保存当前节点的前一个节点的指针 prev = p ;//然后让当前的指针继续往后移动 p = p->next ;    //判断,找到了要删除的数据  if(p->id == data){//两种情况,一种是普通节点,还有一种是尾节点if(p->next != NULL)  //普通节点的情况 {prev->next = p->next ;free(p);}else //尾节点的情况 {prev->next = NULL ; //将这个尾节点的上一个节点的指针域指向空 free(p); }return 0  ;}}printf("没有要删除的节点\n");return -1 ;
}void trave_list(L * pH)
{//保存第一个节点的位置 L *p = pH->next;L *pBack;int i = 0 ;if(p->next == NULL || p == NULL)return ;while(NULL != p->next) //遍历链表 {//保存第一个节点的下一个节点 pBack = p->next ; //找到第一个有效节点,其实就是头指针的下一个节点 if(p == pH->next) {//第一个有效节点就是最后一个节点,所以要指向NULL p->next = NULL ; } else{/*new->next = p->next ;p->next = new ;*/p->next = pH->next ; //尾部连接 }pH->next = p ; //头部连接 p = pBack ; //走下一个节点 }top_insert(pH,p); //插入最后一个节点
}int main(int argc , char **argv)
{//创建第一个节点 int i ;L *header = create_node(0); for(i = 1 ; i < 10 ; i++){tail_insert(header,create_node(i));}Print_node(header);detele_list_node(header,5);putchar('\n');Print_node(header);putchar('\n');trave_list(header);Print_node(header);return 0 ;
}

运行结果:

当然,单链表存在一定的弊端,就是查找数据和删除数据的时候比较麻烦,而双链表的出现就是为了解决它的弊端:

双链表的引入是为了解决单链表的不足:

(1)双链表可以往前遍历,也可以往后遍历,具有两个方向

双链表的节点 = 有效数据 + 两个指针(分别指向前一个节点和后一个节点)

双向链表的图形结构描述:

struct double_list                                  struct double_list
{                                             {数据域;                  ep :------->            int data ;指针域(前向指针) ;                               struct double_list *prev ;指针域(后向指针) ;                               struct double_list *next ;
};                                             };

1、双向链表的创建

struct list *create_node(int data) ;

创建步骤(与单链表类似,就是多了一个指针):

(1)给节点申请空间:

ep : struct double_list *p = malloc(sizeof(struct double_list));

(2)初始化数据域

ep : p->data = data ;

(3)初始化指针域

ep : p->prev = NULL ; p->next = NULL ;

2、双向链表的尾插

双向链表尾插节点的函数可以定义如下:

void double_list_tail_insert(struct double_list *header , struct double_list *new) ;

尾插的步骤:

(1)找到链表的尾节点

ep : 和单链表差不多DL *p = header ;while(NULL != p->next)p = p->next ;

(2)将新的节点连接到尾节点的后面成为新的节点

1.原来的next指针指向新节点的首地址。

p->next = new ;

2.新节点的prev指针指向原来的尾节点的首地址。

 new->prev = p;

3、双向链表的头插

双向链表头插节点的函数可以定义如下:

void double_list_top_insert(struct double_list *header , struct double_list *new) ;

4、双向链表的遍历

4.1 正向遍历

void double_list_for_each(DL *header)

步骤:和单链表完全一致,没什么好写的。

4.2 反向遍历

void double_list_for_each_nx(DL *header)

步骤:

(1)和单链表一样,先循环找到最后一个节点的地址

(2)再依靠prev指针循环往前移动

2.1 先打印最后一个数据

ep : printf("%d ",p->data);

2.2 向前循环遍历

ep : p = p->prev ;     

判断条件:

header->prev != p->prev,

header保存的是头节点的地址,

header->prev保存的是头节点的prev的地址,

header->next保存的是头节点的next的地址,

头节点在创建的时候:

header->prev = NULL ;
header->next = NULL ;

所以这个条件这样写header->prev = NULL也是对的。

5、双向链表节点的删除

假设需要删除节点1:

首先:

(1)获取当前节点的地址:

ep : p = header;

(2)遍历所有的节点,找到要删除的节点:

ep :
while(NULL != p->next)
{p = p->next ;if(p->data == data){}
}

(3)找到要删除的数据以后,需要做判断,判断两种情况,这和单链表差不多

3.1 如果找到当前节点的下一个节点不为空

3.1.1

那就把当前节点的prev节点指向要删除的这个节点的prev

因为当前的prev节点保存的是要删除的上一个节点的指针

  p->next->prev = p->prev ;

3.1.2

然后将当前节点的prev指针(也就是上一个节点的指针)指向当前节点(要删除的)的下一个节点:

 p->prev->next = p->next ;

3.1.3

最后释放删除指针的空间:

 free(p);       

3.2 如果找到当前节点的下一个节点为空

3.2.1

直接把当前指针(要删除的节点)的prev指针(保存着当前指针的上一个节点的地址)的下一个next指针设置为空。

p->prev->next = NULL ;

3.2.2

将删除的指针的空间释放:

 free(p);

看来,双链表学起来比单链表容易多了!确实啊,多了一个方向,操作起来就更加容易了,但是多了一个方向,一维多了一个指针,相比增加了一定的复杂度,但是,只要牢记prev指针和next指针的指向,那么,手画一图,代码即可写出!

下面给一个案例实现一下双向链表:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//创建一个双链表的数据结构
typedef struct __double_list
{int data ;struct __double_list *prev ;struct __double_list *next ;
}DL ; //创建双向链表并插入一个节点
DL *create_dl_node(int data)
{DL *p = malloc(sizeof(DL));if(NULL == p){printf("create dl node fair!\n");return NULL ;}//初始化数据 p->data = data ;//初始化指针 p->next = NULL ;p->prev = NULL ;
}//双向链表的尾插
void double_list_tail_insert(DL *header , DL *new)
{//取得当前节点的地址 DL *p = header ;//找到链表的尾节点 while(NULL != p->next){//找不到接着找 p = p->next ;}//找到了尾节点,指向新节点的首地址 p->next = new ;//新节点的prev指针指向原来的尾节点的首地址。new->prev = p;
}//双向链表的头插(也就是插在两个节点之间的插入方式)
void double_list_top_insert(DL *header , DL *new)
{//新节点的next指针指向原来的节点一的地址new->next = header->next ; //判断当前节点的下一个节点的地址是否为空if(NULL != header->next) header->next->prev = new ; //节点1的prev指针指向新节点的地址 header->next = new ;new->prev = header ;
}//双向链表的正向遍历
void double_list_for_each(DL *header)
{DL *p = header ;while(NULL != p->next){p = p->next ;printf("%d ",p->data);}
}//双向链表的反向遍历
void double_list_for_each_nx(DL *header)
{DL *p = header ;//先找到尾节点while(NULL != p->next){p = p->next ;  } //已经找到了尾节点,向前遍历,注意,遍历到头节点之前//限制条件: != 头结点 while(NULL != p->prev){printf("%d ",p->data);p = p->prev ;}
}//双向链表节点的删除
int double_list_delete_node(DL *header , int data)
{//取得当前节点 DL *p = header;//遍历所有的节点 while(NULL != p->next){p = p->next ;//找到了对应要删除的数据了 if(p->data == data){//一样存在两种情况//(1)当前节点的下一个节点不为空if(p->next != NULL){//那就把当前节点的prev节点指向要删除的这个节点的prev//因为当前的prev节点保存的是要删除的上一个节点的指针 p->next->prev = p->prev ;//还要指定它的next节点 p->prev->next = p->next ;free(p);}//(2)当前节点的下一个节点为空 else{//把 p->prev->next = NULL ;free(p); }return 0 ;}}printf("\n没有找到对应要删除的节点,或者节点已经被删除!\n");return -1 ;
} int main(void)
{int i ;DL *header = create_dl_node(0);for(i = 0 ; i < 10 ; i++){//双向链表的尾插 //double_list_tail_insert(header,create_dl_node(i));//双向链表的头插 double_list_top_insert(header,create_dl_node(i));}//双向链表的正向遍历 printf("双向链表的正向遍历:");double_list_delete_node(header,5);double_list_for_each(header);
//  double_list_delete_node(header,9);double_list_delete_node(header,5);putchar('\n');printf("双向链表的反向遍历:");double_list_for_each_nx(header);return 0 ;
}

运行结果:

发布于 2019-10-07

一步一步教你从零开始写C语言链表(超详细)相关推荐

  1. 链表的数据域怎么使用结构体_一步一步教你从零开始写C语言链表

    为什么要学习链表? 链表主要有以下几大特性: 1.解决数组无法存储多种数据类型的问题. 2.解决数组中,元素个数无法改变的限制(C99的变长数组,C++也有变长数组可以实现). 3.数组移动元素的过程 ...

  2. 是单向链表吗_一步一步教你从零开始写C语言链表

    为什么要学习链表? 链表主要有以下几大特性: 1.解决数组无法存储多种数据类型的问题. 2.解决数组中,元素个数无法改变的限制(C99的变长数组,C++也有变长数组可以实现). 3.数组移动元素的过程 ...

  3. 10分钟教你用python打造贪吃蛇超详细教程

    更多精彩尽在微信公众号[程序猿声] 10分钟教你用python打造贪吃蛇超详细教程 在家闲着没妹子约, 刚好最近又学了一下python,听说pygame挺好玩的.今天就在家研究一下, 弄了个贪吃蛇出来 ...

  4. 从零开始写一个RPC框架的详细步骤

    http://blog.csdn.net/liu88010988/article/details/51547592 定位 所谓定位就是回答几个问题,我出于什么目的要写一个框架,我的这个框架是干什么的, ...

  5. 读者写者问题(超详细讲解)看不懂算我输

    文章目录 读者写者问题 读者优先 1.初始化 2.写者 3.读者 写者优先 1.初始化 2.读者 3.写者 4.考虑读者1->写者1->读者2的情况 读者写者问题 1.允许多个读者可以同时 ...

  6. 编程语言python入门-手把手教你从零开始用Python语言写爬虫程序

    简单来说互联网是由一个个站点和网络设备组成的大网,我们通过浏览器访问站点,站点把HTML.JS.CSS代码返回给浏览器,这些代码经过浏览器解析.渲染,将丰富多彩的网页呈现我们眼前.如果我们把互联网比作 ...

  7. 一步步教你搭建SSM整合+前提配置超详细版(IDEA版本)

    为了应付学校的实训课作业,开始自学SSM框架-- 我!太南了!! 所以,从下午两点写到现在晚上九点,终于搭出了这个框架并成功运行(其实就是一直运行不出来). 为了这八个小时的奋战,我都得记录这个艰难的 ...

  8. mybatis 使用in 查询时报错_不会Mybatis?一文教你手写实现Mybatis(超详细),吊打面试官!...

    一.MyBatis核心组件 在开始实现我们的mybatis框架之前我觉得有必要先学习一下[MyBatis核心组件],如下示意图(出自前文),在图中可以了解到更多的细节. 二.MyBatis手写实现 1 ...

  9. java 压缩二进制流_Java:自己动手写压缩软件,超详细解释(哈夫曼实现)

    三.哈夫曼编码生成步骤: ① 扫描要压缩的文件,对字符出现的频率进行计算. ② 把字符按出现的频率进行排序,组成一个队列. ③ 把出现频率最低(权值)的两个字符作为叶子节点,它们的权值之和为根节点组成 ...

最新文章

  1. java数据结构教程_Java数据结构
  2. 自动化测试框架cucumber_基于Cucumber和Testng的WebUI自动化测试方法与流程
  3. 智能音箱AEC中的回采信号
  4. WiFiDemon – iOS WiFi RCE 0-Day漏洞利用
  5. filter函数的用法_JavaScript中forEach和filter的用法和原理
  6. 前端笔试能查吗_老码农的字节跳动前端面试总结
  7. 说说基于网络的五种IO模型
  8. spring batch_Spring Batch作为Wildfly模块
  9. MyEclipse的Debug功能最基本的操作
  10. mysql去重的最方便的两种方法_mysql去重的最方便的两种方法
  11. php网页部分截图,关于php:PHP实现网页截图
  12. cad没有命令输入框_CAD命令,教您CAD命令栏不见了怎么调出来
  13. uniapp 设置ios safri浏览器 添加到主屏幕 自定义图片及名称
  14. 三角公式(三角函数)
  15. 计算机视觉 图像形成 几何图形和变换
  16. 全排列问题(AcWing 823. 排列)
  17. 北斗短报文和北斗定位入门篇
  18. 使用Arduino开发板进行语音识别
  19. MySQL自定义中文转拼音函数
  20. C++ 策略模式的具体案例与优缺点

热门文章

  1. 超越影像——RSNA 2016参展随感 (三.完结)
  2. MySQL添加用户并授予权限
  3. java char和int的区别_int类型和char类型的区别
  4. java -双列表LinkedHashMap
  5. AVPlayer 真机测试没有声音
  6. 高仿网易云音乐(vue实战项目)
  7. 词向量与词向量拼接_中文字词向量和方面词向量联合嵌入情感分析方法与流程...
  8. 大数据丶机器学习丶人工智能的区别与联系
  9. 经典回顾《阿甘正传》之音乐篇(三)
  10. 不是第七代的 Win 7