413.Leetcode

126 阅读3分钟

反转链表

迭代

时间复杂度:O(n),n是链表的长度。需要遍历链表一次。

空间复杂度:O(1)。

在遍历链表时,将当前节点的next指针改为向前一个节点。 (节点没有引用其前一个节点,因此事先存储其前一个节点。更改引用前需存储后一个节点) 最后返回新的头引用。

var reverseList = function(head) {
    let prev = null;
    let curr = head;
    while(curr){
        const next = curr.next;
        curr.next = prev;
        prev = curr;
        curr = next;
    }
    return prev;
};

递归

假设链表为:

n1nk1nknk+1nmn_1\rightarrow \ldots \rightarrow n_{k-1} \rightarrow n_k \rightarrow n_{k+1} \rightarrow \ldots \rightarrow n_m \rightarrow \varnothing\\

若从节点nk+1n_{k+1}nmn_m已经被反转,而我们正处于nkn_k

n1nk1nknk+1nmn_1\rightarrow \ldots \rightarrow n_{k-1} \rightarrow n_k \rightarrow n_{k+1} \leftarrow \ldots \leftarrow n_m

我们希望nk+1n_{k+1}的下一个节点指向nkn_k。所以,nk.next.next=nkn_k.\textit{next}.\textit{next} = n_k

需要注意的是n1n_1的下一个节点必须指向\varnothing。如果忽略了这一点,链表中可能会产生环。

var reverseList = function(head){
    if(head == null || head.next == null){
        return head;
    }
    const newHead = reverseList(head.next);
    head.next.next = head;
    head.next = null;
    return newHead;
}

最大子序和

动态规划法

f(i)=max{f(i1)+nums[i],nums[i]}f(i)=max\{f(i−1)+nums[i],nums[i]\}

var maxSubArray = function(nums){
    let pre = 0, maxAns = nums[0];
    nums.forEach((x)=>{
        pre = Math.max(pre+x,x);
            maxAns = Math.max(maxAns, pre);
    });
    return maxAns;
};

分治(线段树)

对于一个区间 [l,r],我们可以维护四个量:

lSum\textit{lSum} 表示 [l,r] 内以l为左端点的最大子段和

rSum\textit{rSum} 表示 [l,r] 内以r为右端点的最大子段和

mSum\textit{mSum} 表示 [l,r] 内的最大子段和

iSum\textit{iSum} 表示 [l,r] 的区间和 iSum\textit{iSum} 就等于「左子区间」的 iSum\textit{iSum} 加上「右子区间」的 iSum\textit{iSum}

对于 [l,r]的 lSum\textit{lSum},存在两种可能,它要么等于「左子区间」的 lSum\textit{lSum},要么等于「左子区间」的 iSum\textit{iSum} 加上「右子区间」的 lSum\textit{lSum},二者取大。

对于 [l,r]的 rSum\textit{rSum},同理,它要么等于「右子区间」的 rSum\textit{rSum},要么等于「右子区间」的 iSum\textit{iSum} 加上「左子区间」的 rSum\textit{rSum},二者取大。

当计算好上面的三个量之后,就很好计算 [l,r]的 mSum\textit{mSum} 了。我们可以考虑 [l,r] 的 mSum\textit{mSum}对应的区间是否跨越 m。

它可能不跨越 m,也就是说 [l,r]的 mSum\textit{mSum} 可能是「左子区间」的 mSum\textit{mSum} 和 「右子区间」的 mSum\textit{mSum} 中的一个;

它也可能跨越 m,可能是「左子区间」的 rSum\textit{rSum} 和 「右子区间」的 lSum\textit{lSum} 求和。三者取大。

function Status(l, r, m, i) {
    this.lSum = l;
    this.rSum = r;
    this.mSum = m;
    this.iSum = i;
}

const pushUp = (l, r) => {
    const iSum = l.iSum + r.iSum;
    const lSum = Math.max(l.lSum, l.iSum + r.lSum);
    const rSum = Math.max(r.rSum, r.iSum + l.rSum);
    const mSum = Math.max(Math.max(l.mSum, r.mSum), l.rSum + r.lSum);
    return new Status(lSum, rSum, mSum, iSum);
}

