链表可以说是一种最为基础的数据结构。链表由一组元素以一种特定的顺序组合或链接而成,在维护数据的集合时很有用。这一点同我们常用的数组很相似。然而,链表在很多情况下比数组更有优势。特别是在执行插入和删除操作时链表拥有更高的效率。链表需要动态的开辟存储空间,也就是存储空间是在程序运行时分配的。由于在很多应用中数据的大小在编译时并不能确定,因此这种动态分配空间的特性也是链表的一个优点。

单链表介绍

单链表(简称为链表)由各个元素之间通过一个指针彼此链接起来而组成。每个元素包含两个部分:数据成员和一个称为next的指针。通过采用这种二成员的结构,将每个元素的next指针设置为指向其后面的元素(见图1)。最后一个元素的next指针设置为NULL,简单的表示链表的尾端。链表开始处的元素是“头”,链表未尾的元素称为尾。

要访问链表中的某个元素,从链表头开始,通过next指针从一个元素到另一个元素连续地遍历直到找到所需要的那个元素为止。以单链表来说,只能以一个方向进行遍历:从头到尾,因为每个元素并没有维护指向其前一个元素的链接。

从概念上说,可以把链表想象成一系列连续的元素。然而,由于这些元素是动态分配的(在C语言中调用malloc),因此很重要的一点是,切记这些元素通常实际上都是分散在内存空间中的(见图2)。元素与元素之前的链接关系只是为了确保所有的元素都可以访问到。带着这种思考,我们将会看到当维护元素之间的链接信息时需要特别小心。如果我们错误地丢失了一个链接,则从这个位置开始,往后的所有元素都无法访问到了。“你的弱点有多弱,你的强度就有多强”,非常适用于描述链表的特点。

单链表接口的定义

list_init                                                                         

void list_init(List *list,void (*destroy)(void *data));

返回值 无

描述  初始化由list指定的链表。

该函数必须在链表做其他操作之前调用。当调用list_destroy时,destroy参数提供了一种释放动态分配的数据的方法。例如,如果链表包含采用malloc动态分配的数据,当链表被销毁时,destroy应该设置为free用来释放数据。对于包含好几个动态分配成员的结构化数据,destroy应该设置为一个用户自定义的析构函数,通过对每一个动态分配的成员以及对结构体自身调用free来释放数据。如果链表包含不应该释放的数据,destroy应该设置为null。

复杂度   O(1)

list_destroy                         

void list_destroy(List *list);

返回值 无

描述  销毁由参数list指定的链表。

调用list_destroy后任何其他的操作都不允许执行,除非再次调用list_init。list_destroy将链表中所有的元素都移除,如果传给list_init的参数destroy不为null,则移除链表中每个元素时都调用该函数一次。

复杂度  O(n),n代表链表中的元素个数


list_ins_next                                                                                         

int list_ins_next(List *list,ListElmt *element,const void *data);

返回值  如果插入元素成功则返回0,否则返回-1.

描述  在list指定的链表中element后面插入一个新元素。

如果element设置为NULL,则新元素插入链表头部。新元素包含一个指向data的指针,因此只要该元素还在链表中,data所引用的内存空间就应该保持合法。管理data所引用的存储空间是调用者的责任。

复杂度  O(1)

list_rem_next                                                                                

int list_rem_next(List *list,ListElmt *element,void **data);

返回值  如果移除元素成功则返回0,否则返回-1.

描述  移除由list指定的链表中element后面的那个元素。

如果element被设置为NULL,则移除链表头元素。调用返回后,data指向已移除元素中存储的数据。由调用者负责管理data所引用的存储空间。

复杂度  O(1)


list_size                                    

int list_size(const List *list);

返回值  链表中元素的个数。

描述  这是一个宏,用来计算由参数list指定的链表中的元素的个数。

复杂度  O(1)


list_head                                               

ListElmt *list_head(const List *list);

返回值  指向链表中头元素的指针。

描述  这是一个宏,返回由参数list指定的链表中头元素的指针。

复杂度  O(1)


list_tail                                               

ListElmt *list_tail(const List *list);

返回值  指向链表中尾元素的指针。

描述  这是一个宏,返回由参数list指定的链表中尾元素的指针。

复杂度  O(1)


list_is_head

int list_is_head(const ListElmt *element);

