[LC] 336. Palindrome Pairs

这一题有着最straight forward的做法。就是把每俩个字符串组装一下然后检查一下是否是Palindrome。思路非常明白。代码如下:

    public List<List<Integer>> palindromePairs(String[] words) {
        List<List<Integer>> result = new LinkedList<>();
        for (int i = 0; i < words.length; i++) {
            for (int j = 0; j < words.length; j++) {
                if (i == j) continue;
                String s = words[i] + words[j];
                if (isPalindrome(s)) {
                    Integer[] resRow = {i, j};
                    result.add(Arrays.asList(resRow));
                }
            }
        }
        
        return result;
    }
    
    public boolean isPalindrome(String s) {
        int head = 0, tail = s.length() - 1;
        while (head < tail) {
            if (s.charAt(head) != s.charAt(tail)) {
                return false;
            }
            head++;
            tail--;
        }
        
        return true;
    }

如果单词个数是n,单词平均长度是k。那么这个做法的复杂度就是O(kn^2)。这个做法会超时。所以我们得想另一个做法。

下面这个做法可以认为是一种小型的改善,把复杂度变成了O(nk^2)。这种改善基于一个假设就是字符串数组的长度一般要比字符串的平均长度要高。这个算法基于下面一个匹配算法:

如果把一个字符串从中间分成两个部分,如果其中一部分本身已经是palindrome了,那么另一部分只需要反向匹配就可以了。譬如说,ababacd,其中一种分法就是aba和bacd,其中aba就是一个palindrome,所以我们需要在已有的字符串里面找到dcab就可以了,找的话用一个HashMap记住所有字符串,以字符串为键,位置为值就可以了。因为这个题目强调了这个列表里面的全是unique strings,所以不用担心有重复。记住了之后,找dcab只需要O(1)的操作就可以了。ababacd也可以分成ababa和cd,找dc就行了。另外一种分法的例子是cdbab,这里就是右边的是bab是palindrome,我们找是否有dc就行。根据这个匹配算法,我们可以得到下面的算法:

1.先把所有字符串放进哈希表里。

2. 然后对每个字符串遍历分成两边,就是abcbd就分成['', abcbd],[a,bcbd], [ab, cbd], [abc, bd], [abcb, d], [abcbd, ""]。然后应用于上面提到的那个匹配算法。

根据上述算法,得到代码如下:

    public List<List<Integer>> palindromePairs(String[] words) {
        HashMap<String, Integer> strMap = new HashMap<>();
        List<List<Integer>> res = new LinkedList<>();
        if (words == null || words.length < 2) return res;
        for (int i = 0; i < words.length; i++) strMap.put(words[i], i);
        for (int i = 0; i < words.length; i++) {
            for (int j = 0; j <= words[i].length(); j++) {
                String fHalf = words[i].substring(0, j);
                String sHalf = words[i].substring(j);
                if (isPalindrome(fHalf)) {
                    String newStr = new StringBuilder(sHalf).reverse().toString();
                    if (strMap.containsKey(newStr) && strMap.get(newStr) != i) {
                        Integer[] resArr = {strMap.get(newStr), i};
                        res.add(Arrays.asList(resArr));
                    }
                }

                if (sHalf.length() != 0 && isPalindrome(sHalf)) {
                    String newStr = new StringBuilder(fHalf).reverse().toString();
                    if (strMap.containsKey(newStr) && strMap.get(newStr) != i) {
                        Integer[] resArr = {i, strMap.get(newStr)};
                        res.add(Arrays.asList(resArr));
                    }
                }

            }
        }
        
        return res;
    }
        
    public boolean isPalindrome(String word) {
        int head = 0, tail = word.length() - 1;
        while (head < tail) {
            if (word.charAt(head) != word.charAt(tail)) {
                return false;
            }
            head++;
            tail--;
        }
        
        return true;
    }

