将单向链表按某值划分成左边小、中间相等、右边大的形式
此贴为笔者学习左神算法的记录。
【题目】 给定一个单向链表的头节点 head,节点的值类型是整型,再给定一个整数 pivot。实现一个调整链表的函数,将链表调整为左部分都是值小于 pivot 的节点,中间部分都是值等于 pivot 的节点,右部分都是值大于 pivot 的节点。除这个要求外,对调整后的节点顺序没有更多的要求。 例如:链表 9->0->4->5->1,pivot=3。 调整后链表可以是 1->0->4->9->5,也可以是 0->1->9->5->4。总之,满足左部分都是小于 3的节点,中间部分都是等于 3 的节点(本例中这个部分为空),右部分都是大于 3 的节点即可。 对某部分内部的节点顺序不做要求。 进阶: 在原问题的要求之上再增加如下两个要求。
- 在左、中、右三个部分的内部也做顺序要求,要求每部分里的节点从左到右的顺序与原链表中节点的先后次序一致。 例如:链表 9->0->4->5->1,pivot=3。调整后的链表是 0->1->9->4->5。在满足原问题要求的同时,左部分节点从左到右为 0、1。在原链表中也是先出现 0,后出现 1;中间部分在本例中为空,不再讨论;右部分节点从左到右为 9、4、5。在原链表中也是先出现 9,然后出现 4,最后出现 5。
- 如果链表长度为 N,时间复杂度请达到 O(N),额外空间复杂度请达到 O(1)。
【解答】 普通解法的时间复杂度为 O(N),额外空间复杂度为 O(N),就是把链表中的所有节点放入一个额外的数组中,然后统一调整位置的办法。具体过程如下: 1.先遍历一遍链表,为了得到链表的长度,假设长度为 N。 2.生成长度为 N 的 Node 类型的数组 nodeArr,然后遍历一次链表,将节点依次放进 nodeArr中。这里不用 LinkedList 或 ArrayList 等 Java 提供的结构,因为一个纯粹的数组结构比较利于步骤 3 的调整。 3.在 nodeArr 中把小于 pivot 的节点放在左边,把相等的放中间,把大于的放在右边。也就是改进了快速排序中 partition 的调整过程,即如下代码中的 arrPartition 方法。 4.经过步骤 3 的调整后,nodeArr 是满足题目要求的节点顺序,只要把 nodeArr 中的节点依次重连起来即可,整个过程结束。 全部过程请参看如下代码中的 listPartition1 方法。
public class ListPartition { class Node { private int value; private Node next; public Node(int data) { this.value = data; } } public Node listPartition1(Node head, int pivot) { if (head == null || head.next == null) { return head; } int i = 0; Node cur = head; while (cur != null) { i++; cur = cur.next; } Node[] arrNode = new Node[i]; // 空间复杂度O(N) cur = head; for (i = 0; i < arrNode.length; i++) { arrNode[i] = cur; cur = cur.next; } arrPartition(arrNode, pivot); // 重新链接链表 for (i = 1; i < arrNode.length; i++) { arrNode[i - 1].next = arrNode[i]; } arrNode[i - 1].next = null; return arrNode[0]; } public void arrPartition(Node[] arr, int pivot) { int small = -1; int big = arr.length; int index = 0; while (index < big) { if (arr[index].value < pivot) { // 和小于区域下一个元素做交换,小于区域向右扩,index++ swap(arr, ++small, index++); } else if (arr[index].value == pivot) { // 当前元素向前走 index++; } else { // 和大于区域左侧元素做交换,大于区域向左扩,index不动 swap(arr, --big, index); } } } public void swap(Node[] arr, int i, int j) { Node temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; }
下面来看看增加要求之后的进阶解法。对每部分都增加了节点顺序要求,同时时间复杂度 仍然为 O(N),额外空间复杂度为 O(1)。既然额外空间复杂度为 O(1),说明实现时只能使用有限的几个变量来完成所有的调整。 进阶解法的具体过程如下: 1.将原链表中的所有节点依次划分进三个链表,三个链表分别为 small 代表左部分,equal代表中间部分,big 代表右部分。 例如,链表 7->9->1->8->5->2->5,pivot=5。在划分之后,small、equal、big 分别为: small:1->2->null equal:5->5->null big:7->9->8->null 2.将 small、equal 和 big 三个链表重新串起来即可。 3.整个过程需要特别注意对 null 节点的判断和处理。 进阶解法主要还是考查面试者利用有限几个变量调整链表的代码实现能力,全部进阶解法 请参看如下代码中的 listPartition2 方法。
public Node listPartition(Node head, int pivot) { if (head == null || head.next == null) { return head; } Node sH = null; // 小的头 Node sT = null; // 小的尾 Node eH = null; // 相等的头 Node eT = null; // 相等的尾 Node bH = null; // 大的头 Node bT = null; // 大的尾 Node next = null; // 辅助空间,保存当前节点的下一个节点 while (head != null) { next = head.next; // 当前节点的next指向空 head.next = null; if (head.value < pivot) { if (sH == null) { // 如果小链表的头为空,表示当前还没有元素,则把这个元素作为链表的头部 sH = head; // 只有一个元素,那么这个元素既是链表的头部也是链表的尾部 sT = head; } else { // 如果头节点不为空,就说明链表有元素,则将元素插到链表的尾部 sT.next = head; // 尾节点则是最新的那个元素 sT = head; } } else if (head.value == pivot) { if (eH == null) { eH = head; eT = head; } else { eT.next = head; eT = head; } } else { if (sH == null) { sH = head; eT = head; } else { sT.next = head; sT = head; } } // 当前节点向下移动 head = next; } // 如果小于部分链表头节点不为空,那么链接小部分链表尾部到等于部分链表的头部 if (sT != null) { sT.next = eH; // 如果等于部分没有节点,那么合并后的尾节点还是sT eT = eT == null ? sT : eT; } if (eT != null) { eT.next = sH; } return sH != null ? sH : eH != null ? eH : bH; } }
#链表##partition#
笔者学习数据结构与算法的心得与经验。