leetcode思想java

一.归并

思路

归并的思想就是先拆分,计算子序列的【题目要求计算的目标】,然后存储到一个全局的数据结构中,然后不断地归并,计算更到子序列的【题目要求计算的目标】。大多数的题目在合并的时候子序列的排序是会影响到上一层的,所以在合并之前需要将下一层做好排序,这其中可能会使用到额外的数据结构。

<2101题><315题>

<2101题>

思路:
在这里插入图片描述
代码:

class Solution{
    int count;
    public int reversePairs(int[] nums) {
        this.count = 0;
        merge(nums, 0, nums.length - 1);
        return count;
    }

    public void merge(int[] nums, int left, int right) {
        int mid = left + ((right - left) >> 1);
        if (left < right) {
            merge(nums, left, mid);
            merge(nums, mid + 1, right);
            mergeSort(nums, left, mid, right);
        }
    }

    public void mergeSort(int[] nums, int left, int mid, int right) {
        int[] temparr = new int[right - left + 1];
        int index = 0;
        int temp1 = left, temp2 = mid + 1;

        while (temp1 <= mid && temp2 <= right) {
            if (nums[temp1] <= nums[temp2]) {
                temparr[index++] = nums[temp1++];
            } else {
                //用来统计逆序对的个数
                count += (mid - temp1 + 1);
                temparr[index++] = nums[temp2++];
            }
        }
        //把左边剩余的数移入数组
        while (temp1 <= mid) {
            temparr[index++] = nums[temp1++];
        }
        //把右边剩余的数移入数组
        while (temp2 <= right) {
            temparr[index++] = nums[temp2++];
        }
        //把新数组中的数覆盖nums数组
        for (int k = 0; k < temparr.length; k++) {
            nums[k + left] = temparr[k];
        }
    }
   }

<315题>

思路:
1.将nums的看成是由大到小的排序是正常的排序,每一次拆分的时候,子序列也看成是由大到小的。
2.因为由小的子序列合并成大的子序列时已经计算过小的子子序列的【右侧小于当前元素的个数】了,所以为了保证子序列是排好序的,这时需要一个数组来不断地排序,即 helper[k];
3.helper数组里面每次存储的都是目前这两个子序列按照有大到小排序完,元素之前在nums中的下标。
并通过index[k] = helper[k];来更新排完序之后的下标。

class Solution {
  private int[] index;
  private int[] helper;
  private int[] count;

  public List<Integer> countSmaller(int[] nums) {
    List<Integer> res = new ArrayList<>(nums.length);

    index = new int[nums.length];
    helper = new int[nums.length];
    count = new int[nums.length];
    for (int i = 0; i < index.length; i++) {
      index[i] = i;
    }

    merge(nums, 0, nums.length - 1);

    for (int i = 0; i < count.length; i++) {
      res.add(i, count[i]);
    }
    return res;
  }

  private void merge(int[] nums, int s, int e) {
    if (s == e || s > e) return;
    int mid = (s + e) >> 1;

    if (s < mid) {
      merge(nums, s, mid);
    }

    if (mid + 1 < e) {
      merge(nums, mid + 1, e);
    }

    int i = s, j = mid + 1;
    int hi = s;
    //helper里面存储的是按照数字大小排列从大到小的元素下标
    while (i <= mid && j <= e) {
      if (nums[index[i]] <= nums[index[j]]) {
        // 右侧出
        helper[hi++] = index[j++];
      } else {
        // 左侧出 计数
        count[index[i]] += e - j + 1;
        helper[hi++] = index[i++];
      }
    }
    //左右元素数量不是平均分的情况下,将没遍历到的元素下标直接补到helper后面
    while (i <= mid) {
      //左侧出
      helper[hi++] = index[i++];
    }

    while (j <= e) {
      // 右侧出
      helper[hi++] = index[j++];
    }

    for (int k = s; k <= e; k++) {
      index[k] = helper[k];
    }
  }
}

二.回溯

https://mp.weixin.qq.com/s/nrTpZ9b9RvfNsaEkJoHMvg
三类问题:
1.子集 78题
2.求组合 77题
3.求全排列 44题

