算法修炼之路——【链表】Leetcode 725 分隔链表

题目描述

给定一个头节点为root的链表,编写一个函数,将链表分隔为k个连续的部分。

每部分的长度应该尽可能的相等:认一两部分的长度差距不能超过1,也就是说可能有些部分为null;

k个部分应该按照在链表中出现的顺序进行输出,并且排在前面的部分的长度大于等于后面的长度。

返回一个符合上述规则的链表的列表。

示例1:

输入: root= [1, 2, 3], k = 5
输出: output = [[1], [2], [3], [], []]
解释: 输入节点root.val = 1, root.next.val = 2; 输出output[0].val = 1, output[0].next = null; output[1].val = 2, output[1].next = null; output[3] = null

示例2:

输入: root= [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], k = 3
输出: output = [[1, 2, 3, 4], [5, 6, 7], [8, 9, 10]]
解释: 输入被分成了几个连续的部分,并且每部分的长度相差不超过1,前面部分的长度大于后面部分的长度。

思路分析

我们依旧先考虑一般情况,当len恰好可被链表长度k整除时,我们直接可通过求得的商确定每个分区的节点个数。我们直接给出核心代码:

代码1: 刚好均分情况下的一般规则

// record numbers of node in every section (0 - default)
int[] numsNodePerSec = new int[k]; 
ListNode[] output = new ListNode[k];
ListNode pA = root;

// Step1: obtain length of root
int len = 0;

while (pA != null) {
	len++;
	pA = pA.next;
}

 /* Step2: obtain carry */

int NumPerSec = len / k;

if (NumPerSec > 0) {
	for(int i = 0; i< k; i++)
		numsNodePerSec[i] = NumPerSec;
}

/* Step3: split root according to numsNodePreSec */
pA = root;

for (int i = 0; i < k; i++) {
	int steps = numsNodePerSec[i] - 1; // start inclued
	output[i] = pA;
	while (steps-- > 0) {
		pA = pA.next;
    }
    // upgrate pos of pA
    ListNode tmpNode = pA;
    if(pA.next == null)
          break;
    pA = pA.next;
    tmpNode.next = null;
}

难点分析

我们通过代码1了解到了一般规则实现,这里主要讨论当链表无法被均分情况下的代码完善,我们不妨设root = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],即包含10个节点;分别讨论k = 3、9、12三种情况:

  1. root = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; k = 3

    我们通过简单分析可知,输出应为[[1, 2, 3, 4], [5, 6, 7], [8, 9, 10]]; 这里我们的3个分区的长度分别为4, 3, 3
    我们根据代码1,可知此时numsNodePerSec = [3, 3, 3], 则可知我们需要对长度整除k后的余数进行保存,这里为rmdr = 1; 此时直接将余数与分区1的节点个数进行相加即可,numsNodePerSec[0] = numsNodePerSec[0] + rmdr = numsNodePerSec[0] + 1,此时numsNodePerSec = [4, 3, 3],为我们的期望值。
    我们对这个情况进行了简单分析后,再接着对情况2进行进一步分析。

  2. root = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; k = 7

    此时,输出应为[ [1, 2], [3, 4], [5, 6], [7], [8], [9], [10] ], numsNodePerSec = [2, 2, 2, 1,1,1,1]。 此时,我们知道链表长度与k进行除法操作后,商NumPerSec = 1,余数rmdr = 3, 此时若依旧按照情况1中的直接令numsNodePerSec[0]更新额为numsNodePerSec[0] + rmdr = numsNodePerSec[0] + 3, 则有numsNodePerSec = [4, 1, 1, 1,1,1,1],显然时不正确的
    我们需要将余数进行均衡分配,将[0, rmdr-1]范围内numsNodePerSec均加1; 即numsNodePerSec[ 0 : rmdr-1] ++,这里我们可以给出改进后的代码2

