链表是线性表的链式存储结构
线性表的链式存储表示的特点是用一组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的)。因此,为了表示每个数据元素与其直接后继数据元素 之间的逻辑关系,对数据元素来说,除了存储其本身的信息之外,还需存储一个指示其直接后继的信息(即直接后继的存储位置)。由这两部分信息组成一个“结点”,表示线性表中一个数据元素 。
链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针(Pointer)。由于不必按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表:顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而顺序表相应的时间复杂度分别是O(logn)和O(1)。
使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。在计算机科学中,链表作为一种基础的数据结构可以用来生成其它类型的数据结构。
链表通常由一连串节点组成,每个节点包含任意的实例数据(data fields)和一或两个用来指向明上一个/或下一个节点的位置的链接("links")。链表最明显的好处就是,常规数组排列关联项目的方式可能不同于这些数据项目在记忆体或磁盘上顺序,数据的存取往往要在不同的排列顺序中转换。而链表是一种自我指示数据类型,因为它包含指向另一个相同类型的数据的指针(链接)。链表允许插入和移除表上任意位置上的节点,但是不允许随机存取。
链表有很多种不同的类型:单向链表,双向链表以及循环链表。链表可以在多种编程语言中实现。像Lisp和Scheme这样的语言的内建数据类型中就包含了链表的存取和操作。程序语言或面向对象语言,如C,C++和Java依靠易变工具来生成链表。
更详细解说链表
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。
由于链表这种数据结构必须利用指针变量才能实现,因此先介绍指针的概念。
由前述可知,计算机的内存储器被划分为一个个的存储单元,每个存储单元存放8个二进制位(即一个字节)。存储单元按一定的规则编号,这个编号就是存储单元的地址。也就是说计算机中存储的每个字节是一个基本内存单元,有一个地址。计算机就是通过这种地址编号的方式来管理内存数据读写的准确定位的。
计算机是如何从内存单元中存取数据的呢?从程序设计的角度看,有两种办法:一是通过变量名;二是通过地址。程序中声明的变量是要占据一定的内存空间的,例如,C语言中整型变量占2字节,实型变量占4字节。程序中定义的变量在程序运行时被分配内存空间。在变量分配内存空间的同时,变量名也就成为了相应内存空间的名称,在程序中可以用这个名字访问该内存空间,表现在程序语句中就是通过变量名存取变量内容(这就是程序中定义变量的用途,即程序中通过定义变量来实现数据在内存中的存取)。但是,有时使用变量名不够方便或者根本没有变量名可用,这时就可以直接用地址来访问内存单元。例如,学生公寓中每个学生住一间房,每个学生就相当于一个变量的内容,变量名指定为学生姓名,房间是存储单元,房号就是存储单元地址。如果知道了学生姓名,可以通过这个名字来访问该学生,这相当于使用变量名访问数据。如果知道了房号,同样也可以访问该学生,这相当于通过地址访问数据。
由于通过地址能访问指定的内存存储单元,因此可以说,地址“指向”该内存存储单元(如同说,房间号“指向”某一房间一样)。故将地址形象化地称为“指针”,意思是通过它能找到以它为地址的内存单元。一个变量的地址称为该变量的“指针”。如果有一个变量专门用来存放另一个变量的地址(即指针),则它称为“指针变量”。在许多高级程序设计语言中有专门用来存放内存单元地址的变量类型,这就是指针类型。指针变量就是具有指针类型的变量,它是用于存放内存单元地址的。
通过变量名访问一个变量是直接的,而通过指针访问一个变量是间接的。就好像要在学生公寓中找一位学生,不知道他的姓名,也不知道他住哪一间房,但是知道101房间里有他的地址,走进101房间后看到一张字条:“ 找我请到302”,这时按照字条上的地址到302去,便顺利地找到了他。这个101房间,就相当于一个指针变量,字条上的字便是指针变量中存放的内容(另一个内存单元的地址),而住在302房间的学生便是指针所指向的内容。
指针作为维系结点的纽带,可以通过它实现链式存储。假设有五个学生某一门功课的成绩分别为A、B、C、D和E,这五个数据在内存中的存储单元地址分别为1248、1488、1366、1022和1520,其链表结构如下图所示。
单链表
链表有一个“头指针”变量,上图中以 head表示,它存放一个地址,该地址指向链表中第一个结点,第一个结点又指向第二个结点……直到最后一个结点。该结点不再指向其他结点,它称为“表尾”,它的地址部分存放一个“NULL”(表示“空地址”),链表到此结束。链表中每个结点都包括两个部分:用户需要用的实际数据和下一个结点的地址。
可以看到链表中各结点在内存中可以不是连续存放的。要找到某一结点C,必须先找到其上一个结点B,根据结点B提供的下一个结点地址才能找到C。链表有一个“头指针”,因此通过“头指针”可以按顺序往下找到链表中的任一结点,如果不提供“头指针”,则整个链表都无法访问。链表如同一条铁链一样,一环扣一环,中间是不能断开的。打个比方,幼儿园的老师带领孩子出来散步,老师(作为头指针)牵着第一个小孩的手,第一个小孩的另一只手牵着第二个孩子……这就是一个“链”,最后一个孩子有一只手空着,他是“链尾”。要找这个队伍,必须先找到老师,然后按顺序找到每一个孩子。
上图的链表每个结点中只有一个指向后继结点的指针,该链表称为单链表。其实结点中可以有不止一个用于链接其他结点的指针。如果每个结点中有两个用于链接其他结点的指针,一个指向前趋结点(称前趋指针),另一个指向后继结点(称后继指针),则构成双向链表。双向链表如下图所示。
双向链表
链表的一个重要特点是插入、删除操作灵活方便,不需移动结点,只需改变结点中指针域的值即可。而数组由于用存储单元的邻接性体现数组中元素的逻辑顺序关系,因此对数组进行插入和删除运算时,可能需要移动大量的元素,以保持这种物理和逻辑的一致性。如数组中有m个元素,往第i(i < m)个元素后面插入一个新元素,需要将第i+1个元素至第m个元素共m-i个元素向后移动。
一点看法
C语言是学习数据结构的很好的工具。理解了C中用结构体描述数据结构,那么对于理解其C++描述,Java描述都就轻而易举了!
链表的提出主要在于顺序存储中的插入和删除的时间复杂度是线性时间的,而链表的操作则可以是常数时间的复杂度。对于链表的插入与删除操作,个人做了一点总结,适用于各种链表如下:
- 插入操作处理顺序:中间节点的逻辑,后节点逻辑,前节点逻辑。按照这个顺序处理可以完成任何链表的插入操作。
- 删除操作的处理顺序:前节点逻辑,后节点逻辑,中间节点逻辑。
按照此顺序可以处理任何链表的删除操作。如果不存在其中的某个节点略过即可。上面的总结,大家可以看到一个现象,就是插入的顺序和删除的顺序恰好是相反的,很有意思!