返回值  如果element所指定的元素是链表头结点则返回1,否则返回-1.

描述  这是一个宏,用来判断由element所指定的元素是否是链表的链表头结点。

复杂度  O(1)


list_is_tail

int list_is_tail(const ListElmt *element);

返回值  如果element所指定的元素是链表头结点则返回1,否则返回-1.

描述  这是一个宏,用来判断由element所指定的元素是否是链表的链表尾结点。

复杂度  O(1)


list_data                                                         

void  *list_data(const ListElmt *element);

返回值   节点中保存的数据。

描述  这是一个宏,返回由element所指定的链表节点元素中保存的数据。

复杂度  O(1)


list_next                                                                 

ListEmlt *list_next(const ListElmt *element);

返回值  返回由element所指定的节点的下一个节点。

描述  这是一个宏,返回链表中由element所指定的结点的下个节点。

复杂度  O(1)

单链表的实现与分析

结构体ListElmt表示链表中的单个元素(见示例1),这个结构体拥有两个成员,就是前面介绍的数据成员和指针成员。

结构体List则表示链表这种数据结构(见示例1)。这个结构由5个成员组成:size表示链表中元素个数;match并不由链表本身使用,而是由链表数据结构派生而来的新类型所使用;destroy是封装之后传递给list_init的析构函数;head是指向链表中头结点元素的指针;tail则是指向链表中末尾结点元素的指针。

示例1:链表抽象数据类型的头文件

/*list.h*/
#ifndef LIST_H
#define LIST_H#include <stdio.h>
/*Define a structure for linked list elements.*/
typedef struct ListElmt_
{void *data;struct ListElmt_ *next;
} ListElmt;/*Define a structure for linked lists.*/
typedef struct List_
{int size;int (*match)(const void *key1,const void *key2);void (*destroy)(void *data);ListElmt *head;ListElmt *tail;
} List;/*Public Interface*/
void list_init(List *list,void(*destroy)(void *data));
void list_destroy(List *list);
int list_ins_next(List *list,ListElmt *element,const void *data);
int list_rem_next(List *list,listElmt *element,void **data);
#define list_size(list)((list)->size)#define list_head(list)((list)->head)
#define list_tail(list)((list)->tail)
#define list_is_head(list,element)(element==(list)->head ? 1 : 0)
#define list_is_tail(element)((element)->next==NULL ? 1 : 0)
#define list_data(element)((element)->data)
#define list_next(element)((element)->next)#endif // LIST_H

list_init

list_init用来初始化一个链表以便能够执行其他操作(见示例2)。

初始化链表是一种简单的操作,只要把链表的size成员设置为0,把函数指针成员destroy设置为定义的析构函数,head和tail指针全部设置为NULL即可。

list_init的复杂度为O(1),因为初始化过程中的所有步骤都能在一段恒定的时间内完成。

示例2:链表抽象数据类型的实现

/*list.c*/
#include <stdio.h>
#include <string.h>#include "lish.h"/*list_init*/
void list_init(List *list,void(*destroy)(void *data))
{list->size = 0;list->destroy = destroy;list->head = NULL;list->tail = NULL;return;
}/*list_destroy*/
void list_destroy(List *list)
{void *data;/*Remove each element*/while(list_size(list)>0){if(list_rem_next(list,NULL,(void **)&data)==0 && list->destroy!=NULL){/*Call a user-defined function to free dynamically allocated data.*/list->destroy(data);}}memset(list,0,sizeof(list));return;
}/*list_ins_next*/
int list_ins_next(List *list,ListElmt *element,const void *data)
{ListElmt *new_element;/*Allocate storage for the element*/if((new_element=(ListElmt *)malloc(sizeof(ListElmt)))==NULL)return -1;/*insert the element into the list*/new_element->data=(void *)data;if(element == NULL){/*Handle insertion at the head of the list. */if(list_size(list)==0)list_tail = new_element;new_element->next = list->head;list->head = new_element}else {/*Handle insertion somewhere other than at the head*/if(element->next==NULL)list->tail = new_element;new_element->next = element->next;element->next = new_element;}/*Adjust the size of the list of account for the inserted element. */list->size++;return 0;
}/*list_rem_next*/
int list_rem_next(List *list,ListElmt *element,void **data)
{ListElmt *old_element;/*Do not allow removal from an empty list. */if(list_size(list) == 0)return -1;/*Remove the element from the list. */if(element == NULL){/*Handle removal from the head of the list. */*data = list->head->data;old_element = list->head;list->head = list->head->next;if(list_size(list) == 1)list->tail = NULL;}else {/*Handle removal from somewhere other than the head. */if(element->next == NULL)return -1;*data = element->next->data;old_element = element->next;element->next = element->next->next;if(element->next == NULL)list->tail = element;}/*Free the storage allocated by the abstract datatype.*/free(old_element);/*Adjust the size of the list account for the removed element. */list->size--;return 0;
}