要注意到的是,在sHalf的判断里,我加入了一个sHalf.length() != 0的判断。其实这不是边界判断,而是一个去重复的判断。举个例子,如果列表里面是"abc"和"cba"。abc其中一个分组是[abc, ""]。其中空字符串被算作palindrome,abc可以匹配到cba形成abccba,这样就会出现一次匹配。而cba也会有一个分组["", cba],这个时候cba也会往前匹配到abc形成abccba,但实际上这两组匹配会得到重复的结果。这种情况只会出现在分组两边有一边是完整的字符串另一边是空的情况下出现。所以,我们既可以在第一个fHalf那里加一个!fHalf.isEmpty()或者第二个sHalf那里加一个!sHalf.isEmpty()都行。这样就可以消除其中一边的重复。

 

接下来要介绍第三种算法,这种算法其实算是第二种算法的进化。匹配的原理还是基于上面提到的最基本的匹配算法。就是某一段匹配完之后剩余部分也能形成palindrome这样的一个原理。但为了提高匹配的速度,就不能那么简单的用哈希表了。这里可以用到的是trie tree。其实很多字典匹配里,dictionary的表示都可以用trie tree来替换哈希表来提高字符串匹配的速度。在这里,我们并不能实际提高算法复杂度的公式,公式仍然为O(nk^2)。但实际跑的速度变快了大约一倍左右。首先,先介绍trie tree的节点的结构

class TrieNode {
    // 26 children nodes
    TrieNode[] children;
    // When iterating to this node, how many palindromes substrings
    // it leads to. List of index of words that hit a palindromes substrings
    // at this node
    List<Integer> palindromes;
    // If any word ends at this point, since all words are unique
    // it would be at most one index here, null means no word ends at this node
    Integer index;
    
    public TrieNode() {
        this.children = new TrieNode[26];
    }
}

我加了点备注,除了一般性质的26个节点和表示是否某个单词的终结的index以外,还需要另外一个东西。一个是palindromes列表,这个列表存放的东西比较有意思,就是说当走到这个节点之后,剩下的子字符串是否是一个palindrome。举个例子,aba和ababa。当走到a->b这个节点的时候,aba剩下的a和ababa剩下的aba都是一个有效的palindrome,所以这个列表里面就会放这俩单词的index。另外那个index就比较明白了,就表示这个节点是否为一个单词的终结。因为题目里面表示了所有单词是unique的,所以这里就只需要一个Integer而非一个链表,用这个integer来表示对应的是input链表中的哪一个单词即可。

和解法二相同,构建词典(也就是树)或者匹配过程里,需要有一个过程里用的是reverse过的字符串,在这里,我们选择建树的过程reverse字符串,也就是这里建出来的的trie tree,是一个所有字符串reverse之后的字符串组合的trie tree。建树过程如下:

    public void addBranch(TrieNode root, String word, int index) {
        TrieNode currentNode = root;
        word = new StringBuilder(word).reverse().toString();
        for (int i = 0; i < word.length(); i++) {
            if (this.isPalindrome(word.substring(i))) {
                if (currentNode.palindromes == null) {
                    currentNode.palindromes = new LinkedList<>();
                }
                currentNode.palindromes.add(index);
            }
            
            int next = (int)(word.charAt(i) - 'a');
            if (currentNode.children[next] == null) {
                currentNode.children[next] = new TrieNode();                
            }
            
            currentNode = currentNode.children[next];
        }
        
        currentNode.index = index;
    }

只需要遍历每个词然后跑一遍这个就能把树给建好。至于为什么需要palindromes这个东西,下面进行搜索匹配的时候再解释

    public void searchBranch(TrieNode root, String word, int index, List<List<Integer>> result) {
        TrieNode currentNode = root;
        for (int i = 0; i < word.length() && currentNode != null; i++) {
            if (currentNode.index != null && this.isPalindrome(word.substring(i)) && !currentNode.index.equals(new Integer(index))) {
                Integer[] resRow = {index, currentNode.index};
                result.add(Arrays.asList(resRow));
            }
            
            int next = (int)(word.charAt(i) - 'a');
            currentNode = currentNode.children[next];
        }
        
        if (currentNode != null) {
            if (currentNode.index != null && !currentNode.index.equals(new Integer(index))) {
                Integer[] resRow = {index, currentNode.index};
                result.add(Arrays.asList(resRow));
            }
            
            if (currentNode.palindromes != null) {
                for (Integer rightIndex : currentNode.palindromes) {
                    Integer[] resRow = {index, rightIndex};
                    result.add(Arrays.asList(resRow));
                }
            }
        }
    }
    

