LeetCode题目-合并K个排序链表

合并K个排序链表是我在刷LeetCode的时候遇到的题目,描述大致如下:

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

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

一开始的思路是类似于合并两个有序链表的思路,就是每次在这k个链表中取出一个最小的节点,以此类推,不过这里还存在一个问题,那就是对于两个链表来说,只需要一个简单的判断就可以得到它们之中的最值了,而对于k个链表则不行,所以,我们还需要一个类似于缓冲区一样的东西。步骤大致如下:

  1. 从这k个链表中遍历取出第一个不为nullptr的节点并放入缓冲区中
  2. 从缓冲区中拿出一个最小值,取名为min
  3. 判断min->next是否指向了nullptr,如果不为空的话,则以min->next节点代替之前的min节点。

重复2-3步骤直至缓冲区为空即可。

根据描述可以推断出此法可行,不过需要对缓冲区进行一定的优化,因为每次取的都是最值,所以我们可以对这个缓冲区进行排序。

1.插入排序

我第一个想到的就是插入排序,原因无它,简单。

	/*倒序*/
	void insertOne(vector<ListNode*>& list, ListNode* node) {
		if (node == nullptr)
			return;
		auto it = list.begin();
		//找到合适的插入位置
		for (; it != list.end(); it++)
		{
			auto temp = *it;
			if (node->val > temp->val)
				break;
		}
		//插入
		list.insert(it, node);
	}

insertOne就是不完整插入排序,该函数需要保证list在这之前已经完全有序。

这里采用的是倒序,即最小值在最后的位置,这样的好处就是在取出来的时候不需要把所有的元素都向前移位。

	ListNode* mergeKLists(vector<ListNode*>& lists) {
		//队列
		vector<ListNode*> queue;
		//保存结果
		ListNode* result = new ListNode(0);
		ListNode* p = result;
		//目前需要保证不是空数组 插入头
		for (int i = 0; i < lists.size(); i++)
			insertOne(queue, lists[i]);
		while (!queue.empty()) {
			//从队列中取出最小值
			auto min = queue.back();
			p->next = min;
			p = p->next;
			queue.pop_back();
			//判断是否存在后续
			if (min->next != nullptr)
				insertOne(queue, min->next);
		}
		return result->next;
	}

mergeKList就是合并这k个链表的函数。

result指针保存着插入的结果,它用到了dummy head(头节点),使用它可以避免对特殊情况进行判断。

queue数组则是之前谈到的缓冲区。

            //从队列中取出最小值
			auto min = queue.back();
			p->next = min;
			p = p->next;
			queue.pop_back();

从缓冲区queue中取出最小值,然后把这个值链到了result头节点链表之中。

			//判断是否存在后续
			if (min->next != nullptr)
				insertOne(queue, min->next);

由于在一开始存放的是各个非空链表的头指针,所以可以凭借着它的next的值可以遍历这个链表。

假设k=3.数据为{{1, 4, 5, }, {1, 3, 3}, {2, 6}}。

按照之前的思路,缓冲区如下:

 此时我们取出的最小值min则是{1, 4, 5}链表中的值为1的节点,接着我们判断该节点的next不为空,则去除掉min节点。

 接着把{4,5}插入到缓冲区中。

 这时弹出的则是缓冲区最右边的值为1的节点,以此类推,直至缓冲区为空为止。

接着在leetcode中运行代码,结果如下:

嗯,运行速度比较慢。

需要注意的是,上述的缓冲区可以使用链表,但是注意不要覆盖掉ListNode的next属性,否则会破坏原有的链表结构。可以使用c++提供的单向链表slist 或双向链表list,也可以自己封装。运行速度应该会有所提升。

2.最小堆

既然可以使用排序算法,那么堆排序也值得一试。

	void makeHeap(vector<ListNode*>& list, int len) {
		int i;
		for (i = len / 2; i > 0; i--)
			heapAdjustDown(list, i, len);
	}