模板套路
代码模板:
在这里插入图片描述

1.子集

看见子集问题就想这颗树

List<List<Integer>> res = new LinkedList<>();
// 记录回溯算法的递归路径
LinkedList<Integer> track = new LinkedList<>();

// 主函数
public List<List<Integer>> subsets(int[] nums) {
    backtrack(nums, 0);
    return res;
}

// 回溯算法核心函数,遍历子集问题的回溯树
void backtrack(int[] nums, int start) {
    // 前序位置,每个节点的值都是一个子集
    res.add(new LinkedList<>(track));

    // 回溯算法标准框架
    for (int i = start; i < nums.length; i++) {
        // 做选择
        track.addLast(nums[i]);
        // 通过 start 参数控制树枝的遍历,避免产生重复的子集
        backtrack(nums, i + 1);
        // 撤销选择
        track.removeLast();
    }
}

2.组合问题

题目描述:
在这里插入图片描述
思路:
在子集的问题上修改递归的出口条件,当递归深度到k时,将路径收集起来返回。
在这里插入图片描述

List<List<Integer>> res = new LinkedList<>();
// 记录回溯算法的递归路径
LinkedList<Integer> track = new LinkedList<>();

// 主函数
public List<List<Integer>> combine(int n, int k) {
    backtrack(1, n, k);
    return res;
}

void backtrack(int start, int n, int k) {
    // base case
    if (k == track.size()) {
        // 遍历到了第 k 层,收集当前节点的值
        res.add(new LinkedList<>(track));
        return;
    }

    // 回溯算法标准框架
    for (int i = start; i <= n; i++) {
        // 选择
        track.addLast(i);
        // 通过 start 参数控制树枝的遍历,避免产生重复的子集
        backtrack(i + 1, n, k);
        // 撤销选择
        track.removeLast();
    }
}

3.全排列

在这里插入图片描述
思路:
每选择下一个元素的时候都要从for循环第一个元素开始,还要判别是否前面的路径中已经添加了这个元素,可以通过一个数组boolean[ ]记录当前元素是不是已经存在了,如果存在了直接continue,不存在就添加。

List<List<Integer>> res = new LinkedList<>();
// 记录回溯算法的递归路径
LinkedList<Integer> track = new LinkedList<>();
// track 中的元素会被标记为 true
boolean[] used;

/* 主函数,输入一组不重复的数字,返回它们的全排列 */
public List<List<Integer>> permute(int[] nums) {
    used = new boolean[nums.length];
    backtrack(nums);
    return res;
}

// 回溯算法核心函数
void backtrack(int[] nums) {
    // base case,到达叶子节点
    if (track.size() == nums.length) {
        // 收集叶子节点上的值
        res.add(new LinkedList(track));
        return;
    }

    // 回溯算法标准框架
    for (int i = 0; i < nums.length; i++) {
        // 已经存在 track 中的元素,不能重复选择
        if (used[i]) {
            continue;
        }
        // 做选择
        used[i] = true;
        track.addLast(nums[i]);
        // 进入下一层回溯树
        backtrack(nums);
        // 取消选择
        track.removeLast();
        used[i] = false;
    }
}

<46>

1.后进先出;
2.每次拆分成的子问题又可以拆成多个子问题
3.向上的过程需要恢复的步骤(失败的尝试需要恢复到以前的状态,再去尝试其他的步骤)。
在这里插入图片描述
<140>
通过path回溯,每走过一种path,并走到头,就证明这个路径是好使的,把它加入到最终结果中res中。在这里插入图片描述
对比代码进行解析:

    /**
     * s[0:len) 如果可以拆分成 wordSet 中的单词,把递归求解的结果加入 res 中
     *
     * @param s
     * @param len     长度为 len 的 s 的前缀子串
     * @param wordSet 单词集合,已经加入哈希表
     * @param dp      预处理得到的 dp 数组
     * @param path    从叶子结点到根结点的路径
     * @param res     保存所有结果的变量
     */
    private void dfs(String s, int len, Set<String> wordSet, boolean[] dp, Deque<String> path, List<String> res) {
        if (len == 0) {
            res.add(String.join(" ",path));
            return;
        }

        // 可以拆分的左边界从 len - 1 依次枚举到 0
        for (int i = len - 1; i >= 0; i--) {
            String suffix = s.substring(i, len);
            if (wordSet.contains(suffix) && dp[i]) {
                path.addFirst(suffix);
                dfs(s, i, wordSet, dp, path, res);
                path.removeFirst();
            }
        }
    }

