算法题 仅仅反转字母

仅仅反转字母

问题描述

给你一个字符串 s,根据以下规则反转字符串:

  • 所有非字母字符保持原来的位置
  • 所有字母字符反转它们的位置

返回反转后的字符串。

示例

输入: s = "ab-cd"
输出: "dc-ba"

输入: s = "a-bC-dEf-ghIj"
输出: "j-Ih-gfE-dCba"

输入: s = "Test1ng-Leet=code-Q!"
输出: "Qedo1ct-eeLg=ntse-T!"

算法思路

核心思路

  • 双指针法:使用左右两个指针,分别从字符串两端向中间移动
  • 跳过非字母字符:当指针遇到非字母字符时,继续移动直到找到字母字符
  • 交换字母字符:当左右指针都指向字母字符时,交换它们的位置

关键点

  • 需要正确判断字符是否为字母(包括大小写字母)
  • 双指针相遇时停止,避免重复交换
  • 保持非字母字符的原始位置不变

代码实现

方法一:双指针 + 字符数组

class Solution {
    /**
     * 仅仅反转字符串中的字母字符,非字母字符保持原位置
     * 使用双指针法,时间复杂度O(n),空间复杂度O(n)
     * 
     * @param s 输入字符串
     * @return 反转字母后的字符串
     */
    public String reverseOnlyLetters(String s) {
        // 边界情况处理
        if (s == null || s.length() <= 1) {
            return s;
        }
        
        // 转换为字符数组便于修改
        char[] chars = s.toCharArray();
        int left = 0;                    // 左指针
        int right = s.length() - 1;      // 右指针
        
        // 双指针向中间移动
        while (left < right) {
            // 左指针跳过非字母字符
            while (left < right && !Character.isLetter(chars[left])) {
                left++;
            }
            
            // 右指针跳过非字母字符
            while (left < right && !Character.isLetter(chars[right])) {
                right--;
            }
            
            // 交换左右指针指向的字母字符
            if (left < right) {
                char temp = chars[left];
                chars[left] = chars[right];
                chars[right] = temp;
                left++;
                right--;
            }
        }
        
        return new String(chars);
    }
}

方法二:手动判断字母

class Solution {
    /**
     * 手动实现字母判断,不依赖Character.isLetter()
     * 
     * @param s 输入字符串
     * @return 反转字母后的字符串
     */
    public String reverseOnlyLetters(String s) {
        if (s == null || s.length() <= 1) {
            return s;
        }
        
        char[] chars = s.toCharArray();
        int left = 0;
        int right = s.length() - 1;
        
        while (left < right) {
            // 左指针找字母
            while (left < right && !isLetter(chars[left])) {
                left++;
            }
            
            // 右指针找字母
            while (left < right && !isLetter(chars[right])) {
                right--;
            }
            
            // 交换字母
            if (left < right) {
                char temp = chars[left];
                chars[left] = chars[right];
                chars[right] = temp;
                left++;
                right--;
            }
        }
        
        return new String(chars);
    }
    
    /**
     * 手动判断字符是否为字母
     * 
     * @param c 待判断字符
     * @return 是否为字母
     */
    private boolean isLetter(char c) {
        return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
    }
}

方法三:使用栈

import java.util.*;

class Solution {
    /**
     * 使用栈存储所有字母,然后重新构建字符串
     * 空间复杂度较高,但思路直观
     * 
     * @param s 输入字符串
     * @return 反转字母后的字符串
     */
    public String reverseOnlyLetters(String s) {
        if (s == null || s.length() <= 1) {
            return s;
        }
        
        // 将所有字母压入栈中(自然反转顺序)
        Stack<Character> letterStack = new Stack<>();
        for (char c : s.toCharArray()) {
            if (Character.isLetter(c)) {
                letterStack.push(c);
            }
        }
        
        // 重新构建字符串
        StringBuilder result = new StringBuilder();
        for (char c : s.toCharArray()) {
            if (Character.isLetter(c)) {
                // 字母位置用栈顶元素(反转后的字母)
                result.append(letterStack.pop());
            } else {
                // 非字母字符保持原样
                result.append(c);
            }
        }
        
        return result.toString();
    }
}

算法分析

  • 时间复杂度:O(n)
    • 双指针法:每个字符最多被访问两次
    • 栈方法:需要两次遍历字符串
  • 空间复杂度
    • 双指针法:O(n) - 字符数组存储
    • 栈方法:O(n) - 栈存储所有字母 + StringBuilder
  • 方法对比
    • 双指针法:空间效率高,一次遍历完成,推荐使用
    • 栈方法:思路直观,但空间开销大
    • 手动判断:避免依赖内置方法

算法过程

输入:s = "a-bC-dEf-ghIj"

双指针过程:

