《算法题讲解指南:优选算法-链表》--53.重排链表,54.合并 K 个升序链表,55.K个一组翻转链表

🔥小叶-duck个人主页

❄️个人专栏《Data-Structure-Learning》《C++入门到进阶&自我学习过程记录》
《算法题讲解指南》--优选算法
《算法题讲解指南》--递归、搜索与回溯算法
《算法题讲解指南》--动态规划算法

未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游


目录

53.重排链表

题目链接:

题目描述:

题目示例:

解法(模拟):

C++算法代码(写法一:从slow->next位置翻转链表):

C++算法代码(写法二:从slow位置翻转链表):

算法总结及流程解析:

[54.合并 K 个升序链表](#54.合并 K 个升序链表)

题目链接:

题目描述:

题目示例:

解法一(利用堆):

算法思路:

解法二(递归/分治):

算法思路:

C++算法代码(优先级队列):

C++算法代码(递归):

算法总结及流程解析:

55.K个一组翻转链表

题目链接:

题目描述:

题目示例:

解法(模拟):

算法思路::

C++算法代码:

算法总结及流程解析:

结束语


53.重排链表

题目链接:

143. 重排链表 - 力扣(LeetCode)

题目描述:

题目示例:

解法(模拟):

画图画图画图,重要的事情说三遍!

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 个升序链表

题目链接:

23. 合并 K 个升序链表 - 力扣(LeetCode)

题目描述:

题目示例:

解法一(利用堆):

算法思路:

合并两个有序链表是比较简单,就是用双指针依次比较链表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个一组翻转链表

题目链接:

25. K 个一组翻转链表 - 力扣(LeetCode)

题目描述:

题目示例:

解法(模拟):

算法思路::

本题的目标非常清晰易懂,不涉及复杂的算法,只是实现过程中需要考虑的细节比较多。

我们可以把链表按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个分组,每组进行反转并记录头尾节点与前后面连接。**希望大家能有所收获!

相关推荐
Alicx.3 小时前
dfs——偏难
算法·蓝桥杯·深度优先
北顾笙9803 小时前
day08-数据结构力扣
数据结构·算法·leetcode
j_xxx404_3 小时前
蓝桥杯基础--前缀和
数据结构·c++·算法·蓝桥杯·排序算法
华农DrLai3 小时前
什么是知识图谱?实体、关系、属性分别是什么?
人工智能·算法·llm·nlp·prompt·知识图谱
chh5633 小时前
从零开始学习C++ -- 基础知识
开发语言·c++·windows·学习·算法
Lzh编程小栈3 小时前
【数据结构与算法】C语言实现双向链表 (Double Linked List) 全解析
c语言·开发语言·数据结构·链表
AlenTech3 小时前
142. 环形链表 II - 力扣(LeetCode)
数据结构·leetcode·链表
剑心诀3 小时前
【C语言 数据结构】易错题集
c语言·数据结构·算法
旺仔.2913 小时前
顺序容器:list双向链表 详解
数据结构·c++·链表·list