掌握要求:
1、字符串查找的朴素算法
2、字符串查找的KMP算法
3、哈希表
4、字典树
1、字符串的查找之朴素算法
朴素算法思想很简单,就是将待查找字符串t在被查找的字符串s中一一对比,如果遇到相同字符就将t和s同时往后挪,再依次比较;如果将t子串一直比较完了都与s中对应的连续位置元素相等的话,则就成功找到了字串的位置,但若t子串未比较完就与s中元素不相同,则将t返回到第一个元素,而s返回到刚与t相同的第一个元素的下一个位置,再重复上面的步骤依次比较。
eg: 在原始字符串s中,寻找字符串t,如果找到返回t在s中首字母下标,否则返回-1
代码实现如下:
public static int find01(String s,String t){
int i = 0;
int j = 0;
int k = 0;
for(;i < s.length();i++){
k = i;
for(;j < t.length();j++){
if(s.charAt(k) == t.charAt(j)){
k++;
}else{
break;
}
}
if(j == t.length()){
return i;
}
}
return -1;
}
还有另外一种好看的写法,同样是朴素算法,代码如下:
private static int find(String s, String t) {
int i = 0;
int j = 0;
while(i < s.length() && j < t.length()){
if(s.charAt(i) == t.charAt(j)) {
i++;
j++;
}else{
i = i-j+1;
j = 0;
}
if(j == t.length()){
return i-j;
}
}
return -1;
}
但是吧,朴素算法的查找效率明显不高@_@
2、字符串的查找之kmp算法
kmp算法主要是对于朴素算法的优化,使得字符串查找更高效,kmp算法的原理:
查找过程大致与朴素算法相同,但区别在于在查找过程中在字符串s中每个元素只过一遍,不会对任意的某个元素反复判断两次。最核心的操作在于判断子串t中最长的公共前缀,过程如图:
代码实现如下:
private static int kmp(String s, String t) {
int i = 0;
int j = 0;
int[] next = getNext(t);
while(i < s.length() && j < t.length()) {
if (j == -1 || s.charAt(i) == t.charAt(j)) {
i++;
j++;
} else {
j = next[j];
}
}
if(j == t.length()){
return i-j;
}else{
return -1;
}
}
private static int[] getNext(String t) {
int[] next = new int[t.length()];
int k = -1;
int j = 0;
next[0] = -1;
//检测每一个字符之前的字符串,计算他们的前后缀最大长度
//然后把长度记录在当前的next数组位置当中
while(j < t.length()) {
if (k == -1 || t.charAt(k) == t.charAt(j)) {
k++;
j++;
}else{
k = next[k];//前后缀长度缩减到原来k前面的最长前后缀对应的坐标
}
}
return next;
}
对于朴素算法与kmp算法的测试代码如下:
public class NormalStringSearch {
public static void main(String[] args) {
String s = "aeiylahfsdjkh";
String t = "fsd";
int pos = find(s,t);
int p = kmp(s,t);
System.out.println("pos:"+pos);
}
}
3、哈希表
应用:哈希表通常用来解决类似“在100000个单词中查找某个单词出现的次数”这种针对大数据中查找次数的问题,即用于海量数据处理:数据处理和求top k。
原理:
(1)普通哈希表
定义一个数组,将元素放入元素值与数组元素下标相同的位置,这样想要找到某元素只需要直接访问该数组下标位置的元素即可,时间复杂度仅为O(1);
但是,如此一来假如给出的几个数据中某个数据值很大的话,创建数组就也很大,这样就有可能让创建的这个数组中有诸多空间被浪费,并未得到使用,所以常常用除留余数法并且将数组循环起来用;但如此一来,如果给出的数据有重复值,那么数组中此位置就有不止一个元素放在这里,这样就产生了哈希冲突(也叫哈希碰撞),由于哈希冲突的存在,哈希表的增删改查时间复杂度只能趋近为O(1),却不为O(1),可以用O(n)表示。
那么,如何解决哈希冲突呢?需要用**线性探测法:**通常是如果某一位置被占用,则原本该存放在此位置的元素顺次存到下一个位置,以此类推。
(2)链式哈希表
与普通哈希表一样,但不同的是处理哈希冲突时,是在当前位置以链表的形式存储产生哈希冲突的元素,大概是酱紫的,如下图:
所以给出代码:
(1)线性哈希表的增删查操作:
import java.util.Arrays;
/**
* 线性探测哈希表实现
*/
class LinerHashMap<T extends Comparable<T>>{
// 散列表数组
private Entry<T>[] hashTable;
// 被占用的桶的个数
private int usedBucketNum;
// 哈希表的装载因子
private double loadFactor;
// 定义素数表
private static int[] primTable;
// 记录当前使用的素数的下标
private int primIndex;
// 类的静态初始化块
static{
primTable = new int[]{3, 7, 23, 47, 97, 127};
}
/**
* 构造函数,初始化
*/
public LinerHashMap(){
this.primIndex = 0;
this.hashTable = new Entry[primTable[this.primIndex]];
this.usedBucketNum = 0;
this.loadFactor = 0.75;
}
/**
* 增加元素
*/
public void put(T key){
// 计算哈希表是否需要扩容
double ret = this.usedBucketNum*1.0 / this.hashTable.length;
if(ret > this.loadFactor){
resize(); // 哈希表的扩容
}
// 先计算key应该放的桶的下标
int index = key.hashCode() % this.hashTable.length;
int idx = index;
do{
// 表示是从未使用过的桶
if(this.hashTable[index] == null){
this.hashTable[index] = new Entry<>(key, State.USING);
this.usedBucketNum++;
return;
}
// 表示使用过的桶
if(this.hashTable[index].getState() == State.USED){
this.hashTable[index].setData(key);
this.hashTable[index].setState(State.USING);
this.usedBucketNum++;
return;
} else {
// 正在使用中的桶,不插入重复元素
if(this.hashTable[index].getData().compareTo(key) == 0){
return;
}
}
idx = (idx+1)%this.hashTable.length;
} while(idx != index);
}
/**
* 哈希表的扩容函数
*/
private void resize() {
Entry<T>[] oldHashTable = this.hashTable;
this.hashTable = new Entry[primTable[++this.primIndex]];
this.usedBucketNum = 0;
for (int i = 0; i < oldHashTable.length; i++) {
if(oldHashTable[i] != null
&& oldHashTable[i].getState() == State.USING){
this.put(oldHashTable[i].getData());
}
}
}
/**
* 删除元素
* @param key
*/
public void remove(T key){
// 先计算key应该放的桶的下标
int index = key.hashCode() % this.hashTable.length;
// 从当前位置开始找元素
int idx = index;
do{
// 如果遍历桶的过程中,发现了从未使用过的桶,直接返回
if(this.hashTable[idx] == null){
return;
}
if(this.hashTable[idx].getState() == State.USING
&& this.hashTable[idx].getData().compareTo(key) == 0){
this.hashTable[idx].setData(null);
this.hashTable[idx].setState(State.USED);
this.usedBucketNum--;
return;
}
idx = (idx+1)%this.hashTable.length;
} while(idx != index);
}
/**
* 查询元素 返回key的值,找不到返回null
* HashMap
*/
public T get(T key){
// 先计算key应该放的桶的下标
int index = key.hashCode() % this.hashTable.length;
// 从当前位置开始找元素
int idx = index;
do{
// 如果遍历桶的过程中,发现了从未使用过的桶,直接返回
if(this.hashTable[idx] == null){
return null;
}
if(this.hashTable[idx].getState() == State.USING
&& this.hashTable[idx].getData().compareTo(key) == 0){
return key;
}
idx = (idx+1)%this.hashTable.length;
} while(idx != index);
return null;
}
/**
* 定义桶的状态值
*/
static enum State{
UNUSE,// 桶从未使用过
USED,// 桶被用过了
USING// 桶正在使用中
}
/**
* 定义桶的元素类型
* @param <T>
*/
static class Entry<T extends Comparable<T>>{
T data;
State state;
public Entry(T data, State state) {
this.data = data;
this.state = state;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public State getState() {
return state;
}
public void setState(State state) {
this.state = state;
}
}
}
/*描述:线性哈希表的测试*/
public class LinerHashMapTest {
public static void main(String[] args) {
LinerHashMap<Integer> hash = new LinerHashMap<>();
for(int i = 0;i < 20;i++){
hash.put(i);
}
System.out.println(hash);
}
}
4、字典树
应用:字典树常用来解决类似“在100000个单词中,以某前缀开头的单词有多少个”或者“某个单词出现了多少次”这样的问题。
字典树结构如下:
字典树的时间复杂度为O(m),其中m为待查找的字符串的长度。
代码稍后补上: