C/C++练习题之合并k个已排序的链表

 更新时间:2023年06月19日 10:46:00   作者:春人.  
这篇文章主要给大家介绍了关于C/C++练习题之合并k个已排序的链表的相关资料,文中通过图文以及实例代码介绍的非常详细,对大家学习或者使用C/C++具有一定的参考学习价值,需要的朋友可以参考下

前言:

今天给大家分享一道面试中常见的题目——合并K个升序链表,我会用暴力和分治两钟方法去求解这道题目,通过动图展示问题求解的全过程。这里提醒大家,画图是我们求解复杂问题的有效手段,有时可以起到事半功倍的效果,各位小伙伴在做题的过程中如果遇到麻烦,不妨动手画画图哟。

题目描述:

合并K个升序的链表并将结果作为一个升序的链表返回其头节点。例如:

  • 输入:[{1,2},{1,4,5},{6},{2,3,5}]
  • 输出:{1,1,2,2,3,4,5,5,6}

思路一:暴力求解法

首先根据题目的描述,画出如下模拟图。

第一步:确定合并后链表的头节点rhead

从上图中可以看出:lists中存放是每个链表的头节点,那合并后链表的头节点一定是这四个头结点中最小的那个,因此我们只需要遍历lists就可以找到最小的头节点,然后把它赋值给rhead,执行完第一步得到的结果如下图,用黄色标注已经排好序的节点:

第二步:选择次小的进行尾插

如上图,接下来我们需要在所有蓝色节点中选出最小的一个尾插到rhead指向的链表,因此我们再定义一个rtail指向合并后链表的最后一个节点。但是我们发现如果按照上图的结构,直接从四个蓝色节点里面选出最小的,显然十分困难, 因为第一个链表中待比较的节点不再数组中。如果向第一步那样,所有的节点都在数组中,那选出最小的节点简直是易如反掌,只需要遍历一遍数组即可。因为lists数组中存的永远都是头节点,所以这里我们直接修改第一个链表的头节点即可,通过lists[0] = lists[0]->next就可以实现。修改后的结构如下图所示:

此时所有待比较的节点都来到了数组中,和第一步的逻辑一样,只需要遍历一遍数组就可以找到最小的节点,找到后尾插到rhead指向的链表。如下图,其中黄色节点是已排好序的节点,蓝色节点是待比较的节点。

总体逻辑就是这样,接下来循环执行第二步,找到次小的进行尾插,直到数组中的所有节点都为空,此时说明数组中的所有链表都已经排好序了,返回rhead即可。总的过程动图如下:

🐼代码实现:

class Solution {
public:
    ListNode* mergeKLists(vector<ListNode*>& lists) {
        ListNode* rhead = nullptr;//定义合并后链表的头节点
        ListNode* rtail = nullptr;//定义合并后链表的尾结点
        while (true)
        {
            int minIndex = -1;//定义lists数组中最小节点下标,最初等于-1,(下简称最小位置)
            int i = lists.size();//
            while (i--)
            {
                if (lists[i] != nullptr)//首先判断数组当前位置的节点是否为空
                {
                    //当前节点不为空再进来
                    if (minIndex == -1)//判断最小位置是否是-1,如果是就直接把当前位置赋值给minIndex
                    {
                        minIndex = i;
                    }
                    else if (lists[i]->val < lists[minIndex]->val)//如果minIndex不为-1,则用数组当前位置节点的val与记录的最小位置上节点的val进行比较
                    {
                        minIndex = i;//如果当前节点的val值更小,那就更新minIndex
                    }
                }
            }
            if (minIndex == -1)//遍历完一遍数组如果minIndex的值还是-1,说明当前数组中的所有节点全是空
            {
                return rhead;//此时说明所有链表已经合并完成,可以返回头节点
            }
            if (rhead == nullptr)//确定新链表的头节点
            {
                rhead = rtail = lists[minIndex];
            }
            else//之后每找出一个最小的节点,进行尾插即可
            {
                rtail->next = lists[minIndex];
                rtail = rtail->next;
            }
            lists[minIndex] = lists[minIndex]->next;//最小的节点已经尾插到新链表,因此要对最小位置上的节点更新
        }
    }
};

上面代码中需要注意的有:minIndex 是用来记录lists中最小节点的位置,它的初始值必须是-1,不能是0或其他数字,因为我们不知道0或其他位置上的节点是否为空。还有需要注意的地方是,每当遍历到一个节点,首先要判断是否为nullptr,当不为空的时候再进行比较,避免出现空指针问题,其次要判断minIndex当前是否为-1,如果是-1,就不用比较,直接把当前位置赋值给minIndex 即可,因为如果当minIndex == -1 的时候进行比较,会导致数组越界。

思路二:分治归并法

分治即“分而治之”,“分”指的是将一个大而复杂的问题划分成多个性质相同但是规模更小的问题,子问题继续按照这样划分,直到问题可以被轻易解决;“治”指的是将子问题单独进行处理。经过分治后的子问题,需要将解进行合并,也就是所谓的“归并”,这样才能得到原问题的解,因此整个分支过程经常采用递归来实现。

针对这道题目来说,我们比较熟悉的是合并两个有序链表,因此我们就可以把合并K个有序链表的问题,划分成合并两个有序链表的问题,具体过程我通过动图来给大家演示,其中相同的颜色代表一个子问题

通过上面的动图我们可以发现,当子问题被分解到只剩一个链表的时候,就无法再进行分解,此时就需要返回了,其实这就是递归的终止条件,也就是当left == right的时候就只剩一个链表,此时就应该返回了,返回的结果就是这一个链表。等左右区间各返回一个链表,此时就要开始对这两个链表进行合并,从而得到一个新的有序链表,再把这个链表作为子问题的处理结果进行返回。这其实很像二叉树的后序遍历,如此循环往复,直到最终的大问题被解决。

