答应我 你一定要学会字典树!!字典树+Java实现+Leetcode题目--回文对

答应我 你一定要学会字典树

  • 写在前面
  • 字典树详解
      • 字典树概念
      • 字典树特点
      • 字典树应用
      • 字典树具体实现(Java)
  • Leetcode原题结合理解
  • 写在最后

写在前面

大概是一周前的每日一题吧,当初初遇字典树,惊讶于其精妙,昨晚睡前的时候又看到字典树用于解决最长公共子前缀问题,想想具体实现,发现又给忘了,故今日抓紧时间整理,方便日后查看。
本文章包括字典树概念的说明、使用场景以及leetcode题解的具体分析。

字典树详解

字典树概念

答应我 你一定要学会字典树!!字典树+Java实现+Leetcode题目--回文对_第1张图片

先上个图,此图来自维基百科,先看看较为官方的定义:在计算机科学中,trie,又称前缀树字典树,是一种有序树,用于保存关联数组,其中的键通常是字符串。 与二叉查找树不同,键不是直接保存在节点中,而是由节点在树中的位置决定。 一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,而根节点对应空字符串。

字典树特点

按照个人理解,字典树主要用于统计、排序、保存大量字符串查找等,优点在于能够利用字符串的公共前缀来减少查询的时间,最大限度地减少字符串的比较,是一种效率很高的数据结构。
Trie树的特点如下:

  1. 每个节点都有一个指向26个字母的数组,而从上往下连成的字符串就会代表一个单词,而每个单词的最后一个字母的节点也会相应地标记代表结束
  2. 根节点不保存字符的,除根节点外的每一个子节点都包含一个字符
  3. 每个节点的所有子节点包含的字符互不相同(最多26个子节点,看看第1点)

字典树应用

  1. 前缀匹配
  2. 字符串检索
  3. 词频统计
  4. 字符串排序

字典树具体实现(Java)