在这里会解释一下我们在干嘛。我们首先把每个单词都放进去这个逆向单词树。然后试着是否能不停的往下走。有两种情况是可以认为当前遍历的词和某个节点可以形成一个结果配对的。如果说我们当前遍历到的单词叫str的话。

1. 当前节点index非空。这就表示str在当前节点上在已经走过了一个完整的单词。此时所需要判断的就是如果str剩下的部分是一个palindrome的话,那么str和该节点对应的index的单词就可以合并成为一个palindrome。当然你需要判断str所在的位置和index不是同一个位置即可。举个例子,如果你有一个word是abc,那么它在树里对应的分支就是c -> b -> a (因为是反向的树),如果str是cbaaba,那么它走到c - > b - > a的时候就会发现abc这个单词已经完结了,然后aba也是一个palindrome, 那么,cbaaba + abc就是cbaabaabc也是一个palindrome。

2. 当前节点的palindromes非空,也就是在这个节点有最少一个单词剩余的substring部分是palindrome。如果此时str已经走完了,就表示这个节点所有的palindromes对应的index都可以和str配对。 举个例子,如果有单词dedabc,dabc,edeabc,这样在c->b->a这个branch的a节点上,palindromes链表会有三个index对应前面三个单词。这个时候如果str是"cba"的话,他就会走到这个节点上,然后和前面三个单词凑成palindrome。

所以整个算法的解答代码如下:

class TrieNode {
    // 26 children nodes
    TrieNode[] children;
    // When iterating to this node, how many palindromes substrings
    // it leads to. List of index of words that hit a palindromes substrings
    // at this node
    List<Integer> palindromes;
    // If any word ends at this point, since all words are unique
    // it would be at most one index here, null means no word ends at this node
    Integer index;
    
    public TrieNode() {
        this.children = new TrieNode[26];
    }
}

class Solution {
    public List<List<Integer>> palindromePairs(String[] words) {
        TrieNode root = new TrieNode();
        for (int i = 0; i < words.length; i++) {
            this.addBranch(root, words[i], i);
        }
        
        List<List<Integer>> result = new LinkedList<>();
        for (int i = 0; i < words.length; i++) {
            this.searchBranch(root, words[i], i, result);
        }
        
        return result;
    }
    
    public void addBranch(TrieNode root, String word, int index) {
        TrieNode currentNode = root;
        word = new StringBuilder(word).reverse().toString();
        for (int i = 0; i < word.length(); i++) {
            if (this.isPalindrome(word.substring(i))) {
                if (currentNode.palindromes == null) {
                    currentNode.palindromes = new LinkedList<>();
                }
                currentNode.palindromes.add(index);
            }
            
            int next = (int)(word.charAt(i) - 'a');
            if (currentNode.children[next] == null) {
                currentNode.children[next] = new TrieNode();                
            }
            
            currentNode = currentNode.children[next];
        }
        
        currentNode.index = index;
    }
    
    public void searchBranch(TrieNode root, String word, int index, List<List<Integer>> result) {
        TrieNode currentNode = root;
        for (int i = 0; i < word.length() && currentNode != null; i++) {
            if (currentNode.index != null && this.isPalindrome(word.substring(i)) && !currentNode.index.equals(new Integer(index))) {
                Integer[] resRow = {index, currentNode.index};
                result.add(Arrays.asList(resRow));
            }
            
            int next = (int)(word.charAt(i) - 'a');
            currentNode = currentNode.children[next];
        }
        
        if (currentNode != null) {
            if (currentNode.index != null && !currentNode.index.equals(new Integer(index))) {
                Integer[] resRow = {index, currentNode.index};
                result.add(Arrays.asList(resRow));
            }
            
            if (currentNode.palindromes != null) {
                for (Integer rightIndex : currentNode.palindromes) {
                    Integer[] resRow = {index, rightIndex};
                    result.add(Arrays.asList(resRow));
                }
            }
        }
    }
    
    public boolean isPalindrome(String word) {
        int head = 0, tail = word.length() - 1;
        while (head < tail) {
            if (word.charAt(head) != word.charAt(tail)) {
                return false;
            }
            head++;
            tail--;
        }
        
        return true;
    }
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值