Leetcode 148.排序链表的三种解法

https://leetcode.cn/problems/sort-list/

题面如下:

给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表 。

示例 1:

4f898cf92e5466e9b4f4fe061207f168.jpeg

输入:head = [4,2,1,3]
输出:[1,2,3,4]

示例 2:

b82da4a47187906142f64152f8ffc93a.jpeg

输入:head = [-1,5,3,4,0]
输出:[-1,0,3,4,5]

示例 3:

输入:head = []
输出:[]

提示:

  • 链表中节点的数目在范围 [0, 5 * 104] 内
  • -105 <= Node.val <= 105

这道题说是中等难度,实际上要是方法没选对的话,需要考虑很多边边角角的特殊临界条件。于我而言,hard题无非就是方法难想一点,想到了就是一气呵成(当然,大多数情况下想不到),但这样需要考虑一堆临界条件的题,我打心底里觉得不比hard题容易多少。

先来说一个在官方题解下面看到的十分好理解的取巧“速通”解法:把链表遍历一遍,放到数组里再自己排序一遍,然后在同时遍历数组和链表,把数组里的值覆盖到链表中去。

很难说我第一次看到这样的方法时在想些什么,但很明显,我第一个想到的方法比这复杂得多,反而是这样简单粗暴的,放以前我第一个能想出来的方法,现在的我却想不出来。看来多半是已经被这些个算法给驯化力(悲)。

说回正题,这个解法的代码也很简单:

ListNode* sortList(ListNode* head)
{
	vector<int> v;
	ListNode* p = head;
	while (p)
	{
		v.push_back(p->val);
		p = p->next;
	}
	sort(v.begin(), v.end());
	p = head;
	for (int i = 0; p; i++, p = p->next)
	{
		p->val = v[i];
	}
	return head;
}

然后咱们来说第二种正经的方法:快速排序法

所谓快速排序,就是在待排序序列中确定一个基准值,然后遍历序列,比这个值小的放他左边,比这个值大的放他右边,然后就可以将待排序序列分成两个序列,再分别对那两个序列重复这个过程,这实际上是一个递归的过程。递归完成之后,序列也就排序好了。

放在这道以链表为待排序序列的题中,就是每一次都将链表分为两个链表,等到这两个链表分别排序完了之后再将它们合并起来得到一个已经排序好的链表,接着返回即可。这就是大致的思路,当然在这个思路的实现上还有很多细节,先贴出我的代码:

ListNode* sortList1(ListNode* head)
{
	if (head == NULL)
		return head;
	int l, r, z;
	l = r = head->val;
	ListNode* p, * q;
	p = head;
	while (p)
	{
		l = min(p->val, l);
		r = max(p->val, r);
		p = p->next;
	}
	if (l == r)
		return head;
	p = head;
	ListNode* h1, * h2;
	h1 = h2 = NULL;
	z = (l + r) >> 1;
	while (p)
	{
		q = p->next;
		if (p->val <= z)
		{
			p->next = h1;
			h1 = p;
		}
		else
		{
			p->next = h2;
			h2 = p;
		}
		p = q;
	}
	h1 = sortList1(h1);
	h2 = sortList1(h2);
	p = h1;
	while (p->next)
		p = p->next;
	p->next = h2;
	return h1;
}

插句题外话:在这道题中我学到的最重要的经验应该是:当你觉得代码过于臃肿,而且总是在不停的缝缝补补还不能解决问题时,放弃他,否则他还会浪费你更多时间,至少对于解决算法题来说是这样的。

这段代码的核心是第一个while循环。这段代码的目的是:遍历整个链表,将小于基准值的节点用头插法插入到头结点h1中,大于的则插入到h2中,之后进行递归操作,分别对h1与h2执行与上述相同的操作。执行完了在进行链表的拼接即可。

我们选取的基准值是链表最大值与最小值之和除以二这个数,这其实保证了每次递归传入的head节点不会为空。那么应该怎么判断递归的结束条件呢?这里有一个比直接判断“当前传入的头结点是否只有一个这个判断条件”更有效率的方法:直接判断传入的头节点的链表的最大值与最小值是否相等,若是相等,则该链表的所值都应该是相等的,也就没有判断的必要,直接返回头结点即可。这个判断条件其实还囊括了一个我一开始没能想到的临界条件:有可能出现长度不为一的值都相等的链表,它若是放在之后的快速排序中比较的话,就会出现接下来递归时传入空节点nullptr的情况。

还有一点就是关于基准值的选取。常规的(l+r)/2并不能适配负数的情况,因为我们要的是向下取整,否则程序就会陷入无限递归,导致爆栈。而上面的那个表达式是向零取整,也就是说当其取值为负数时,它取整的值会更靠近0。举个例子:假设咱递归到最后就只剩下两个数:-1和-2,他们的带入上面的式子得到的就是-1,那这两个值都要小于等于-1,那岂不是到下一层递归还是这两个值?如此往复,系统栈就爆了(这个情况真的很难想到的,我就在这儿抓狂了好一会)。因此我们采用位运算来计算其中值。

接下来我们说第三个方法:归并排序法

所谓归并排序就是,将待排序序列从中间分开,然后也分别对这两个子序列进行归并排序,这同样也是一个递归的过程。然后得到两个已经排序完成的有序数列,再把他们合并为一大个有序数列即可。时间复杂度为o(nlogn)。

int getlength(ListNode* head)
{
	int n = 0;
	ListNode* p = head;
	for (; p; p = p->next)
	{
		n++;
	}
	return n;
}
ListNode* merge(ListNode* head, int n)
{
	if (n <= 1)
		return head;
	ListNode* p = head;
	int mid = n / 2;
	for (int i = 1; i < mid; i++)
	{
		p = p->next;
	}
	ListNode* p1 = head, * p2 = p->next;
	p->next = NULL;
	p1 = merge(p1, mid);
	p2 = merge(p2, n - mid);
	ListNode* ans = new ListNode;
	p = ans;
	while (p1 || p2)
	{
		if (p1 && !p2)
		{
			while (p1)
			{
				p->next = p1;
				p = p->next;
				p1 = p1->next;
			}
			break;
		}
		else if (!p1 && p2)
		{
			while (p2)
			{
				p->next = p2;
				p = p->next;
				p2 = p2->next;
			}
			break;
		}
		if (p1->val <= p2->val)
		{
			p->next = p1;
			p = p->next;
			p1 = p1->next;
		}
		else if (p1->val > p2->val)
		{
			p->next = p2;
			p = p->next;
			p2 = p2->next;
		}
	}
	return ans->next;
}
ListNode* sortList(ListNode* head) 
{
	int n = getlength(head);
	head = merge(head, n);
	return head;
}

那么在这道题中,我们若是要达到常数级的空间复杂度,就要在原链表的基础上对其进行排序(包括上一个解法也是这么做的)。我们首先需要求出链表节点个数的方法,接着进行归并排序。归并排序的递归中,递归结束的条件是当当前的需要进行归并排序的节点个数小于等于一个时。

接着我们将待排序链表一分为二,分别进行归并排序,得到返回的链表头结点后,由于返回的两个链表都是有序的,我们使用虚拟头结点法,对这两个链表分别同时进行遍历操作,每一次都找出其中的未被找出的最小值然后放入插入带虚拟头结点的链表的末尾。最后返回虚拟头结点的next指针便完成了归并排序的操作。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值