字符串的查找以及查重方法总结

掌握要求:

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为待查找的字符串的长度。
代码稍后补上:

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值