LeetCode Top100之114,121,124,128,136题

114. 二叉树展开为链表
① 题目描述
  • 给定一个二叉树,原地将它展开为链表。
  • 例如,给定二叉树
    在这里插入图片描述
  • 将其展开为:
    在这里插入图片描述
② 自己的笨办法 —— 先序遍历再构造二叉树
  • 发现展开后的二叉树是原二叉树先序遍历后,只添加右孩子构成。
  • 先序遍历获得所有的节点的val,再在以前root的基础上不停地添加右孩子。
  • 注意: 方法返回为void,所以直接在root根节点的基础上构造右孩子。
  • 代码如下,但没有达到原地构造的要求:
public void flatten(TreeNode root) {
    if (root == null) {
        return;
    }
    List<Integer> list = new ArrayList<>();
    PreOrder(root, list);
    TreeNode p = root;
    for (int i = 1; i < list.size(); i++) {
        p.left=null;
        p.right = new TreeNode(list.get(i));
        p = p.right;
    }
}

public void PreOrder(TreeNode root, List<Integer> list) {
    if (root == null) {
        return;
    }
    list.add(root.val);
    PreOrder(root.left, list);
    PreOrder(root.right, list);
}
③ 迭代
  • 获取root的左子树的最右节点,将其指向root的右子树。
    在这里插入图片描述
  • 将root的左子树移到右边,成为右子树。
    在这里插入图片描述
  • 将根节点的右节点作为当前的根节点,循环进行展开。
    在这里插入图片描述
  • while循环的条件是,只要左右子树有一个为空,则继续循环,以下两种情况都要进行调整操作。
    在这里插入图片描述
  • 代码如下:
public void flatten(TreeNode root) {
    if (root == null) {
        return;
    }
    while (root.right != null || root.left != null) {
        if (root.left != null) {
            TreeNode node = root.left;
            while (node.right != null) {
                node = node.right;// 找到左子树的最右节点
            }
            node.right = root.right;// 左子树的最右节点指向root的右子树
            root.right = root.left;// root的右子树指向其左子树
            root.left = null;
        }
        root = root.right;// 指向root的右子树,对右子树构造平摊子树
    }
}
121. 买卖股票的最佳时机
① 题目描述
  • 给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
  • 如果你最多只允许完成一笔交易(即买入和卖出一支股票),设计一个算法来计算你所能获取的最大利润。
  • 注意你不能在买入股票前卖出股票。
  • 示例 1:

输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意:利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。

  • 示例 2:

输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。

② 暴力法
  • 由于股票的买入和卖出有时间先后顺序,第i天买入的股票,一定在第i天以后才能卖出。
  • 使用暴力法,查找第i天和第i天以后的价格差,取最大价格差。
  • 代码如下,花了192ms,效果并不是很好:
public int maxProfit(int[] prices) {
    int profit = 0;
    for (int i = 0; i < prices.length - 1; i++) {
        for (int j = i + 1; j < prices.length; j++) {
            int temp = prices[j] - prices[i];
            if (temp > profit) {
                profit = temp;
            }
        }
    }
    return profit;
}
  • 时间复杂度: O ( n 2 ) O(n^2) O(n2),循环运行 n ( n − 1 ) 2 \dfrac{n (n-1)}{2} 2n(n1)次。
  • 空间复杂度: O ( 1 ) O(1) O(1)。只使用了两个变量 —— maxprofit \text{maxprofit} maxprofit profit \text{profit} profit
③ 一次遍历 —— 查找波谷和最大利润
  • [7, 1, 5, 3, 6, 4]绘制在图表上,我们将会得到:
    在这里插入图片描述
  • 可以发现: 最大利润一定出现在最低波谷之后的最高波峰处,因此我们需要找到最小的谷之后的最大的峰。 我们可以维持两个变量——minpricemaxprofit,它们分别对应迄今为止所得到的最小的谷值和最大的利润(最低波谷之后的最高波峰与最低波谷之间的差值)。
  • 代码如下,运行时间只花费了1ms
public int maxProfit(int[] prices) {
    int profit = 0;
    int minPrice = Integer.MAX_VALUE;
    for (int i = 0; i < prices.length; i++) {
        if (prices[i] < minPrice) {// 寻找最低波谷
            minPrice = prices[i];
        } else if (prices[i] - minPrice > profit) {// 寻找最低波谷后的最高波峰,计算出二者的差值便是利润
            profit = prices[i] - minPrice;
        }
    }
    return profit;
}
124. 二叉树中的最大路径和
① 题目描述
  • 给定一个非空二叉树,返回其最大路径和。
  • 本题中,路径被定义为一条从树中任意节点出发,达到任意节点的序列。该路径至少包含一个节点,且不一定经过根节点。
  • 示例 1:
    在这里插入图片描述
  • 示例 2:
    在这里插入图片描述
