数据结构与算法 -- 字符串匹配

1、BF算法

public class BF {
     
    public static void main(String[] args) {
        String a = "aaabbaaaccssdd";
        String b = "acc";
        System.out.println(bfFind(a, b));
        String c = "aaab";
        System.out.println(bfFind(a, c));
    }
    
    public static int bfFind(String mainStr, String pattern) {
        char[] mainArray = mainStr.toCharArray();
        char[] patternArray = pattern.toCharArray();
        int i = 0;//主串指针
        int j = 0;//模式串匹配指针
        int p = i;//主串匹配指针
        while(i < mainArray.length-patternArray.length+1 && j < patternArray.length) {
            if(mainArray[p] == patternArray[j]) {//如果主串与模式串对应字符匹配,指针右移
                p++;
                j++;
            }else {//如果不匹配,模式串右移一位,重新从头匹配
                i++;
                j=0;
                p=i;
            }
        }
        if(j == patternArray.length) {//如果模式串完全匹配,返回模式串在主串中的起始位置
            return i;
        }else {//否则返回 -1
            return -1;
        }
    }
}

2、RK算法

public class RK {
     
    public static void main(String[] args) {
        String a = "aaabbaaaccssdd";
        String b = "acc";
        System.out.println(rkFind(a, b));
        String c = "aaab";
        System.out.println(rkFind(a, c));
        String d = "";
        System.out.println(rkFind(a, d));
        String e = null;
        System.out.println(rkFind(a, e));
    }
    public static int rkFind(String mainStr, String pattern) {
        if(null == mainStr || null == pattern) {
            return -1;
        }
        char[] mainArray = mainStr.toCharArray();
        char[] patternArray = pattern.toCharArray();
        int mainStrLength = mainStr.length();
        int patternLength = pattern.length();
        int[] charHash = new int[mainStrLength];//主串各个字符对应的hash值
        int[] matchStrHash = new int[mainStrLength-patternLength+1];//主串待比较的字符串对应的hash值
        int patternHash = 0;//模式串hash值
        for(int i=0; i<mainStrLength; i++) {//计算主串各个字符对应的hash值,'a'-0,'b'-1,......依此类推
            charHash[i] = mainArray[i] - 'a';
        }
        for(int i=0; i < mainStrLength-patternLength+1; i++) {//计算主串待比较的字符串对应的hash值
            for(int j=0; j<patternLength; j++) {
                matchStrHash[i] += charHash[i+j];//使用各个字符的hash值相加得到主串待比较的字符串对应的hash值
            }
        }
        for(int i=0; i<patternLength; i++) {//计算模式串hash值
            patternHash += patternArray[i]-'a';
        }
        for(int i=0; i < mainStrLength-patternLength+1; i++) {//主串和模式串hash值比较
            if(patternHash == matchStrHash[i]) {
                //如果主串和模式串hash值相等,则继续比较字符串是否完全匹配,避免hash冲突
                int j=0;
                for(; j<patternLength; j++) {
                    if(mainArray[i+j] != patternArray[j]) {
                        break;
                    }
                }
                if(j == patternLength) {
                    return i;
                }
            }
        }
        return -1;
    }
}

3.1、BM算法【仅使用坏字符规则】

public class BM {
    private static final int SIZE = 256;//全局变量或成员变量
    private void generateBC(char[] b, int m, int[] bc) {
        for(int i=0; i<SIZE; i++) {
            bc[i] = -1;//初始化bc
        }
        for(int i=0; i<m; i++) {
            int ascii = (int)b[i];//计算b[i]的ASCII值
            bc[ascii] = i;//同一个字符取靠后的位置
        }
    }
    
    public int bm(char[] a, int n, char[] b, int m) {
        int[] bc = new int[SIZE];//记录模式串中每个字符最后出现的位置
        generateBC(b, m, bc);//构建坏字符哈希表
        int i = 0;//i表示主串与模式串对齐的第一个字符
        while(i <= n-m) {
            int j;
            for(j = m-1; j >= 0; j--) {//模式串从后往前匹配
                if(a[i+j] != b[j]) {
                    break;//坏字符对应模式串中的下标是j
                }
            }
            if(j < 0) {
                return i;//匹配成功,返回主串与模式串第一个匹配的字符的位置
            }
            //这里等同于将模式串往后滑动j-bc[(int)a[i+j]]位
            i = i + (j - bc[(int)a[i+j]]);
        }
        return -1;
    }
}

3.2、BM算法【坏字符规则 +  好后缀规则】

