链表理论基础

## 1. 数据结构三要素 <img src="https://cs-wiki.oss-cn-shanghai.aliyuncs.com/img/20201229111427.png" style="zoom: 55%;" /> 1)**逻辑结构** 指的是数据间的逻辑关系,与数据的存储无关,独立于计算机之外。它又分为线性结构和非线性结构 - 线性结构:线性表,栈,队列,串,数组和广义表 - 非线性结构:树,图,集合 2)**存储结构** 是逻辑结构的存储映像,就是数据间的关系在计算机中的表现形式。也成为物理结构。它又分为 4 类:顺序存储 ,链式存储,索引存储和散列存储 - 顺序存储:把逻辑上相邻的元素存储在物理位置也相邻的存储单元里 - 链式存储:不要求物理位置的相邻,借助指示元素存储地址的指针表示元素之间的逻辑关系 - 索引存储:在存储元素信息的同时,添加附加的索引表,通过索引对节点进行操作 - 散列存储:也称 Hash 存储,根据结点的关键字通过散列函数计算出结点的存储地址 **相同的逻辑结构在计算机里可以用不同的存储结构实现**。比如逻辑结构中的线性结构,可以用数组(顺序存储)或单向链表(链接存储)来实现。 3)**数据运算**: 施加在数据上的运算(包括定义与实现)。运算的定义是针对逻辑结构,运算的实现是针对物理结构 ## 2. 线性表的定义 <img src="https://cs-wiki.oss-cn-shanghai.aliyuncs.com/img/20201229111934.png" style="zoom:60%;" /> **线性表定义**: - 具有相同数据类型的 n 个数据元素的有限序列 - 线性表是一种逻辑结构,表示元素之间一对一的逻辑关系 使用线性表存储数据的方式可以这样理解,**把所有数据用一根线儿串起来,再存储到物理空间中** 下图中,左侧是“串”起来的数据,右侧是空闲的物理空间。把这 “一串儿” 数据放置到物理空间,我们可以选择以下两种方式: <img src="https://cs-wiki.oss-cn-shanghai.aliyuncs.com/img/20201229115716.png" style="zoom:67%;" /> **将具有“一对一”关系的数据“线性”地存储到物理空间中**,这种存储结构就称为线性存储结构: - ① **顺序表**(如上图左边):将数据依次存储在连续的整块物理空间中 - ② **链表**(如上图右边):数据分散的存储在物理空间中,通过一根线保存着它们之间的逻辑关系 - 单链表 - 双链表 - 循环单链表 - 循环双链表 下面详细讲解这两种存储结构 👇 ## 3. 顺序表 ### ① 顺序表定义 线性表的顺序存储。**逻辑上相邻的两个元素在物理位置上也相邻** <img src="https://cs-wiki.oss-cn-shanghai.aliyuncs.com/img/20201229112953.png" /> **数组**就是顺序表,下标一般从 0 开始: <img src="https://cs-wiki.oss-cn-shanghai.aliyuncs.com/img/20201229113127.png" style="zoom: 65%;" /> **顺序表的特点**: - 随机访问(通过首地址和元素序号可在时间 O(1) 内找到元素) - 插入和删除需要移动大量元素 - 存储密度高,每个结点只存储数据元素 ### ② 顺序表基本操作 #### Ⅰ 插入 在数组 a 的第 `i` 个位置 (下标 `i-1`) 插入元素 `e` ```java // 第i个元素及其之后的元素后移 for (int j = a.length; j >= i; j--) a[j] = a[j - 1]; a[i - 1] = e; length++; ``` ![](https://cs-wiki.oss-cn-shanghai.aliyuncs.com/img/20201229114105.png) - 最好情况:在表尾插入,时间复杂度 O(1) - 最坏情况:在表头插入,时间复杂度 O(n) - 平均情况:时间复杂度 O(n) #### Ⅱ 删除 删除数组 a 的第 `i` 个位置的元素 ```java // 从第 i 个位置元素前移 for(int j = i; j < a.length; j++) a[j-1] = a[j]; length --; ``` <img src="https://cs-wiki.oss-cn-shanghai.aliyuncs.com/img/20201229114331.png" style="zoom: 62%;" /> - 最好情况:删除表尾元素,时间复杂度 O(1) - 最坏情况:删除表头元素,时间复杂度 O(n) - 平均情况:时间复杂度 O(n) #### Ⅲ 按值查找 查找数组 a 中值为 `e` 的元素的下标 ```java for (int i = 0; i < a.length; i++) if (a[i] == e) return i; ``` - 最好情况:查找元素在表头,时间复杂度 O(1) - 最坏情况:查找元素在表尾,时间复杂度 O(n) - 平均情况:时间复杂度 O(n) ## 4. 链表 ### ① 链表的定义与结构 线性表的链式存储。**逻辑上相邻的两个元素在物理位置不一定也相邻** 例如,使用链表存储 `{1,2,3}`,数据的物理存储状态如下图所示: <img src="https://cs-wiki.oss-cn-shanghai.aliyuncs.com/img/20201229120404.png" style="zoom: 62%;" /> 可以看到,上图根本无法体现出各数据之间的逻辑关系。对此,链表的解决方案是,每个数据元素在存储时都配备一个**指针**,用于指向自己的直接后继元素。如下图所示: <img src="https://cs-wiki.oss-cn-shanghai.aliyuncs.com/img/20201229120659.png" style="zoom: 65%;" /> 当然,指针可以指向自己的直接后继元素,也可以指向自己的直接前驱元素。为此,链表可分为: - **单链表**(指针指向自己的直接后继元素) - **双链表**(指针指向自己的直接后继元素和直接前驱元素) - **循环单链表**(指针指向自己的直接后继元素,表尾节点的指针指向头节点) - **循环双链表**(指针指向自己的直接后继元素和直接前驱元素,表尾节点的指针指向头节点) 通过以上大家应该也知道了,链表中每个数据的存储都由以下两部分组成: - **数据域**:数据元素本身 - **指针域**:指向该元素直接后继/前驱/...元素的指针 <img src="https://cs-wiki.oss-cn-shanghai.aliyuncs.com/img/20201229122012.png" style="zoom:60%;" /> 上图所示的结构在链表中称为**节点**。也就是说,链表实际存储的是一个一个的节点,真正的数据元素包含在这些节点中,举个单链表的例子: <img src="https://cs-wiki.oss-cn-shanghai.aliyuncs.com/img/20201229122427.png" style="zoom:60%;" /> 🚨 当然,上所示的链表结构并不完整。一个完整的链表需要由以下几部分构成: - **头指针**:一个普通的指针,它的特点是永远指向链表第一个节点的位置。很明显,头指针用于指明链表的位置,便于后期找到链表并使用表中的数据 - **节点**:链表中的节点又分为头节点、首元节点和其他节点 - **头节点**:其实就是一个<u>不存任何数据的空节点,通常作为链表的第一个节点。对于链表来说,头节点不是必须的,它的作用只是为了方便解决某些实际问题</u> - **首元节点**:由于头节点(也就是空节点)的缘故,链表中称<u>第一个存有数据的节点为首元节点</u>。首元节点只是对链表中第一个存有数据节点的一个称谓,用于和头节点进行区分,没有实际意义 - **其他节点**:链表中其他的节点 因此,一个存储 `{1,2,3}` 的完整单链表结构如图所示: ![](https://cs-wiki.oss-cn-shanghai.aliyuncs.com/img/20201229123550.png) 💡 **引入头节点的优点:** - 链表的首元节点的操作与其他位置的元素操作一样,无须进行特殊处理 - 无论链表是否为空,其头指针都是指向头节点的非空指针,空表和非空表的处理得到了统一 ### ② 单链表 #### Ⅰ 定义 单链表就是指针指向自己的直接后继元素的链表 <img src="https://cs-wiki.oss-cn-shanghai.aliyuncs.com/img/20201229122747.png" style="zoom:60%;" /> 以 Java 为例,我们自定义一个单链表结构: ```java public class Node{ private T t; private Node next; ...... } ``` **单链表可以解决顺序表需要大量连续存储空间的缺点,但单链表附加指针域,也存在浪费存储空间的缺点** 单链表是非随机存储的存储结构:即不能直接找到表中某个特点的结点。需要从头开始遍历。 单链表访问前驱的时间复杂度为 O(n),访问后继 O(1) #### Ⅱ 基本操作 ##### 头插法 在头节点 `L` 的后面插入节点 `s`,即 `s` 节点成为当前链表的首元节点 **头插法的读入顺序和生成顺序相反** ![](https://cs-wiki.oss-cn-shanghai.aliyuncs.com/img/20201229125226.png) ```java s.next = L.next; L.next = s; ``` ##### 尾插法 在链表最后一个节点 `r` 的后面插入节点 `s`,即 `s` 节点成为当前链表的最后一个节点 **尾插法的读入顺序和生成顺序相同** ![](https://cs-wiki.oss-cn-shanghai.aliyuncs.com/img/20201229125708.png) ```java r.next = s; r = s; // s 节点成为链表的最后一个节点 ``` ##### 插入节点 `p` 节点之后插入 `s` 节点 ![](https://cs-wiki.oss-cn-shanghai.aliyuncs.com/img/20201229124420.png) ```java s.next = p.next; p.next = s; ``` ##### 删除节点 `p` 节点之后删除 `q` 节点 <img src="https://cs-wiki.oss-cn-shanghai.aliyuncs.com/img/20201229130037.png" style="zoom:60%;" /> ```java // 删除 p 之后的 q q = p.next; p.next = q.next; ``` > 还有一种方法就是把要删除的节点和它的后继节点值进行交换,然后直接删除这个后继节点就行,这样就不用找前驱节点了。 ### ③ 双链表 #### Ⅰ 定义 双链表就是同时具有前驱指针和后继指针的链表 双链表访问前驱和后继结点时间复杂度都是 O(1) <img src="https://cs-wiki.oss-cn-shanghai.aliyuncs.com/img/20201229130635.png" style="zoom:67%;" /> 以 Java 为例,我们自定义一个双链表结构: ```java public class Node{ private T t; private Node next; private Node prior; ......} ``` #### Ⅱ 基本操作 ##### 插入节点 `p` 节点之后插入 `s` 节点 <img src="https://cs-wiki.oss-cn-shanghai.aliyuncs.com/img/20201229131613.png" style="zoom:60%;" /> 上图中 2、3 顺序可调换 ```java s.next = p.next;p.next.prior = s;p.next = s;s.prior = p; ``` ##### 删除节点 删除 `p` 节点之后的 `s` 节点 <img src="https://cs-wiki.oss-cn-shanghai.aliyuncs.com/img/20201229131945.png" style="zoom:60%;" /> ```java p.next = s.next;s.next.prior = p; ``` ### ③ 循环单链表 循环单链表就是**表尾节点的指针指向头节点的单链表** <img src="https://cs-wiki.oss-cn-shanghai.aliyuncs.com/img/20201229132250.png" style="zoom:67%;" /> 判空条件:表尾结点的 `next` 是否是等于头指针 ### ④ 循环双链表 循环双链表就是**表尾节点的指针指向头节点的双链表** <img src="https://cs-wiki.oss-cn-shanghai.aliyuncs.com/img/20201229132433.png" style="zoom: 60%;" /> ## 5. 顺序表和链表的比较 **1)存取方式** - 顺序表可以顺序存取,也可以随机存取 - 链表只能从表头顺序存取元素 **2)逻辑结构与物理结构** - 顺序存储时,逻辑上相邻的元素,对应的物理存储位置也相邻 - 链式存储时,逻辑上相邻的元素,物理存储位置不一定相邻,其逻辑关系是通过指针链接来表示的 **3)查找、插入和删除操作** - 对于按值查找,顺序表无序时,两者的时间复杂度均为 O(n);顺序表有序时,可采用折半查找,此时的时间复杂度为 O(log2n) 对于按序号查找,顺序表支持随机访问,时间复杂度仅为 O(1),而链表的平均时间复杂度为 O(n)。 - 顺序表的插入、删除操作,平均需要移动半个表长的元素 链表的插入、删除操作,只需要修改相关结点的指针域既可。由于链表的每个结点都有指针域,所以在存储空间上要比顺序表付出的代价大,存储密度不够大。 **4)空间分配** - 顺序存储在静态存储分配情况下,一旦存储空间装满就不能扩充,若再加入新元素,则会出现溢出,所以需要预先分配足够大的存储空间。预先分配过大,可能会导致顺序表候补大量闲置;预先分配过小,又会造成溢出。 动态存储分配虽然存储空间可以扩充,但需要移动大量元素,导致操作效率降低,而且若内存中没有更大的连续存储空间,则会导致分配失败 - 链表只在需要时申请分配,高效灵活