const getInfo = (a, l, r) => {
    if (l === r) {
        return new Status(a[l], a[l], a[l], a[l]);
    }
    const m = (l + r) >> 1;
    const lSub = getInfo(a, l, m);
    const rSub = getInfo(a, m + 1, r);
    return pushUp(lSub, rSub);
}

var maxSubArray = function(nums) {
    return getInfo(nums, 0, nums.length - 1).mSum;
};

合并两个有序链表

递归

list1[0]+merge(list1[1:],list2) list1[0]<list2[0]

list2[0]+merge(list1,list2[1:]) otherwise

两个链表头部值较小的一个节点与剩下元素的 merge 操作结果合并。

如果 l1 或者 l2 一开始就是空链表 ,那么没有任何操作需要合并,所以我们只需要返回非空链表。否则,我们要判断 l1 和 l2 哪一个链表的头节点的值更小,然后递归地决定下一个添加到结果里的节点。如果两个链表有一个为空,递归结束。

var mergeTwoLists = function(l1,l2) {
    if(l1 == null){
        return l2;
    }else if (l2 == null){
        return l1;
    }else if (l1.val < l2.val){
        l1.next = mergeTwoLists(l1.next, l2);
    }else{
        l2.next = mergeTwoLists(l1, l2.next);
        return l2;
    }
};

时间复杂度:O(n + m),其中 n 和 m 分别为两个链表的长度。因为每次调用递归都会去掉 l1 或者 l2 的头节点(直到至少有一个链表为空),函数 mergeTwoList 至多只会递归调用每个节点一次。因此,时间复杂度取决于合并后的链表长度,即 O(n+m)。

空间复杂度:O(n + m),其中 n 和 m 分别为两个链表的长度。递归调用 mergeTwoLists 函数时需要消耗栈空间,栈空间的大小取决于递归调用的深度。结束递归调用时 mergeTwoLists 函数最多调用 n+m 次,因此空间复杂度为 O(n+m)。

迭代

首先,我们设定一个哨兵节点 prehead ,这可以在最后让我们比较容易地返回合并后的链表。我们维护一个 prev 指针,我们需要做的是调整它的 next 指针。然后,我们重复以下过程,直到 l1 或者 l2 指向了 null :如果 l1 当前节点的值小于等于 l2 ,我们就把 l1 当前的节点接在 prev 节点的后面同时将 l1 指针往后移一位。否则,我们对 l2 做同样的操作。不管我们将哪一个元素接在了后面,我们都需要把 prev 向后移一位。

在循环终止的时候, l1 和 l2 至多有一个是非空的。由于输入的两个链表都是有序的,所以不管哪个链表是非空的,它包含的所有元素都比前面已经合并链表中的所有元素都要大。这意味着我们只需要简单地将非空链表接在合并链表的后面,并返回合并链表即可。

var mergeTwoLists = function(l1,l2){
    const prehead = new ListNode(-1);

    let prev = prehead;
    while (l1 != null && l2 != null){
        if(l1.val <= l2.val){
            prev.next = l1;
            l1 = l1.next;
        }else{
            prev.next = l2;
            l2 = l2.next;
        }
        prev = prev.next;
    }
    prev.next = l1 === null? l2 : l1;
    return prehead.next;
};

时间复杂度:O(n + m)O(n+m),其中 nn 和 mm 分别为两个链表的长度。因为每次循环迭代中,l1 和 l2 只有一个元素会被放进合并链表中, 因此 while 循环的次数不会超过两个链表的长度之和。所有其他操作的时间复杂度都是常数级别的,因此总的时间复杂度为 O(n+m)O(n+m)。

空间复杂度:O(1)O(1)。我们只需要常数的空间存放若干变量。

合并两个有序数组

直接合并后排序

var merge = function(nums1, m, nums2, n) {
    nums1.splice(m, nums1.length - m, ...nums2);
    nums1.sort((a, b) => a - b);
};

时间复杂度:O(log(m+n))。 排序序列长度为 m+n,套用快速排序的时间复杂度即可,平均情况为 O(log(m+n))。

空间复杂度:O(log(m+n))。 排序序列长度为 m+n,套用快速排序的空间复杂度即可,平均情况为 O(log(m+n))。

双指针

两个数组看作队列,每次从两个数组头部取出比较小的数字放到结果中。