代码2: 余数均分

        /* Step1:
        Init. pointers and integers
         */
        ListNode pA = root;

        // record numbers of node in every section (0 - default)
        int[] numsNodePerSec = new int[k]; 
        ListNode[] output = new ListNode[k];

        // Step1: obtain length of root
        int len = 0;

        while (pA != null) {
            len++;
            pA = pA.next;
        }

        /* Step2: obtain carry and remainder 
        and
        average number if rmdr more than 0.
        */
        int minNumPerSec = len / k;
        int rmdr = len % k;

        if (minNumPerSec > 0) {
            for(int i = 0; i< k; i++)
                numsNodePerSec[i] = minNumPerSec;
        }
        
        if (rmdr > 0) {
            for (int i = 0; i < Math.abs(rmdr); i++) {
                numsNodePerSec[i]++;
            }
        }

        /* Step3: split root according to numsNodePreSec
         */
        pA = root;
        
        for (int i = 0; i < k; i++) {
            int steps = numsNodePerSec[i] - 1; // start inclued
            output[i] = pA;
            while (steps-- > 0) {
                pA = pA.next;
            }
            
            // upgrate pos of pA
            ListNode tmpNode = pA;
            if(pA.next == null)
                break;
            pA = pA.next;
            tmpNode.next = null;
        }

        return output;
  1. root = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; k = 12

    这里主要考虑,当输出数组存在空链表时,代码2是否能够正常运行。 我们先进行简单的分析,当len = 10, k = 12时,此时NumPerSec = 0, rmdr = 10, numsNodePerSec长度为k,且元素值均为0; 此时,根据代码2,会对numsNodePerSec的前rmdr的元素进行+1操作,这里即前10个元素进行+1操作,这时候我们则有numsNodePerSec = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0]
    接着进入到Step 3: spilt root according to numsNodePerSec,根据指定的分区个数对root 链表进行分区。当pA.next = null时,终止循环,且输出链表数组output的链表元素默认初始化为null,故output中最后的两个null链表不需要做额外的处理,已经满足题干要求。

至此,我们对一般规则及难点进行了完善及统一,接下来我们直接给出解题步骤及完整代码:

解题步骤

  1. 求得链表长度len;
  2. 求取余数与商,并根据余数对各分区节点个数进行均摊;
  3. 根据分区节点数切分链表;

解题代码

    public static ListNode[] solution(ListNode root, int k) {

        /* Step1:
        Init. pointers and integers
         */
        ListNode pA = root;

        // record numbers of node in every section (0 - default)
        int[] numsNodePerSec = new int[k]; 
        ListNode[] output = new ListNode[k];

        // Step1: obtain length of root
        int len = 0;

        while (pA != null) {
            len++;
            pA = pA.next;
        }

        /* Step2: obtain carry and remainder 
        and
        average number if rmdr more than 0.
        */
        int minNumPerSec = len / k;
        int rmdr = len % k;

        if (minNumPerSec > 0) {
            for(int i = 0; i< k; i++)
                numsNodePerSec[i] = minNumPerSec;
        }
        
        if (rmdr > 0) {
            for (int i = 0; i < Math.abs(rmdr); i++) {
                numsNodePerSec[i]++;
            }
        }

        /* Step3: split root according to numsNodePreSec
         */
        pA = root;
        
        for (int i = 0; i < k; i++) {
            int steps = numsNodePerSec[i] - 1; // start inclued
            output[i] = pA;
            while (steps-- > 0) {
                pA = pA.next;
            }
            
            // upgrate pos of pA
            ListNode tmpNode = pA;
            if(pA.next == null)
                break;
            pA = pA.next;
            tmpNode.next = null;
        }

        return output;
    }

复杂度分析

**时间复杂度:**我们对数据进行了两遍历,2N次节点访问;且对numsNodePerSec进行了k次访问,故时间复杂度为O(N + k); (这里N = len)

**空间复杂度:**这里需要k长度的整型数组numsNodePerSec,输出链表output是直接对原始链表root的拆分,故空间复杂度可计为O( k)

GitHub源码

完整可运行文件请访问GitHub

发布了47 篇原创文章 · 获赞 55 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/u011106767/article/details/105504811