线性表的链式表示
- 单链表的定义
- 单链表的基本操作
- 双链表
- 双链表的基本操作
- 循环链表
- 块状链表
- 存储结构
- 顺序表和链表的比较
线性表的顺序存储结构的特点是逻辑关系上相邻的两个数据元素在物理位置上也是相邻的。我们会发现虽然顺序表的查询很快,时间复杂度为 O ( 1 ) O(1) O(1),但是增删的效率是比较低的,因为每一次增删操作都伴随着大量的数据元素移动。为了解决这个问题我们可以使用另外一种存储结构实现线性表,链式存储结构。
链式存储线性表时,不需要使用地址连续的存储单元,即不要求逻辑上相邻的元素在物理位置上也相邻,它通过 “链” 建立起数据元素之间的逻辑关系,因此插入和删除操作不需要移动元素,而只需修改指针,但也会失去顺序表可随机存储的优点。
单链表的定义
线性表的链式存储结构(也称之为链表)的特点是逻辑关系上相邻的两个数据元素在物理位置上不一定是相邻的,换言之数据元素在存储器中的位置可以是任意的。为了表示每个数据元素 a i a_i ai与其直接后继 a i + 1 a_i+1 ai+1之间的逻辑关系,对于数据元素 a i a_i ai来说,除了存储其本身的信息外,还需存储一个能够保存直接后继的存储位置的指针,这两部分信息组成数据元素 a i a_i ai的存储映像,我们称之为结点(node)。
结点包含两个或者三个域:
- 存储数据元素信息的域叫做数据域
- 存储直接后继存储位置的域叫做指针域
- 存储直接前继存储位置的域也叫指针域
如果只有一个指针域保存直接后继存储位置,这样的链表我们称之为单链表
利用单链表可以解决顺序表需要大量连续存储单元的缺点,但单链表附加指针域,也存在浪费存储空间的缺点。由于单链表的元素离散地分布在存储空间中,所以单链表是非随机存储的存储结构,即不能直接找到某个特定的节点。查找某个特定的结点时,需要从头开始遍历,依次查找。
为了方便对链表进行插入结点和删除结点的操作,我们在链表中的第一个结点之前加一个不存储实际的数据元素的结点,该结点我们称之为:头结点。
头节点的指针域指向线性表的第一个元素结点:
在对单向链表进行访问时,需要使用一个指针指向链表中的第一个结点(头结点),这个指针我们称之为头指针。
头指针保存了链表中头结点的存储位置,当链表为空时,头结点的指针域为空,即如果头指针为NULL时表示一个空表。
头节点与头指针的区别:不管带不带头结点,头指针都始终指向链表的第一个结点,而头结点是带头结点的链表中的第一个结点,节点内通常不存储信息。
引入头节点后,可以带来两个优点:
- 由于第一个数据结点的位置被存放在头节点的指针域中,因此在链表的第一个位置上的操作和在表中的其他位置上的操作一致,无须进行特殊处理。
- 无论链表是否为空,其头指针都是指向头节点的非空指针(空表中头节点的指针域为空),因此空表和非空表的处理就得到了统一。
单链表的基本操作
方法 | 说明 | 时间复杂度 |
---|---|---|
头插法建立链表 | 该方法是从一个空表开始,生成新结点,并将读取到的数据存放在新结点的数据域中,然后将新节点插入到当前链表的表头,即头节点之后。采用头插法建立单链表时,读取数据的顺序与生成的链表中的元素的顺序是相反的。 | O ( n ) O(n) O(n) |
尾插法建立单链表 | 该方法将新的结点插入到当前链表的表尾。读取数据的顺序与生成的链表中的元素的顺序是一致的。 | O ( n ) O(n) O(n) |
按序号查找结点 | 在单链表中从第一个结点出发,顺指针域逐个往下搜索,直到找到第i个结点位置,否则返回最后一个结点指针域NULL。 | O ( n ) O(n) O(n) |
按值查找表结点 | 从单链表的第一个结点开始,由前往后依次比较表中各结点数据域的值,若某结点数据域的值等于给定值e,则返回该结点的指针,若没有,则返回最后一个结点指针域NULL。 | O ( n ) O(n) O(n) |
插入结点操作 | 插入结点操作将值为x的新结点插入到单链表的第i个位置上。先检查插入位置的合法性(是否为空),然后找到待插入位置的前驱节点,即第i-1个结点,再在其后插入新结点。 | O ( 1 ) O(1) O(1)或 O ( n ) O(n) O(n) |
删除结点操作 | 删除结点操作时将单链表的第i个结点删除。先检查删除位置的合法性,后查找表中第i-1个结点,即被删除的结点的前驱结点,再将其删除。 | O(1) 或 或 或O(n)$ |
求表长操作 | 求表长操作就是计算单链表中数据结点(不含头节点)的个数,需要从第一个结点开始顺序依次访问表中的每个结点,为此需要设置一个计数器变量,没访问一个结点,计数器就加1,直到访问到空结点为止 | O ( n ) O(n) O(n) |
双链表
单链表结点中只有一个指向其后继的指针,使得单链表只能从头结点依次顺序地向后遍历。要访问某个结点的前驱结点(插入、删除操作时),只能从头开始遍历,访问后继结点的时间复杂为 O ( 1 ) O(1) O(1),访问前驱结点的时间复杂度为 O ( n ) O(n) O(n)
为了克服上述缺点,引入了双链表,双链表结点中有两个指针pre(前驱指针)和next(后继指针),分别指向其前驱结点和后继结点:
双链表的基本操作
双链表在单链表的结点中增加了一个指向其前驱的pre指针,因此在双链表中的按值查找和按位查找的操作与单链表的相同。但双链表在插入和删除操作的实现上,与单链表有着较大的不同。这是因为“链”变化时也需要对pre指针做出修改,其关键是保证在修改的过程中不断链。此外,双链表可以很方便地找到其前驱结点,因此,插入、删除操作地时间复杂度仅为 O ( 1 ) O(1) O(1)。
循环链表
在一个 循环链表中, 首节点和末节点被连接在一起。这种方式在单向和双向链表中皆可实现。要转换一个循环链表,你开始于任意一个节点,然后沿着列表的任一方向直到返回开始的节点。再来看另一种方法,循环链表可以被视为“无头无尾”。这种列表很利于节约数据存储缓存。
循环单链表和单链表的区别在于,表中最后一个结点的指针不是NULL,而改为指向头结点,从而整个链形成一个环,因此循环链表的判空条件不是头结点的指针是否为空,而是他是否等于头指针。
双向循环链表最后一个节点的next指向头结点,头结点的pre指向最后一个结点。
块状链表
块状链表本身是一个链表,但是链表储存的并不是一般的数据,而是由这些数据组成的顺序表。每一个块状链表的节点,也就是顺序表,可以被叫做一个块。
块状链表通过使用可变的顺序表的长度和特殊的插入、删除方式,可以在达到 O ( n ) O({\sqrt n}) O(n)的复杂度。块状链表另一个特点是相对于普通链表来说节省内存,因为不用保存指向每一个数据节点的指针。
存储结构
链表中的结点不需要以特定的方式存储,但是集中存储也是可以的,主要分下面这几种具体的存储方法:
- 共享存储空间
链表的结点和其它的数据共享存储空间,优点是可以存储无限多的内容(不过要处理器支持这个大小,并且存储空间足够的情况下),不需要提前分配内存;缺点是由于内容分散,有时候可能不方便调试。 - 独立存储空间
一个链表或者多个链表使用独立的存储空间,一般用数组或者类似结构实现,优点是可以自动获得一个附加数据:唯一的编号,并且方便调试;缺点是不能动态的分配内存。
顺序表和链表的比较
相比较顺序表,链表插入和删除的时间复杂度虽然一样,但仍然有很大的优势,因为链表的物理地址是不连续的,它不需要预先指定存储空间大小,或者在存储过程中涉及到扩容等操作,同时它并没有涉及的元素的交换。
相比较顺序表,链表的查询操作性能会比较低。因此,如果我们的程序中查询操作比较多,建议使用顺序表,增删操作比较多,建议使用链表。