在这里插入图片描述

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

摘要

字符串转换整数(atoi)工具实现了将字符串转换为整数的功能,支持前导空格、正负号、数字解析、溢出处理等。本文详细解析了输入解析、atoi 核心算法、溢出检测、字符分析等核心功能的代码实现细节。

1. 算法背景

1.1 问题描述

实现一个类似 C 语言 atoi 函数的字符串转整数功能,需要处理:

  • 前导空格
  • 可选的正负号
  • 连续的数字字符
  • 整数溢出(超出 32 位整数范围)
  • 遇到非数字字符时停止

示例

  • 输入:" -42"
  • 输出:-42

1.2 应用场景

  • 配置文件解析
  • 命令行参数处理
  • 数据验证
  • 字符串格式化
  • 算法竞赛

2. 核心算法原理

2.1 状态机思想

使用状态机思路处理字符串:

  1. 跳过前导空格
  2. 读取可选的正负号
  3. 读取连续的数字字符
  4. 遇到非数字字符或字符串结束则停止

2.2 溢出检测

在计算过程中实时检测溢出,避免结果超出 32 位整数范围。

3. 代码实现详细解析

3.1 输入解析模块

var s: String? = null

payload.lines()
    .map { it.trim() }
    .filter { it.isNotEmpty() }
    .forEach { line ->
        when {
            line.startsWith("s=", ignoreCase = true) -> {
                var value = line.substringAfter("=").trim()
                // 移除可能的引号
                if ((value.startsWith("\"") && value.endsWith("\"")) ||
                    (value.startsWith("'") && value.endsWith("'"))) {
                    value = value.substring(1, value.length - 1)
                }
                s = value
            }
        }
    }

if (s == null) {
    // 尝试直接使用输入作为字符串
    val lines = payload.lines().map { it.trim() }.filter { it.isNotEmpty() }
    if (lines.isNotEmpty()) {
        var value = lines[0]
        // 移除可能的引号
        if ((value.startsWith("\"") && value.endsWith("\"")) ||
            (value.startsWith("'") && value.endsWith("'"))) {
            value = value.substring(1, value.length - 1)
        }
        s = value
    }
}

代码解析

这段代码实现了灵活的输入解析机制,支持两种输入格式,并且能够处理带引号的字符串。首先声明一个可空字符串变量 s,用于存储解析后的输入字符串。

然后使用函数式编程风格处理输入:payload.lines() 将输入按行分割,map { it.trim() } 去除每行的首尾空白字符,filter { it.isNotEmpty() } 过滤空行。

forEach 循环中,如果行以 s= 开头(忽略大小写),使用 substringAfter("=") 提取等号后面的部分。然后检查提取的值是否被引号包围(双引号或单引号),如果是,则使用 substring(1, value.length - 1) 移除首尾的引号。这种处理使得用户可以用引号包围包含空格的字符串,提高了输入的灵活性。

如果解析后字符串为 null,则尝试按行解析:将输入按行分割并过滤空行,如果至少有 1 行,则第一行作为输入字符串。同样处理引号的情况。这种备选方案允许用户直接输入字符串而不需要指定参数名。

3.2 atoi 核心算法实现

fun myAtoi(s: String): Pair<Int, String> {
    var i = 0
    var sign = 1
    var result = 0
    val maxInt = Int.MAX_VALUE
    val minInt = Int.MIN_VALUE
    val steps = mutableListOf<String>()

    // 步骤1: 跳过前导空格
    while (i < s.length && s[i] == ' ') {
        steps.add("步骤${steps.size + 1}: 跳过前导空格,位置 $i")
        i++
    }

    // 步骤2: 检查正负号
    if (i < s.length && (s[i] == '+' || s[i] == '-')) {
        sign = if (s[i] == '-') -1 else 1
        steps.add("步骤${steps.size + 1}: 检测到${if (sign == -1) "负" else "正"}号,sign = $sign")
        i++
    }

    // 步骤3: 读取数字
    var digitCount = 0
    while (i < s.length && s[i].isDigit()) {
        val digit = s[i].toString().toInt()
        
        // 检查溢出
        if (result > maxInt / 10 || (result == maxInt / 10 && digit > maxInt % 10)) {
            steps.add("步骤${steps.size + 1}: 检测到溢出!result=$result, digit=$digit")
            return Pair(if (sign == 1) maxInt else minInt, steps.joinToString("\n"))
        }
        
        result = result * 10 + digit
        steps.add("步骤${steps.size + 1}: 读取数字 '$digit', result = $result")
        digitCount++
        i++
    }

    if (digitCount == 0) {
        steps.add("步骤${steps.size + 1}: 未读取到任何数字,返回 0")
        return Pair(0, steps.joinToString("\n"))
    }

    val finalResult = result * sign
    steps.add("步骤${steps.size + 1}: 应用符号,最终结果 = $finalResult")
    
    return Pair(finalResult, steps.joinToString("\n"))
}

