算法入门-字符串1

第二部分:字符串

13.罗马数字转整数(简单)

题目:罗马数字包含以下七种字符: IVXLCDM

字符          数值
I             1
V             5
X             10
L             50
C             100
D             500
M             1000

例如, 罗马数字 2 写做 II ,即为两个并列的 1 。12 写做 XII ,即为 X + II27 写做 XXVII, 即为 XX + V + II

通常情况下,罗马数字中小的数字在大的数字的右边。但也存在特例,例如 4 不写做 IIII,而是 IV。数字 1 在数字 5 的左边,所表示的数等于大数 5 减小数 1 得到的数值 4 。同样地,数字 9 表示为 IX。这个特殊的规则只适用于以下六种情况:

  • I 可以放在 V (5) 和 X (10) 的左边,来表示 4 和 9。

  • X 可以放在 L (50) 和 C (100) 的左边,来表示 40 和 90。

  • C 可以放在 D (500) 和 M (1000) 的左边,来表示 400 和 900。

给定一个罗马数字,将其转换成整数。

示例 1:

输入: s = "III"
输出: 3
  • IL 和 IM 这样的例子并不符合题目要求,49 应该写作 XLIX,999 应该写作 CMXCIX 。

第一种思路:

先解决对应问题,很快会想到存储时使用Map集合(字典形式)。然后根据题目的意思,便有两种情况了:

  • 左边的数字小于右边的数字时,左边的数字取负号

  • 左边的数字大于右边的数字时,左边的数字取正号

  • 在循环遍历中因为要同时访问当前和后一位字符,所以单独判断当当前的字符就是最后一位时之间取正好并返回结果

class Solution {
    public int romanToInt(String s) {
        int sum = 0;
        Map<Character, Integer> values = new HashMap<>();
        values.put('I', 1);
        values.put('V', 5);
        values.put('X', 10);
        values.put('L', 50);
        values.put('C', 100);
        values.put('D', 500);
        values.put('M', 1000);

        for(int i = 0;i<s.length(); i++){
            if(i == s.length() - 1){
                sum += values.get(s.charAt(i));
                return sum;
            }else if(values.get(s.charAt(i)) < values.get(s.charAt(i+1))){
                sum -= values.get(s.charAt(i));
            }else{
                sum 
                += values.get(s.charAt(i));
            }
        }
        return sum;
    }
}

第二种思路:

看了一个击败百分百的大佬的解答,深受启发,Map集合(字典)这个数据结构好像会大幅降低性能(在算法逻辑简单的情况下),他的办法是在存储罗马数字和对应整数时不采用Map集合,而是定义了一个方法,在方法里使用switch语句得出对应整数,确实拓展了思路!

import java.util.*;

class Solution {
    public int romanToInt(String s) {
        int sum = 0;
        for (int i = 1; i < s.length(); i++) {

            if (value(s.charAt(i-1)) < value(s.charAt(i))) {
                sum -= value(s.charAt(i-1));
            } else {
                sum += value(s.charAt(i-1));
            }
        }
        sum += value(s.charAt(s.length()-1));
        return sum;
    }

    public static int value(char c) {
        switch (c) {
            case 'I':
                return 1;
            case 'V':
                return 5;
            case 'X':
                return 10;
            case 'L':
                return 50;
            case 'C':
                return 100;
            case 'D':
                return 500;
            case 'M':
                return 1000;
            default:
                return 0;
        }
    }
}

28.找出字符串中第一个匹配项的下标(简单)

题目:给你两个字符串 haystackneedle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1

示例 1:

输入:haystack = "sadbutsad", needle = "sad"
输出:0
解释:"sad" 在下标 0 和 6 处匹配。
第一个匹配项的下标是 0 ,所以返回 0 。

第一种思路(暴力匹配):

  1. 使用两层循环遍历 haystack:

    • 外层循环从位置 i = 0 开始,直到 i+m <= n,其中 n 为 haystack 的长度,m 为 needle 的长度,确保剩余长度能够完全包含 needle。

    • 内层循环从位置 j = 0 开始,逐个比较 haystack 中 i+j 位置的字符和 needle 中 j 位置的字符是否相等。

    • 如果发现任意字符不相等,则将 flag 置为 false,并跳出内层循环。

  2. 如果内层循环全部字符匹配成功(即 flag 仍为 true),则返回当前起始位置 i;否则继续外层循环。

  3. 如果整个 haystack 遍历完仍未找到匹配的位置,则返回 -1。

class Solution {
    public int strStr(String haystack, String needle) {
        int n = haystack.length(), m = needle.length();
        for (int i = 0; i + m <= n; i++) {
            boolean flag = true;
            for (int j = 0; j < m; j++) {
                if (haystack.charAt(i + j) != needle.charAt(j)) {
                    flag = false;
                    break;
                }
            }
            if (flag) {
                return i;
            }
        }
        return -1;
    }
}

第二种思路:

很容易想到上数据结构课程时候学到的KMP算法。不过徒手敲还是有点难度的,毕竟没有很熟悉的掌握代码😂

