LeetCode25 K个节点一组翻转链表
昨天面试腾讯订阅号搜索推荐事业群的实习面试,遇到了这道面试题。
题目
给你链表的头节点 head
,每 k
个节点一组进行翻转,请你返回修改后的链表。k
是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k
的整数倍,仍然正常翻转。
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
例,
1->3->2->4->5->6->7->8, k = 3
结果:2->3->1->6->5->4->8->7
我给出的原始代码如下:
Node* reverse(Node* first, int k){
if(first==NULL){
return NULL;
}
int t = 0;
Node* pre = first;
Node* cur = first;
// 1-> 2-> 4-> 5-> 6 k = 2;
Node* cur_tail = first;
Node* last_tail = first;
while(cur!=NULL){
Node* tem = new Node(-1);
tem = cur;
tem->next = pre;
cur = cur->next;
pre = tem;
t++;
t = t%k;
if(t % k == 0){
cur_tail = cur;
last_tail->next = cur_tail;
last_tail = cur_tail;
}
}
}
我采用的分组方式是使用数字进行特判定,并且没有记录翻转之后的链表的头节点。
经查,该题目是LeetCode25的变体。
力扣25: 给你链表的头节点 head
,每 k
个节点一组进行翻转,请你返回修改后的链表。k
是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k
的整数倍,则将最后的剩余节点保持原有顺序。
这里有机会可以写一篇关于文字表述的文章,因为察觉到在使用动宾短语时使用肯定句比使用否定句更加有感染力。例如这里的最后一句话也可以表达成:如果节点总数不是k的整数倍,则不翻转最后的剩余节点。表达起来,就没有“则将最后的剩余节点保持原有顺序”更加的形象、直观。
言归正传。
思路
将链表按照k个一组分组,分别翻转。所以可以使用一个指针tail依次指向每一组的尾部节点。这个指针每次向前移动k步,直至链表结尾,同时使用一个指针head记录每一组的头节点. 下一组的头节点即为当前组的尾节点的后序节点。对于每个分组,我们先判断它的长度是否大于等于k。若是,则翻转这部分链表,否则不需要翻转。
接下来的问题是如何反转一个分组内的子链表。同时还需要将子链表的头部与上一个子链表连接,以及子链表的尾部与下一个子链表连接。因此,我们需要记录子链表的头节点head, head的上一个节点pre, 当前链表的尾节点tail,以及使用一个临时变量nex记录下一个链表的头节点.
对于第一个子链表,它的头节点前面没有节点。没有节点就创建一个结点hair,将pre指向该节点。
如果链表的整体长度小于k, 则返回hair->next. 如果链表的整体长度大于k, 在循环的过程中,会通过修改pre->next指向下一个子链表的头节点。在第一个子链表中,pre指向的是hair, 因此hair->next也指向了第一个子链表翻转之后的头节点。因此,无论第一个子链表是翻转还是保持原有顺序,hair->next返回的都是最终链表的头节点。
// 翻转一个子链表,并且返回新的头与尾
pair<ListNode*, ListNode*> myReverse(ListNode* head, ListNode* tail){
ListNode* prev = tail->next;
ListNode* p = head;
while(prev!=tail){
ListNode* nex = p->next;
p->next = pre;
prev = p;
p = nex;
}
return {prev,head};
}
ListNode* reverseKGroup(ListNode* head, int k){
ListNode* hair = new ListNode(0);
hair->next = head;
ListNode* pre = hair;
while(head){
ListNode* tail = prev;
//查看剩余部分长度是否大于等于k
for(int i = 0;i<k;++i){
tail = tail->next;
if(!tail){
return hair->next;
}
}
//现在的tail是第一个k个节点的子链表的尾节点
ListNode* nex = tail->next; //下一组的头节点
// head是当前一组的头节点
pair<ListNode*, ListNode*> result = myReverse(head, tail);
head = result.first; // head是当前组的尾节点,反转之后成了头节点
tail = result.second; // tail是当前组的头节点,翻转之后成了尾节点
//把子链表重新接回原链表
pre->next = head;
tail->next = nex;
pre = tail; // pre指向了当前组翻转之后的尾节点。
head = nex; // head指向下一组的头节点
}
return hair->next;
}
理解翻转链表的子程序
就好比现在小学生站队,所有人都向右站,且手都搭在右边人的肩膀上,最后一个人没有地方可以搭,搭在了一棵大树上,这棵大树叫做NULL.
1.现在只有最左边的一个人脑子是清醒的,其余的人都喝醉了,只有左边的人可以唤醒右边的人。
2.并且左边的人转过方向之后就会忘记右边的人,
3.左边的第一个人脑子里始终清晰地记得最后一个人的位置。
4.现场有一个人可以记住很多位置。
请问如何让所有右边的人的手转向左边的人身上?
A->B->C->D->E->大树
答:首先现场人记住小A右边人B的位置和小A自己的位置,然后小A的手搭在大树上。
请现场人记住B右边人C的位置和自己的位置,B将自己的手搭在A肩膀上。
请现场人记住C右边人D的位置和自己的位置,C将自己的手搭在B肩膀上。
请现场人记住D右边人E的位置和自己的位置,D将自己的手搭在C肩膀上。
请现场人记住E右边人大树的位置和自己的位置,E将自己的手搭在D肩膀上。
至此,E已经将自己的手搭在了D身上,转身完毕。(因此转身完毕的条件是现场人上一个记忆的子集的位置是最右边的一个人的位置)
心得: 当用生活中的例子去理解算法的时候,算法就变得活灵活现了起来,就变得有趣了起来。
反思总结
在设计程序的过程中,可以提前列出哪些是全局指针,全局指针中哪些的指向是变化的,哪些的指向是固定的。并且指出每个指针的用途,有利于思路的厘清。
全局指针:
- 动态指针
head指向当前子链表的头节点。
tail 指向当前子链表的尾节点
pre指向当前子链表的上一个节点
- 不变指针
head之前的头节点 hair.
局部指针:
nex指针:存储下一个子链表的头节点
变体:如果该组少于k个节点,依然翻转
只需要将判断当前组的节点数是否大于等于k部分的代码改为寻找当前组的尾节点即可。
ListNode* reverseKGroup(ListNode* head, int k){
ListNode* hair = new ListNode(0);
hair->next = head;
ListNode* pre = hair;
while(head){
ListNode* tail = prev;
//找出该组的尾节点
for(int i = 0;i<k && tail->next!=NULL;++i){
tail = tail->next;
}
//现在的tail是第一个k个节点的子链表的尾节点
ListNode* nex = tail->next; //下一组的头节点
// head是当前一组的头节点
pair<ListNode*, ListNode*> result = myReverse(head, tail);
head = result.first; // head是当前组的尾节点,反转之后成了头节点
tail = result.second; // tail是当前组的头节点,翻转之后成了尾节点
//把子链表重新接回原链表
pre->next = head;
tail->next = nex;
pre = tail; // pre指向了当前组翻转之后的尾节点。
head = nex; // head指向下一组的头节点
}
return hair->next;
}