public class BM {
    private static final int SIZE = 256;//全局变量或成员变量
    private void generateBC(char[] b, int m, int[] bc) {
        for(int i=0; i<SIZE; i++) {
            bc[i] = -1;//初始化bc
        }
        for(int i=0; i<m; i++) {
            int ascii = (int)b[i];//计算b[i]的ASCII值
            bc[ascii] = i;//同一个字符取靠后的位置
        }
    }
    
    public int bm(char[] a, int n, char[] b, int m) {
        int[] bc = new int[SIZE];//记录模式串中每个字符最后出现的位置
        generateBC(b, m, bc);//构建坏字符哈希表
        int[] suffix = new int[m];
        boolean[] prefix = new boolean[m];
        generateGS(b, m, suffix, prefix);
        int i = 0;//i表示主串与模式串对齐的第一个字符
        while(i <= n-m) {
            int j;
            for(j = m-1; j >= 0; j--) {//模式串从后往前匹配
                if(a[i+j] != b[j]) {
                    break;//坏字符对应模式串中的下标是j
                }
            }
            if(j < 0) {
                return i;//匹配成功,返回主串与模式串第一个匹配的字符的位置
            }
            
            int x = j - bc[(int)a[i+j]];
            int y = 0;
            if(j < m-1) {//如果有好后缀的话
                y = moveByGS(j, m, suffix, prefix);
            }
            i = i + Math.max(x, y);
        }
        return -1;
    }
    //j表示坏字符对应的模式串中的字符下标; m表示模式出长度
    private int moveByGS(int j, int m, int[] suffix, boolean[] prefix) {
        int k = m-1-j;//好后缀长度
        if(suffix[k] != -1) {
            return j - suffix[k] + 1;
        }
        for(int r = j+2; r <= m-1; r++) {
            if(prefix[m-r]) {
                return r;
            }
        }
        return m;
    }
    
    //b表示模式串,m表示长度,suffix、prefix数组事先申请好了
    private void generateGS(char[] b, int m, int[] suffix, boolean[] prefix) {
        for(int i=0; i<m; i++) {//初始化
            suffix[i] = -1;
            prefix[i] = false;
        }
        for(int i=0; i<m-1; i++) {//b[0, i]
            int j = i;
            int k = 0;//公共后缀子串长度
            while(j >= 0 && b[j] == b[m-1-k]) {//与b[0, m-1]求公共后缀子串
                j--;
                k++;
                suffix[k] = j+1;//j+1表示公共后缀子串在b[0, i]中的起始下标
            }
            if(j == -1) {
                prefix[k] = true;//如果公共后缀子串也是模式串的前缀子串
            }
        }
    }
}

6、Trie树

  Trie树,也叫字典树,是一种专门用于处理字符串匹配树形数据结构,用来解决在一组字符窜集合中快速查找某个字符串的问题,经典使用场景是实现搜索引擎的搜索关键词提示功能。

  Trie树的本质就是利用字符串之间的公共前缀,将重复的前缀合并在一起。Trie树的根节点不包含任何信息,每个节点表示一个字符串中的字符,从根节点到其它节点的每一条路径都表示一个字符串。

  Trie树主要有两个操作,一个是将字符串集合构造成Trie树,具体就是将字符串插入到Trie树的过程,另一个就是在Trie树中查询一个字符串。

  Trie树是一个多叉树,每个节点包含字符值和子节点引用数组。

public class TrieNode{
    public char data;
    public TrieNode[] children;
    public TrieNode(char data){
        this.data = data;
        children = new TrieNode[n];//根据实际需要确定n
    }
}    

  如果要在一组字符串中频繁地查询某些字符串,用Trie树会非常高效。构建Trie树时需要扫描所有字符串的所有字符,时间复杂度是O(n)(n表示所有字符串的长度和),但是一旦构建成功之后,后续的查询操作会非常高效。每次查询时,如果要查询的字符串长度是k,那我们就只需要比对大约k个节点就能完成查询操作,所以说,构建好Trie树后,在其中查找字符串的时间复杂度是O(k)(k表示要查找的字符串的长度)

  Trie树是非常耗内存的,是一种空间换时间的思路。Trie树的每个节点都要存储一个指向其子节点引用的数组,并且即使该节点只有很少的子节点,我们也要维护固定长度的数组,如果字符串包含英文大小写字母,数字,甚至是中文,那需要的存储空间就更多了。

public class TrieTree {
    private TrieNode root = new TrieNode('/');//存储无意义字符
    
    //往Trie树中插入一个字符串
    public void insert(char[] text) {
        TrieNode p = root;
        for(int i=0; i<text.length; i++) {
            int index = text[i] - 'a';
            if(p.children[index] == null) {
                TrieNode newNode = new TrieNode(text[i]);
                p.children[index] = newNode;
            }
            p = p.children[index];
        }
        p.isEndingChar = true;
    }
    