代码解析

这是 atoi 算法的核心实现,使用状态机思路逐步解析字符串。函数首先初始化变量:i 是当前处理的字符索引(初始值为 0),sign 是符号标志(初始值为 1,表示正数),result 是累积的结果值(初始值为 0),maxIntminInt 是 32 位整数的最大值和最小值,steps 是用于记录处理步骤的列表,便于后续展示详细过程。

步骤1:跳过前导空格。使用 while 循环,条件是 i < s.length && s[i] == ' ',即当前位置未越界且字符是空格。在循环中,记录跳过空格的步骤,并将索引 i 递增。这一步符合 atoi 函数的行为规范,允许字符串前面有任意数量的空格。

步骤2:检查正负号。在跳过空格后,检查当前位置的字符是否是 +-。如果是,根据字符设置符号标志:如果是 -,设置 sign = -1;如果是 +,设置 sign = 1(实际上保持为 1)。记录检测到符号的步骤,并将索引 i 递增,移动到下一个字符。这一步处理了可选的正负号,符合 atoi 函数的规范。

步骤3:读取数字。这是算法的核心部分,使用 while 循环读取连续的数字字符。条件是 i < s.length && s[i].isDigit(),即当前位置未越界且字符是数字。在循环中,首先将当前字符转换为整数:使用 toString().toInt() 将字符转换为字符串再转换为整数。

然后进行溢出检测。这是算法的关键部分,需要在计算之前检查,避免实际溢出。检测条件有两个:

  • result > maxInt / 10:如果当前结果已经大于最大值的十分之一,那么再乘以 10 就会溢出。
  • result == maxInt / 10 && digit > maxInt % 10:如果当前结果等于最大值的十分之一,那么需要检查要添加的数字是否大于最大值的个位数。

如果检测到溢出,根据符号返回相应的边界值:如果是正数,返回 maxInt;如果是负数,返回 minInt。这种处理方式符合 atoi 函数的溢出处理规范。

如果没有溢出,则正常计算:result = result * 10 + digit。这是标准的数字构建方式:将之前的结果乘以 10(相当于左移一位),然后加上新的数字。记录读取数字的步骤,递增数字计数器和索引,继续下一次循环。

循环结束后,检查是否读取到任何数字。如果 digitCount == 0,说明字符串中没有有效的数字字符,返回 0。否则,将结果乘以符号得到最终结果:finalResult = result * sign。这样处理了负数的情况,因为之前我们一直用正数计算,最后应用符号。

最后返回结果和处理步骤的字符串表示。使用 Pair 返回两个值:转换后的整数和详细的处理步骤。

这个算法的时间复杂度是 O(n),其中 n 是字符串的长度,因为只需要遍历字符串一次。空间复杂度是 O(1),因为只使用了固定数量的变量,虽然有一个 steps 列表,但在实际应用中可以省略,这里保留是为了提供详细的处理过程。

3.3 溢出检测机制

if (result > maxInt / 10 || (result == maxInt / 10 && digit > maxInt % 10)) {
    steps.add("步骤${steps.size + 1}: 检测到溢出!result=$result, digit=$digit")
    return Pair(if (sign == 1) maxInt else minInt, steps.joinToString("\n"))
}

代码解析

这段代码实现了精确的溢出检测机制,这是 atoi 算法中最关键也最容易出错的部分。溢出检测必须在实际计算之前进行,因为一旦发生溢出,结果就会不正确。

检测条件分为两种情况:

第一种情况:result > maxInt / 10。如果当前累积的结果已经大于最大值的十分之一(即 214748364),那么无论下一个数字是什么,将结果乘以 10 后都会超过最大值 2147483647。例如,如果 result = 214748365,那么 result * 10 = 2147483650,这已经超过了 Int.MAX_VALUE

第二种情况:result == maxInt / 10 && digit > maxInt % 10。这是边界情况。如果当前结果等于最大值的十分之一(即 214748364),那么只有当要添加的数字大于最大值的个位数(即 7)时才会溢出。例如,如果 result = 214748364digit = 8,那么 result * 10 + digit = 2147483648,超过了最大值。

