数据结构与算法 -- 字符串匹配
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("------------------"); } }