深入理解归并与排序:从Leetcode实践出发(题号21、147、148)

时间:2024-10-02 15:20:58

排序是算法设计中最基本的问题之一。很多语言也都内置了排序函数,实际开发中需要手工编写排序函数的情况并不多见。但如果涉及到链表排序的话,通常内置函数就不能直接使用了。事实上,LeetCode题库中有相当多涉及排序的问题,也有很多操作链表的问题,例如:逆转链表(题号#206)、删除链表中倒数第N个结点(题号#19)等。本文主要讨论其中几个把链表和排序结合在一起的问题,并复习一下关于归并排序的知识。本文的题解中涉及很多链表的基本操作,如果读者对链表还不是很熟悉,请参考《算法之美:隐匿在数据结构背后的原理》一书(本书繁体中文版已授权博硕文化出版集团在中国*地区发行)。

  • 探秘算法世界,求索数据结构之道;
  • 汇集经典问题,畅享编程技法之趣;
  • 点拨求职热点,敲开业界名企之门。

本书围绕算法与数据结构这个话题,循序渐进、深入浅出地介绍了现代计算机技术中常用的四十余个经典算法,以及回溯法、分治法、贪婪法和动态规划等算法设计思想。在此过程中,本书也系统地讲解了链表(包括单向链表、单向循环链表和双向循环链表)、栈、队列(包括普通队列和优先级队列)、树(包括二叉树、哈夫曼树、堆、红黑树、AVL树和字典树)、图、集合(包括不相交集)与字典等常用数据结构。同时,通过对22个经典问题(包括约瑟夫环问题、汉诺塔问题、八皇后问题和骑士周游问题等)的讲解,逐步揭开隐匿在数据结构背后的算法原理,力图帮助读者夯实知识储备,激活思维技巧,并最终冲破阻碍编程能力提升的重重藩篱。辅有完整的C++源代码,并穿插介绍了STL中的各种容器


题目21:Merge Two Sorted Lists

Merge two sorted linked lists and return it as a sorted list. The list should be made by splicing together the nodes of the first two lists.

题目要求合并两个有序链表,难度等级为“简单”。而且在《算法之美:隐匿在数据结构背后的原理》一书,还讨论过一样的问题(见如下4.2.3节)。

彼时,我们在书中给出的是采用C++实现的算法,下面给出的参考答案是基于Python3实现的。

  1. class Solution:
  2. def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode:
  3. cur_node = ListNode(0)
  4. dummy = cur_node
  5. while l1 and l2:
  6. if <= :
  7. cur_node.next = l1
  8. l1 = l1.next
  9. else:
  10. cur_node.next = l2
  11. l2 = l2.next
  12. cur_node = cur_node.next
  13. if l1:
  14. cur_node.next = l1
  15. elif l2:
  16. cur_node.next = l2
  17. return dummy.next

题目147:Insertion Sort List

Given the head of a singly linked list, sort the list using insertion sort, and return the sorted list's head

LeetCode网站上还贴心地提供了一个动画用以说明插入排序的基本流程,如果你对插入排序本书不是很熟悉,请参考《算法之美:隐匿在数据结构背后的原理》一书中第11章的相关内容。

题解中更多考察的是操作链表的能力,而不是排序算法本身。

  1. class Solution:
  2. def insertionSortList(self, head: ListNode) -> ListNode:
  3. dummy = ListNode(-1)
  4. dummy.next, cur_node = head, head
  5. while cur_node and cur_node.next:
  6. if cur_node.val <= cur_node.next.val:
  7. cur_node = cur_node.next
  8. else:
  9. tmp = cur_node.next
  10. cur_node.next = cur_node.next.next
  11. prev = dummy
  12. while prev.next.val <= :
  13. prev = prev.next
  14. tmp.next = prev.next
  15. prev.next = tmp
  16. return dummy.next

题目148:Sort List

Given the head of a linked list, return the list after sorting it in ascending order.

Follow up: Can you sort the linked list in O(n logn) time and O(1) memory (. constant space)?

这是第147题的一个升级版,同样是对链表进行排序,但要求算法时间复杂度是O(nlogn),而插入排序的时间复杂度是O(n^2)。下面引用在《算法之美:隐匿在数据结构背后的原理》一书中关于归并排序的一些基本介绍。

基于递归来实现归并排序是比较直观而且容易的,尽管以上描述中都是在讲数组的归并排序,其实转移到链表上,思路也是一样的。

  1. class Solution:
  2. def sortList(self, head: ListNode) -> ListNode:
  3. if not head or not head.next: return head
  4. mid = (head)
  5. left = (head)
  6. right = (mid)
  7. return (left, right)
  8. def midPoint(self, head):
  9. slow, fast = head, head
  10. while fast.next and fast.next.next:
  11. slow = slow.next
  12. fast = fast.next.next
  13. mid = slow.next
  14. slow.next = None
  15. return mid
  16. def merge(self, left, right):
  17. dummy = cur = ListNode(0)
  18. while left and right:
  19. if < :
  20. cur.next = left
  21. cur = cur.next
  22. left = left.next
  23. else:
  24. cur.next = right
  25. cur = cur.next
  26. right = right.next
  27. cur.next = left or right
  28. return dummy.next

对n个记录的序列进行归并排序时,必须进行\lceil \log n \rceil趟归并,而每趟归并操作的时间复杂是O(n),所以归并排序算法的时间复杂度是O(n\log n)。到此为止,如果能够写出递归实现的链表归并排序代码,题目的难度仍然属于是中等难度。但题目的Follow up是要求实现O(1)的空间复杂度。递归实现的成本就在于逐层递归返回时,需要额外的空间来存储结果,空间复杂度是O(\log n)。要真正实现O(1)的空间复杂度,就要设法使用迭代而非递归来实现链表的归并排序,这个代码的实现其实难度等级又上升了一个层次。注意,这个过程跟图11-19所示的二路归并排序算法示意是一致的。基于上述递归代码,我们需要做如下改动:

  • 递归换成迭代,因此找到链表中点的函数midPoint()便不再需要了
  • 归并算法不仅要返回Head指针,还要返回Tail指针,这是为了后续的组合拼接
  • 还需要一个函数split(),它把输入的链表分成两部分,一部分是开始的n个结点,剩余的结点作为第二部分
  1. class Solution:
  2. def sortList(self, head: ListNode) -> ListNode:
  3. if not head or not head.next: return head
  4. size, interval, cur = 0, 1, head
  5. while cur:
  6. size += 1
  7. cur = cur.next
  8. dummy = ListNode(-1)
  9. dummy.next = head
  10. while interval < size:
  11. cur = dummy.next
  12. tail = dummy
  13. while cur:
  14. l = cur
  15. r = (l, interval)
  16. cur = (r, interval)
  17. m0, m1 = (l, r)
  18. tail.next = m0
  19. tail = m1
  20. interval <<= 1
  21. return dummy.next
  22. #splits the list into 2 parts, first n elements and the rest
  23. def split(self, head: ListNode, size: int) -> ListNode:
  24. rest = head
  25. while rest and size > 0:
  26. size -= 1
  27. head = rest
  28. rest = rest.next
  29. if head:
  30. head.next = None
  31. return rest
  32. def merge(self, left, right):
  33. dummy = cur = ListNode(0)
  34. while left and right:
  35. if < :
  36. cur.next = left
  37. cur = cur.next
  38. left = left.next
  39. else:
  40. cur.next = right
  41. cur = cur.next
  42. right = right.next
  43. cur.next = left or right
  44. ## Return both head and tail
  45. while cur.next:
  46. cur = cur.next
  47. return dummy.next, cur

 

【本文完】