list_destroy

list_destroy用来销毁链表(见示例2),其意义就是移除链表中的所有的元素。

如果调用list_init时destroy参数不为NULL,则当每个元素被移除时都将调用list_destroy一次。

list_destroy的运行时复杂度为O(n),n代表链表中的元素个数,这是因为list_rem_next的复杂度为O,而移除每个元素时都将调用list_rem_next一次。

list_ins_next

list_ins_next将一个元素插入由element参数所指定的元素之后(见示例2)。该调用将新元素的数据指向由用户传递进来的数据。向链表中插入新元素的处理步骤很简单,但需要特别小心。有两种情况需要考虑:插入链表头部和插入其他位置。

一般来说,要把一个元素插入链表中,需要将新元素的next指针指向它之后的那个元素,然后将新元素位置之前的结点next指针指向新插入的元素(见图3)。但是,当从链表头部插入时,新元素之前没有别的结点了。因此在这种情况下,将新元素的next指针指向链表的头部,然后重置头结点指针,使其指向新元素。回顾一下前面接口设计,当传入element为null时代表新的元素将插入链表头部。另外需要注意的是,无论何时当插入的元素位于链表末尾时,都必须重置链表数据结构的tail成员使其指向新的结点。最后,更新统计链表中结点个数的size成员。

list_rem_next

list_rem_next从链表中移除由element所指定的元素之后的那个结点(见示例2)。移除的是element之后的那个结点而不是移除element本身。这个调用也需要考虑两个因素,移除的是头结点以及移除其余位置上的结点。

移除操作是很简单的,但同样需要注意一些细节问题(见图4)。一般来说,从链表中移除一个元素,需要将移除的目标结点前一个元素的next结点指针指向目标结点的下一个元素。但是,当移除的目标结点是头指针时,目标结点之前并没有其他元素了。因此,在这种情况下,只需要将链接表的head成员指向目标结点的下一个元素。同插入操作一样,当传入的element为NULL时就代表链表的头结点需要移除。另外,无论何时当移除的目标结点是链表的尾部结点时,都必须更新链表数据结构中的tail成员,使其指向新的尾结点,或者当移除操作使得整个链表为空链表时,需要把tail设置为NULL。最后,更新链表的size成员,使其减1。当这个调用返回时,data将指向已经移除结点的数据域。

list_rem_next的复杂度为O(1),因为所有的移除步骤都在恒定的时间内完成。

list_size、list_head、list_tail、list_is_tail、list_data以及list_next

这些宏实现了一些链表中的一些简单操作(见示例1)。一般来说,它们提供了快速访问和检测List和ListElmt结构体中成员的能力。

这些操作的复杂度都是O(1),因为访问和检测结构体成员都可以在恒定的时间内完成。

************************************

请关注后续关于链表的应用实例详解。

************************************