初始状态:['a', '-', 'b', 'C', '-', 'd', 'E', 'f', '-', 'g', 'h', 'I', 'j']

  • left=0 (‘a’), right=12 (‘j’) → 交换 → ['j', '-', 'b', 'C', '-', 'd', 'E', 'f', '-', 'g', 'h', 'I', 'a']
  • left=1 (‘-’) 跳过 → left=2 (‘b’)
  • right=11 (‘I’) → 交换 → ['j', '-', 'I', 'C', '-', 'd', 'E', 'f', '-', 'g', 'h', 'b', 'a']
  • left=3 (‘C’), right=10 (‘h’) → 交换 → ['j', '-', 'I', 'h', '-', 'd', 'E', 'f', '-', 'g', 'C', 'b', 'a']
  • left=4 (‘-’) 跳过 → left=5 (‘d’)
  • right=9 (‘g’) → 交换 → ['j', '-', 'I', 'h', '-', 'g', 'E', 'f', '-', 'd', 'C', 'b', 'a']
  • left=6 (‘E’), right=8 (‘-’) 跳过 → right=7 (‘f’) → 交换 → ['j', '-', 'I', 'h', '-', 'g', 'f', 'E', '-', 'd', 'C', 'b', 'a']
  • left=7, right=6 → left >= right,结束

结果:"j-Ih-gfE-dCba"

栈方法过程:

  1. 提取字母:['a', 'b', 'C', 'd', 'E', 'f', 'g', 'h', 'I', 'j']
  2. 压入栈后:栈顶到栈底为 ['j', 'I', 'h', 'g', 'f', 'E', 'd', 'C', 'b', 'a']
  3. 重建字符串:
    • ‘a’ → ‘j’
    • ‘-’ → ‘-’
    • ‘b’ → ‘I’
    • ‘C’ → ‘h’
  4. 结果:"j-Ih-gfE-dCba"

测试用例

public static void main(String[] args) {
    Solution solution = new Solution();
    
    // 测试用例1:标准示例
    System.out.println("Test 1: " + solution.reverseOnlyLetters("ab-cd")); // "dc-ba"
    System.out.println("Test 2: " + solution.reverseOnlyLetters("a-bC-dEf-ghIj")); // "j-Ih-gfE-dCba"
    System.out.println("Test 3: " + solution.reverseOnlyLetters("Test1ng-Leet=code-Q!")); // "Qedo1ct-eeLg=ntse-T!"
    
    // 测试用例2:无字母
    System.out.println("Test 4: " + solution.reverseOnlyLetters("123!@#")); // "123!@#"
    
    // 测试用例3:全字母
    System.out.println("Test 5: " + solution.reverseOnlyLetters("abcdef")); // "fedcba"
    System.out.println("Test 6: " + solution.reverseOnlyLetters("ABCDEF")); // "FEDCBA"
    
    // 测试用例4:单字符
    System.out.println("Test 7: " + solution.reverseOnlyLetters("a")); // "a"
    System.out.println("Test 8: " + solution.reverseOnlyLetters("1")); // "1"
    System.out.println("Test 9: " + solution.reverseOnlyLetters("-")); // "-"
    
    // 测试用例5:空字符串
    System.out.println("Test 10: " + solution.reverseOnlyLetters("")); // ""
    
    // 测试用例6:只有非字母
    System.out.println("Test 11: " + solution.reverseOnlyLetters("!@#$%^&*()")); // "!@#$%^&*()"
    
    // 测试用例7:字母和数字混合
    System.out.println("Test 12: " + solution.reverseOnlyLetters("a1b2c3")); // "c1b2a3"
    
    // 测试用例8:大小写字母混合
    System.out.println("Test 13: " + solution.reverseOnlyLetters("AbCdEf")); // "fEdCbA"
    
    // 测试用例9:边界情况
    System.out.println("Test 14: " + solution.reverseOnlyLetters("a-")); // "a-"
    System.out.println("Test 15: " + solution.reverseOnlyLetters("-a")); // "-a"
    
    // 测试用例10:长字符串
    StringBuilder longStr = new StringBuilder();
    for (int i = 0; i < 100; i++) {
        if (i % 3 == 0) {
            longStr.append((char)('a' + i % 26));
        } else {
            longStr.append('-');
        }
    }
    System.out.println("Test 16: Length = " + solution.reverseOnlyLetters(longStr.toString()).length()); // 验证长度不变
    
    // 测试用例11:null值
    System.out.println("Test 17: " + solution.reverseOnlyLetters(null)); // null
}

关键点

  1. 双指针的核心思想

    • 左右指针分别寻找字母字符
    • 跳过非字母字符,只交换字母字符
    • 保证非字母字符位置不变
  2. 字母判断方法

    • Character.isLetter():Java内置方法,支持Unicode
    • 手动判断:(c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'),仅支持ASCII
  3. 边界处理

    • 空字符串和null值
    • 单字符字符串
    • 无字母或全字母的特殊情况
  4. 空间效率

    • 双指针法只需要字符数组的空间
    • 栈方法需要额外的栈空间存储所有字母

常见问题

  1. 为什么不用String直接操作?

    • Java中String是不可变的,无法直接修改字符
    • 必须转换为字符数组或使用StringBuilder
  2. Character.isLetter()和手动判断有什么区别?

    • Character.isLetter() 支持所有Unicode字母
    • 手动判断只支持ASCII字母(A-Z, a-z)
  3. 双指针法的时间复杂度真的是O(n)吗?

    • 虽然有嵌套循环,但每个字符最多被访问两次
    • 左指针和右指针总共移动n次,所以是O(n)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值