② 递归
  • 给定一个非空节点,最终路径经过这个节点有4种情况:
  1. 只有该节点本身(左右子树的路径和都是负数);
  2. 该节点+左子树路径;
  3. 该节点+右子树路径;
  4. 该节点+左子树路径+右子树路径。`
  5. 其中1,2,3都可以作为子树路径和向上延伸,而4则不行。
  • 在上图中,20,15,7是完整路径。如果向上连接父节点,加上-10,整个路径就出现了分叉,不再是唯一路径。因此第4中情况不能向上发展。
  • temp记录可以向上发展父节点的三种情况的最大值,用curMax记录四种情况中的最大值,递归过程中返回可以向上发展父节点的temp值。
  • 上面的情况,就返回20 + 15 = 35作为左子树,向上发展父节点。
  • 代码如下:
int maxSum = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
    getMaxPathSum(root);
    return maxSum;
}
public int getMaxPathSum(TreeNode root) {
    if (root == null) {// 当前节点为空,不可以向上连接父节点,直接返回0
        return 0;
    }
    int left = getMaxPathSum(root.left);// 获取左子树的最大路径和
    int right = getMaxPathSum(root.right);// 获取右子树的最大路径和
    int temp = Math.max(Math.max(left + root.val, right + root.val), root.val);// 可以向上发展父节点的三种情况里面的最大路径和
    int curMax=Math.max(temp,root.val+left+right);// 四种情况的最大路径和
    if (maxSum<curMax){// 更新最大路径和
        maxSum=curMax;
    }
    return temp;// 返回可以向上连接父节点的三种情况的最大路径和
}
128. 最长连续序列
① 题目描述
  • 给定一个未排序的整数数组,找出最长连续序列的长度。
  • 要求算法的时间复杂度为 O(n)。
  • 示例:

输入: [100, 4, 200, 1, 3, 2]
输出: 4
解释: 最长连续序列是 [1, 2, 3, 4]。它的长度为 4。

② 自己的笨办法
  • 主要思想:先使用Arrays.sort(nums)对数组进行排序,然后使用startend双指针,如果nums[end] = nums[end - 1] + 1,说明是连续的,end指针后移;否则更新连续序列的长度maxLen = Math.max(maxLen, end - start);
  • 问题1: 如果nums的长度为1,只在循环中更新了maxLen。没有考虑到循环到end = nums.length退出循环时,也需要更新maxLen
  • 问题2:没有考虑到,排序后的数组可能为[1, 2, 2, 3, 5],可以认定最长连续序列为1, 2, 3,不考虑重复的数字。因此,需要将排序后的nums数组转化为List,进行去重。
  • 问题3:去重时,使用list.get(i + 1) == (list.get(i)),这种判断方式貌似对负数并不友好,最后的case中,-999, -999, -999, -998, -998, -997 ...并没有达到去重的目的,导致结果错误。需要使用list.get(i + 1).equals(list.get(i))进行相等判断,才能达到去重的目的。
  • 问题4:要考虑到nums的长度为0或者为null的情况。
  • 代码如下,运行花费33ms
public int longestConsecutive(int[] nums) {
    if (nums.length == 0 || nums == null) {
        return 0;
    }
    Arrays.sort(nums);
    List<Integer> list = new ArrayList<>();
    for (int i = 0; i < nums.length; i++) {
        list.add(nums[i]);
    }
    for (int i = 0; i < list.size() - 1; i++) {// 去掉重复元素
        if (list.get(i + 1).equals(list.get(i))) {
            list.remove(i + 1);
            i--;
        }
    }
    int start = 0;
    int end = 1;
    int maxLen = 0;
    for (; end < list.size(); end++) {
        if (list.get(end) != (list.get(end - 1) + 1)) {
            maxLen = Math.max(maxLen, end - start);
            start = end;
        }
    }
    maxLen = Math.max(maxLen, end - start);
    return maxLen;
}
  • 时间复杂度: O ( 3 n ) O(3n) O(3n),一次用于将nums转为List,一次用于List去重,一次用于查找最长连续序列。
  • 空间复杂度: O ( n ) O(n) O(n),需要开辟新的空间,存储List。
③ 哈希表和线性空间的构造
  • 时间复杂度: O ( n ) O(n) O(n)
    尽管在 for 循环中嵌套了一个 while 循环,时间复杂度看起来像是二次方级别的。但其实它是线性的算法。因为只有当 currentNum 遇到了一个序列的开始, while 循环才会被执行(也就是 currentNum-1 不在数组 nums 里)。 while 循环在整个运行过程中只会被迭代n次。这意味着尽管看起来时间复杂度为 O ( n ⋅ n ) O(n \cdot n) O(nn),实际这个嵌套循环只会运行 O ( n + n ) = O ( n ) O(n + n) = O(n) O(n+n)=O(n) 次。所有的计算都是线性时间的,所以总的时间复杂度是 O ( n ) O(n) O(n)的。
  • 空间复杂度: O ( n ) O(n) O(n)。为了实现 O ( 1 ) O(1) O(1) 的查询,我们对哈希表分配线性空间,以保存 nums 数组中的 O ( n ) O(n) O(n)个数字。除此以外,所需空间与暴力解法一致。
  • 主要思想:
    ① 将nums转化为HashSet,可以去掉重复。解决了自己构造List并去重的问题。
    ② 查找连续序列时,使用!set.contains(num - 1)作为条件,使得每次while循环,都是从从新序列开始。与自己之前更新start = end有异曲同工之处。
  • 代码如下,花费了6ms,比自己的快。
public int longestConsecutive(int[] nums) {
    if (nums.length == 0 || nums == null) {
        return 0;
    }
    int maxLen = 0;
    Set<Integer> set = new HashSet<>();
    for (int num : nums) {
        set.add(num);
    }
    for (int num : set) {
        if (!set.contains(num - 1)) {// 不包含当前数-1,说明当前数是新序列的起点,避免了重复查找当前数已经存在于其他序列的情况
            int len = 1;
            int curNum = num;
            while (set.contains(curNum + 1)) {// 查找以当前数为序列起点的新序列
                len++;
                curNum++;
            }
            maxLen = Math.max(maxLen, len);
        }
    }
    return maxLen;
}
136. 只出现一次的数字
① 题目描述
  • 给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
  • 说明: 你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?
  • 示例 1:

输入: [2,2,1]
输出: 1

  • 示例 2:

输入: [4,1,2,1,2]
输出: 4

② 暴力法
  • 查找nums[i]是否出现两次,不经要向前查找,还要向后查找,因为可能越往前,很多的数的第二个数早已经出现过。于是第二层循环j = 0。如果没有找到nums[i]对应的第二个数,直接nums[i];找到了,跳出循环,继续查找nums[i + 1]
  • 代码如下,运行时间154ms
public int singleNumber(int[] nums) {
    for (int i = 0; i < nums.length; i++) {
        boolean flag = false;
        for (int j = 0; j < nums.length; j++) {// 以nums[i]为中心,向前或向后查找
            if (i != j && nums[i] == nums[j]){
                flag=true;
                break;
            }
        }
        if (!flag){
            return nums[i];
        }
    }
    return -1;
}
③ 使用HashMap,存储每个数字出现的次数
  • 遍历nums数组,使用HashMap存储每个数字出现的次数。然后遍历HashMap,查找出现次数为1的数字。
  • 代码如下,花费了10ms
public int singleNumber(int[] nums) {
    HashMap<Integer, Integer> map = new HashMap<>();
    for (int num : nums) {
        map.put(num, map.getOrDefault(num, 0) + 1);
    }
    for (Integer key : map.keySet()) {
        if (map.get(key) == 1) {
            return key;
        }
    }
    return -1;
}
④ 位操作
  • 如果我们对 0 和二进制位做 XOR 运算,得到的仍然是这个二进制位
    a ⊕ 0 = a a \oplus 0 = a a0=a
  • 如果我们对相同的二进制位做 XOR 运算,返回的结果是 0
    a ⊕ a = 0 a \oplus a = 0 aa=0
  • XOR 满足交换律和结合律
    a ⊕ b ⊕ a = ( a ⊕ a ) ⊕ b = 0 ⊕ b = b a \oplus b \oplus a = (a \oplus a) \oplus b = 0 \oplus b = b aba=(aa)b=0b=b
  • 只需要对所有的数字进行异或操作,最后的结果便是single number
  • 代码如下,所用时间0ms超级高效
public int singleNumber(int[] nums) {
    int result = nums[0];
    for (int i = 1; i < nums.length; i++) {
        result = result ^ nums[i];
    }
    return result;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值