zoukankan      html  css  js  c++  java
  • 【LeetCode】336.回文对(前缀树,散列,暴力三种方法,java实现)

    题目

    链接

    image-20200701164406409

    解析

    方法一:暴力法(超时)

    暴力法去解决这个问题是个很好的开始。对于这个问题是非常简单的。我们遍历所有可能的字符串组合结果并检查它们是否是回文

    算法:

    我们可以使用嵌套循环来实现这一点,每个循环遍历数组中的每个索引。我们检查每一对是否形成回文。这个步骤有很多方法。在这里我推荐最简单的方法:判断组合成的字符串正向和反转后的字符串是否相等。

    需要注意的一个重要的边界情况是 i = jij 必须是不一样的。识别这个边界情况是很重要的,因为我们在优化算法时也需要小心。

    class Solution {
        public List<List<Integer>> palindromePairs(String[] words) {
    
            List<List<Integer>> pairs = new ArrayList<>();
    
            for (int i = 0; i < words.length; i++) {
                for (int j = 0; j < words.length; j++) {
                    if (i == j) continue;
                    String combined = words[i].concat(words[j]);
                    String reversed = new StringBuilder(combined).reverse().toString();
                    if (combined.equals(reversed)) {
                        pairs.add(Arrays.asList(i, j));
                    }
                }   
            }
    
            return pairs;
        }
    }
    
    • 时间复杂度为:O(NNK) 其中,N 为words列表的长度,K为每个单词的平均长度
    • 当数据量较大时,超时

    方法二:前缀树

    字符串s1(长度为k1),字符串s2(长度为k2)组合在一起判断是否是一个回文串有哪几种情况?

    1. k1 = k2
    2. k1 < k2
      s2剩下的部分必须要满足回文
    3. k1 > k2
      s1剩下的部分必须要满足回文

    image-20200701172952313

    思考两个问题:

    • 假设s1的首字符是a,怎么快速找到以a结尾的字符串?
      构建Trie,只不过要反着构建
    • 第2,3种情况,如何快速的知道剩下的部分是否是回文?
      • 第2种情况,已知s1,查找s2,s1先遍历完,因此在遍历到s2的节点b时,为了能快速判断剩下的部分是回文,只要记录从该节点向后所有剩余能构成回文的字符串的下标即可。
      • 第3种情况,已知s1,查找s2,s2先遍历完,只需要判断s1剩下的部分是回文即可。

    因此,需要构建的前缀树的结构如下:

    class TrieNode {
        public TrieNode[] children;
        public int index;  //题目要求返回下标,因为这里记录下标
        public List<Integer> suffixs;  //记录节点向后所有的剩余能构成回文的字符串下标
        
        public TrieNode(){
            this.children = new TrieNode[26];
            this.index = -1;
            this.suffixs = new ArrayList<>();
        }
    }
    

    一个简单示例:

    构建图示

    image-20200701173105349

    查找图示(以字符串"a"为例)

    image-20200701173116837

    完整代码如下:

    • 构建
      • 字符串取反;
      • 遍历word,创建节点;
      • word.substring(j+1)为回文,则添加到该节点的suffixs列表中;这里要注意word本身为回文则添加到root节点的suffixs列表中;
    • 查找
      • 遍历word;
      • 如果word.substring(j)为回文,则要看当前节点是否为一个单词,如果是,添加到结果中;(对应第三种情况,k1 > k2)
      • word遍历结束且有以word结尾的单词,则要看当前节点的suffixs列表;(对应第二种情况,k1 < k2)
    class Solution {
        private TrieNode root;
        public boolean isPalindrome(String s){
            int i=0, j=s.length()-1;
            while (i < j){
                if (s.charAt(i) != s.charAt(j)){
                    return false;
                }
                i++;
                j--;
            }
    
            return true;
        }
        public List<List<Integer>> palindromePairs(String[] words) {
            this.root = new TrieNode();
            int n = words.length;
    
            //build TrieNode Tree
            for (int i=0; i<n; i++){
                String word = new StringBuilder(words[i]).reverse().toString();
                TrieNode cur = root;
    
                if (isPalindrome(word.substring(0)))
                    cur.suffixs.add(i);
                for (int j=0; j<word.length(); j++){
                    int index = word.charAt(j) - 'a';
                    if (cur.children[index] == null)
                        cur.children[index] = new TrieNode();
                    cur = cur.children[index];
                    if (isPalindrome(word.substring(j+1)))
                        cur.suffixs.add(i);
                }
                cur.index = i;
            }
    
            //search 
            List<List<Integer>> res = new ArrayList<>();
            for (int i=0; i<n; i++){
                String word = words[i];
                TrieNode cur = root;
    
                int j=0;
                for (; j<word.length(); j++){
                    if (isPalindrome(word.substring(j)) && cur.index!=-1){
                        res.add(Arrays.asList(i, cur.index));
                    }
    
                    int index = word.charAt(j) - 'a';
                    if (cur.children[index] == null)
                        break;
                    cur = cur.children[index];
                }
    
                if (j == word.length()){
                    for (int k : cur.suffixs){
                        if (k != i)
                            res.add(Arrays.asList(i, k));
                    }
                }
            }
    
            return res;
        }
    }
    
    class TrieNode {
        public TrieNode[] children;
        public int index;
        public List<Integer> suffixs;
        
        public TrieNode(){
            this.children = new TrieNode[26];
            this.index = -1;
            this.suffixs = new ArrayList<>();
        }
    }
    

    时间复杂度:O(n×k×k),因为k在一定范围内,所以这个问题优化为一个线性问题。

    方法三:散列

    每一对都进行检查开销太大,有没有一种方法能够避免检查绝对不会形成回文的情况呢?为了回答这个问题,我们需要探索形成回文的特性。

    如果你不习惯用这种类型的探索和推理可能会有点挑战性,所以我们会用一些例子来解决它,然后将尝试证明我们的发现。之后,再研究如何在代码中高效的实现它。

    我们有什么方法用两个词组成回文?

    最简单的方法就是取两个彼此相反的单词组合在一起。在这种情况下,我们得到两个回文,因为可以调换顺序。

    在这里插入图片描述

    我们知道,总是两个单词能够形成彼此相反的回文,且单词是不同的。问题陈述中很清楚,单词列表中没有重复的单词。

    现在让我们思考所有可以与单词 1 "CAT" 单词配对形成回文的单词。我们假设单词 2 的所有可能性都是 8 个字母长。虽然这个假设过于具体,但是记住我们知识将其最为识别可能案例的起点。我们稍后会做一个更全面的证明。

    在这里插入图片描述

    根据回文的逻辑,我们知道倒数第二和第三个字母必须是 "A""T"

    在这里插入图片描述

    接下来我们使用数字强调字母必须相同,因为要满足回文的特性。

    在这里插入图片描述

    在这里插入图片描述

    最后一个字母可以是任何情况。

    在这里插入图片描述

    我们的是实验表明如果单词 2 是一个长度为 5 的回文连接着单词 1 的逆转单词。则组合单词 1 和单词 2 可以形成一个回文。

    在这里插入图片描述

    另一个情况是,如果单词 1 是单词 2 的逆转单词连接 长度为 5 的回文,则合并单词 1 和单词 2 也将是一个回文。

    在这里插入图片描述

    以下是已经确定的三种情况:

    在这里插入图片描述

    别忘了,空字符串也是一个有效的词。这是一个重要的边界情况,我们想想如何形成一个回文呢?

    空字符串只会与非空字符串组合。如果这个非空字符串是一个回文,则我们将得到一个有效的回文。反之则不会。所有任何单词本身是回文则会与空字符串组合形成回文。

    根据你的实现,可能不需要将情况视作特殊情况,因为它实际上是情况 2 和情况 3 的子情况。只是反转的长度为-0。不过一定要在此情况测试你的实现。

    我们怎么样才能证明我们已经确定了所有的情况?

    通过实验,我们发现了一些情况。对于这类问题让自己相信我们已经考虑了所有情况是非常重要的。一个方法是考虑每对单词的相对长度。每对单词中的相对长度有两种情况。

    1. 两个单词是同样的长度。
    2. 两个单词是不同的长度。

    然后我们需要展示这两种情况如何完全映射到我们已经发现的回文对情况。我们将通过考虑组合词(在第一个词后面附加第二个词)的中间位置来实现这一点。

    第一个可能性是组合词的中心是这两个词之间。

    在这里插入图片描述

    若形成回文对,则字母应该在中间形成中心对称,下图用数字说明两个字母必须相同。

    在这里插入图片描述

    我们还可以看到,这说明单词 1 必须与单词 2 彼此相反。

    在这里插入图片描述

    因此,当长度相同的两个词形成回文,那一定是单词 1 与单词 2 彼此相反。这相当于情况 1。

    第二个可能性是一个词短于另一个词。我们先假设单词 1 是较短的。

    像之前一个样,因为单词 1 较短,则组合词的中心将在单词 2 中。

    在这里插入图片描述

    我们知道回文必须在中心点形成中心对称,因此在单词 2 的末尾必须是单词 1 的相反单词。

    在这里插入图片描述

    现在剩下单词 1 和末尾单词 1 相反词之间的中间区域。由于要使得整个组合词是回文,则中间的区域也必须是个回文。

    在这里插入图片描述

    这也相当于情况 2。

    使用同样的思路,很容易就可以表明,当单词 2 是较短的情况下,相当于情况 3。

    因此,我们已经证明了在探索过程中发现的 3 个回文情况涵盖了两个单词形成回文对的情况。

    我们如何将该思路写进代码里?

    把这些思路放进代码中最简单的方法是遍历单词列表并对每个单词执行以下操作。

    如果这初步的解释令人困惑,不用紧张。有进一步的例子在下面进行解释。

    1. 检查单词中是否存在相反的词,如果有,这对应于情况 1。
    2. 检查每个单词的后缀是否为回文。如果是回文,则反转剩余的前缀并检查它是否在列表中。如果是,则这对应于情况 2。
    3. 检查每个单词的前缀是否为回文。如果是回文,则反转剩余的后缀并检查是否在列表中。如果是,则这对应于情况 3。

    例如,我们有单词 "banana"。首先检查 "ananab" 是否存在在列表中。

    然后确认 "banana" 的所有回文后缀。对于每一个情况,我们取剩余的前缀反转并检查列表中是否有该词。

    在这里插入图片描述

    "banana" 所有的回文前缀做相同的操作,这里只有一种情况。

    在这里插入图片描述

    这里最具挑战的想法是,我们将当前单词视为情况 2 中的单词 2。我们这样做的原因是,若将其视为单词 1 意味着我们必须猜测单词 2 的所有可能前缀,这将非常低效。

    为了保证实现的有效性,我们将所有的单词放到以单词为键,原始索引为值的哈希表中(因为输出必须是单词的原始索引)。

    算法:

    如果一个单词的前缀形成一个回文,则将其后缀称为有效后缀。函数 allvalidsuffix 查找所有这样的后缀。例如单词 "exempt" 的有效后缀是 "xempt"(移除 "e") 和 "mpt"(移除 "exe")。

    如果一个单词的后缀形成一个回文,则将其前缀称为有效前缀。函数 allValidPrefixes 以与 allvalidsuffix 类似的方式查找所有这样的前缀。在这里,可以将函数的很多代码组合在一起,但是在反复讨论这个问题后,决定不使用这种做法,因为虽然减少了代码的长度和重复。但是理解它的认知负荷更高。在你自己的单吗中,合并它是可以的。

    情况 1 的例子是通过反转当前词并查找。需要注意的一个边界情况是,如果单词本身就是一个回文,那么我们并不想添加包含同一个单词的对。这种情况只出现在情况 1,因为情况 1 就是处理单词长度相同的情况。

    通过调用 allvalidsuffix,然后反转找到的每个后缀并在哈希表中查找它们。

    通过调用 allValidPrefixes,然后反转找到的每个前缀并在哈希表中查找它们。

    通过认识到情况 1 实际上只是情况 2 和情况 3 的一个特例,可以进一步简化(这里不做)。这是因为空字符串是任何单词的回文前缀和后缀。

    class Solution {
    
        private List<String> allValidPrefixes(String word) {
            List<String> validPrefixes = new ArrayList<>();
            for (int i = 0; i < word.length(); i++) {
                if (isPalindromeBetween(word, i, word.length() - 1)) {
                    validPrefixes.add(word.substring(0, i));
                }
            }
            return validPrefixes;
        }
    
        private List<String> allValidSuffixes(String word) {
            List<String> validSuffixes = new ArrayList<>();
            for (int i = 0; i < word.length(); i++) {
                if (isPalindromeBetween(word, 0, i)) {
                    validSuffixes.add(word.substring(i + 1, word.length()));
                }
            }
            return validSuffixes;
        }
    
        // Is the prefix ending at i a palindrome?
        private boolean isPalindromeBetween(String word, int front, int back) {
            while (front < back) {
                if (word.charAt(front) != word.charAt(back)) return false;
                front++;
                back--;
            }
            return true;
        }
    
        public List<List<Integer>> palindromePairs(String[] words) {
            // Build a word -> original index mapping for efficient lookup.
            Map<String, Integer> wordSet = new HashMap<>();
            for (int i = 0; i < words.length; i++) {
                wordSet.put(words[i], i);
            }
    
            // Make a list to put all the palindrome pairs we find in.
            List<List<Integer>> solution = new ArrayList<>();
    
            for (String word : wordSet.keySet()) {
    
                int currentWordIndex = wordSet.get(word);
                String reversedWord = new StringBuilder(word).reverse().toString();
    
                // Build solutions of case #1. This word will be word 1.
                if (wordSet.containsKey(reversedWord)
                  && wordSet.get(reversedWord) != currentWordIndex) {
                    solution.add(Arrays.asList(currentWordIndex, wordSet.get(reversedWord)));
                }
    
                // Build solutions of case #2. This word will be word 2.
                for (String suffix : allValidSuffixes(word)) {
                    String reversedSuffix = new StringBuilder(suffix).reverse().toString();
                    if (wordSet.containsKey(reversedSuffix)) {
                        solution.add(Arrays.asList(wordSet.get(reversedSuffix), currentWordIndex));
                    }
                }
    
                // Build solutions of case #3. This word will be word 1.
                for (String prefix : allValidPrefixes(word)) {
                    String reversedPrefix = new StringBuilder(prefix).reverse().toString();
                    if (wordSet.containsKey(reversedPrefix)) {
                        solution.add(Arrays.asList(currentWordIndex, wordSet.get(reversedPrefix)));
                    }
                }
            }
            return solution;
        }
    }
    

    image-20200701173435341

  • 相关阅读:
    数据结构与算法系列——排序(6)_树形选择排序
    数据结构与算法系列——排序(7)_堆排序
    数据结构与算法系列——排序(5)_简单选择排序
    数据结构与算法系列——排序(4)_Shell希尔排序
    数据结构与算法系列——排序(3)_折半插入排序
    数据结构与算法系列——排序(2)_直接插入排序
    数据结构与算法系列——排序(1)_概述
    Java高级开发_性能优化的细节
    图形推理
    美团点评面试20190515
  • 原文地址:https://www.cnblogs.com/hzcya1995/p/13308005.html
Copyright © 2011-2022 走看看