LeetCode中常用技巧

LeetCode

1. 递归

递归要素:

  • 参数中要有操作的数组或集合,以及目标。
  • 结束条件,结束的判断需要由其它参数参与确定。
  • 根据实际情况方法是否有返回。

2. 动态规划

动态规划要素:

  • 创建一个(n+1)的数组来存放规划值。
  • 数组第一个需要虚拟赋值,一般为0或false等。
  • 由数组的最后一个或最后几个确定结果。
2.1 LeetCode198. 打家劫舍

创建一个长于数组的数组,用来存放每一次计算的结果,而每一次的计算又是从第一个到当前的最佳结果,一直重复到结束。

    public int rob(int[] nums) {
        int result = 0;
        int length = nums.length;

        if (length == 1) {
            result = nums[0];
        } else if (length == 2) {
            result = Math.max(nums[0], nums[1]);
        } else if (length > 2) {
            int[] dp = new int[length + 1];
            dp[0] = 0;
            dp[1] = nums[0];
            dp[2] = nums[1];
            for (int i = 3; i <= length; i++) {
                // 动态规划(dynamic programming), 妙 啊!
                dp[i] = Math.max(dp[i - 2], dp[i - 3]) + nums[i - 1];
            }
            result = Math.max(dp[length], dp[length - 1]);
        }

        return result;
    }
2.2 LeetCode139. 单词拆分
    /**
     * 动态规划 (dynamic programming)
     * 以示例1为例,s为"leetcode",wordDict为{"leet", "code"}
     * dp数组为[true, false, false, false, true, false, false, false, true]
     */
    public static boolean wordBreak(String s, List<String> wordDict) {
        HashSet<String> wordSet = new HashSet<>(wordDict); // 去除词典重复项

        int length = s.length();
        boolean[] dp = new boolean[length + 1];
        dp[0] = true;
        for (int i = 1; i <= length; i++) {
            for (int j = 0; j < i; j++) {
                if (dp[j] && wordSet.contains(s.substring(j, i))) {
                    dp[i] = true;
                    break;
                }
            }
        }

        return dp[length];
    }

3. 队列

3.1 优先队列 (PriorityQueue) 重排序

实例化示例:

// 需要指定队列中存储的对象类型,如下为Integer。
// 可以指定初始容量,initialCapacity。
// 可以指定排序规则,如下为从小到大排序。
PriorityQueue<Integer> queueMin = new PriorityQueue<Integer>(new Comparator<Integer>() {
				@Override
				public int compare(Integer o1, Integer o2) {
					return (o1 < o2) ? 1 : ((o1 == o2) ? 0 : -1);
				}
			});
常用方法描述返回参数
size()获取队列大小int:队列中元素的个数
isEmpty()队列是否为空boolean:元素的个数是否为0,size()==0
offer(E e)向队列中添加元素,自动排在最末尾boolean:是否添加成功要添加的元素
peek()获取最后位置的元素,但实际是类中一个数组的第一个E:最后位置的元素
poll()弹出最后位置的元素,并且能得到此元素E:最后位置的元素
3.1 阻塞队列 (BlockingQueue) 重阻塞

实例化示例:

// 可以指定初始容量,initialCapacity。
// fair为阻塞时的策略,为true按队列先进先出的顺序处理元素,否则为未指定处理顺序。
BlockingQueue<CachedLocation> pendingLocations = new ArrayBlockingQueue<>(10000, true);
常用方法描述返回参数
size()获取队列大小,获取时会加锁,所以尽量少获取int:队列中元素的个数
isEmpty()队列是否为空,会使用size(),所以尽量少获取boolean:元素的个数是否为0,size()==0
offer(E e)向队列中添加元素,自动排在最末尾boolean:是否添加成功要添加的元素
drainTo(Collection<? super E> c)从此队列中删除所有可用元素并将它们添加到给定集合int:传输的元素数存放所有元素的集合

4. 前缀和

4.1 LeetCode437. 路径总和 III

前缀和结合二叉树TreeNode来使用。还使用Map来记录一些特殊值的次数。

    public static void main(String[] arg0) {
        Object[] nums = {10, 5, -3, 3, 2, null, 11, 3, -2, null, 1};
        System.out.println("路径总和 = " + pathSum(new TreeNode(nums), 8));
    }

    public static int pathSum(TreeNode root, int targetSum) {
        Map<Integer, Integer> counts = new HashMap<>();
        // 这里设置0对应的值为1,这样如果第一个根节点就等于目标和,在递归中给res赋的值就是1,不需要再判断
        counts.put(0, 1);
        return childMethod(root, counts, 0, targetSum);
    }
    
    /**
     * 递归 + 前缀和
     */
    public static int childMethod(TreeNode node, Map<Integer, Integer> counts, int currSum, int targetSum) {
        if (node == null || !node.isValidVal()) { // 递归到了最末端终止,并且返回0条符合条件路径
            return 0;
        }

        int res = 0;

        currSum += node.val;
        res = counts.getOrDefault(currSum - targetSum, 0); // 这里赋值不明白

        counts.put(currSum, counts.getOrDefault(currSum, 0) + 1); // 记录前缀和的次数
        res += childMethod(node.left, counts, currSum, targetSum);
        res += childMethod(node.right, counts, currSum, targetSum);
        counts.put(currSum, counts.getOrDefault(currSum, 0) - 1); // 检查完当前分支就要减少当前前缀和的次数

        return res;
    }

