本期导航
导语:
大家好,我是刚从汤锅跳入算法坑不久的算法萌新任易仙!已经c语言基础+小进阶毕业的我觉得缺少实战的打磨。尤其是链表相关的内容,运用起来并没有那么熟练。这不,今天这道算法题,难度不高,但让我对链表的操作有了更熟练的掌握。
正文:
题目:
题目来源:
2807. 在链表中插入最大公约数https://leetcode.cn/problems/insert-greatest-common-divisors-in-linked-list/
思考历程:
与我使用过的其他oj平台不同的是,在力扣(LeeCode)上的题目提交基本只要提交一个自定义函数。虽然这样可以减少对次要部分的构思,但是对我这样的萌新来说,审题的要求就更高了(连输入的内容都不确定是什么形式,直接架构一个方法函数属实有些困难)。
好在,这题的审题难度不大,从题意我们可知已存有一个链表,链表长度未知,我们需要做的是在每俩个节点之间插入一个新节点,并且新节点的数值是相邻俩个节点的最大公约数,最后返回插入之后的链表头指针。题目给出的初始自定义函数是这样的:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* insertGreatestCommonDivisors(struct ListNode* head){
}
这里给出了一个结构体类型ListNode,包含了数值域——整型的val与指针域——ListNode结构类型指针next;定义了一个返回ListNode*类型且参数为ListNode*类型的函数。一开始我对这个题目抱有一个疑问——既然传入的参数是头指针,头指针也没变,直接定义void不好嘛?不过抱着问为什么答什么的思路,还是“不情愿”的先写上了:
return head;
萌新要爽,VS登场(鬼知道一开始没开编译器时,平台测试报错了多少次)!由于不熟练这种直接写自定义函数的题型,我在VS编译器里自己写了这样几个函数,实现链表的建立和输出:
//创建一个测试链表,结点手动修改,仅供测试使用
struct ListNode* buildListNode() {
a1.val = 18;
a1.next = &a2;
a2.val = 6;
a2.next = &a3;
a3.val = 10;
a3.next = &a4;
a4.val = 3;
a4.next = NULL;
struct ListNode* head = &a1, * p;
return insertGreatestCommonDivisors(head);
}
//遍历打印链表
void coutListNode(struct ListNode* p) {
while (p != NULL) {
printf("%d ", p->val);
p = p->next;
}
}
int main() {
coutListNode(buildListNode());
return 0;
}
接下来就开始实现函数内部的功能了:按题意有俩部分要求,第一个要实现最大公约数的查找,还有一个是要实现链表的插入。
实现最大公约数我们采用辗转相除法,这个方法已经有很多大佬详细解释过了,这里不做赘述,具体实现如下:
int getGreatestCommonDivisors(int a, int b) {
int t, c;
//交换使a≥b
if (a < b) {
t = a;
a = b;
b = t;
}
//辗转相除法
c = a % b;
while (c != 0) {
a = b;
b = c;
c = a % b;
}
return b;
}
然后,我们需要对原链表进行操作,不过在操作之前,我们还需要判定一下是否需要求取最大公约数并执行插入操作。而是判定的条件便是当前节点的next指针是否指向空,如果指向空,则当前节点即为末节点,不需要执行任何操作,反之才计算最大公约数并进行插入。下面是我实现插入的代码:
struct ListNode* insertGreatestCommonDivisors(struct ListNode* head) {
struct ListNode* p, * s;
s = head;
while (s->next!= NULL) {
p = (struct ListNode*)malloc(sizeof(struct ListNode*));
p->val = getGreatestCommonDivisors(s->val, s->next->val);
p->next = s->next;
s->next = p;
s= s->next->next;
}
return head;
}
这里是在俩个节点之间插入,首先我声明了俩个ListNode结构体类型指针变量p(用于指向需要新插入的节点),s(用于遍历原链表节点)。接着用malloc函数,开辟一块空间存放新节点的数据。然后先将p的next指针指向s所指向节点的next指针所指向的节点,再修改s所指向节点的next指针指向p所指向的节点(空间地址) 。最后让s指针后移俩个节点位置(即让s指向s所指向节点的next指针所指向节点的next指针所指向的地址),我们就完成了节点的插入工作。用图片可以更形象地描述这一过程:
下面是对这种建表情况的输出:
当只有一个节点时,
struct ListNode* buildListNode() {
a1.val = 18;
a1.next = NULL;
struct ListNode* head = &a1, * p;
return insertGreatestCommonDivisors(head);
}
也是正常处理,结果正确:
而当我兴致冲冲地再去平台测试时:
这里报错的大致含义是p->next所指向的空间没有足够的空间容纳s->next所指向的空间。一开始我并不是很明白如何解决这个问题,直到我进入题解区发现应该这样开辟空间:
p = (struct ListNode*)malloc(sizeof(struct ListNode));
才能通过(此次提交是编写博客时提交的)
这是这道题我的最终代码:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
int getGreatestCommonDivisors(int a, int b) {
int t, c;
if (a < b) {
t = a;
a = b;
b = t;
}
c = a % b;
while (c != 0) {
a = b;
b = c;
c = a % b;
}
return b;
}
struct ListNode* insertGreatestCommonDivisors(struct ListNode* head) {
struct ListNode* p, * s;
s = head;
while (s->next != NULL) {
p = (struct ListNode*)malloc(sizeof(struct ListNode));
p->val = getGreatestCommonDivisors(s->val, s->next->val);
p->next = s->next;
s->next = p;
s = s->next->next;
}
return head;
}
辗转相除算法复杂度O(logn),链表遍历插入算法时间复杂度O(n),本算法采用c语言编写。
疑问思考区:
虽然题目是过了,不过针对开辟空间大小和空间是否足够存储问题,我进行了一番研究,首先我输出了这俩个类型的所占字节空间:
printf("%d\n", sizeof(struct ListNode));
printf("%d", sizeof(struct ListNode*));
打印结果分别是:16和8:
回想了一下基础知识:指针类型所占空间是一个固定值,在64位系统占8字节,在32位系统占4字节,我这里明显是64位的情况。可是,我这个p指针不就是指针变量吗?为什么存不下呢?经过反思,我发现我这里开辟的空间应该是由指针p指向的一块结构体节点的空间,而不是p的空间。至于编译器没有报错,也能正常输出结果,有可能只是因为在我这种数值情况下节点的值均为正数且节点关系正确(具体原理未知),而并非所有情况下都能正常运行输出。故此,破案了!
结语:
实实在在书写总结过,我才发现没有实战的磨练,光是知道并不能很轻松的解决实际问题。插个题外话,结束这篇博客的这天是高数的期末考之日。哎,属实是低估了难度,认为只要平时简单看看,大概题能做出来就不必重视了。实际上,我不是记忆力高超之人,手生了,题自然做不快,还容易概念混淆,磕磕碰碰,结果必然是漏洞百出。放在哪行其实都一样,于我而言不经常思考,没有复习总结与长期的实战,等上战场时,思考的吃力将会百倍奉还。所以,只有平时不断的磨练,不断的实战,不断的强化,手才不会生,思考才能少些磕磕碰碰。
不能盲目自信!抱有一颗重视之心,才能在各种战场中所向披靡。当然事情已经发生了,我应该选择向前,不能因为失意而落魄,过分在意结果的不佳,反而会影响前进的方向和动力。