KMP算法(Knuth-Morris-Pratt算法)是一种高效的字符串匹配算法,其基本思路是利用模式串自身的特点,避免重复匹配,实现快速的字符串匹配。下面是KMP算法的基本思路:

  1. 构建模式串的部分匹配表(部分匹配值),即针对模式串的每个位置i,找出模式串的前缀与后缀的最长公共长度(即为部分匹配值)。

  2. 利用部分匹配表,在匹配过程中实现跳转(即通过部分匹配表值来调整模式串的起始位置),避免对已经匹配过的部分进行重复匹配。

  3. 在文本串中通过模式串的滑动匹配,不断调整模式串的位置,直至找到匹配位置或遍历完整个文本串。

KMP算法的核心优化在于构建部分匹配表和利用部分匹配表进行跳转,可以有效避免不必要的字符比较,提高匹配效率。KMP算法的平均时间复杂度为O(n + m),其中n为文本串长度,m为模式串长度,因此在实际应用中效率比暴力匹配算法更高。

部分匹配表是KMP算法中非常重要的一部分,用于在匹配过程中实现跳转,提高匹配效率。下面是部分匹配表的求法思路:

  1. 针对模式串 needle 中的每个位置 i,求出以该位置结尾的子串的最长前缀与后缀的公共长度。

  2. 从第一个位置开始,初始化部分匹配表,将第一个位置的部分匹配值设为0(也可以是-1)。

  3. 维护两个指针,分别指向模式串的当前位置i和已匹配的最长公共长度j。

  4. 如果当前位置字符匹配成功(即 needle.charAt(i) == needle.charAt(j)),则部分匹配值为 j+1,同时 i 和 j 同时向后移动一位。

  5. 如果当前位置字符匹配失败,根据已匹配的最长公共长度 j,在部分匹配表中回溯得到新的 j 值,并继续比较。

  6. 直至遍历完整个模式串,得到每个位置的部分匹配值。

通过以上思路,可以求得模式串的部分匹配表。这个部分匹配表的构建过程比较关键,是KMP算法高效匹配的关键之一。部分匹配表的构建复杂度为 O(m),m 为模式串的长度,求解部分匹配表后,匹配过程中的跳转操作可以在O(n)的时间内完成,提高了匹配效率。

下面简单介绍一下最长前缀与后缀:

  1. 最长前缀:给定一个字符串,它的前缀是指从第一个字符开始,依次包含前面的字符构成的子串。最长前缀即为在该字符串中,以第一个字符开始的子串中同时也是该字符串的前缀的最长子串。例如,字符串 "abcabcd" 中,"abc" 是最长前缀。

  2. 最长后缀:给定一个字符串,它的后缀是指从最后一个字符开始,依次包含后面的字符构成的子串。最长后缀即为在该字符串中,以最后一个字符开始的子串中同时也是该字符串的后缀的最长子串。例如,字符串 "abcdabcd" 中,"abcd" 是最长后缀。

下面举个例子:

class Solution {
    public int strStr(String haystack, String needle) {
        // 步骤1: 检查目标字符串 needle 是否为空
        if (needle.isEmpty()) {
            return 0;
        }
        
        // 步骤2: 检查原始字符串 haystack 是否为空或 needle 的长度是否大于 haystack 的长度
        if (haystack.isEmpty() || needle.length() > haystack.length()) {
            return -1;
        }
        
        // 步骤3: 构建 KMP 算法需要使用的部分匹配表
        int[] table = buildKMPTable(needle);
        
        int i = 0, j = 0;
        while (i < haystack.length()) {
            // 步骤4: 如果当前字符匹配成功,继续比较下一个字符
            if (haystack.charAt(i) == needle.charAt(j)) {
                i++;
                j++;
                // 步骤5: 如果 j 等于目标字符串 needle 的长度,说明匹配成功,返回起始位置
                if (j == needle.length()) {
                    return i - j;
                }
            } else if (j > 0) {
                // 步骤6: 如果当前字符匹配失败且 j 大于 0,则根据部分匹配表进行跳转
                j = table[j - 1];
            } else {
                // 步骤7: 如果当前字符匹配失败且 j 等于 0,则直接将 i 后移一位
                i++;
            }
        }
        
        // 步骤8: 遍历完整个原始字符串仍未找到匹配,返回 -1
        return -1;
    }
    
    // 构建 KMP 算法需要使用的部分匹配表
    private int[] buildKMPTable(String needle) {
        int[] table = new int[needle.length()];
        int j = 0;
        for (int i = 1; i < needle.length(); i++) {
            // 步骤9: 如果当前字符匹配成功,更新部分匹配表
            if (needle.charAt(i) == needle.charAt(j)) {
                j++;
                table[i] = j;
            } else {
                // 步骤10: 如果当前字符匹配失败且 j 大于 0,根据部分匹配表进行跳转
                if (j > 0) {
                    j = table[j - 1];
                    i--;
                }
            }
        }
        return table;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值