    //在Trie树中查找一个字符串
    public boolean find(char[] pattern) {
        TrieNode p = root;
        for(int i=0; i<pattern.length; i++) {
            int index = pattern[i] - 'a';
            if(p.children[index] == null) {
                return false;//不存在pattern
            }
            p = p.children[index];
        }
        if(p.isEndingChar) {//完全匹配到pattern
            return true;
        }else {//不能完全匹配,只是前缀
            return false;
        }
    }
    public class TrieNode{
        public char data;
        public TrieNode[] children = new TrieNode[26];
        public boolean isEndingChar = false;
        public TrieNode(char data) {
            this.data = data;
        }
    }
    
    public static void main(String[] args) {
        TrieTree trieTree = new TrieTree();
        trieTree.insert("hello".toCharArray());
        trieTree.insert("world".toCharArray());
        trieTree.insert("word".toCharArray());
        trieTree.insert("teacher".toCharArray());
        trieTree.insert("wild".toCharArray());
        String pattern = "word";
        System.out.println(trieTree.find(pattern.toCharArray()) ? "找到了 " + pattern : "没有完全匹配的字符串 " + pattern);
        pattern = "wor";
        System.out.println(trieTree.find(pattern.toCharArray()) ? "找到了 " + pattern : "没有完全匹配的字符串 " + pattern);
    }
}

 2、利用Trie树实现搜索引擎的搜索关键词提示功能

import java.util.ArrayList;
import java.util.List;

public class TrieTree {
    private TrieNode root = new TrieNode('/');//存储无意义字符
    
    //往Trie树中插入一个字符串
    public void insert(char[] text) {
        TrieNode p = root;
        for(int i=0; i<text.length; i++) {
            int index = text[i] - 'a';
            if(p.children[index] == null) {
                TrieNode newNode = new TrieNode(text[i]);
                p.children[index] = newNode;
            }
            p = p.children[index];
        }
        p.isEndingChar = true;
    }
    
    //在Trie树中查找一个字符串
    public List<String> find(char[] pattern) {
        TrieNode p = root;
        for(int i=0; i<pattern.length; i++) {
            int index = pattern[i] - 'a';
            if(p.children[index] == null) {
                return dfsResult;//不存在pattern
            }
            p = p.children[index];
        }
        if(p.isEndingChar) {//完全匹配到pattern
            dfsResult.add(new String(pattern));
            return dfsResult;
        }else {//不能完全匹配,只是前缀
            String startPath = new String(pattern);
            //模式串 pattern的最后一个字符保存在p中,所以传入的path去掉该字符
            dfs(p, new StringBuffer(startPath.substring(0, startPath.length()-1)));
            return dfsResult;
        }
    }
    
    private List<String> dfsResult = new ArrayList<String>();
    private void dfs(TrieNode p, StringBuffer path) {
        if(p.isEndingChar) {
            dfsResult.add(new String(path.append(p.data).toString()));
        }else {
            for(int j=0; j<26; j++) {
                if(p.children[j] != null) {
                    StringBuffer pathCopy = new StringBuffer(path.toString());
                    dfs(p.children[j], pathCopy.append(p.data));
                }
            }
        }
    }
    public class TrieNode{
        public char data;
        public TrieNode[] children = new TrieNode[26];
        public boolean isEndingChar = false;
        public TrieNode(char data) {
            this.data = data;
        }
    }
    
    public static void main(String[] args) {
        TrieTree trieTree = new TrieTree();
        trieTree.insert("hello".toCharArray());
        trieTree.insert("world".toCharArray());
        trieTree.insert("word".toCharArray());
        trieTree.insert("teacher".toCharArray());
        trieTree.insert("wild".toCharArray());
        String pattern = "w";
        List<String> findResult = trieTree.find(pattern.toCharArray());
        for(String item : findResult) {
            System.out.println(item);
        }
        System.out.println("------------------");
        
        trieTree.dfsResult.clear();
        pattern = "wor";
        findResult = trieTree.find(pattern.toCharArray());
        for(String item : findResult) {
            System.out.println(item);
        }
        System.out.println("------------------");
        
        trieTree.dfsResult.clear();
        pattern = "word";
        findResult = trieTree.find(pattern.toCharArray());
        for(String item : findResult) {
            System.out.println(item);
        }
        System.out.println("------------------");
    }
}

 

posted @ 2019-06-29 12:33  将王相  阅读(453)  评论(0编辑  收藏  举报