题目(15分)已知一个带有表头结点的单链表,结点结构为: 假设该链表只给出了头指针list。在不改变链表的前提下,请设计一个尽可能高效的算法: 查找链表中倒数第 k 个位置上的结点(k 为正整数)。 若查找成功,算法输出该结点的 data 域的值,并返回 1;否则,只返回 0。 要求: ⑴ 描述算法的基本设计思想; ⑵ 描述算法的详细实现步骤; ⑶ 根据设计思想和实现步骤,采用程序设计语言描述算法
注意题目所给信息 1.带头结点 2.单链表:不能访问前继节点,只能访问后继节点。 3.未知单链表长度 4.k为正整数,即k>0。不需要做k<=0越界判断。
单链表 单链表 (单向链表)数据结构回顾: 单链表是线性表的链式存储。由多个节点组成,每个节点又由数据域和指针域构成。如图:
结点结构 用一个结构体描述节点类型:
1 2 3 4 struct ListNode { int data; struct ListNode *link ; };
这里的节点结构内容和题目所给的一致。
头节点 关于头节点一些需要的注意的,做出如下总结梳理说明: 1.头节点不是链表第一个节点,而是头节点随后紧邻的后继节点。 2.头节点是非必须的,可以不设置。 3.在计算链表长度时,头节点不计入总数。 4.头节点的数据域没有意义。
好处: 1.使链表首个位置的插入删除更加方便,和其他位置一样,不需要涉及到头指针的移动。 2.统一空表和非空表的操作处理。当非空时头指针指向的是首个节点的地址,即*ListNode类型,而对空表处理的时候却是NULL,因此造成空表和非空表操作不一致。
头指针 其实指向某个节点的地址的指针丢失,也会造成这个节点无法访问。特别是头指针,一旦丢失,导致链表最前面的节点(头节点或者是第一个节点)无法访问,从而导致整个链表无法访问,出现内存泄漏等问题。作用: 具有标识作用,故常用头指针冠以链表的名字
单链表基本操作 链表的基本操作如下:
链表的初始化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 struct ListNode* initLinkList () { struct ListNode *head ; head = (struct ListNode *)malloc (sizeof (struct ListNode)); if (head == NULL ){ printf ("内存不足!申请内存空间失败\n" ); } head->link = NULL ; return head; }
节点的创建 将创建新节点这个过程封装到一个函数,便于复用。
1 2 3 4 5 6 7 8 9 10 11 struct ListNode* newNode (int data, struct ListNode *link) { struct ListNode *newNode ; newNode = (struct ListNode*)malloc (sizeof (struct ListNode)); newNode->data = data; newNode->link = link; return newNode; }
节点的插入 思路: 调用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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 int addNodeByIndex (struct ListNode *list , int index, int data) { if (index == 1 || list ->link == NULL ){ list ->link = newNode(data, list ->link); return 1 ; } struct ListNode *preNode ; preNode = findNodeByIndex(list , index - 1 ); if (preNode == NULL ){ printf ("插入失败, 位置越界!\n" ); return 0 ; } preNode->link = newNode(data, preNode->link); return 1 ; }
节点的删除 原理如图: 思路: 调用find方法(查找节点操作),获得第 i-1 节点,然后再让 i-1 位置节点的指针域指向 i 位置节点后继节点。 时间性能:O(n) 注意:先修改指针再释放节点,避免断链。 代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 int deleteNodeByIndex (struct ListNode *list , int index) { struct ListNode *preNode ; struct ListNode *deleteNode ; if (index == 1 ){ preNode = list ; deleteNode = list ->link; }else { preNode = findNodeByIndex(list , index - 1 ); if (preNode == NULL || preNode->link==NULL ){ printf ("删除失败, 位置越界!\n" ); return 0 ; } deleteNode = preNode->link; } preNode->link = preNode->link->link; free (deleteNode); return 1 ; }
节点的修改 思路: 调用find方法(查找节点操作),然后再进行修改操作。 时间性能O(n)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int updateNodeByIndex (struct ListNode *list , int index, int data) { struct ListNode *updateNode ; updateNode = findNodeByIndex(list , index); if (updateNode == NULL ){ printf ("修改失败, 位置越界!\n" ); return 0 ; } updateNode->data = data; return 1 ; }
节点的查找 思路: 插入前需要进行合法性判断,例如插入位置是 -1 或者是超过表长时,显然不合法。 位置合法以后,因为单链表中每个节点的查找都通过它的前继节点来访问,因此进行逐一遍历查找。 时间性能:O(n)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 struct ListNode* findNodeByIndex (struct ListNode *list , int index) { if (index <= 0 ){ printf ("节点位置异常, 不能为负数\n" ); return NULL ; } int len = getLinkLength(list ); if (index > len){ printf ("节点位置异常, 位置超出表长\n" ); return NULL ; } int i = 0 ; struct ListNode *p ; p = list ; while (i != index){ p = p->link; i++; } return p; }
求链表长度 思路: 设置一个计数器,然后逐一遍历节点,每经过一个节点计数器+1。 时间性能:O(n)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 int getLinkLength (struct ListNode *list ) { int len = 0 ; struct ListNode *p ; p = list ; while (p->link != NULL ){ p = p->link; len++; } return len; }
打印单链表 打印链表信息,输出每个节点地址,指向的下一个节点,内容以及表长。 时间性能:O(n)
1 2 3 4 5 6 7 8 9 10 11 12 void printfLink (struct ListNode *list ) { int len = 0 ; struct ListNode *p ; p = list ; printf ("头节点地址:%d\n" , p); while (p->link != NULL ){ p = p->link; len++; printf ("第%d个节点地址:%d\t数据域内容:%d\t指针域指向地址:%d\n" , len, p, p->data, p->link); } printf ("链表长度:%d\n" , len); }
代码的一些说明: 代码为了易于初学者掌握,并没有使用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 25 int main () { struct ListNode *linkList ; linkList = initLinkList(); addNodeByIndex(linkList, 1 , 1 ); addNodeByIndex(linkList, 1 , 0 ); addNodeByIndex(linkList, 1 , 2 ); printfLink(linkList); addNodeByIndex(linkList, 99 , 2 ); deleteNodeByIndex(linkList, 2 ); printfLink(linkList); 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 int find1 (struct ListNode *list , int k) { int len = 0 ; struct ListNode *p = list ; while (p->link != NULL ){ p = p->link; len++; } if (k > len) return 0 ; int i = len - k + 1 ; p = list ; for (i; i > 0 ;i--) p =p->data; printf ("%d" , p->data); return 1 ; }
方法2: 最优解法,一次遍历完成查询。
思路: 1.使用双指针:定义两个指针*p,*q。 2.*q指向单链表第一个节点不动,*p向后遍历k个节点。 合法性判断也比较简单:在两个指针间隔达不到k时,*p提前移动到尾结点处,则返回0 3.两个指针同时移动,直到*p移动到尾结点处。则*q指向的节点则是题目所求。
画了个图: 举了个例子,求倒数第2个节点的data值,最后q所指向的第3个节点是题目所求。
代码实现: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 int find2 (struct ListNode *list , int k) { struct ListNode *p = list ; struct ListNode *q = list ; int i = k; while (i > 0 ){ p = p->link; i--; } if (p == NULL ) return 0 ; while (p != NULL ){ q = q->link; p = p->link; } printf ("%d" , q->data); return 1 ; }
本文作者: spg2021本文链接: https://spg2021.github.io/2020/03/31/408-2009/ 版权声明: 文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!
]]>