
🔥小叶-duck:个人主页
❄️个人专栏:《Data-Structure-Learning》《C++入门到进阶&自我学习过程记录》
《算法题讲解指南》--优选算法
《算法题讲解指南》--递归、搜索与回溯算法
《算法题讲解指南》--动态规划算法
✨未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游
目录
C++算法代码(写法一:从slow->next位置翻转链表):
[54.合并 K 个升序链表](#54.合并 K 个升序链表)
53.重排链表
题目链接:
题目描述:

题目示例:

解法(模拟):
画图画图画图,重要的事情说三遍!
1.找中间节点;
2.中间部分往后的逆序;
3.合并两个链表。
C++算法代码(写法一:从slow->next位置翻转链表):
cpp
class Solution {
public:
void reorderList(ListNode* head)
{
//方法一:找中间结点(断开链表后中间结点不包含在翻转范围内)
ListNode* slow = head;
ListNode* fast = head;
while(fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
}
//再翻转后半链表(头插法)
ListNode* cur = slow->next;
slow->next = nullptr;
//在翻转前一定要把原链表从slow->next断开,否则后面的合并会非常麻烦
//因为不将原链表head从中断开,后面合并就无法确定结束位置
//这里提一下为什么让slow->next置空而不是slow置空断开原链表?
//因为slow是我们创建的结点,slow本身置空是不会影响原链表lists的变化
//而slow->next指向的是原链表对应slow下一个位置的结点,所以置空就可以将原链表从中断开
ListNode* rhead = new ListNode(0);
while(cur)
{
ListNode* rnext = rhead->next;
ListNode* curnext = cur->next;
rhead->next = cur;
cur->next = rnext;
cur = curnext;
}
//合并两个链表
ListNode* ret = new ListNode(0);
//先引入一个虚拟头节点连接合并的结果,最后再传给head
ListNode* tail = ret;
ListNode* cur1 = head;
ListNode* cur2 = rhead->next;
//由于中间结点不包含在翻转范围内,所以对于奇数个结点的链表
//翻转链表一定比前半部分链表更短,所以判断结束条件为cur1到结尾
while(cur1)
{
tail->next = cur1;
cur1 = cur1->next;
tail = tail->next;
if(cur2)
{
tail->next = cur2;
cur2 = cur2->next;
tail = tail->next;
}
}
head = ret->next;
delete rhead;
delete ret;
}
};
C++算法代码(写法二:从slow位置翻转链表):
cpp
class Solution {
public:
void reorderList(ListNode* head)
{
//方法二:找中间结点(断开链表后中间结点包含在翻转范围内)
if(head->next == nullptr)
{
return;
}
ListNode* slow = head;
ListNode* fast = head->next->next;
while(fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
}
//再翻转后半链表(头插法)
ListNode* cur = slow->next;
slow->next = nullptr;
ListNode* rhead = new ListNode(0);
while(cur)
{
ListNode* rnext = rhead->next;
ListNode* curnext = cur->next;
rhead->next = cur;
cur->next = rnext;
cur = curnext;
}
//合并两个链表
ListNode* ret = new ListNode(0);
ListNode* tail = ret;
ListNode* cur1 = head;
ListNode* cur2 = rhead->next;
//由于中间结点包含在翻转范围内,所以对于奇数个结点的链表
//翻转链表一定比前半部分链表更长,所以判断结束条件为cur2到结尾
while(cur2)
{
if(cur1)
{
tail->next = cur1;
cur1 = cur1->next;
tail = tail->next;;
}
tail->next = cur2;
cur2 = cur2->next;
tail = tail->next;
}
head = ret->next;
delete rhead;
delete ret;
}
};
算法总结及流程解析:



54.合并 K 个升序链表
题目链接:
题目描述:

题目示例:

解法一(利用堆):
算法思路:
合并两个有序链表是比较简单,就是用双指针依次比较链表1、链表 2未排序的最小元素,选择更小的那一个加入有序的答案链表中。
合并K个升序链表时,我们依旧可以选择K个链表中,头结点值最小的那一个。那么如何快速的得到头结点最小的是哪一个呢?用优先级队列 这个数据结构就可以很好的解决:
我们可以把所有的头结点放进一个优先级队列中,这样就能快速的找到每次K个链表中,最小的元素是哪个。
解法二(递归/分治):
算法思路:
逐一比较时,答案链表越来越长,每个跟它合并的小链表的元素都需要比较很多次才可以成功排序。比如,我们有8个链表,每个链表长为100。
逐一合并时,我们合并链表的长度分别为(0,100),(100,100),(200,100),(300,100),(400,100),(500,100),(600,100),(700,100)。所有链表的总长度共计3600。
如果尽可能让长度相同的链表进行两两合并呢?这时合并链表的长度分别是(100,100)x4,(200,200)x2,(400,400),共计2400。比上一种的计算量整整少了1/3。
C++算法代码(优先级队列):
cpp
class Solution {
public:
struct compare
{
bool operator()(const ListNode* l1, const ListNode* l2)
{
return l1->val > l2->val;
}
};
ListNode* mergeKLists(vector<ListNode*>& lists)
{
//方法一:利用优先级队列,创建小根堆解决
priority_queue<ListNode*, vector<ListNode*>, compare> pq;
//先让所有链表的头节点放入队列中
for(int i = 0; i < lists.size(); i++)
{
//注意空链表不能放入队列中,因为队列的排序是通过node->val的值实现的
//空指针不能访问val
if(lists[i])
{
pq.push(lists[i]);
}
}
ListNode* ret = new ListNode(0);
ListNode* tail = ret;
while(pq.size())
{
ListNode* cur = pq.top();
pq.pop();
tail->next = cur;
tail = tail->next;
if(cur->next)
//cur下一个位置为空说明cur当前所在链表已经走完了
//则无需再放入到队列中,继续取堆顶元素即可
{
pq.push(cur->next);
}
}
return ret->next;
}
};
C++算法代码(递归):
cpp
class Solution {
public:
ListNode* mergeKLists(vector<ListNode*>& lists)
{
//方法二:递归
if(lists.size() == 0)
{
return nullptr;
}
return merge(lists, 0, lists.size() - 1);
}
ListNode* merge(vector<ListNode*>& list, int left, int right)
{
//merge函数多加两个形参left、right的目的就是得到递归的结束条件
if(left == right)
{
//当left=right说明递归到只有一个链表,一个链表无需合并直接返回即可
return list[left];
}
int mid = (left + right) / 2;//递归处理左右区间
ListNode* cur1 = merge(list, left, mid);
ListNode* cur2 = merge(list, mid + 1, right);
//此时[left, mid]区间和[mid + 1, right]区间内的链表就已经分别合并且有序了
//合并两个有序链表
ListNode* ret = new ListNode(0);
ListNode* tail = ret;
while(cur1 && cur2)
{
if(cur1->val > cur2->val)
{
tail->next = cur2;
tail = tail->next;
cur2 = cur2->next;
}
else
{
tail->next = cur1;
tail = tail->next;
cur1 = cur1->next;
}
}
while(cur1)
{
tail->next = cur1;
tail = tail->next;
cur1 = cur1->next;
}
while(cur2)
{
tail->next = cur2;
tail = tail->next;
cur2 = cur2->next;
}
return ret->next;
}
};
算法总结及流程解析:



55.K个一组翻转链表
题目链接:
题目描述:

题目示例:

解法(模拟):
算法思路::
本题的目标非常清晰易懂,不涉及复杂的算法,只是实现过程中需要考虑的细节比较多。
我们可以把链表按K个为一组进行分组,组内进行反转,并且记录反转后的头尾结点,使其可以和前、后连接起来。思路比较简单,但是实现起来是比较复杂的。
我们可以先求出一共需要逆序多少组(假设逆序n组),然后重复n次长度为k的链表的逆序即可。
C++算法代码:
cpp
class Solution {
public:
ListNode* reverseKGroup(ListNode* head, int k)
{
int n = 0;
ListNode* cur = head;
while(cur)
{
cur = cur->next;
n++;
}//计算链表的结点总数
int count = n / k;//count为翻转链表的组数
ListNode* ret = new ListNode(0);//ret为最终的结果链表
ListNode* newhead = ret;
//newhead的作用是:每次翻转完一组链表后,让newhead到翻转链表的结尾处
// 目的是为了获取下一组链表翻转的开头
ListNode* tmp = ret;//tmp的作用就是:记录每一组翻转链表的尾结点
cur = head;//cur用来遍历原链表
while(count--)
{
int i = k;
while(i--)
{
ListNode* curnext = cur->next;
ListNode* next = newhead->next;
if(next == nullptr)
{
//当newhead->next为空则说明是当前翻转链表插入的第一个结点
//这个结点也就是翻转链表的尾结点,所以需要用tmp进行记录
tmp = cur;
//以便翻转完链表后newhead能找到
}
newhead->next = cur;
cur->next = next;
cur = curnext;
}//循环结束则一组链表完成了翻转
newhead = tmp;//将newhead置于tmp位置进行下一组的链表反转
}
newhead->next = cur;
newhead = ret->next;
delete ret;
return newhead;
}
};
算法总结及流程解析:

结束语
到此,53.重排链表,54.合并 K 个升序链表,55.K个一组翻转链表 这三道算法题就讲解完了。**重排链表,通过找中间节点、后半部分逆序、合并两个链表三个步骤实现,提供了两种代码实现方式; 合并K个升序链表,给出优先级队列和递归分治两种解法,前者利用小根堆快速获取最小值节点,后者通过递归两两合并减少比较次数; K个一组翻转链表,将链表按K个分组,每组进行反转并记录头尾节点与前后面连接。**希望大家能有所收获!