算法精解_C语言 链表_单链表(接口定义+类型实现)相关推荐

  1. 资料 | O‘Reilly精品图书系列:算法精解 C 语言描述 (简体中文)

    下载地址:资料 | O'Reilly精品图书系列:算法精解 C 语言描述 (简体中文) 内容简介 · · · · · · 本书是数据结构和算法领域的经典之作,十余年来,畅销不衰! 全书共分为三部分:第 ...

  2. c语言单链表_C语言笔试题—单链表逆序

    前情回顾 之前更多的是给大家推荐的是好用的软件,经过反思之后觉得这些东西并不是我想要的,所以从今天开始我要转变方向了,更多的往我的专业方向去发展(虽然我是个小白),当然如果有说的不对的地方,希望大家能 ...

  3. 算法精解 c语言描述 豆瓣,斯坦福大学教授亲授,这本美亚4.7星的算法书,新手程序员都看得懂!...

    原标题:斯坦福大学教授亲授,这本美亚4.7星的算法书,新手程序员都看得懂! "算法会扩展并提高大家的编程技巧,而学习基本的算法设计范式,可以和许多不同领域的不同问题密切相关,还能作为预测算法 ...

  4. python怎么反转单链表_单链表反转python实现代码示例

    单链表的反转可以使用循环,也可以使用递归的方式 1.循环反转单链表 循环的方法中,使用pre指向前一个结点,cur指向当前结点,每次把cur->next指向pre即可. 代码: class Li ...

  5. c语言链表查找的代码与题目,链表的C语言实现之单链表的查找运算_c语言

    建立了一个单链表之后,如果要进行一些如插入.删除等操作该怎么办?所以还须掌握一些单链表的基本算法,来实现这些操作.单链表的基本运算包括:查找.插入和删除.下面我们就一一介绍这三种基本运算的算法,并结合 ...

  6. c语言数组指定位置插入和删除_玩转C语言链表,单链表/双向链表的建立/遍历/插入/删除...

    最近临近期末的C语言课程设计比平时练习作业一下难了不止一个档次,第一次接触到了C语言的框架开发,了解了View(界面层).Service(业务逻辑层).Persistence(持久化层)的分离和耦合, ...

  7. 线性表:3.链表,单链表详解与C语言实现

    逻辑结构上一个挨一个的数据,在实际存储时,并没有像顺序表那样也相互紧挨着.恰恰相反,数据随机分布在内存中的各个位置,这种存储结构称为 线性表的链式存储 . 由于分散存储,为了能够体现出数据元素之间的逻 ...

  8. 线性表详解(静态链表、单链表、双向链表、循环链表)

    目录 申明 1. 线性表的定义 2. 线性表的抽象数据类型 3. 线性表的顺序存储结构 3. 1 顺序存储定义 3. 2 顺序存储方式 3. 3 数据长度与线性表长度区别 3. 4 地址计算方法 4. ...

  9. 单链表删除所有值为x的元素_C/C++编程笔记:如何使用C++实现单链表?单链表的基本定义...

    如何弥补顺序表的不足之处? 第一次学习线性表一定会马上接触到一种叫做顺序表(顺序存储结构),经过上一篇的分析顺序表的优缺点是很显然的,它虽然能够很快的访问读取元素,但是在解决如插入和删除等操作的时候, ...

最新文章

  1. 标准caffe中实现darknet相关层。caffe和darknet模型的相互转换和加速(分类、检测、分割)
  2. 【小样本学习】什么是小样本学习?这篇综述文章用166篇参考文献告诉你答案...
  3. [羊城杯 2020]RRRRRRRSA
  4. 获取多台主机命令执行结果
  5. Python学习笔记:返回函数
  6. 【FLink】Flink exactly once 每次都是产生一个新的生产者吗?
  7. 编程疑难杂症の真的非常一样的文本?!
  8. mongodb 常用操作(转)
  9. Tornado 一些资料
  10. 班级管理系统(SSM+LayUI)
  11. 安卓一键清理内存_豆豆清理大师免费下载-豆豆清理大师老年版 v1.0.0手机版
  12. 悲剧历史人物(一)李广难封
  13. 程序员的未来之路[转]
  14. UVA-12304 Race(递推)
  15. Azkaban停留在 Logging initialized using configuration in jar:file:/application/cloudera/parcels/XXXXXX
  16. 淘宝新上架的产品如何在站内SEO优化
  17. 用 Jupyter Notebook 爬取微博图片保存本地!
  18. 【无标题】软件企业认定条件(双软企业认定条件2022)
  19. 丑数(Ugly Number)的判别和证明
  20. 盘点国内好用的企业网盘

热门文章

  1. LibreOJ10082. 「一本通 3.3 例 1」Word Rings【二分+SPFA】
  2. mysql stdistance_SQL Server 利用 geography 计算地理位置距离、距我最近排序
  3. VTK笔记——医学图像的切片提取(vtkImageReslice)
  4. Camera Framework 分析
  5. 【实战】恶搞图片生成器
  6. 南京工业大学乐学python答案_乐学Python
  7. 如何扛住100亿次请求?后端架构应该这样设计!
  8. P2356 弹珠游戏
  9. Centos7安装jq
  10. mac连接手机 vm_苹果 Mac 上的虚拟机怎么联接 iPhone