用合适的数据结构,事倍功半——leetcode 21 & 23

用合适的数据结构,事倍功半——leetcode 21 & 23

先看23题:

将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 

示例:

输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4

题目是很简单的,它也的确就是个easy题。直接上代码:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution 
{
public:
    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) 
    {
        /*if (!l1) { return l2; }
        if (!l2) { return l1; }
        ListNode* l = NULL;
        if (l1->val < l2->val)
        {
            l = l1;
            l1 = l1->next;
        }
        else 
        {
            l = l2;
            l2 = l2->next;
        }*/
        ListNode* l = new ListNode(INT_MIN);
        ListNode* head = l;
        while (l1 && l2)
        {
            if (l1->val < l2->val)
            {
                l->next = l1;
                l1 = l1->next;
            }
            else 
            {
                l->next = l2;
                l2 = l2->next;
            }
            l = l->next;
        }
        l->next = l1 ? l1 : l2;
        //return head;
        return head->next;
    }
};

没什么好说的,唯一值得注意的地方就是,对于头结点的处理,怎么样可以使代码更简洁一点。想象一下,新的链表要求升序,那么第一个结点必须是最小的,但是现在有两个原链表,我们还要比较一下这两个链表的头结点,选那个小的作为新链表的头结点。这一段代码,逻辑上不难理解,但还是要几行的。

一种简单的实现方法是,把新链表地头结点设为INT_MAX,然后我们就可以放心大胆地往新链表中添加结点了。这样操作在时间复杂度上没有任何改进,但是因为在循环外少了个判断,因此代码更简洁了。如果这样做的话,记得返回的结点是头结点的后继结点。

再看23题:

合并 k 个排序链表,返回合并后的排序链表。请分析和描述算法的复杂度。

示例:

输入:
[
  1->4->5,
  1->3->4,
  2->6
]
输出: 1->1->2->3->4->4->5->6

21题的解法居然也可以过

这个题在leetcode上的难度是hard,但我觉得它是不配这个难度的。我们完全可以按照21题的做法去做这个题,代码如下:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* mergeKLists(vector<ListNode*>& lists) 
    {
        int min_index = findMinIndex(lists);
        if (min_index == -1) { return NULL; }
        ListNode* l = lists[min_index], *head = l;
        lists[min_index] = lists[min_index]->next;
        while(1)
        {
            min_index = findMinIndex(lists);
            if (min_index == -1) { break; }
            l->next = lists[min_index];
            lists[min_index] = lists[min_index]->next;
            l = l->next;
        }
        return head;      
    }
    int findMinIndex(vector<ListNode*>& lists)
    {
        int min_val = INT_MAX, n = lists.size(), min_index = -1;
        for (int i = 0; i < n; ++i)
        {
            if (lists[i])
            {
                if (lists[i]->val < min_val)
                {
                    min_index = i;
                    min_val = lists[i]->val;
                }
            }
        }
        return min_index;
    }
};

这样做,是能过的。那么它的时间复杂度是多少呢?我们算一下,假设总共有n个数,分别在k个链表中,每次我们都要比较k个链表的当前结点,找到最小的那个结点。因此时间复杂度是O(kn)。

最小堆

有没有更简单的解法呢?要是我们能把所有数都拿出来,然后再排序,时间复杂度会不会低一点?我们分析一下时间复杂度。现在的问题就是,我们把拿出来的数放到什么数据结构呢?

最容易想到的自然就是vector了,我们算一下这种做法的时间复杂度是多少。把所有数都拿出来是O(n),排序的时间复杂度是O(nlgn)。因此整个解法的时间复杂度是O(nlgn)。这个解法不见得时间复杂度更低的,需要比较k和lgn的值。但是在k比较大的情况下,这个解法显然是比较好的。

但是,说到排序,其实还有一种数据结构有这个作用。之前我们用得不算特别多,但还是应该会用的。它就是优先队列(实质上是一个最大堆或者最小堆,默认是最大堆)。事实上我们只能访问到堆顶元素,但是堆的优势在于它会自动维护,也就是说,以最小堆为例,无论我们向堆中插入或者删除多少数据,堆顶的元素始终是堆中元素最小的那个。所以,我们把所有数据扔进堆中,再一个一个拿出来,那么肯定是从小到大的。

然后,我们算一下用最小堆做这个题的时间复杂度是多少。堆的性质当中有很重要的一条,就是向堆中插入元素的时间复杂度是O(lgn),n为堆的大小。因此向堆中插入n个元素,时间复杂度就是O(lgn)(事实上时间复杂度是 lg1 + lg2 + … + lgn = O(nlgn))。而将这些元素依次取出也是这个时间复杂度。所以整个解题过程的时间复杂度就是O(nlgn),和用vector是一样的。实现代码如下:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* mergeKLists(vector<ListNode*>& lists) 
    {
        int n = lists.size();
        priority_queue<int, vector<int>, greater<int> > que;
        	//默认最大堆,所以要定义为最小堆
        ListNode* l = new ListNode(INT_MIN), *head = l;
        for (int i = 0; i < n; ++i)
        {
            while(lists[i])
            {
                que.push(lists[i]->val);
                lists[i] = lists[i]->next;
            }
        }
        while (!que.empty())
        {
            l->next = new ListNode(que.top());
            que.pop();
            l = l->next;
        }
        return head->next;
    }
};

真的需要这么大的最小堆吗?

还能再简单点吗?我们现在有k个链表,最小堆的大小是n,因为我们把链表的所有结点都扔进去了。事实上有没有这个必要呢?我们能不能在向堆中放入数据的同时,取出一部分元素先进行排序呢?我们推演一下,现在k个链表的头结点入堆,然后我们想找出最小结点的后驱结点,并且把它放入堆。我们发现,当这个最小结点的后驱结点入堆之后,我们就不需要在堆中保存这个最小结点了,因为这个最小结点所在链表的所有元素,可以通过后驱结点找到。因此,我们可以放心大胆地把这个最小结点弹出堆。

想明白这一点,就很容易知道,堆的大小为k就够用了。我们看一下这个时间复杂度,堆的大小是k,有n个结点要ru’dui出堆,因此时间复杂度是O(nlgk),这个时间复杂度显然是比之前的O(nlgn)要小的,因此这种改进是有意义的。实现的代码如下:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
 struct cmp1
 {
    ListNode* l;
    cmp1(ListNode *ll) { l = ll; }
    cmp1() {}
    bool operator<(const cmp1& ob) const    
    	//优先队列默认的是大顶堆,改成小顶堆
    {
        return l->val > ob.l->val; 
    }
};

class Solution {
public:
    ListNode* mergeKLists(vector<ListNode*>& lists) 
    {
        if (lists.empty()) { return NULL; }
        int n = lists.size();
        priority_queue<cmp1> que;
        cmp1 ob;
        for (auto it: lists)
        {
            if (it) { que.push(it); }    //有些链表为空,没必要头结点入堆
        }
        if (que.empty()) { return NULL; }    //所有链表都为空
        ListNode* head = new ListNode(INT_MIN), *cur = head;
        while (!que.empty())
        {
            ob = que.top();
            que.pop();
            if (ob.l->next)    //若if不满足,说明该链表已经到末端 
            {
                cmp1 ob2 = cmp1(ob.l->next);
                que.push(ob2);
            }
            cur->next = ob.l;
            cur = cur->next;
        }
        return head->next;
    }
};

这两道题到这里就结束了,我之前做到这里的时候也以为事情告一段落了,但过了几天才发现这才刚开始,后面又有几道题,和堆这个数据结构脱不开干系。这就留到下一篇再说了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值