附:时间复杂度 和 空间复杂度

  1. 时间复杂度:假设单次操作的时间为单位时间,求出结果总共需要的大概次数。
    - 递归的时间复杂度一般为n。
    - 动态规划的时间复杂度一般为n的平方。
  1. 空间复杂度:整个操作需要开辟的内存空间。
    - 递归需要在栈上开辟空间。首次执行开辟一个空间,每次递归多开辟一个空间,最多递归多少次就是空间复杂度。
    - 动态规划需要创建一个(总长度+1)的数组来存放规划值,空间复杂度即为数组长度。

牛客

1. 对一个数组或者集合中的所有元素做全部组合

– 采用递归,而且递归中含有for循环
– 数组或集合数据要全局,判断标准要全局
– 新建两个全局的数组,大小要和源数据相同,一个记录选中下标,一个记录对应的值
– dfs递归两个参数,一个p记录当前赋值位置并且下次递归也可使用,一个count记录使用前几个数据

public static int[] data; // 源数据
public static int number; // 判断标准
public static int[] tempIndex; // 记录选中下标
public static int[] tempData; // 记录选中下标对应的数据

    {
        List<List<Integer>> ans = new ArrayList<>();
        dfs(0, 0, ans); // p 和 count 必须从0开始
    }

    // 递归方法,第三个参数是结果,其实也可以做成全局变量
    public static void dfs(int p, int count, List<List<Integer>> ans) {
        int total = 0;
        List<Integer> temp = new ArrayList<>();
        for (int i = 0; i < count; i++) {
            temp.add(tempData[i]);
            total += temp.get(i);
            if (total == number) {
                ans.add(temp);
                return; // 要根据判断标准来写,是否需要再增加count
            }
            if (total > number) {
                return; // 要根据判断标准来写,是否需要再增加count
            }
        }

        int i = 0;
        if (p - 1 >= 0 && tempIndex[p - 1] != -1) {
            i = tempIndex[p - 1] + 1;
        }
        for (; i < data.length; i++) {
            tempData[p] = data[i];
            tempIndex[p] = i;
            dfs(p + 1, count + 1, ans);
        }
    }
  • 举个例子并列下递归的执行顺序,例如:4, 3, 2, 1
  • dfs for循环第一个 4 ,临时数据数组第一个位置为4, count也变成1,之后第二次递归又for循环, 临时数据数组第一个位置为3,count也变成2,得到4, 3,类推走完 4, 3, 24, 3, 2, 14, 3, 1
  • 之后第二次递归的for循环走下一个,也就是临时数据第二个位置是源数组第三个位置,得到4, 2,再4, 2, 1结束。
  • 之后第二次递归的for循环走最后一个,得到4, 1,整个第一个循环以4开头的就全部完成。
  • 接下来第一个循环以3开头,顺序33, 23, 2, 13, 1
  • 继续第一个循环以2开头,顺序为22, 1
  • 最后第一次循环只剩下1

2. 数组只能向上下左右感染的问题

– 先将上下左右的偏移数组列出来,循环用
– 需要有一个队列记录已感染位置,先进先处理的原则,并且处理后做标记已处理
– 用感染数增加到最后的值做判断得结论,或者有dp[]记录感染步骤得结论

static int[][] offsets = new int[][]{{1, 0}, {-1, 0}, {0, 1}, {0, -1}};

Queue<int[]> queue = new LinkedList<>();
queue.offer(p1);
queue.offer(p2);

int count = m * n - 2; // 已经有两个作为扩散点了
int[][] dp = new int[m][n];
dp[p1[0]][p1[1]] = 1;
dp[p2[0]][p2[1]] = 1;

int day = 1;
while (!queue.isEmpty() && count > 0) {
  int[] p = queue.poll(); // 弹出并获取到队列首部对象
  int x = p[0];
  int y = p[1];
  day = dp[x][y] + 1;

  for (int[] offset : offsets) {
    int newX = x + offset[0];
    int newY = y + offset[1];

    if (newX >= 0 && newX < m && newY >= 0 && newY < n && dp[newX][newY] == 0) {
      dp[newX][newY] = day;
      queue.offer(new int[]{newX, newY});
      count --;
    }
  }
}

System.out.println(day - 1);
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
滑动窗口是一种常用算法技巧,可以用于解决一类问题,其包括一些LeetCode上的题目。通过维护一个窗口,我们可以在线性时间内解决一些需要处理连续子数组或子字符串的问题。以下是一些常见的滑动窗口问题: 1. 最小覆盖子串(Minimum Window Substring):给定一个字符串S和一个字符串T,在S找出包含T所有字符的最小子串。 2. 字符串的排列(Permutation in String):给定两个字符串s1和s2,判断s2是否包含s1的排列。 3. 找到字符串所有字母异位词(Find All Anagrams in a String):给定一个字符串s和一个非空字符串p,找到s所有是p的字母异位词的子串。 4. 替换后的最长重复字符(Longest Repeating Character Replacement):给定一个只包含大写英文字母的字符串s,你可以将一个字母替换成任意其他字母,使得包含重复字母的最长子串的长度最大化。 5. 至多包含两个不同字符的最长子串(Longest Substring with At Most Two Distinct Characters):给定一个字符串s,找出至多包含两个不同字符的最长子串的长度。 以上只是几个例子,滑动窗口可以应用于更多类型的问题。在解决这些问题时,我们通常使用两个指针来表示窗口的左右边界,并根据具体问题的要求移动窗口。在每次移动窗口时,我们可以更新窗口的状态,例如统计字符出现次数、判断窗口是否满足条件等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值