Loading... ## **线性表**  <img src="https://wmicheng.top/images/note/img/线性表11.png" alt="线性表11" align="left" style="zoom:80%;" style=""> `线性表`(**Linear List**)是具有相同数据类型的$n(n≥0)$个数据元素的有限序列,其中$n$为表长,当$n = 0$时线性表是一个空表 若用$L$命名线性表,则其一般表示为 $L = (a_1, a_2, … , a_i, a_{i+1}, … , a_n)$ $a_1$是`表头元素` $a_n$是`表尾元素` 除第一个元素外,每个元素有且`仅有一个直接前驱` 除最后一个元素外,每个元素有且`仅有一个直接后继` > #### 线性表的基本操作 `InitList(&L)`:**初始化表**。构造一个空的线性表L,分配内存空间。 `DestroyList(&L)`:**销毁操作**。销毁线性表,并释放线性表L所占用的内存空间。 `ListInsert(&L,i,e)`:**插入操作**。在表L中的第`i`个位置上插入指定元素e。 `ListDelete(&L,i,&e)`:**删除操作**。删除表L中第`i`个位置的元素,并用`e`返回删除元素的值。 `LocateElem(L,e)`:**按值查找操作**。在表L中查找具有给定关键字值的元素。 `GetElem(L,i)`:**按位查找操作**。获取表L中第`i`个位置的元素的值。 `Length(L)`:**求表长**。返回线性表`L`的长度,即`L`中数据元素的个数。 `PrintList(L)`:**输出操作**。按前后顺序输出线性表L的所有元素值。 `Empty(L)`:**判空操作**。若L为空表,则返回`true`,否则返回`false`。 > #### C++引用关键字`&` 什么时候要传入参数的引用“`&`”: 对参数的修改结果需要“`带回来`”,是引用类型而不是值类型  ## **顺序表** > #### 顺序表定义 用顺序存储的方式实现线性表顺序存储,把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。 > #### 顺序表的实现 | 顺序表的实现-静态分配 | 顺序表的实现-动态分配 | | ------------------------------------------------------------ | ------------------------------------------------------------ | |  |  | |  |  | **如果“数组”存满了怎么办:** `直接放弃` > #### 顺序表的特点 **顺序表的实现:** - **随机访问**,即可以在$O(1)$时间内找到第`i`个元素。 - **存储密度高**,每个结点只存储数据元素 - **拓展容量不方便**(即便采用动态分配的方式实现,拓展长度的时间复杂度也比较高) - **插入、删除操作不方便**,需要移动大量元素 > #### 顺序表的插入、删除 | 顺序表的插入 | 顺序表的删除 | | ------------------------------------------------------------ | ------------------------------------------------------------ | |  |  | > #### 增加`i`的合法性判断:  | 最好时间复杂度 | 最坏时间复杂度 | 平均时间复杂度 | | :------------: | :------------: | :------------: | | $O(1)$ | $O(n)$ | $O(n)$ | > ### 顺序表的查找 | 顺序表的按位查找 | 顺序表的按值查找 | | ------------------------------------------------------------ | ------------------------------------------------------------ | |  |  | - 正是如此,在初始化顺序表时候malloc需要强制转换为与数据元素的数据类型相对应的指针 - `时间复杂度=O(1)` - 随机存取:由于顺序表的各个数据元素在内存中连续存放,因此可以根据**起始地址**和**数据元素大小**立即找到第`i`**个元素, | 最好时间复杂度 | 最坏时间复杂度 | 平均时间复杂度 | | :------------: | :------------: | :------------: | | $O(1)$ | $O(n)$ | $O(n)$ | ## **单链表** ### 单链表定义  > #### 带头结点的&不带头结点单链表 | 带头结点 | 不带头结点 | | ------------------------------------------------------------ | ------------------------------------------------------------ | |  |  | `区别:` - 不带头结点,写代码更麻烦 - 对第一个数据结点和后续数据结点的处理需要用不同的代码逻辑 - 对空表和非空表的处理需要用不同的代码逻辑 - 一般使用的都是带头结点的单链表  ### `单链表的插入操作` #### 按位序插入 | 按位序插入(带头结点) | 按位序插入(不带头结点) | | ------------------------------------------------------------ | ------------------------------------------------------------ | |  |  | > #### 按位序插入(不带头结点) **单链表插入函数**:`ListInsert(&L,i,e)`: 插入操作,在表`L`中的第`i`个位置上插入指定元素e 1. 找到第`i-1`个结点,将新结点插入其后 2. 若带有头结点,插入更加方便,头结点可以看作“`第0个`”结点,直接做上面的操作即可 3. 若插在表中则与插在表头一样进行操作,可以插入成功 4. 若插在表尾则`s->next`为`NULL`(在表的定义时规定的),可以插入成功 5. 若`i`插在表外`(i>Lengh)`则p指针指向`NULL`(`While循环一直指向p->next`)不能插入成功 | 最好时间复杂度 | 最坏时间复杂度 | 平均时间复杂度 | | :------------: | :------------: | :------------: | | $O(1)$ | $O(n)$ | $O(n)$ | > #### 按位序插入(不带头结点) **单链表插入函数**:`ListInsert(&L,i,e)`: 插入操作,在表L中的第`i`个位置上插入指定元素e 1. `不存在“第0个`结点,因此`i=1`时需要特殊处理 2. 找到第`i-1`个结点,将新结点插入其后 3. 若`i!=1`则处理方法和带头结点相同 4. 值得注意的是`int j =1`而非带头结点的0(带头结点的头结点为第0个结点) `结论:不带头结点写代码更不方便,推荐用带头结点` #### 指定结点插入操作 | 指定结点的前插操作 | 指定结点的后插操作 | | ------------------------------------------------------------ | ------------------------------------------------------------ | |  |  | > #### 指定结点的前插操作注意事项 因为**仅知道指定结点的信息和后继结点的指向**,因此**无法直接获取到前驱结点** | `方法1`:获取头结点然后再一步步找到指定结点的前驱 | `方法2`:将新结点先连上指定结点`p`的`后继`,接着指定结点p连上新结点`s`,将`p`中元素复制到`s`中,将p中元素覆盖为要插入的元素`e` | | ------------------------------------------------------------ | ------------------------------------------------------------ | |  |  | ### **单链表的删除** #### 按位序删除(带头结点) **单链表删除函数**:`ListDelete(&L,i,&e)` - 删除操作,删除表L中第`i`个位置的元素,并用`e`返回删除元素的值。 - 找到第`i-1`个结点,将其指针指向第`i+1`个结点,并释放第`i`个结点 .png) #### 指定结点的删除 - 删除结点p,需要修改其前驱结点的next指针,和指定结点的前插操作一样 - > **方法1**:传入头指针,循环寻找p的前驱结点 -  - > **方法2**:偷天换日,类似于结点前插的实现 -  如果要删除的结点`p`是最后一个结点: - 只能从表头开始依次寻找`p`的前驱,时间复杂度$O(n)$ - 这就体现了**单链表的局限性**:**无法逆向检索**,某些时候不太方便 ### **单链表的查找** | 按位查找 | 按值查找 | | ------------------------------------------------------------ | ------------------------------------------------------------ | |  |  | > #### 按位查找 - `GetElem(L,i)`:**按位查找操作**。获取表`L`中第`i`个位置的元素的值。 - 实际上单链表的插入中找到`i-1`部分就是按位查找`i-1`个结点 - 如果`i=0`则直接不满足`j<i`则指针`p`直接返回头结点`L` - 如果`i`超界则当时`p`指向了`NULL`,指针`p`返回`NULL` - **平均时间复杂度**:$O(n)$ > #### 按值查找 **单链表按值查找函数**:`LocateElem(L,e);` - 在表`L`中查找具有给定**关键字值**的元素。 - **能找到的情况**:`p`指向了`e`值对应的元素,返回该元素 - **不能找到的情况**:`p`指向了`NULL`,指针`p`返回`NULL` - **平均时间复杂度**:$O(n)$ > #### 求表的长度:  - **平均时间复杂度**:$O(n)$ ### **单链表的建立** > #### 尾插法 | 方法1:套用之前学过的位序插入,每次都要从头开始往后面遍历,时间复杂度为$O(n^2)$ | `方法2`:增加一个尾指针`r`,每次插入都让`r`指向新的表尾结点,时间复杂度为$O(n)$ | | ------------------------------------------------------------ | ------------------------------------------------------------ | |  |  | > #### 头插法 - 每次插入元素都插入到单链表的表头 - 头插法和之前学过的单链表后插操作是一样的,可以直接套用 - `L->next=NULL`;可以防止野指针  **总结:** - 头插法、尾插法:核心就是初始化操作、指定结点的后插操作 - 注意设置一个指向表尾结点的指针 - 头插法的重要应用:**链表的逆置** ## **双链表** > #### 为什么要要使用双链表 - 单链表:无法逆向检索,有时候不太方便 - 双链表:可进可退,但是存储密度更低一丢丢  > #### 双链表的初始化(带头结点)  > #### 双链表的插入  - 小心如果p结点为最后一个结点产生的空指针问题,因此循环链表应运而生(详见后面的循环链表插入删除) - 注意指针的修改顺序 > #### 双链表的删除  > #### 双链表的遍历  ## **循环链表** **循环单链表与单链表的区别:** | 单链表 | 循环单链表 | | ------------------------------------ | -------------------------------------- | | 表尾结点的`next`指针指向`NULL` | 表尾结点的`next`指针指向`头结点` | | 从一个结点出发只能找到后续的各个结点 | 从一个结点出发可以找到其他任何一个结点 | ### 循环单链表初始化  - 从头结点找到尾部,时间复杂度为$O(n)$ - **如果需要频繁**的**访问表头、表尾**,可以让`L`指向表尾元素(插入、删除时可能需要修改`L`) - 从尾部找到头部,时间复杂度为$O(1)$ ### 循环双链表与双链表的区别 | 双链表 | 循环双链表 | | --------------------------- | ----------------------------- | | 表头结点的`prior`指向`NULL` | 表头结点的`prior`指向表尾结点 | | 表尾结点的`next`指向`NULL` | 表尾结点的`next`指向头结点 | ### 循环双链表的初始化  ### 循环链表的插入  - 对于双链表来说如果p结点为最后一个结点,因为`next`结点为`null`,`p->next->prior=s`会产生的**空指针问题** - 循环链表规避因为最后结点的`next`结点为头结点因此不会发生问题 ### 循环链表的删除 与循环链表的插入相同  **注意点:** 写代码时候注意以下几点,以此规避错误: - 如何**判空** - 如何判断结点`p`是否是表尾/表头元素(后向/前向遍历的实现核心) - 如何在表头、表中、表尾插入/删除一个结点 ## **静态链表** ### 静态链表定义 - 分配**一整片连续的内存空间**,各个结点集中安置 - 每个结点由两部分组成:`data`(**数据元素**)和`next`(**游标**) - **0号结点**充当“头结点”,不具体存放数据 - 游标为`-1`表示已经到达表尾 - 游标充当“**指针**”,表示下个结点的存放位置,下面举一个例子: - 每个数据元素`4B`,每个游标`4B`(每个结点共`8B`),设起始地址为`addr`,`e1`的存放地址为`addr`$+ 8×2$(游标值) ### 定义静态链表 | 方法1 | 方法2 | | ------------------------------------------------------------ | ------------------------------------------------------------ | |  |  | > #### 基本操作 **初始化** - 把`a[0]`的`next`设为`-1` - 把其他结点的`next`设为一个特殊值用来表示结点空闲,如`-2` ## **顺序表和链表的比较** ### 逻辑结构 都属于`线性表`,是**线性结构** | 类型 | 顺序表 | 单链表 | | -----: | :------------------------------- | :------------------------------------------------------- | | `功能` | 每个结点中只存放数据元素 | 每个结点除了存放数据元素外,还要存储指向下一个结点的指针 | | `优点` | 可随机存取,存储密度高 | 不要求大片连续空间,改变容量方便 | | `缺点` | 要求大片连续空间,改变容量不方便 | 存储密度低,不可随机存取,要耗费一定空间存放指针 | ### 基本操作 > #### 创建 <img src="https://wmicheng.top/images/note/img/线性表比较3.jpg" alt="线性表比较3" style="zoom:50%;" style=""> > #### 销毁 <img src="https://wmicheng.top/images/note/img/线性表比较3.1.jpg" alt="线性表比较3.1" style="zoom:50%;" style=""> > #### 增删 <img src="https://wmicheng.top/images/note/img/线性表比较3.2.jpg" alt="线性表比较3.2" style="zoom:50%;" style=""> > #### 查找 <img src="https://wmicheng.top/images/note/img/线性表比较3.3.jpg" alt="线性表比较3.3" style="zoom:50%;" style=""> > #### 使用条件 <img src="https://wmicheng.top/images/note/img/线性表比较3.4.jpg" alt="线性表比较3.4" style="zoom:50%;" style=""> ### 开放式问题的解题思路 > **问题**: 请描述顺序表和链表的`bla bla bla…`实现线性表时,用顺序表还是链表好? 1. 顺序表和链表的逻辑结构都是线性结构,都属于线性表。 2. 但是二者的存储结构不同,顺序表采用顺序存储…(特点,带来的优点缺点);链表采用链式存储…(特点、导致的优缺点)。 3. 由于采用不同的存储方式实现,因此基本操作的实现效率也不同。 4. 当初始化时…;当插入一个数据元素时…;当删除一个数据元素时…;当查找一个数据元素时… --- 最后修改:2025 年 02 月 11 日 © 允许规范转载 打赏 赞赏作者 微信 赞 如果觉得我的文章对你有用,请随意赞赏