这里使用Java来对字典树进行一个简单的实现,使用一个Node来声明字典树的每一个节点,节点包括一个int的数组和一个int值。int数组的大小为26,代表26个字母。int值flag作为当前节点是否为最后一个节点的标记,默认值为-1,在构建字典树时可以拿来存对应字符串在字符串数组的下标。(注:此种做法是参照leetcode官方题解的,当然你也可以声明一个包含26个Node的数组

class Node {
        int[] ch = new int[26];
        int flag;

        public Node() {
            flag = -1;
        }
    }

这里的Node搭配一个ArrayList来进行使用,是一种单链表形式的字典树,具体可以看下面的题解结合理解。

Leetcode原题结合理解

答应我 你一定要学会字典树!!字典树+Java实现+Leetcode题目--回文对_第2张图片

class Solution {
    //传说中的字典结点 每个结点都包含着结点对应的值和26个链接
    //对应的值其实就是该值在数组的的下标
    //此数据结构其实不会保存存储任何的字符串或字符 只保存链接数组和值
    class Node {
        int[] ch = new int[26];
        int flag;

        public Node() {
            flag = -1;
        }
    }
    //单链表形式的字典树吧 
    //每一次循环insert 都是往单链表中加上字符串长度-1的结点 
    //根结点保存着每一个字符串首位字母以及开始的下标
    List<Node> tree=new ArrayList<>();

    //往字典树中生成具体结点 并插入树中
    public void insert(String s,int id){
        //字符串长度
        int len=s.length(),add=0;
        //此循环中 如果是第一个词 add的值会从0慢慢递增到len
        for(int i=0;i<len;i++){
            //使用int数组代替26个字母 直接减去a取得ascill码值
            int x=s.charAt(i)-'a';
            //如果是从根结点开始 那么生成一个结点来保存当前的x
            if(tree.get(add).ch[x]==0){
                tree.add(new Node());
                tree.get(add).ch[x]=tree.size()-1;
            }
            //其实add代表的应该是当前字典树的深度-1 也就是List的长度-1 
            add=tree.get(add).ch[x];
        }
        //该字符对应的字典树节点链接生成并在最后一个空结点设置值 最后一个结点int数组全为0
        tree.get(add).flag=id;
    }

    //寻找字典树中是否有与传入字符串构成回文串的字符串
    public int findWord(String s,int left,int right){
        int add=0;
        //因为找的是回文串 因此从右往左匹配字典树
        for(int i=right;i>=left;i--){
            int x=s.charAt(i)-'a';
            if(tree.get(add).ch[x]==0) return -1;
            //定位到下一个字母的node节点上
            add=tree.get(add).ch[x];
        }
        return tree.get(add).flag;
    }

    public List<List<Integer>> palindromePairs(String[] words) {
        //给树生成一个根结点
        tree.add(new Node());
        int n=words.length;
        for(int i=0;i<n;i++){
            insert(words[i],i);
        }
        List<List<Integer>> res=new ArrayList<List<Integer>>();
        for(int i=0;i<n;i++){
            //当前字符串长度
            int m=words[i].length();
            for(int j=0;j<=m;j++){
                //如果从j到末尾是回文串
                if(isPalindrome(words[i], j, m - 1)){
                    //那么寻找能与0到j-1位置组成回文串的字符串
                    int leftId=findWord(words[i],0,j-1);
                    //不等于i表示不能是自己
                    if(leftId!=-1&&leftId!=i){
                        res.add(Arrays.asList(i,leftId));
                    }
                }
                //如果0到j是回文串 注意这个串至少长度要为1
                if(j!=0&&isPalindrome(words[i],0,j-1)){
                    int rightId=findWord(words[i],j,m-1);
                    if(rightId!=-1&&rightId!=i){
                        res.add(Arrays.asList(rightId,i));
                    }
                }   
            }
        }
        return res;
    }
    //判断是否为回文 双指针法
    public boolean isPalindrome(String s,int left,int right){
        int len = right - left + 1;
        for (int i = 0; i < len / 2; i++) {
            if (s.charAt(left + i) != s.charAt(right - i)) {
                return false;
            }
        }
        return true;
    }

}

以上是从官方搬运过来的题解以及自己的一些理解,如有错误,还请不吝赐教。
按照我的理解,这份题解使用的是ArrayList的单链表形式来实现字典树,这跟刚刚前面字典树有点类似多叉树的形态略微有些不同,但具体的实现还是遵循字典树的定义的。
字典树的使用,主要包括字典树的生成和对字典树进行删除查找两方面。这里我主要分析下这段代码中生成字典树的部分。

//往字典树中生成具体结点 并插入树中
    public void insert(String s,int id){
        //字符串长度
        int len=s.length(),add=0;
        //此循环中 如果是第一个词 add的值会从0慢慢递增到len
        //如果是后面的词 则add会从0直接跳到对应的节点再递增
        for(int i=0;i<len;i++){
            //使用int数组代替26个字母 直接减去a取得ascill码值
            int x=s.charAt(i)-'a';
            //如果该字符从未出现 那么我们需要新建节点来保存了
            
            if(tree.get(add).ch[x]==0){
                tree.add(new Node());
                tree.get(add).ch[x]=tree.size()-1;
            }
            //其实add代表的应该是当前List的长度 
            add=tree.get(add).ch[x];
        }

这里需要注意,我们刚刚说字典树每一个节点都会存一个字符,在这段代码里其实是以整型数组中对应位置存的数字来体现的,这里的数字代表List中的下标,表示的是当前单词会从List中的哪一个位置开始。
也就是说,节点中的数组会帮助我们跳往相应的下标开始插入,这里巧妙的利用了List的长度来唯一表示一个节点的位置,从而保证他们一一对应。
感觉这个链表的命名有点误导人,这里应该是单链表而不是树形结构。
可能听完你还是很懵,那么我只好祭出我的名作来帮助你理解了!
答应我 你一定要学会字典树!!字典树+Java实现+Leetcode题目--回文对_第3张图片
根据本题目的用例,我们来尝试往字典树中插入“abcd”和“lls”两个字符串,看看会是什么样的结果。
丑是丑了点,凑合看吧…我们可以看到,根节点其实只是保存了每个字符串的第一个字符的开始位置,在循环中,add会帮我们定位到该位置进行插入操作。这里节点上的字符其实应该是不存在的,只是我为了容易看才补上的,实际上保存节点字符的应该是上一节点的整型数组!而节点的flag,则是保存当前单词处于用例中的下标位置。

写在最后

这里所分享的字典树实现方式,好像还是比较奇葩的一种?但我觉得一种数据结构,重要的是能够用来解决问题跟达到预期的效果,而如何实现则应该结合实际情况来进行调整跟优化。就比如本题中需要字符串在字符串数组中的下标以返回,就需要存入下标而不是布尔值来标记是否为最后一个字符。
以上,有空的话我会尝试着使用HashMap和Node数组对字典树进行实现并更新上来,希望大家一起加油!若本文有错漏,请不吝指出。

你可能感兴趣的:(面试,算法与数据结构,LeetCode)