题目
(15分)已知一个带有表头结点的单链表,结点结构为:
假设该链表只给出了头指针list
。在不改变链表的前提下,请设计一个尽可能高效的算法:
查找链表中倒数第 k 个位置上的结点(k 为正整数)。
若查找成功,算法输出该结点的 data
域的值,并返回 1;否则,只返回 0。
要求:
⑴ 描述算法的基本设计思想;
⑵ 描述算法的详细实现步骤;
⑶ 根据设计思想和实现步骤,采用程序设计语言描述算法
注意题目所给信息
1.带头结点
2.单链表:不能访问前继节点,只能访问后继节点。
3.未知单链表长度
4.k为正整数,即k>0。不需要做k<=0越界判断。
单链表
单链表(单向链表)数据结构回顾:
单链表是线性表的链式存储。由多个节点组成,每个节点又由数据域和指针域构成。如图:
结点结构
用一个结构体描述节点类型:
1 | struct ListNode { |
这里的节点结构内容和题目所给的一致。
头节点
关于头节点一些需要的注意的,做出如下总结梳理
说明:
1.头节点不是链表第一个节点,而是头节点随后紧邻的后继节点。
2.头节点是非必须的,可以不设置。
3.在计算链表长度时,头节点不计入总数。
4.头节点的数据域没有意义。
好处:
1.使链表首个位置的插入删除更加方便,和其他位置一样,不需要涉及到头指针的移动。
2.统一空表和非空表的操作处理。当非空时头指针指向的是首个节点的地址,即*ListNode
类型,而对空表处理的时候却是NULL
,因此造成空表和非空表操作不一致。
头指针
其实指向某个节点的地址的指针丢失,也会造成这个节点无法访问。特别是头指针,一旦丢失,导致链表最前面的节点(头节点或者是第一个节点)无法访问,从而导致整个链表无法访问,出现内存泄漏等问题。
作用:具有标识作用,故常用头指针冠以链表的名字
单链表基本操作
链表的基本操作如下:
链表的初始化
1 | /* |
节点的创建
将创建新节点这个过程封装到一个函数,便于复用。
1 | /* |
节点的插入
思路:
调用findNodeByIndex函数(查找节点操作),获得第 i-1 节点,然后再进行插入操作。
具体的原理实现如下图所示:
说明:
这里一个数值,以及新节点所在位置来实现单链表中新节点插入操作。
新节点创建在函数内进行,并不是通过真正意义上传入一个节点类型实现插入操作。
时间性能:O(n)
具体步骤:
- 首先需要找到插入位置的前一个节点, 也就是图上节点
preNode
。若找不到,则是越界等问题,返回报错信息。 - 然后需要创建新的节点
newNode
。 - 插入操作实际上就是把
preNode
的后继节点改为新节点newNode
,然后再把新节点newNode
的后继节点改为第 i节点。需要注意顺序,以防出现断链。改动指针操作顺序正如图所示,先①后②。
代码如下:
①:newNode->link = preNode->link;
②:preNode->link = newNode;
对于①表示把新节点newNode
的后继节点改为第 i节点。第 i节点的地址通过它的前继节点来寻找,即:preNode->link
对于②表示把preNode
的后继节点改为新节点newNode
1 | /* |
节点的删除
原理如图:
思路:
调用find方法(查找节点操作),获得第 i-1 节点,然后再让 i-1 位置节点的指针域指向 i 位置节点后继节点。
时间性能:O(n)
注意:先修改指针再释放节点,避免断链。
代码如下:
1 | /* |
节点的修改
思路:
调用find方法(查找节点操作),然后再进行修改操作。
时间性能O(n)
1 | /* |
节点的查找
思路:
插入前需要进行合法性判断,例如插入位置是 -1 或者是超过表长时,显然不合法。
位置合法以后,因为单链表中每个节点的查找都通过它的前继节点来访问,因此进行逐一遍历查找。
时间性能:O(n)
1 | /* |
求链表长度
思路:
设置一个计数器,然后逐一遍历节点,每经过一个节点计数器+1。
时间性能:O(n)
1 | /* |
打印单链表
打印链表信息,输出每个节点地址,指向的下一个节点,内容以及表长。
时间性能:O(n)
1 | void printfLink(struct ListNode *list){ |
代码的一些说明:
- 代码为了易于初学者掌握,并没有使用
typedef
别名定义来简化一些复杂的类型声明。想要代码更简洁,可以去尝试一下。 - 调用
malloc()
函数勿忘记头文件加上#include<stdlib.h>
。 - 测试代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25int main(){
//定义头指针,指针变量名表示链表名称
struct ListNode *linkList;
//linkList的初始化
linkList = initLinkList();
//插入第1个元素,位置1
addNodeByIndex(linkList, 1, 1);
//插入第2个元素,位置1
addNodeByIndex(linkList, 1, 0);
//插入第3个元素,位置1
addNodeByIndex(linkList, 1, 2);
printfLink(linkList); //输出操作结果
//插入第4个元素,位置99
addNodeByIndex(linkList, 99, 2);
//删除位置2的元素
deleteNodeByIndex(linkList, 2);
printfLink(linkList); //输出操作结果
//更新位置1的元素数据域为666,
updateNodeByIndex(linkList, 1 ,666);
printfLink(linkList); //输出操作结果
return 0;
} - 对于单链表的插入,删除,修改,查找都是通过位置来实现的。这四种操作还可以通过数据域进行值查找。等有时间了再补全。
- 关于下标从0开始的问题,本程序默认第一个节点下标是1,便于理解!!如果想写成从0开始,可以直接让index参数整体-1以及边界判断条件也做小修改,整体思路不变。
题目求解
前面回顾了单链表的一些基本知识,下面来求解本题。
方法1:
蛮力法,硬算。通过多次遍历单链表,一定能求解出问题,但是时间性能得不到保障。
思路:
1.求表长len。
2.倒数第k个数,实际上就是:len-k+1,下标为len-k+1-1=len-k。
自己做个简短分析:
长度为5,倒数第5个,实际上就是第5-5+1=1个,下标为0。
长度为5,倒数第3个,实际上就是第5-3+1=3个,下标为2。
长度为5,倒数第2个,实际上就是第5-2+1=4个,下标为3。
长度为5,倒数第1个,实际上就是第5-1+1=5个,下标为4。
不难得出上面式子。
合法性判断也比较简单:倒数的数绝对超不出len的长度,如果k>len,直接返回0
3. 然后再进行遍历单链表,到达第len-n+1节点。输出data,返回1。
代码实现:
1 | int find1(struct ListNode *list, int k){ |
方法2:
最优解法,一次遍历完成查询。
思路:
1.使用双指针:定义两个指针*p
,*q
。
2.*q
指向单链表第一个节点不动,*p
向后遍历k个节点。
合法性判断也比较简单:在两个指针间隔达不到k时,*p
提前移动到尾结点处,则返回0
3.两个指针同时移动,直到*p
移动到尾结点处。则*q
指向的节点则是题目所求。
画了个图:
举了个例子,求倒数第2个节点的data值,最后q所指向的第3个节点是题目所求。
代码实现:
1 | int find2(struct ListNode *list, int k){ |
本文作者: spg2021
本文链接: https://spg2021.github.io/2020/03/31/408-2009/
版权声明:文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!