var merge = function(nums1, m, nums2, n) {
    let p1 = 0, p2 = 0;
    const sorted = new Array(m + n).fill(0);
    var cur;
    while (p1 < m || p2 < n) {
        if (p1 === m) {
            cur = nums2[p2++];
        } else if (p2 === n) {
            cur = nums1[p1++];
        } else if (nums1[p1] < nums2[p2]) {
            cur = nums1[p1++];
        } else {
            cur = nums2[p2++];
        }
        sorted[p1 + p2 - 1] = cur;
    }
    for (let i = 0; i != m + n; ++i) {
        nums1[i] = sorted[i];
    }
};

时间复杂度:O(m+n)。 指针移动单调递增,最多移动 m+n 次,因此时间复杂度为 O(m+n)。

空间复杂度:O(m+n)。 需要建立长度为 m+n 的中间数组sorted。

逆向双指针

在此遍历过程中的任意一个时刻,nums1\textit{nums}_1数组中有 mp11m-p_1-1个元素被放入 nums1{nums}_1的后半部,nums2\textit{nums}_2数组中有np21 n-p_2-1个元素被放入 nums1\textit{nums}_1的后半部,而在指针 p1p_1的后面,nums1{nums}_1数组有m+np11 m+n-p_1-1个位置。由于

m+np11mp11+np21m+n-p_1-1\geq m-p_1-1+n-p_2-1

等价于p21p_2\geq -1 永远成立,因此 p1p_1后面的位置永远足够容纳被插入的元素,不会产生 p1p_1 的元素被覆盖的情况。

var merge = function(nums1, m, nums2, n){
    let p1 = m - 1, p2 = n - 1;
    let tail = m + n - 1;
    var cur;
    while(p1 >= 0 || p2 >= 0){
        if(p1 === -1){
            cur = nums2[p2--];
        } else if (p2 === -1){
            cur = nums1[p1--];
        } else if (nums1[p1] > nums2[p2]){
            cur = nums1[p1--];
        } else {
            cur = nums2[p2--];
        }
        nums1[tail--] = cur;
    }
};

时间复杂度:O(m+n)。指针移动单调递减,最多移动 m+n 次,因此时间复杂度为 O(m+n)。

空间复杂度:O(1)。直接对数组nums1\textit{nums}_1原地修改,不需要额外空间。

两数之和

暴力枚举

最容易想到的方法是枚举数组中的每一个数 x,寻找数组中是否存在 target - x。

当我们使用遍历整个数组的方式寻找 target - x 时,需要注意到每一个位于 x 之前的元素都已经和 x 匹配过,因此不需要再进行匹配。而每一个元素不能被使用两次,所以我们只需要在 x 后面的元素中寻找 target - x。

ver twoSum = function(nums, target){
    const sorted = new Array(2).fill(0);
    var n = nums.length;
    for(let i = 0; i < n; ++i){
        for(let j = i + 1; j < n; ++j){
            if(nums[i] + nums[j] == target){
                sorted[0] = i;
                sorted[1] = j;
                return sorted
            }
        }
    }
};

时间复杂度:O(N2)O(N^2) ),其中 N 是数组中的元素数量。最坏情况下数组中任意两个数都要被匹配一次。

空间复杂度:O(1)。

哈希表

注意到方法一的时间复杂度较高的原因是寻找 target - x 的时间复杂度过高。因此,我们需要一种更优秀的方法,能够快速寻找数组中是否存在目标元素。如果存在,我们需要找出它的索引。

使用哈希表,可以将寻找 target - x 的时间复杂度降低到从 O(N) 降低到 O(1)。

这样我们创建一个哈希表,对于每一个 x,我们首先查询哈希表中是否存在 target - x,然后将 x 插入到哈希表中,即可保证不会让 x 和自己匹配。

class Solution {
    public int[] twoSum(int[] nums, int target) {
        Map<Integer, Integer> hashtable = new HashMap<Integer, Integer>();
        for (int i = 0; i < nums.length; ++i) {
            if (hashtable.containsKey(target - nums[i])) {
                return new int[]{hashtable.get(target - nums[i]), i};
            }
            hashtable.put(nums[i], i);
        }
        return new int[0];
    }
}