🐼代码实现:

class Solution {
public:
    ListNode* mergeKLists(vector<ListNode*>& lists) {
        return dividemerge(lists, 0, lists.size() - 1);
    }
private:
    ListNode* dividemerge(vector<ListNode*>& lists, int left, int right)
    {
        if (left > right)
        {
            return nullptr;
        }
        else if (left == right)
        {
            return lists[left];
        }
        int mid = left + (right - left) / 2;
        //分治
        ListNode* head1 = dividemerge(lists, left, mid);
        ListNode* head2 = dividemerge(lists, mid + 1, right);
        //对子问题处理
        return Merge(head1, head2);
    }
private:
    ListNode* Merge(ListNode* pHead1, ListNode* pHead2)//合并两个有序链表
    {
        if (pHead1 == nullptr)
        {
            return pHead2;
        }
        if (pHead2 == nullptr)
        {
            return pHead1;
        }
        ListNode* pHeadret, * tail;
        if (pHead1->val < pHead2->val)
        {
            pHeadret = tail = pHead1;
            pHead1 = pHead1->next;
        }
        else
        {
            pHeadret = tail = pHead2;
            pHead2 = pHead2->next;
        }
        while (pHead1 != nullptr && pHead2 != nullptr)
        {
            if (pHead1->val < pHead2->val)
            {
                tail->next = pHead1;
                pHead1 = pHead1->next;
            }
            else
            {
                tail->next = pHead2;
                pHead2 = pHead2->next;
            }
            tail = tail->next;
        }
        if (pHead2 != nullptr)
        {
            tail->next = pHead2;
        }
        if (pHead1 != nullptr)
        {
            tail->next = pHead1;
        }
        return pHeadret;
    }
};

分治的大思想,其实就体现在下面三行代码中:

//分治
ListNode* head1 = dividemerge(lists, left, mid);        
ListNode* head2 = dividemerge(lists, mid + 1, right);

//对子问题处理        
return Merge(head1, head2);

我们只需要知道,前两行代码将一个大问题进行分解,得到的两个子问题,并且经过dividemerge函数把这两个子问题都处理好了,并且得到了两个结果,针对本题,就是得到了两个有序的链表,分别是head1head2,然后我们只需要对这两个链表进行合并就可以完成题目的要求,也就是第三行代码实现的功能。这就是分治的思想

总结

到此这篇关于C/C++练习题之合并k个已排序的链表的文章就介绍到这了,更多相关C/C++合并k个已排序的链表内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • C++ 智能指针原理、使用与最佳实践指南

    C++ 智能指针原理、使用与最佳实践指南

    本文全面介绍了C++智能指针的使用,包括RAII思想、标准库实现、核心原理和实际问题解决,重点讲解了unique_ptr、shared_ptr和weak_ptr的使用场景、优缺点及特殊资源管理,感兴趣的朋友跟随小编一起看看吧
    2025-11-11
  • C++STL教程之vector模板的使用

    C++STL教程之vector模板的使用

    模板类vector是一个动态数组,类似于string类,可存放任意类型,能够末尾、中间增加数据,基本上是是new创建动态数组的替代品,vector可以自动完成new和delete
    2022-08-08
  • 浅谈socket TCP编程中connect的一些坑

    浅谈socket TCP编程中connect的一些坑

    下面小编就为大家带来一篇浅谈socket TCP编程中connect的一些坑。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2016-12-12
  • 函数外初始化与函数内初始化详细解析

    函数外初始化与函数内初始化详细解析

    函数内初始化:bool FillStr(char *&szDst, int nSize);第一个参数中的&一定不能少,这是因为在函数外部我们只声明了这个指针,具体这个指针指向内存中的哪个地址我们并不知道,所以&是为了说明传递的是这个指针的引用,那么在函数内初始化后这个指针的地址也就是外面指针的地址了
    2013-09-09
  • Qt绘制时钟效果

    Qt绘制时钟效果

    这篇文章主要为大家详细介绍了Qt绘制时钟效果,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-05-05
  • C++详解如何实现动态数组

    C++详解如何实现动态数组

    这篇文章主要为大家详细介绍了C++实现动态数组的方法,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-06-06
  • C语言经典算法例题求100-999之间的“水仙花数”

    C语言经典算法例题求100-999之间的“水仙花数”

    本文的主要内容,设计一个程序,找出100-999之间的“水仙花数”,需要的朋友可以参考下
    2015-07-07
  • C++ Qt开发之ComboBox下拉组合框组件用法详解

    C++ Qt开发之ComboBox下拉组合框组件用法详解

    Qt 是一个跨平台C++图形界面开发库,利用Qt可以快速开发跨平台窗体应用程序,在Qt中,ComboBox(组合框)是一种常用的用户界面控件,它提供了一个下拉列表,允许用户从预定义的选项中选择一个,本文给大家介绍QComboBox类的一些常用方法,需要的朋友可以参考下
    2023-12-12
  • 利用mmap实现文件拷贝功能

    利用mmap实现文件拷贝功能

    这篇文章主要为大家详细介绍了利用mmap实现文件拷贝功能,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-10-10
  • 利用c++写一个简单的推箱子小游戏

    利用c++写一个简单的推箱子小游戏

    推箱子想必是很多人童年时期的经典游戏,我们依旧能记得抱个老人机娱乐的场景,下面这篇文章主要给大家介绍了关于如何利用c++写一个简单的推箱子小游戏的相关资料,需要的朋友可以参考下
    2021-09-09

最新评论