427.LeetCode

203 阅读7分钟

二叉树的最近公共祖先

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

image.png

示例 1:

输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1

输出:3

解释:节点 5 和节点 1 的最近公共祖先是节点 3 。

方法一:递归

我们递归遍历整棵二叉树,定义 fxf_x 表示 x 节点的子树中是否包含 p 节点或 q 节点,如果包含为 true,否则为 false。那么符合条件的最近公共祖先 x 一定满足如下条件:

(flson && frson)  ((x = p  x = q) && (flson  frson))(f_{\text{lson}}\ \&\&\ f_{\text{rson}})\ ||\ ((x\ =\ p\ ||\ x\ =\ q)\ \&\&\ (f_{\text{lson}}\ ||\ f_{\text{rson}}))

其中 lson\text{lson}rson\text{rson} 分别代表 x 节点的左孩子和右孩子。初看可能会感觉条件判断有点复杂,我们来一条条看,flson && frsonf_{\text{lson}}\ \&\&\ f_{\text{rson}} 说明左子树和右子树均包含 p 节点或 q 节点,如果左子树包含的是 pp 节点,那么右子树只能包含 q 节点,反之亦然,因为 p 节点和 q 节点都是不同且唯一的节点,因此如果满足这个判断条件即可说明 x 就是我们要找的最近公共祖先。再来看第二条判断条件,这个判断条件即是考虑了 x 恰好是 p 节点或 q 节点且它的左子树或右子树有一个包含了另一个节点的情况,因此如果满足这个判断条件亦可说明 x 就是我们要找的最近公共祖先。

你可能会疑惑这样找出来的公共祖先深度是否是最大的。其实是最大的,因为我们是自底向上从叶子节点开始更新的,所以在所有满足条件的公共祖先中一定是深度最大的祖先先被访问到,且由于 fxf_x本身的定义很巧妙,在找到最近公共祖先 xx 以后,fxf_x 按定义被设置为 true ,即假定了这个子树中只有一个 p 节点或 q 节点,因此其他公共祖先不会再被判断为符合条件。

var lowestCommonAncestor = function(root, p, q) {
    let ans;
    const dfs = (root, p, q) => {
        if (root === null) return false;
        const lson = dfs(root.left, p, q);
        const rson = dfs(root.right, p, q);
        if ((lson && rson) || ((root.val === p.val || root.val === q.val) && (lson || rson))) {
            ans = root;
        } 
        return lson || rson || (root.val === p.val || root.val === q.val);
    }
    dfs(root, p, q);
    return ans;
};

时间复杂度:O(N),其中 N 是二叉树的节点数。二叉树的所有节点有且只会被访问一次,因此时间复杂度为 O(N)。

空间复杂度:O(N) ,其中 N 是二叉树的节点数。递归调用的栈深度取决于二叉树的高度,二叉树最坏情况下为一条链,此时高度为 N,因此空间复杂度为 O(N)。

方法二:存储父节点

从根节点开始遍历整棵二叉树,用哈希表记录每个节点的父节点指针。

从 p 节点开始不断往它的祖先移动,并用数据结构记录已经访问过的祖先节点。

同样,我们再从 q 节点开始不断往它的祖先移动,如果有祖先已经被访问过,即意味着这是 p 和 q 的深度最深的公共祖先,即 LCA 节点。

class Solution{
	Map<Integer , TreeNode> parent = new HashMap<Integer,TreeNode>();
	Set<Integer> visited = new HashSet<Integer>();

	public void dfs(TreeNode root){
		if(root.left != null){
			parent.put(root.left.val,root);
			dfs(root.left);
		}
		if (root.right != null) {
            parent.put(root.right.val, root);
            dfs(root.right);
        }
	}
	 public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        dfs(root);
        while (p != null) {
            visited.add(p.val);
            p = parent.get(p.val);
        }
        while (q != null) {
            if (visited.contains(q.val)) {
                return q;
            }
            q = parent.get(q.val);
        }
        return null;
    }
}

时间复杂度:O(N),其中 N 是二叉树的节点数。二叉树的所有节点有且只会被访问一次,从 p 和 q 节点往上跳经过的祖先节点个数不会超过 N,因此总的时间复杂度为 O(N)。

空间复杂度:O(N) ,其中 NN 是二叉树的节点数。递归调用的栈深度取决于二叉树的高度,二叉树最坏情况下为一条链,此时高度为 N,因此空间复杂度为 O(N),哈希表存储每个节点的父节点也需要 O(N) 的空间复杂度,因此最后总的空间复杂度为 O(N)。

142. 环形链表 II

哈希表

var detectCycle = function(head) {
    const visited = new Set();
    while (head !== null) {
        if (visited.has(head)) {
            return head;
        }
        visited.add(head);
        head = head.next;
    }
    return null;
};

时间复杂度:O(N),其中 N 为链表中节点的数目。我们恰好需要访问链表中的每一个节点。

空间复杂度:O(N),其中 N 为链表中节点的数目。我们需要将链表中的每个节点都保存在哈希表当中。

方法二:快慢指针

var detectCycle = function(head) {
    if (head === null) {
        return null;
    }
    let slow = head, fast = head;
    while (fast !== null) {
        slow = slow.next;
        if (fast.next !== null) {
            fast = fast.next.next;
        } else {
            return null;
        }
        if (fast === slow) {
            let ptr = head;
            while (ptr !== slow) {
                ptr = ptr.next;
                slow = slow.next;
            }
            return ptr;
        }
    }
    return null;
};

时间复杂度:O(N),其中 N 为链表中节点的数目。在最初判断快慢指针是否相遇时,slow\textit{slow} 指针走过的距离不会超过链表的总长度;随后寻找入环点时,走过的距离也不会超过链表的总长度。因此,总的执行时间为 O(N)+O(N)=O(N)。

空间复杂度:O(1)。我们只使用了 slow,fast,ptr\textit{slow}, \textit{fast}, \textit{ptr} 三个指针。

合并两个有序数组

给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。

初始化 nums1 和 nums2 的元素数量分别为 m 和 n 。你可以假设 nums1 的空间大小等于 m + n,这样它就有足够的空间保存来自 nums2 的元素。

方法一:直接合并后排序

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

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

空间复杂度:O(log(m+n))O(\log(m+n))。 排序序列长度为 m+nm+n,套用快速排序的空间复杂度即可,平均情况为 O(log(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)O(m+n)

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

空间复杂度:O(m+n)O(m+n)

需要建立长度为 m+nm+n 的中间数组sorted \textit{sorted}

方法三:逆向双指针

方法二中,之所以要使用临时变量,是因为如果直接合并到数组 nums1\textit{nums}_1中,nums1\textit{nums}_1中的元素可能会在取出之前被覆盖。那么如何直接避免覆盖 nums1\textit{nums}_1中的元素呢?观察可知,nums1\textit{nums}_1的后半部分是空的,可以直接覆盖而不会影响结果。因此可以指针设置为从后向前遍历,每次取两者之中的较大者放进 nums1\textit{nums}_1的最后面。

严格来说,在此遍历过程中的任意一个时刻,nums1\textit{nums}_1数组中有mp11 m-p_1-1个元素被放入 nums1\textit{nums}_1的后半部,nums2\textit{nums}_2数组中有 np21n-p_2-1个元素被放入nums1 \textit{nums}_1的后半部,而在指针p1 p_1的后面,nums1\textit{nums}_1数组有 m+np11m+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 = 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(1)。

直接对数组 nums1\textit{nums}_1 原地修改,不需要额外空间。