Ruby's Louvre

每天学习一点点算法

导航

leetcode 336. Palindrome Pairs

查找回文对

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

 function palindromePairs(words) {
      var result = []
      for (var i = 0; i < words.length; i++) {
        for (var j = 0; j < words.length; j++) {
          if (i == j) continue;
           var s = words[i] + words[j];
          if (isPalindrome(s)) {
            result.push([i, j])
          }
        }
      }
      return result;
    }

    function isPalindrome(s) {
      var 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.先把所有字符串放进哈希表里。

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

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

   function reverse(str) {
      return str.split('').reverse().join('')
    }

    function palindromePairs(words) {
      var res = [], hash = {}
      if (words == null || words.length < 2) {
        return res
      }
      var blank = false
      for (let i = 0; i < words.length; i++) {
        if (words[i] === '') {
          blank = true
        }
        hash[reverse(words[i])] = i;
      }

      for (let i = 0; i < words.length; i++) {
        var word = words[i]
        if (blank && isPalindrome(word) && word !== '') {
          res.push([hash[''], i])
        }
        for (var j = 0; j < word.length; j++) {
          var left = word.substring(0, j);
          var right = word.substring(j);
          if (hash[left] != null && isPalindrome(right) && hash[left] !== i) {
            res.push([i, hash[left]])
          }
          if (hash[right] != null && isPalindrome(left) && hash[right] !== i) {
            res.push([hash[right], i])
          }
        }
      }
      return res;
    }

    function isPalindrome(s) {
      var head = 0, tail = s.length - 1;
      while (head < tail) {
        if (s.charAt(head) != s.charAt(tail)) {
          return false;
        }
        head++;
        tail--;
      }
      return true;
    }

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

function Node(value) {
      this.value = value
      this.word = null
      this.palindromes = []
      this.children = new Array(26)
    }
    class Tire {
      constructor() {
        this.root = new Node(null)
      }
      addWord() { }
      seatchWord(){ }
  }

我加了点备注,除了一般性质的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。建树通过addWord实现:

   addWord(word, index) {
        var node = this.root;
        var word = reverse(word);
        for (var i = 0; i < word.length; i++) {
          if (isPalindrome(word.substring(i))) {
            node.palindromes.push(index)
          }
          var next = word.charCodeAt(i) - 97
          if (!node.children[next]) {
            node.children[next] = new Node()
          }
          node = node.children[next]
        }
        node.word = word
        node.index = index;
      }

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

searchWord(word, index, res) {
        var node = this.root;
        for (var i = 0; i < word.length && node; i++) {
          if (node.index != null && isPalindrome(word.substring(i)) && node.index !== index) {
            res.push([index, node.index])
          }
          var next = word.charCodeAt(i) - 97;
          node = node.children[next]
        }
        if (node) {
          if (node.index != null && node.index !== index) {
            res.push([index, node.index])
          }
          if (node.palindromes.length) {
            for (let rightIndex of node.palindromes) {
              res.push([index, rightIndex])
            }
          }
        }
      }

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

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

function Node(value) {
      this.value = value
      this.word = null
      this.palindromes = []
      this.children = new Array(26)
    }
    class Tire {
      constructor() {
        this.root = new Node(null)
      }
      addWord(word, index) {
        var node = this.root;
        var word = reverse(word);
        for (var i = 0; i < word.length; i++) {
          if (isPalindrome(word.substring(i))) {
            node.palindromes.push(index)
          }
          var next = word.charCodeAt(i) - 97
          if (!node.children[next]) {
            node.children[next] = new Node()
          }
          node = node.children[next]
        }
        node.word = word
        node.index = index;
      }
      searchWord(word, index, res) {
        var node = this.root;
        for (var i = 0; i < word.length && node; i++) {
          if (node.index != null && isPalindrome(word.substring(i)) && node.index !== index) {
            res.push([index, node.index])
          }
          var next = word.charCodeAt(i) - 97;
          node = node.children[next]
        }
        if (node) {
          if (node.index != null && node.index !== index) {
            res.push([index, node.index])
          }
          if (node.palindromes.length) {
            for (let rightIndex of node.palindromes) {
              res.push([index, rightIndex])
            }
          }
        }
      }
    }

    function reverse(str) {
      return str.split('').reverse().join('')
    }

    function palindromePairs(words) {
      var res = [], hash = {}
      if (words == null || words.length < 2) {
        return res
      }
      var tire = new Tire, res = []

      for (let i = 0; i < words.length; i++) {
        tire.addWord(words[i], i)
      }

      for (let i = 0; i < words.length; i++) {
        tire.searchWord(words[i], i, res)
      }
      return res;
    }

    function isPalindrome(s) {
      var head = 0, tail = s.length - 1;
      while (head < tail) {
        if (s.charAt(head) != s.charAt(tail)) {
          return false;
        }
        head++;
        tail--;
      }
      return true;
    }


    console.log(palindromePairs(["abcd", "dcba", "lls", "s", "sssll"]))

posted on 2019-12-27 01:26  司徒正美  阅读(305)  评论(0编辑  收藏  举报