如果检测到溢出,根据符号返回相应的边界值。如果是正数(sign == 1),返回 maxInt2147483647);如果是负数(sign == -1),返回 minInt-2147483648)。这种处理方式符合 atoi 函数的溢出处理规范:当结果超出 32 位整数范围时,截断到边界值。

这种溢出检测方法避免了使用更大数据类型(如 Long)的需要,可以直接在 Int 类型范围内进行计算和检测,提高了算法的效率和简洁性。

3.4 字符分析功能

var spaceCount = 0
var digitCount = 0
var signCount = 0
var otherCount = 0
var firstNonSpaceIndex = -1
var firstDigitIndex = -1

str.forEachIndexed { index, char ->
    when {
        char == ' ' -> spaceCount++
        char.isDigit() -> {
            digitCount++
            if (firstDigitIndex == -1) firstDigitIndex = index
        }
        char == '+' || char == '-' -> {
            signCount++
            if (firstNonSpaceIndex == -1 && index > 0) {
                // 检查是否在前导空格之后
                var isValid = true
                for (i in 0 until index) {
                    if (str[i] != ' ') {
                        isValid = false
                        break
                    }
                }
                if (isValid) firstNonSpaceIndex = index
            }
        }
        else -> otherCount++
    }
}

代码解析

这段代码实现了详细的字符分析功能,帮助用户了解输入字符串中各类字符的分布情况。首先初始化多个计数器:spaceCount 统计空格数量,digitCount 统计数字字符数量,signCount 统计符号字符(+-)数量,otherCount 统计其他字符数量。还初始化了两个索引变量:firstNonSpaceIndex 记录第一个非空格字符的位置,firstDigitIndex 记录第一个数字字符的位置。

然后使用 forEachIndexed 遍历字符串的每个字符,index 是字符的位置,char 是字符本身。使用 when 表达式根据字符类型进行分类处理。

如果字符是空格,递增空格计数器。如果字符是数字,递增数字计数器,并且如果这是第一个遇到的数字(firstDigitIndex == -1),记录其位置。如果字符是符号(+-),递增符号计数器,并且检查这是否是第一个非空格字符。

对于符号字符,需要验证它是否在前导空格之后。使用一个内部循环检查该符号之前的所有字符是否都是空格。如果是,则记录这个位置作为第一个非空格字符的位置。这种检查确保了符号字符是在合法位置(前导空格之后)出现的。

如果字符不属于上述任何类别,递增其他字符计数器。最后,根据统计结果输出详细的字符分析信息,包括各类字符的数量和关键位置信息。这种详细的分析有助于用户理解字符串的构成和 atoi 算法的处理过程。

4. 算法复杂度分析

4.1 时间复杂度

  • 整体算法:O(n),需要遍历字符串一次
  • 字符分析:O(n),需要遍历字符串一次
  • 总体时间复杂度:O(n)

4.2 空间复杂度

  • 算法核心:O(1),只使用固定数量的变量
  • 步骤记录:O(n),需要存储处理步骤(可选,可省略)
  • 总体空间复杂度:O(1) 或 O(n)(取决于是否记录步骤)

5. 算法优化建议

5.1 溢出检测优化

当前的溢出检测方法已经是最优的,避免了使用更大数据类型的需要。

5.2 性能优化

对于不需要详细步骤的场景,可以省略步骤记录,减少空间使用。

5.3 边界条件处理

代码已经处理了各种边界情况:空字符串、只有空格、只有符号、无数字等情况。

6. 应用场景扩展

  1. 配置文件解析:解析配置文件中的数值参数
  2. 命令行参数处理:处理命令行传入的数值参数
  3. 数据验证:验证字符串是否可以转换为有效的整数
  4. 字符串格式化:将格式化的字符串转换为数值
  5. 算法竞赛:LeetCode 等平台的经典问题

7. 总结

字符串转换整数(atoi)工具实现了完整的字符串到整数转换功能,核心要点:

  1. 状态机思想:按照空格 → 符号 → 数字 → 结束的顺序处理字符串
  2. 溢出检测:在计算过程中实时检测溢出,避免结果超出范围
  3. 边界处理:正确处理空字符串、无数字、溢出等各种边界情况
  4. 详细分析:提供字符分析、处理步骤、边界情况分析等详细信息

通过深入理解代码实现,可以更好地应用这个算法解决实际问题,如配置文件解析、命令行参数处理、数据验证等场景。atoi 算法的思想和溢出检测机制也可以应用到其他类似的字符串解析问题中。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