在这里插入图片描述

三.前缀树/字典树

在这里插入图片描述
前缀树的应用:
1、前缀匹配
2、字符串检索
3、词频统计
4、字符串排序

java模板代码

class Trie {

    class TireNode {
        private boolean isEnd;
        TireNode[] next;

        public TireNode() {
            isEnd = false;
            next = new TireNode[26];
        }
    }

    private TireNode root;

    public Trie() {
        root = new TireNode();
    }

    public void insert(String word) {
        TireNode node = root;
        for (char c : word.toCharArray()) {
            if (node.next[c - 'a'] == null) {
                node.next[c - 'a'] = new TireNode();
            }
            node = node.next[c - 'a'];
        }
        node.isEnd = true;
    }

    public boolean search(String word) {
        TireNode node = root;
        for (char c : word.toCharArray()) {
            node = node.next[c - 'a'];
            if (node == null) {
                return false;
            }
        }
        return node.isEnd;
    }

    public boolean startsWith(String prefix) {
        TireNode node = root;
        for (char c : prefix.toCharArray()) {
            node = node.next[c - 'a'];
            if (node == null) {
                return false;
            }
        }
        return true;
    }
}

前缀树的构建、方法:
1.插入
描述:向 Trie 中插入一个单词 word

实现:这个操作和构建链表很像。首先从根结点的子结点开始与 word 第一个字符进行匹配,一直匹配到前缀链上没有对应的字符,这时开始不断开辟新的结点,直到插入完 word 的最后一个字符,同时还要将最后一个结点isEnd = true;,表示它是一个单词的末尾。

void insert(string word) {
    Trie* node = this;
    for (char c : word) {
        if (node->next[c-'a'] == NULL) {
            node->next[c-'a'] = new Trie();
        }
        node = node->next[c-'a'];
    }
    node->isEnd = true;
}

2.查找
描述:查找 Trie 中是否存在单词 word
实现:从根结点的子结点开始,一直向下匹配即可,如果出现结点值为空就返回 false,如果匹配到了最后一个字符,那我们只需判断 node->isEnd即可。

bool search(string word) {
    Trie* node = this;
    for (char c : word) {
        node = node->next[c - 'a'];
        if (node == NULL) {
            return false;
        }
    }
    return node->isEnd;
}

3.前缀匹配
描述:判断 Trie 中是或有以 prefix 为前缀的单词
实现:和 search 操作类似,只是不需要判断最后一个字符结点的isEnd,因为既然能匹配到最后一个字符,那后面一定有单词是以它为前缀的。

bool startsWith(string prefix) {
    Trie* node = this;
    for (char c : prefix) {
        node = node->next[c-'a'];
        if (node == NULL) {
            return false;
        }
    }
    return true;
}

4.总结
通过以上介绍和代码实现我们可以总结出 Trie 的几点性质:
Trie 的形状和单词的插入或删除顺序无关,也就是说对于任意给定的一组单词,Trie 的形状都是唯一的。
查找或插入一个长度为 L 的单词,访问 next 数组的次数最多为 L+1,和 Trie 中包含多少个单词无关。
Trie 的每个结点中都保留着一个字母表,这是很耗费空间的。如果 Trie 的高度为 n,字母表的大小为 m,最坏的情况是 Trie 中还不存在前缀相同的单词,那空间复杂度就为 O(m^n)O(m ^n)。

最后,关于 Trie 的应用场景,希望你能记住 8 个字:一次建树,多次查询。

《212题》

四.动态规划

1.最长递增子序列

题目
备忘录的更新问题
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值