把list数组调整成最小堆。

    /*向下调整*/
    void heapAdjustDown(vector<ListNode*>& list, int s, int length) {
        //暂存
        ListNode* temp = list[s];
        int j = 0;

        for (j = 2 * s; j <= length; j *= 2) {
            //s节点的子孩子中取较小的那个
            if (j < length && list[j]->val > list[j + 1]->val)
                j++;
            //root的值要小于子节点,则不必调整
            if (temp->val <= list[j]->val)
                break;
            list[s] = list[j];
            s = j;
        }
        //回写
        list[s] = temp;
    }

当栈顶元素被替换后,堆被破坏,将栈顶元素向下调整使其继续保持最小堆的性质,再输出栈顶元素。

而向上调整则是在将新节点放在了堆的末端,再执行的向上调整,由于本次的替换局限于堆顶,所以用不到向上调整的函数。

	ListNode* mergeKLists(vector<ListNode*>& lists) {
		//堆
		vector<ListNode*> heap;
		//保存结果 引入了头节点
		ListNode* head = new ListNode(0);
		ListNode* p = head;
		//插入空指针,以满足完全二叉树的特性
		heap.push_back(nullptr);
		for (int i = 0; i < lists.size(); i++) {
			if (lists[i] != nullptr)
				heap.push_back(lists[i]);
		}
		//调整为最小堆
		makeHeap(heap, heap.size() - 1);
		while (heap.size() > 1) {
			//从最小堆中取出值
			auto min = heap[1];
			p->next = min;
			p = p->next;
			//判断是否存在后续
			if (min->next != nullptr)
				heap[1] = min->next;
				//把最后一个元素放到首部
			else
			{
				heap[1] = heap.back();
				heap.pop_back();
			}
			if (heap.size() != 1)
				heapAdjustDown(heap, 1, heap.size() - 1);
		}
		return head->next;
	}

由于为了使用到完全二叉树的特性,所以堆的有效节点是从1开始存放的。

此时的循环条件改为了heap.size() > 1,和之前的queue.empty()作用是相同的。

			if (heap.size() != 1)
				heapAdjustDown(heap, 1, heap.size() - 1);

需要注意的是,当堆里面不存在有效数据的时候,则不再对堆调整。

接着在leetcode上运行:

几乎相同的代码,第一次跑了30ms,第二次跑了50多ms。不管是哪一个,都要比之前的插入排序快了10倍多一点。 

3.最小堆(c++ algorithm)

当然,在笔试或者是面试的时候,手撸最小堆或最大堆算法还是有一定难度的,因此,除了上面的做法之外,还有一种做法就是使用c++的<algorithm>库中所提供的方法,其代码大致如下:

class Solution {
public:
    ListNode* mergeKLists(vector<ListNode*>& lists) {
        //最小堆
        vector<ListNode*> heap(lists.size());
            for (int i = 0;i < lists.size(); i++){
                if (lists[i] != nullptr)
                    heap[i] = lists[i];
        }
        //避免链表头节点判断
        ListNode* pHead = new ListNode(0);
        ListNode* p = pHead;
        //构建堆
        make_heap(heap.begin(), heap.end(), compare);
        while (!heap.empty()){
            ListNode* min_node = heap[0];
            p->next = min_node;
            p = p->next;
            //从最小堆中删除
            pop_heap(heap.begin(), heap.end(), compare);
            heap.pop_back();
            //插入堆中
            if (min_node->next != nullptr){
                heap.push_back(min_node->next);
                push_heap(heap.begin(), heap.end(), compare);
            }
        }
        auto result = pHead->next;
        delete pHead;
        return result;
    }
private:
    static bool compare(ListNode* node1, ListNode* node2){
        return node1->val > node2->val;
    }
};

主要用到的就是make_heap、push_heap和pop_heap,经过leetcode测试,消耗时间如下:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值