java算法学习索引之字符串问题

一 判断两个字符串是否互为变形词

【题目】给定两个字符串str1和str2,如果str1和str2中出现的字符种类一样且每种字符出现的次数也一样,那么str1与str2互为变形词。请实现函数判断两个字符串是否互为变形词。

public boolean isDeformation(String str1, String str2)
{
    // 判断输入字符串是否为空,以及长度是否相同
    if (str1 == null || str2 == null || str1.length() != str2.length()) {
        return false;
    }

    char[] chas1 = str1.toCharArray(); // 将字符串 str1 转换为字符数组
    char[] chas2 = str2.toCharArray(); // 将字符串 str2 转换为字符数组

    int[] map = new int[256]; // 创建一个长度为 256 的辅助数组 map,用于统计字符的出现次数

    // 统计字符出现的次数
    for (int i = 0; i < chas1.length; i++) {
        map[chas1[i]]++; // 字符 chas1[i] 的 ASCII 值作为索引,自增对应位置的元素
    }

    // 遍历字符数组 chas2,判断字符的出现次数是否与 chas1 一致
    for (int i = 0; i < chas2.length; i++) {
        if (map[chas2[i]]-- == 0) { // 如果字符 chas2[i] 的出现次数为 0,则返回 false
            return false;
        }
    }

    return true; // 返回 true,表示 str1 和 str2 互为变形词
}

函数 isDeformation 的作用是判断两个字符串 str1 和 str2 是否互为变形词。

在方法中,首先判断输入字符串 str1 和 str2 是否为空,以及它们的长度是否相同。如果条件不满足,直接返回 false

然后,将字符串 str1 和 str2 分别转换为字符数组 chas1 和 chas2

接下来,创建一个长度为 256 的辅助数组 map,用于统计字符的出现次数。

通过遍历 chas1 数组,将字符出现的次数统计到 map 数组中。

再遍历 chas2 数组,依次检查字符的出现次数,如果发现有字符的出现次数为 0,就返回 false

最后,如果没有发现出现次数不一致的情况,就返回 true,表示 str1 和 str2 互为变形词。

二  判断两个字符串是否互为旋转词

【题目】如果一个字符串为str,把字符串str前面任意的部分挪到后面形成的字符串叫作str的旋转词。比如str="12345",str的旋转词有"12345"、"23451"、"34512"、"45123"和"51234"。给定两个字符串a和b,请判断a和b是否互为旋转词。

【举例】

【要求】如果a和b长度不一样,那么a和b必然不互为旋转词,可以直接返回false。当a和b长度一样,都为N时,要求解法的时间复杂度为O(N)。

判断两个字符串是否互为旋转词的方法如下:

1. 首先判断两个字符串的长度是否相等,如果不相等,直接返回false。
2. 将字符串a与自身拼接,形成新的字符串newStr。
3. 在newStr中查找是否包含字符串b,如果包含,则说明a和b是互为旋转词,返回true;否则,返回false。

以下是一个Java示例代码实现:

```java
public boolean isRotation(String a, String b) {
    if (a.length() != b.length()) {
        return false;
    }

    String newStr = a + a;

    return newStr.contains(b);
}
```

使用KMP算法

public boolean isRotation(String a, String b) {
    if (a == null || b == null || a.length() != b.length()) {
        return false;
    }

    // 将b字符串重复一遍拼接成新的字符串b2
    String b2 = b + b;

    // 判断b2是否包含a,若包含则a和b互为旋转词,返回true;否则返回false
    return b2.contains(a);
}

// 判断字符串s是否包含子串m
private boolean getIndexOf(String s, String m) {
    if (s == null || m == null || m.length() < 1 || s.length() < m.length()) {
        return false;
    }

    char[] ss = s.toCharArray();
    char[] ms = m.toCharArray();
    int si = 0; // 字符串s的索引
    int mi = 0; // 子串m的索引
    int[] next = getNextArray(ms); // 获取子串m的next数组

    // 使用KMP算法进行匹配
    while (si < ss.length && mi < ms.length) {
        if (ss[si] == ms[mi]) {
            si++;
            mi++;
        } else if (next[mi] != -1) {
            // 当前字符不匹配且子串m的索引不为-1时,向前跳到next[mi]的位置
            mi = next[mi];
        } else {
            // 当前字符不匹配且子串m的索引为-1时,字符串s的索引向前移动一位
            si++;
        }
    }

    // 如果子串m的索引达到末尾,说明匹配成功
    if (mi == ms.length) {
        return true;
    } else {
        return false;
    }
}

// 获取子串m的next数组
private int[] getNextArray(char[] ms) {
    if (ms.length == -1) {
        return new int[] {-1};
    }
    int[] next = new int[ms.length];
    next[0] = -1;
    next[1] = 0;
    int pos = 2; // 下一个计算next值的位置
    int cn = 0; // 当前跳到的位置
    while (pos < next.length) {
        if (ms[pos - 1] == ms[cn]) {
            // 当前字符与跳到的位置字符相等,next值为cn+1,pos和cn都后移
            next[pos++] = ++cn;
        } else if (cn > 0) {
            // 当前字符与跳到的位置字符不相等,且cn>0,向前跳到next[cn]的位置
            cn = next[cn];
        } else {
            // 当前字符与跳到的位置字符不相等,且cn=0,next值为0,pos后移
            next[pos++] = 0;
        }
    }
    return next;
}

三  将整数字符串转成整数值

【题目】给定一个字符串str,如果str符合日常书写的整数形式,并且属于32位整数的范围,返回str所代表的整数值,否则返回0。

/**
 * 判断字符数组chas是否为有效的整数表示形式
 *
 * @param chas 字符数组
 * @return 是否为有效的整数表示形式
 */
public boolean isValid(char[] chas) {
    // 首字符不为负号且不为数字,返回false
    if (chas[0] != '-' && (chas[0] < '0' || chas[0] > '9')) {
        return false;
    }
    // 首字符为负号且长度为1或者第二个字符为0,返回false
    if (chas[0] == '-' && (chas.length == 1 || chas[1] == '0')) {
        return false;
    }
    // 首字符为0且长度大于1,返回false
    if (chas[0] == '0' && chas.length > 1) {
        return false;
    }
    // 遍历字符数组,如果有非数字字符,返回false
    for (int i = 0; i < chas.length; i++) {
        if (chas[i] < '0' || chas[i] > '9') {
            return false;
        }
    }
    return true;
}

/**
 * 将字符串str转换为整数值
 *
 * @param str 字符串
 * @return 转换后的整数值
 */
public int convert(String str) {
    // 字符串为空,返回0
    if (str == null || str.equals("")) {
        return 0;
    }
    char[] chas = str.toCharArray(); // 将字符串转换为字符数组
    if (!isValid(chas)) {
        return 0; // 不是有效的整数表示形式,返回0
    }
    boolean posi = chas[0] == '-'; // 判断是否为负数
    int minq = Integer.MIN_VALUE / 10; // 最小值除以10的商
    int minr = Integer.MIN_VALUE % 10; // 最小值除以10的余数
    int res = 0; // 结果变量
    int cur = 0; // 当前数字位
    for (int i = posi ? 0 : 1; i < chas.length; i++) {
        cur = '0' - chas[i]; // 将字符转换为数字
        // 判断结果是否越界
        if ((res < minq) || (res == minq && cur < minr)) {
            return 0;
        }
        res = res * 10 + cur; // 更新结果
    }
    if (!posi && res == Integer.MIN_VALUE) {
        return 0; // 如果是正数且结果为最小值,返回0
    }
    return posi ? -res : res; // 返回最终结果,带上负号
}

以上代码通过两个方法实现了将字符串转换为整数值的功能。isValid 方法用于判断字符数组是否为有效的整数表示形式,包括判断首字符、负号、非数字字符等。convert 方法将字符串转换为整数值,先根据符号位和字符串的有效性做一些预处理,然后从字符串的第一个字符(如果是负数则从第二个字符)开始遍历,每次将字符转换为数字,并根据已转换的结果和当前字符计算新的结果。在每次更新结果时,都需要检查是否越界,若超出了32位整数的范围,则返回0。最后根据正负号返回最终的整数结果。在注释中详细解释了每个步骤的逻辑和功能。

四  字符串的统计字符串

【题目】

给定一个字符串 str,返回 str 的统计字符串。例如,"aaabbadddffc"的统计字符串为"a3b2a1d3f2c1"。

public String getCountString(String str) {
    // 如果输入字符串为空或者为空字符串,则直接返回空字符串
    if (str == null || str.equals("")) {
        return "";
    }
    
    // 将输入字符串转换为字符数组
    char[] chs = str.toCharArray();
    
    // 初始化结果字符串为第一个字符
    String res = String.valueOf(chs[0]);
    
    // 初始化计数变量为1
    int num = 1;
    
    // 遍历字符数组,从索引 1 开始
    for (int i = 1; i < chs.length; i++) {
        // 如果当前字符和上一个字符不相同
        if (chs[i] != chs[i - 1]) {
            // 将上一个字符和计数加到结果字符串中
			// 调用 concat 方法将字符串连接起来
            res = concat(res, String.valueOf(num), String.valueOf(chs[i]));
            
            // 重置计数变量为 1
            num = 1;
        } else {
            // 如果当前字符和上一个字符相同,计数加1
            num++;
        }
    }
    
    // 将最后一个字符和计数加到结果字符串中
    return concat(res, String.valueOf(num), "");
}

// 定义 concat 方法,将字符串连接起来
private String concat(String s1, String s2, String s3) {
    return s1 + "_" + s2 + (s3.equals("") ? s3 : "_" + s3);
}

补充问题:给定一个字符串的统计字符串cstr,再给定一个整数index,返回cstr所代表的原始字符串上的第index个字符。例如,"a 1 b 100"所代表的原始字符串上第0个字符是'a',第50个字符是'b'。

public char getCharAt(String cstr, int index) {
    // 如果统计字符串为空或者为空字符串,则直接返回空字符
    if (cstr == null || cstr.equals("")) {
        return '\0';
    }
    
    // 将统计字符串转换为字符数组
    char[] chs = cstr.toCharArray();
    
    // 初始化阶段标志和当前字符、计数变量等
    boolean stage = true; // 阶段标志,用于区分字符阶段和计数阶段
    char cur = '\0'; // 当前字符
    int num = 0; // 计数变量,用于累计字符的计数
    int sum = 0; // 已遍历的字符数量,用于判断是否超过目标索引
    
    // 遍历字符数组
    for (int i = 0; i < chs.length; i++) {
        if (chs[i] == '_') {
            stage = !stage; // 切换阶段标志,当遇到 "_" 时切换阶段
        } else if (stage) { // 如果在字符阶段
            sum += num; // 累计已遍历的字符数量
            if (sum > index) {
                return cur; // 当前字符已超过目标索引,返回当前字符
            }
            num = 0; // 重置计数变量
            cur = chs[i]; // 更新当前字符
        } else { // 如果在计数阶段
            num = num * 10 + chs[i] - '0'; // 计算字符的计数,通过解析字符的 ASCII 值计算
        }
    }
    
    // 判断是否存在目标索引所在的字符
    return sum + num > index ? cur : '\0';
}

该方法使用了阶段标志 stage 来切换字符阶段和计数阶段。通过依次遍历字符数组 chs,在每个字符下:

  • 如果字符为下划线 _,则切换阶段标志 stage,从字符阶段切换到计数阶段,或者从计数阶段切换到字符阶段。
  • 如果处于字符阶段,累计已遍历的字符数量 sum,如果 sum 大于目标索引 index,说明当前字符已超过目标索引,返回当前字符 cur。否则,重置计数变量 num 为0,并更新当前字符 cur
  • 如果处于计数阶段,将字符的计数累加到计数变量 num 中。

在遍历结束后,通过判断 sum + num 是否大于目标索引来确定是否存在目标索引所在的字符。

例如,对于统计字符串 “a_1_b_100”,要获取第50个字符,调用 getCharAt("a_1_b_100", 50),将返回字符 ‘b’。如果要获取第0个字符,调用 getCharAt("a_1_b_100", 0),将返回字符 ‘a’。同时,如果统计字符串为空或者为空字符串,则直接返回空字符。

五  判断字符数组中是否所有的字符都只出现过一次

【题目】

给定一个字符类型数组chas[],判断chas中是否所有的字符都只出现过一次,请根据以下不同的两种要求实现两个函数。

【举例】

chas=['a','b','c'],返回true;chas=['1','2','1'],返回false。

【要求】

1.实现时间复杂度为O(N)的方法。

2.在保证额外空间复杂度为O(1)的前提下,请实现时间复杂度尽量低的方法。

解法一

public boolean isUnique1(char[] chas) {
    if (chas == null) {
        return true;
    }
    boolean[] map = new boolean[256]; 

    for (int i = 0; i < chas.length; i++) {
        // 如果当前字符已经在之前出现过,即布尔数组对应位置为true,则返回false
        if (map[chas[i]]) {
            return false;
        }
        // 将当前字符对应的布尔数组位置设置为true,表示该字符已经出现过
        map[chas[i]] = true;
    }

    return true;
}

解法二

public boolean isUnique2(char[] chas) {
    if (chas == null) {
        return true;
    }
    // 使用堆排序对字符数组进行排序
    heapSort(chas);
    // 遍历排序后的字符数组,检查是否有相邻字符相同
    for (int i = 1; i < chas.length; i++) {
        if (chas[i] == chas[i - 1]) {
            return false;
        }
    }
    return true;   
}

// 堆排序的实现
public void heapSort(char[] chas) {
    // 从上往下构建大根堆
    for (int i = 0; i < chas.length; i++) {
        insertHeap(chas, i);
    }
    // 不断从堆顶取出最大值,放到数组末尾,并进行堆调整
    for (int i = chas.length - 1; i > 0; i--) {
        swap(chas, 0, i);
        heapify(chas, 0, i);
    }
}

// 在已有的堆的基础上,插入新的节点并保持大根堆结构
public void insertHeap(char[] chas, int i) {
    int parent = 0;
    while (i != 0) {
        parent = (i - 1) / 2;
        // 如果当前节点比父节点大,交换位置
        if (chas[parent] < chas[i]) {
            swap(chas, parent, i);
            i = parent;
        } else {
            break;
        }
    }
}

// 将某个节点以下标i的子树进行堆调整,使其满足大根堆性质
public void heapify(char[] chas, int i, int size) {
    int left = i * 2 + 1;
    int right = i * 2 + 2;
    int largest = i;
    while (left < size) {
        // 找到左右孩子中值最大的节点
        if (chas[left] > chas[i]) {
            largest = left;
        }
        if (right < size && chas[right] > chas[largest]) {
            largest = right;
        }
        // 如果当前节点不是最大节点,则交换位置,并继续向下调整
        if (largest != i) {
            swap(chas, largest, i);
        } else {
            break;
        }
        i = largest;
        left = i * 2 + 1;
        right = i * 2 + 2;
    }
}

// 交换字符数组中下标为index1和index2的元素
public void swap(char[] chas, int index1, int index2) {
    char tmp = chas[index1];
    chas[index1] = chas[index2];
    chas[index2] = tmp;
}

以上代码使用堆排序对字符数组进行排序,并在排序后遍历数组,检查是否有相邻的字符相同。如果有相邻的字符相同,则返回false,表示有重复字符;否则,返回true,表示所有字符都只出现过一次。在堆排序的实现中,通过构建大根堆和堆调整来完成排序过程,并使用索引和交换操作来维护堆的结构。

六  在有序但含有空的数组中查找字符串

【题目】给定一个字符串数组strs[],在strs中有些位置为null,但在不为null的位置上,其字符串是按照字典顺序由小到大依次出现的。再给定一个字符串str,请返回str在strs中出现的最左的位置。【举例】

public int getIndex(String[] strs, String str) {
    // 检查输入是否有效
    if (strs == null || strs.length == 0 || str == null) {
        return -1; // 如果输入无效,则返回 -1
    }

    int res = -1; // 初始化结果变量为 -1
    int left = 0; // 初始化左指针为第一个索引
    int right = strs.length - 1; // 初始化右指针为最后一个索引
    int mid = 0; // 初始化中间变量
    int i = 0; // 用于存储非空元素的索引

    // 执行二分查找
    while (left <= right) {
        mid = (left + right) / 2; // 计算中间索引

        // 如果 strs[mid] 不为 null 且等于 str
        if (strs[mid] != null && strs[mid].equals(str)) {
            res = mid; // 更新结果为当前中间索引
            right = mid - 1; // 将右指针移动到中间的左侧继续搜索
        }
        // 如果 strs[mid] 不为 null
        else if (strs[mid] != null) {
            // 如果 strs[mid] 小于 str
            if (strs[mid].compareTo(str) < 0) {
                left = mid + 1; // 将左指针移动到中间的右侧继续搜索
            }
            // 如果 strs[mid] 大于等于 str
            else {
                right = mid - 1; // 将右指针移动到中间的左侧继续搜索
            }
        }
        // 如果 strs[mid] 为 null
        else {
            i = mid; // 存储 null 元素的索引
            // 在 mid 的左侧找到最近的非空元素
            while (strs[i] == null && --i > left) {
                // 这里不需要执行任何操作,只是将索引左移
            }

            // 如果 strs[i] 小于 str
            if (strs[mid].compareTo(str) < 0) {
                left = mid + 1; // 将左指针移动到中间的右侧继续搜索
            }
            // 如果 strs[i] 大于等于 str
            else {
                res = strs[i].equals(str) ? i : res; // 如果 strs[i] 等于 str,则更新结果
                right = mid - 1; // 将右指针移动到中间的左侧继续搜索
            }
        }
    }

    return res; // 返回结果
}

这个解决方案通过二分查找来找到给定字符串在数组中的最左位置,并且正确处理了 null 元素。它的时间复杂度是 O(log n),其中 n 是数组 strs 的长度。

七 字符串的调整与替换

【题目】

给定一个字符类型的数组 chas[],chas 右半区全是空字符,左半区不含有空字符。现在想将左半区中所有的空格字符替换成"%20",假设 chas 右半区足够大,可以满足替换所需要的空间,请完成替换函数。

【举例】

如果把chas的左半区看作字符串,为"a b c",假设chas的右半区足够大。替换后,chas的左半区为"a%20b%20%20c"。

【要求】

替换函数的时间复杂度为O(N),额外空间复杂度为O(1)。

补充问题:

给定一个字符类型的数组chas[],其中只含有数字字符和“*”字符。现在想把所有的“*”字符挪到chas的左边,数字字符挪到chas的右边。请完成调整函数。

【举例】如果把chas看作字符串,为"12**345"。调整后chas为"**12345"。

【要求】1.调整函数的时间复杂度为O(N),额外空间复杂度为O(1)。2.不得改变数字字符从左到右出现的顺序。

public void replace(char[] chas) {
    if (chas == null || chas.length == 0) {
        return;
    }
  
    // 统计空格字符的数量和左半区的长度
    int num = 0;
    int len = 0;
    for (len = 0; len < chas.length && chas[len] != 0; len++) {
        if (chas[len] == ' ') {
            num++;
        }
    }
  
    // 计算右半区的起始位置
    int j = len + num * 2 - 1;
  
    // 从右往左遍历左半区,替换空格字符
    for (int i = len - 1; i > -1; i--) {
        if (chas[i] != ' ') {
            // 非空格字符直接放入右半区
            chas[j--] = chas[i];
        } else {
            // 空格字符替换成"%20"
            chas[j--] = '0';
            chas[j--] = '2';
            chas[j--] = '%';
        }
    }
}

public void modify(char[] chas) {
    if (chas == null || chas.length == 0) {
        return;
    }
  
    // 从右往左遍历数组,将数字字符放入右半区,"*"字符放入左半区
    int j = chas.length - 1;
    for (int i = chas.length - 1; i > -1; i--) {
        if (chas[i] != '*') {
            chas[j--] = chas[i];
        }
    }
  
    // 将左半区剩余位置填充为"*"
    for (; j > -1;) {
        chas[j--] = '*';
    }
}

这段代码使用了两个方法replacemodify来解决题目中的两个问题。replace方法将字符数组中的空格字符替换成"%20",modify方法将字符数组中的"*"字符移动到左边,数字字符移动到右边。

对于replace方法,首先统计空格字符的数量和左半区的长度,然后计算右半区的起始位置。从右往左遍历左半区,将非空格字符放到正确的位置,遇到空格字符则替换为"%20"。最后得到替换后的结果。

对于modify方法,从右往左遍历数组,将非"“字符放入右半区,遇到”“字符则放入左半区。最后将左半区剩余的位置填充为”*",得到移动后的结果。

八  翻转字符串

【题目】

给定一个字符类型的数组chas,请在单词间做逆序调整。只要做到单词的顺序逆序即可,对空格的位置没有特别要求。

【举例】

如果把chas看作字符串为"dog loves pig",调整成"pig Loves dog"。如果把chas看作字符串为"I’m a student.",调整成"student.a I'm"。

补充问题:给定一个字符类型的数组chas和一个整数size,请把大小为size的左半区整体移到右半区,右半区整体移到左边。

【举例】如果把chas看作字符串为"ABCDE",size=3,调整成"DEABC"。

【要求】如果chas长度为N,两道题都要求时间复杂度为O(N),额外空间复杂度为O(1)。

public void rotateWord(char[] chas) {
    if (chas == null || chas.length == 0) {  // 检查是否为空或长度为0
        return;
    }
    reverse(chas, 0, chas.length - 1);  // 反转整个字符串
    int l = -1;
    int r = -1;
    for (int i = 0; i < chas.length; i++) {  // 遍历整个字符数组
        if (chas[i] != ' ') {  // 如果字符不是空格
            l = i == 0 || chas[i - 1] == ' ' ? i : l;  // 判断是否是一个单词的开始
            r = i == chas.length - 1 || chas[i + 1] == ' ' ? i : r;  // 判断是否是一个单词的结束
            
        }
        if (l != -1 && r != -1) {  // 当找到一个完整的单词时
            reverse(chas, l, r);  // 反转该单词
            l = -1;  // 重置l和r
            r = -1;
        }
    }
}

private static void reverse(char[] chas, int start, int end) {
    char tmp = 0;
    while (start < end) {
        tmp = chas[start];  // 交换字符
        chas[start] = chas[end];
        chas[end] = tmp;
        start++;
        end--;
    }
}

public static void rotate1(char[] chas, int size) {
    if (chas == null || size <= 0 || size >= chas.length) {  // 检查是否为空,size是否合法
        return;
    }
    reverse(chas, 0, size - 1);  // 反转前size个字符
    reverse(chas, size, chas.length - 1);  // 反转后面的字符
    reverse(chas, 0, chas.length - 1);  // 整体反转字符串
}

public void rotate2(char[] chas, int size) {
    if (chas == null || size <= 0 || size >= chas.length) {  // 检查是否为空,size是否合法
        return;
    }
    int start = 0;
    int end = chas.length - 1;
    int lpart = size;
    int rpart = chas.length - size;
    int s = Math.min(lpart, rpart);  // 取两个部分中较小的部分长度
    int d = lpart - rpart;  // 计算长度差
    while (true) {
        exchange(chas, start, end, s);  // 交换s个字符
        if (d == 0) {
            break;
        } else if (d > 0) {
            start += s;  // 从左边的s继续交换
            lpart = d;
        } else {
            end -= s;  // 从右边的s继续交换
            rpart = -d;
        }
        s = Math.min(lpart, rpart);  // 更新较小的部分长度
        d = lpart - rpart;  // 更新长度差
    }
}

private void exchange(char[] chas, int start, int end, int size) {
    int i = end - size + 1;
    char tmp = 0;
    while (size-- != 0) {
        tmp = chas[start];  // 交换字符
        chas[start] = chas[i];
        chas[i] = tmp;
        start++;
        i++;
    }
}

九  完美洗牌问题

【题目】给定一个长度为偶数的数组arr,长度记为2×N。前N个为左部分,后N个为右部分。arr就可以表示为{L1,L2,..,Ln,R1,R2,..,Rn},请将数组调整成{R1,L1,R2,L2,..,Rn,Ln}的样子。

【举例】arr={1,2,3,4,5,6},调整之后为{4,1,5,2,6,3}。

进阶问题:给定一个数组arr,请将数组调整为依次相邻的数字总是先<=、再>=的关系,并交替下去。比如数组中有五个数字,调整成{a,b,c,d,e},使之满足a<=b>=c<=d>=e

【要求】原问题要求时间复杂度为O(N),额外空间复杂度为O(1)。进阶问题要求时间复杂度为O(NlogN),额外空间复杂度为O(1)。

public void shuffle(int[] arr) {
    // 确保传入的数组不为空且长度为偶数
    if (arr != null && arr.length != 0 && (arr.length & 1) == 0) {
        // 调用内部的shuffle方法,传入数组、起始索引0和结束索引arr.length - 1
        shuffle(arr, 0, arr.length - 1);
    }
}

// 内部的shuffle方法,负责对数组进行重新排列
private void shuffle(int[] arr, int L, int R) {
    // 当待处理子数组的长度大于0时,进行操作
    while (R - L + 1 > 0) {
        int len = R - L + 1; // 计算子数组的长度
        int base = 3; // 基数设为3,用于循环计算
        int k = 1; // k用于记录循环的次数
        // 计算base的幂次方直到大于等于(len + 1) / 3
        while (base <= (len + 1) / 3) {
            base *= 3;
            k++;
        }
        int half = (base - 1) / 2; // 计算base的一半
        int mid = (L + R) / 2; // 计算子数组的中间索引
        // 对特定子数组进行旋转,具体操作在rotate方法中实现
        rotate(arr, L + half, mid, mid + half);
        // 在循环中交换和修改元素,具体操作在cycles方法中实现
        cycles(arr, L, base - 1, k);
        L = L + base - 1; // 更新起始索引,准备处理下一个子数组
    }
}

// modifyIndex2方法用于修改索引
public int modifyIndex2(int i, int len) {
    return (2 * i) % (len + 1);
}

// 在循环中交换和修改元素
private void cycles(int[] arr, int start, int len, int k) {
    for (int i = 0, trigger = 1; i < k; i++, trigger *= 3) {
        int preValue = arr[trigger + start - 1]; // 记录前一个值
        int cur = modifyIndex2(trigger, len); // 获取当前索引
        // 循环直到回到起始位置
        while (cur != trigger) {
            int tmp = arr[cur + start - 1]; // 临时保存当前位置的值
            arr[cur + start - 1] = preValue; // 将前一个值赋给当前位置
            preValue = tmp; // 更新前一个值为当前值
            cur = modifyIndex2(cur, len); // 计算下一个索引
        }
        arr[cur + start - 1] = preValue; // 将最后一个值放回起始位置
    }
}

// 反转数组中指定范围的元素
private void rotate(int[] arr, int L, int M, int R) {
    reverse(arr, L, M); // 反转左半部分
    reverse(arr, M + 1, R); // 反转右半部分
    reverse(arr, L, M); // 再次反转整个子数组,实现旋转
}

// 反转数组中指定范围的元素
private static void reverse(int[] arr, int L, int R) {
    while (L < R) { // 从两端向中间遍历反转元素
        int tmp = arr[L];
        arr[L++] = arr[R];
        arr[R--] = tmp;
    }
}

这个解决方案的思路如下:

shuffle 方法检查给定的数组是否不为空,不为空,且长度为偶数。如果满足这些条件,则调用带有数组和起始和结束索引的 shuffle 方法。
shuffle 方法使用循环实现,该循环迭代直到子数组长度大于零。在循环内部,它执行以下步骤:
a. 根据子数组的长度确定基数和迭代次数(k)。基数计算为大于或等于(len+1)/3 的最近的3的幂。
b. 将一半索引设置为(base-1)/2,将中间索引设置为当前子数组的中间索引。
c. 调用 rotate 方法执行子数组的旋转。
d. 调用 cycles 方法交换和修改循环内的元素。
e. 更新起始索引(L)以移动到下一个子数组。
rotate 方法用于反转子数组的特定部分以实现所需的排列。它调用 reverse 方法来反转左部分、右部分和整个子数组。
reverse 方法交换从起始索引(L)到结束索引(R)的元素,直到它们在中间相遇为止。
总而言之,该解决方案将数组分成子数组,并应用旋转和循环来重新组织元素。旋转是通过反转每个子数组的特定部分来完成的,并且循环确保相邻的数字满足交替条件。

注解:

在此问题的解法中,要求基数为3的幂的原因是为了确保每一个范围内的元素都可以平均分布和交替排列。以{1, 2, 3, 4, 5, 6}为例,将数组分为两个范围,第一个范围为{1, 2, 3},第二个范围为{4, 5, 6}。

如果我们选择基数为2或其他非3的幂,比如选择基数为2,将数组分为两个范围,第一个范围为{1, 2},第二个范围为{3, 4},在进行乱序操作后得到的数组可能是{2, 1, 3, 4},此时左半部分和右半部分的元素仍然是紧挨着的。

而选择基数为3的幂,比如选择基数为3,将数组分为两个范围,第一个范围为{1, 2, 3},第二个范围为{4, 5, 6},在进行乱序操作后得到的数组是{3, 1, 2, 6, 4, 5},这样左半部分和右半部分的元素就能够交替排列。

因此,选择基数为3的幂可以确保在每个范围内的元素都能够平均分布和交替排列,从而实现了要求的乱序效果。

进阶问题

public void wiggleSort(int[] arr) {
    // 检查数组是否有效且长度不为0
    if (arr == null || arr.length == 0) {
        return;
    }
    
    // 对数组进行排序
    Arrays.sort(arr);
    
    // 判断数组长度的奇偶性
    if ((arr.length & 1) == 1) { // 奇数长度的数组
        // 对数组中除了第一个元素之外的所有元素进行乱序操作
        shuffle(arr, 1, arr.length - 1);
    } else { // 偶数长度的数组
        // 对数组中除了第一个元素之外的所有元素进行乱序操作
        shuffle(arr, 1, arr.length - 1);
        
        // 将乱序后的数组中的相邻元素两两交换,满足条件a <= b >= c <= d >= ...
        for (int i = 0; i < arr.length; i += 2) {
            int tmp = arr[i];
            arr[i] = arr[i + 1];
            arr[i + 1] = tmp;
        }
    }
}

段代码实现了一个wiggle排序算法,根据数组的长度奇偶性进行不同的处理:

  1. 对数组进行排序。
  2. 如果数组长度为奇数,则对除了第一个元素之外的所有元素进行乱序操作。
  3. 如果数组长度为偶数,则同样对除了第一个元素之外的所有元素进行乱序操作,并且在乱序后的数组中,将相邻元素两两交换,满足条件a <= b >= c <= d >= …的关系。

乱序操作的具体实现与前面解法相同,将数组分为基数为3的幂的范围,然后对每个范围内的元素进行乱序置换。

十 删除多余字符得到字典序最小的字符串

【题目】给定一个全是小写字母的字符串str,删除多余字符,使得每种字符只保留一个,并让最终结果字符串的字典序最小。

【举例】str="acbc",删掉第一个'c',得到"abc",是所有结果字符串中字典序最小的。str="dbcacbca",删掉第一个'b'、第一个'c'、第二个'c'、第二个'a',得到"dabc",是所有结果字符串中字典序最小的。

public String removeDuplicateLetters(String s) {
    // 将字符串转换为字符数组
    char[] str = s.toCharArray();

    // 记录每个字符出现的次数
    int[] map = new int[26];
    for (int i = 0; i < str.length; i++) {
        map[str[i] - 'a']++;
    }

    // 存储结果的字符数组
    char[] res = new char[26];
    int index = 0; // 记录结果字符数组中的索引位置
    int L = 0; // 左指针起始位置
    int R = 0; // 右指针起始位置

    // 循环处理字符数组
    while (R != str.length) {
        if (map[str[R] - 'a'] == -1 || --map[str[R] - 'a'] > 0) {
            // 如果当前字符已经在结果中或者当前字符的数量大于1,则继续向右移动右指针
            R++;
        } else {
            // 如果当前字符未在结果中且数量为1
            int pick = -1; // 记录需要选择的字符在[L,R]范围内的位置

            // 在[L,R]范围内查找符合条件的字符(字典序最小但未在结果中的字符)
            for (int i = L; i <= R; i++) {
                if (map[str[i] - 'a'] == -1 && (pick == -1 || str[i] < str[pick])) {
                    pick = i;
                }
            }

            // 将选择的字符添加到结果字符数组中
            res[index++] = str[pick];

            // 更新map中的计数
            for (int i = pick + 1; i <= R; i++) {
                if (map[str[i] - 'a'] != -1) {
                    map[str[i] - 'a']++;
                }
            }

            // 将选择的字符标记为-1,表示已经添加到结果中
            map[str[pick] - 'a'] = -1;

            // 更新左指针和右指针位置
            L = pick + 1;
            R = L;
        }
    }

    // 将结果字符数组转换为字符串并返回
    return String.valueOf(res, 0, index);
}

解题思路:

  1. 首先统计字符串s中每个字符出现的次数,使用map数组存储,其中下标表示字符(‘a’-‘z’),值表示对应字符出现的次数。
  2. 使用双指针L和R,分别表示当前子串的起始位置和结束位置。
  3. 如果当前字符已经在结果中或当前字符的数量大于1,则继续向右移动右指针R,直到遇到一个字符在结果中或字符的数量减到1。
  4. 在[L, R]范围内查找满足条件的字符,即选择字典序最小但未在结果中的字符。
  5. 将选择的字符添加到结果字符数组res中,并更新map中的计数。
  6. 继续循环直到右指针R遍历完整个字符串s。
  7. 最后将结果字符数组res转换为字符串并返回。

十一  数组中两个字符串的最小距离

【题目】给定一个字符串数组strs,再给定两个字符串str1和str2,返回在strs中str1与str2的最小距离,如果str1或str2为null,或不在strs中,返回-1。

public int minDistance(String[] strs, String str1, String str2) {
    // 检查输入的字符串是否为空
    if (str1 == null || str2 == null) {
        return -1;
    }
    // 如果两个字符串相同,则它们的距离为0
    if (str1.equals(str2)) {
        return 0;
    }
    
    // 初始化两个字符串在数组中的最后出现位置以及最小距离
    int last1 = -1; // str1的最后出现位置
    int last2 = -1; // str2的最后出现位置
    int min = Integer.MAX_VALUE; // 最小距离,初始设为最大整数

    // 遍历数组中的每个字符串
    for (int i = 0; i != strs.length; i++) {
        // 如果当前字符串等于str1
        if (strs[i].equals(str1)) {
            // 更新最小距离为当前位置与str2的最后出现位置之间的距离
            min = Math.min(min, last2 == -1 ? min : i - last2);
            // 更新str1的最后出现位置为当前位置
            last1 = i;
        }
        // 如果当前字符串等于str2
        if (strs[i].equals(str2)) {
            // 更新最小距离为当前位置与str1的最后出现位置之间的距离
            min = Math.min(min, last1 == -1 ? min : i - last1);
            // 更新str2的最后出现位置为当前位置
            last2 = i;
        }
    }
    
    // 如果最小距离没有被更新过,说明str1或str2至少有一个没有在数组中出现过,返回-1
    return min == Integer.MAX_VALUE ? -1 : min;
}
  1. 首先检查输入的字符串是否为空,若为空,则直接返回-1。
  2. 如果两个字符串相同,则它们的距离为0,直接返回0。
  3. 初始化两个字符串在数组中的最后出现位置为-1,以及最小距离为最大整数。
  4. 遍历数组中的每个字符串:
    • 如果当前字符串等于str1,则更新最小距离为当前位置与str2的最后出现位置之间的距离,并更新str1的最后出现位置为当前位置。
    • 如果当前字符串等于str2,则更新最小距离为当前位置与str1的最后出现位置之间的距离,并更新str2的最后出现位置为当前位置。
  5. 如果最小距离没有被更新过,说明str1或str2至少有一个没有在数组中出现过,返回-1;否则,返回最小距离。

进阶问题。
 
 * 其实是通过数组strs先生成某种记录,在查询时通过记录进行查询,本文提供了一种记录的结构供读者参考,如果strs的长度为N,
 * 那么生成记录的时间复杂度为O(N^2),记录的空间复杂度为O(N^2),在生成记录之后,单次查询操作的时间复杂度可降为O(1)。本
 * 文实现的记录其实是一个哈希表HashMap<String,HashMap<String,Integer>>,这是一个key为String类型、value为哈希表
 * 类型的哈希表。为了描述清楚,我们把这个哈希表叫作外哈希表,把value代表的哈希表叫作内哈希表。外哈希表的key代表strs中的
 * 某种字符串,key所对应的内哈希表表示其他字符串到key字符串的最小距离。
 * 如果生成了这种结构的记录,那么查询str1和str2的最小距离时只用两次哈希查询操作就可以完成。
 *
 * 如下代码的TwoStringsMinDistanceInArray2类就是这种记录结构的具体实现,建立记录过程就是
 * TwoStringsMinDistanceInArray2类的构造函数,TwoStringsMinDistanceInArray2类中的minDistance2方法就是做单 次查询的方法。

import java.util.HashMap;
import java.util.Map;

public class TwoStringsMinDistanceInArray2 {

    // 记录字符串之间的最小距离
    private final HashMap<String, HashMap<String, Integer>> record;

    // 构造函数,预处理字符串数组,记录每个字符串之间的最小距离
    public TwoStringsMinDistanceInArray2(String[] strArr) {
        record = new HashMap<>();
        // 用于记录每个字符串最近出现的索引位置
        HashMap<String, Integer> indexMap = new HashMap<>();
        // 遍历字符串数组
        for (int i = 0; i != strArr.length; i++) {
            String curStr = strArr[i];
            // 更新索引map和记录map
            update(indexMap, curStr, i);
            // 更新当前字符串最近出现的索引位置
            indexMap.put(curStr, i);
        }
    }

    // 更新记录map,计算新添加的字符串与其他字符串之间的最小距离
    private void update(HashMap<String, Integer> indexMap, String str, int i) {
        // 如果记录map中不包含当前字符串,则添加进去
        if (!record.containsKey(str)) {
            record.put(str, new HashMap<>());
        }
        // 获取当前字符串对应的记录map
        HashMap<String, Integer> strMap = record.get(str);
        // 遍历之前的索引map中的每个键值对
        for (Map.Entry<String, Integer> lastEntry : indexMap.entrySet()) {
            String key = lastEntry.getKey();
            int index = lastEntry.getValue();
            // 排除与当前字符串相同的键,计算与其他字符串之间的距离
            if (!key.equals(str)) {
                // 获取上一个字符串对应的记录map
                HashMap<String, Integer> lastMap = record.get(key);
                // 计算当前字符串与上一个字符串之间的距离
                int curMin = i - index;
                // 如果当前字符串与上一个字符串之间的距离更小,则更新记录map
                if (strMap.containsKey(key)) {
                    int preMin = strMap.get(key);
                    if (curMin < preMin) {
                        strMap.put(key, curMin);
                        lastMap.put(str, curMin);
                    }
                } else {
                    strMap.put(key, curMin);
                    lastMap.put(str, curMin);
                }
            }
        }
    }

    // 查询两个字符串之间的最小距离
    public int minDistance2(String str1, String str2) {
        // 如果其中一个字符串为null,则返回-1
        if (str1 == null || str2 == null) {
            return -1;
        }
        // 如果两个字符串相同,则距离为0
        if (str1.equals(str2)) {
            return 0;
        }
        // 如果记录map中包含两个字符串,则返回它们之间的最小距离,否则返回-1
        if (record.containsKey(str1) && record.get(str1).containsKey(str2)) {
            return record.get(str1).get(str2);
        }
        return -1;
    }

    public static void main(String[] args) {
        String[] strs = {"1", "3", "3", "3", "2", "3", "1"};
        String str1 = "1";
        String str2 = "2";
        TwoStringsMinDistanceInArray2 record = new TwoStringsMinDistanceInArray2(strs);
        System.out.printf("The min distance is: %d", record.minDistance2(str1, str2));
    }

}

十二 字符串的调整与替换


十三   添加最少字符使字符串整体都是回文字符串


 * 【题目】
 * 给定一个字符串str,如果可以在str的任意位置添加字符,请返回在添加字符最少的情况下,让str整体都是回文字符串的一种结果。
 *
 * 【进阶题目】
 * 给定一个字符串str,再给定str的最长回文子序列字符串strlps,请返回在添加字符最少的情况下,让str整体都是回文字符串的一
 * 种结果。进阶问题比原问题多了一个参数,请做到时问复杂度比原问题的实现低。
 *
 * 【难度】
 * 困难
 *
 * 【解答】
 * 原问题。
 *
 * 在求解原问题之前,我们先来解决下面这个问题,如果可以在str的任意位置添加字符,最少需要添几个字符可以让str整体都是回文
 * 字符串。这个问题可以用动态规划的方法求解。如果str的长度为N,动态规划表是一个NxN的矩阵记为dp[][]。dp[i][j]值的含义
 * 代表子串str[i..j]最少添加几个字符可以使str[i..j]整体都是回文串。那么,如何求dp[i][j]的值呢?有如下三种情况:
 *
 * 1.如果字符串str[i..j]只有一个字符,此时dp[i][j]=0,这是很明显的,如果str[i..j]只有一个字符,那么str[i..j]已经是
 * 回文串了,自然不必添加任何宇符。
 * 2.如果字符串str[i..j]只有两个字符。如果两个字符相等,那么dp[i][j]=0。如果两个字符不相等,那么dp[i][j]=1。
 * 3.如果字符串str[i..j]多于两个字符。如果str[i]==str[j],那么dp[i][j]=dp[i+1][j-1]。如果str[i]!=str[j],要
 * 让str[i..j]整体变为回文串有两种方法,一种方法是让str[i..j-1]先变成回文串,然后在左边加上字符str[j],就是
 * str[i..j]整体变成回文串的结果。另一种方法是让str[i+1..j]先变成回文串,然后在右边加上字符str[i],就是str[i..j]
 * 整体变成回文串的结果。两种方法中哪个代价最小就选择哪个,即dp[i][j]=min{dp[i][j-1],dp[i+1][j]}+1。
 *
 * 既然dp[i][j]值代表子串str[i..j]最少添加几个字符可以使str[i..j]整体都是回文串,所以根据上面的方法求出整个dp矩阵之
 * 后,我们就得到了str中任何一个子串添加几个字符后可以变成回文串。具体请参看如下代码中的getDP方法。
 *
 * 下面介绍如何根据dp短阵,求在添加字符最少的情况下,让str整体都是回文字符串的一种结果。首先,dp[0][N-1]的值代表整个字
 * 符串最少需要添加几个字符,所以,如果最后的结果记为字符串res,res的长度=dp[0][N-1]+str的长度,然后依次设置res左右
 * 两头的字符。具体过程如下:
 *
 * 1.如果str[i..j]中str[i]==str[j],那么str[i..j]变成回文串的最终结果=str[i]+str[i+1..j-1]变成回文串的结果
 * +str[j],此时res左右两头的字符为str[i](也是str[j]),然后继续根据str[i+1..j-1]和矩阵dp来设置res的中间部分。
 * 2.如果str[i..j]中str[i]!=str[j],看dp[i][j-1]和dp[i+1][j]哪个小。如果dp[i][j-1]更小,那么str[i..j]变成
 * 回文串的最终结果=str[j]+str[i..j-1]变成回文串的结果+str[j],所以此时res左右两头的字符为str[j],然后继续根据
 * str[i..j-1]和矩阵dp来设置res的中间部分。而如果dp[i+1][j]更小,那么str[i..j]变成回文串的最终结果=
 * str[i]+str[i+1..j]变成回文串的结果+str[i],所以此时res左右两头的字符为str[i],然后继续根据str[i+1..j]和矩阵
 * dp来设置res的中间部分。如果一样大,任选一种设置方式都可以得出最终结果。
 * 3.如果发现res所有的位置都已设置完毕,过程结束。
 *
 * 原问题解法的全部过程请参看如下代码中的getPalindrome1方法。
 *
 * 求解dp矩阵的时间复杂度为O(N^2),根据str和dp矩阵求解最终结果的过程为O(N),所以原问题解法中总的时间复杂度为O(N^2)。

public class AddMinCharsMakePalindrome1 {
 
    /**
     * 动态规划求解回文字符串最少添加字符数的二维数组
     * @param str 给定的字符串
     * @return 回文字符串最少添加字符数的二维数组
     */
    public static int[][] getDP(char[] str) {
        // 定义二维数组dp,dp[i][j]表示将str[i...j]变成回文字符串的最少添加字符数
        int[][] dp = new int[str.length][str.length];
        // 遍历字符串
        for (int j = 1; j < str.length; j++) {
            // 初始化对角线上的元素,单个字符必然是回文字符串,所以对角线上的值为0
            dp[j - 1][j] = str[j - 1] == str[j] ? 0 : 1;
            // 遍历当前字符之前的字符
            for (int i = j - 2; i > -1; i--) {
                // 如果str[i]等于str[j],则不需要添加字符,直接取i+1到j-1的回文字符数即可
                if (str[i] == str[j]) {
                    dp[i][j] = dp[i + 1][j - 1];
                } else {
                    // 如果str[i]不等于str[j],则需要在i到j之间添加字符,选择添加字符数最小的一种情况
                    dp[i][j] = Math.min(dp[i + 1][j], dp[i][j - 1]) + 1;
                }
            }
        }
        return dp;
    }
 
    /**
     * 获取经过添加最少字符后的回文字符串
     * @param str 给定的字符串
     * @return 添加最少字符后的回文字符串
     */
    public static String getPalindrome1(String str) {
        // 若字符串为空或长度小于2,则本身已经是回文字符串
        if (str == null || str.length() < 2) {
            return str;
        }
        char[] chas = str.toCharArray();
        // 获取动态规划求解的二维数组
        int[][] dp = getDP(chas);
        // 创建新的字符数组,长度为原字符串长度加上最少添加字符数
        char[] res = new char[chas.length + dp[0][chas.length - 1]];
        int i = 0;
        int j = chas.length - 1;
        int resl = 0;
        int resr = res.length - 1;
        // 根据动态规划求解的结果构造回文字符串
        while (i <= j) {
            if (chas[i] == chas[j]) {
                res[resl++] = chas[i++];
                res[resr--] = chas[j--];
            } else if (dp[i][j - 1] < dp[i + 1][j]) {
                res[resl++] = chas[j];
                res[resr--] = chas[j--];
            } else {
                res[resl++] = chas[i];
                res[resr--] = chas[i++];
            }
        }
        return String.valueOf(res);
    }
 
    public static void main(String[] args) {
        // 示例字符串
        String str = "A1B21C";
        // 输出添加最少字符后的回文字符串
        System.out.printf("The palindrome is: %s", getPalindrome1(str));
    }
 
}

十四 括号字符串的有效性和最长有效长度

【题目】给定一个字符串str,判断是不是整体有效的括号字符串。

【举例】、

补充问题:给定一个括号字符串str,返回最长的有效括号子串。

【举例】str="(()())",返回6;str="())",返回2;str="()(()()(",返回4。

public class Solution {

    // 判断括号字符串是否有效
    public boolean isValid(String str) {
        if (str == null || str.equals("")) {
            return false;
        }
        char[] chas = str.toCharArray();
        int status = 0;
        for (int i = 0; i < chas.length; i++) {
            if (chas[i] != '(' && chas[i] != ')') {
                return false; // 遇到非括号字符,直接返回无效
            }
            if (chas[i] == ')' && --status < 0) {
                return false; // 遇到右括号时status小于0,说明不匹配,直接返回无效
            }
            if (chas[i] == '(') {
                status++;
            }
        }
        return status == 0; // status为0则括号完全匹配,有效
    }

    // 返回最长有效括号子串长度
    public int maxLength(String str) {
        if (str == null || str.equals("")) {
            return 0;
        }
        char[] chas = str.toCharArray();
        int[] dp = new int[chas.length];
        int pre = 0;
        int res = 0;
        for (int i = 1; i < chas.length; i++) {
            if (chas[i] == ')') {
                pre = i - dp[i - 1] - 1;
                if (pre >= 0 && chas[pre] == '(') {
                    dp[i] = dp[i - 1] + 2 + (pre > 0 ? dp[pre - 1] : 0); // 更新dp数组
                }
            }
            res = Math.max(res, dp[i]); // 更新结果
        }
        return res;
    }
}

解题思路:

  1. 判断括号字符串有效性:遍历字符串,使用状态变量 status 维护左括号的数量,遇到右括号时递减并判断是否匹配。
  2. 返回最长有效括号子串长度:使用动态规划,定义 dp 数组表示以当前字符结尾的最长有效括号子串长度,遍历字符串更新 dp 数组并维护最大长度。

十五  公式字符串求值

【题目】给定一个字符串 str,str 表示一个公式,公式里可能有整数、加减乘除符号和左右括号,返回公式的计算结果。

【举例】

【说明】1.可以认为给定的字符串一定是正确的公式,即不需要对str做公式有效性检查。

2.如果是负数,就需要用括号括起来,比如"4*(-3)"。但如果负数作为公式的开头或括号部分的开头,则可以没有括号,比如"-3*4"和"(-3*4)"都是合法的。

3.不用考虑计算过程中会发生溢出的情况。

import java.util.Deque;
import java.util.LinkedList;

public class Solution {

    // 对外提供的计算入口
    public int getValue(String exp) {
        // 将字符串转换为字符数组,并调用 value 方法处理表达式
        return value(exp.toCharArray(), 0)[0];
    }

    // 递归处理表达式字符串
    public int[] value(char[] chars, int i) {
        // 使用 Deque 存储数字和运算符
        Deque<String> deq = new LinkedList<>();
        // pre 用于存储当前数字
        int pre = 0;
        int[] bra = null; // 用于存储括号内的计算结果

        // 遍历字符数组
        while (i < chars.length && chars[i] != ')') {
            // 处理数字
            if (chars[i] >= '0' && chars[i] <= '9') {
                pre = pre * 10 + chars[i++] - '0';
            }
            // 处理运算符
            else if (chars[i] != '(') {
                addNum(deq, pre); // 将之前的数字加入 Deque
                deq.addLast(String.valueOf(chars[i++])); // 将运算符加入 Deque
                pre = 0; // 重置 pre
            }
            // 处理括号
            else {
                // 递归处理括号内的表达式
                bra = value(chars, i + 1);
                pre = bra[0]; // 更新 pre 为括号内计算结果
                i = bra[1] + 1; // 更新索引位置
            }
        }

        addNum(deq, pre); // 将最后的数字加入 Deque
        // 返回结果和下一个处理的索引位置
        return new int[] { getNum(deq), i };
    }

    // 处理数字的运算逻辑
    private void addNum(Deque<String> deq, int num) {
        if (!deq.isEmpty()) {
            int cur = 0;
            String top = deq.pollLast();
            if (top.equals("+") || top.equals("-")) {
                deq.addLast(top); // 将运算符放回 Deque
            } else {
                cur = Integer.valueOf(deq.pollLast());
                num = top.equals("*") ? (cur * num) : (cur / num);
            }
        }
        deq.addLast(String.valueOf(num)); // 将运算后的数字放入 Deque
    }

    // 计算 Deque 中的结果,实现加减运算
    private int getNum(Deque<String> deq) {
        int res = 0;
        boolean add = true;
        String cur = null;
        int num = 0;

        // 循环计算 Deque 中的运算结果
        while (!deq.isEmpty()) {
            cur = deq.pollFirst();
            if (cur.equals("+")) {
                add = true;
            } else if (cur.equals("-")) {
                add = false;
            } else {
                num = Integer.valueOf(cur);
                res += add ? num : (-num);
            }
        }
        return res;
    }

    public static void main(String[] args) {
        Solution solution = new Solution();
        String exp = "3*2+(4-2)*5";
        int result = solution.getValue(exp);
        System.out.println("计算结果是:" + result);
    }
}

这段代码实现了对包含加减乘除运算和括号的表达式字符串进行计算的功能。下面我会详细解释每个方法的作用和实现逻辑,以及整体的解题思路。

### 解题思路
1. 递归处理括号:遇到左括号时,递归调用 value 方法处理括号内的表达式,并将括号内表达式的计算结果返回。
2. 对不含括号的表达式进行计算:逐个处理字符数组,遇到数字则累积数字,遇到运算符则保存之前的数字和运算符,最后进行相应运算。

### 方法解释
1. **getValue(String exp)**:对外提供的计算入口,将字符串转换为字符数组,并调用 value 方法处理表达式。
   
2. **value(char[] chars, int i)**:递归处理不含括号的表达式字符串,返回计算结果和下一个处理的索引位置。
   - 使用 Deque 存储数字和运算符,pre 存储当前数字。
   - 遍历字符数组,遇到数字则累积数字,遇到运算符则保存之前的数字和运算符到 Deque。
   - 遇到左括号时,递归处理括号内表达式,并将结果赋给 pre,更新索引位置。
   - 遇到右括号或字符串末尾时,将最后的数字加入 Deque,调用 getNum 方法计算最终结果。

3. **addNum(Deque<String> deq, int num)**:处理数字的运算逻辑,将数字加入 Deque 进行运算。
   - 如果 Deque 不为空,取出 Deque 中的运算符和数字。
   - 根据运算符进行乘除运算,将运算后的结果放回 Deque。

4. **getNum(Deque<String> deq)**:计算 Deque 中的结果,实现加减运算。
   - 从 Deque 中取出数字和运算符,根据运算符进行加减运算,得到最终结果。

### 总结
该方法通过递归处理括号内表达式,再对不含括号的表达式进行计算,最终得到整体的计算结果。需要注意运算符的优先级和顺序,以正确计算表达式的值。希望以上详细的解释能帮助您理解这段代码的实现逻辑。如果您有任何问题或需要进一步解释,请随时告诉我。

十六  0左边必有1的二进制字符串数量

【题目】 给定一个整数N,求由"0"字符与"1"字符组成的长度为N的所有字符串中,满足"0"字符的左边必有"1"字符的字符串数量。

/**
 * 根据给定整数n,计算Fn数列的第n个元素,其中Fn数列定义如下:F(0) = 0, F(1) = 1,Fn = F(n-1) + F(n-2)(n >= 2)。
 * 本解法利用矩阵快速幂的方法计算Fn数列的第n个元素。
 *
 * @param n 给定整数n
 * @return Fn数列的第n个元素
 */
public int getNum3(int n) {
    if (n < 1) {
        return 0;
    }
    if (n == 1 || n == 2) {
        return n;
    }

    int[][] base = {{1, 1}, {1, 0}};
    int[][] res = matrixPower(base, n - 2);

    return 2 * res[0][0] + res[1][0];
}

/**
 * 计算矩阵m的p次幂
 *
 * @param m 矩阵
 * @param p 幂
 * @return 矩阵m的p次幂
 */
private int[][] matrixPower(int[][] m, int p) {
    int[][] res = new int[m.length][m[0].length];
    for (int i = 0; i < res.length; i++) {
        res[i][i] = 1;
    }
    int[][] tmp = m;
    for (; p != 0; p >>= 1) {
        if ((p & 1) != 0) {
            res = multiplyMatrix(res, tmp);
        }
        tmp = multiplyMatrix(tmp, tmp);
    }
    return res;
}

/**
 * 两个矩阵相乘
 *
 * @param m1 矩阵1
 * @param m2 矩阵2
 * @return 矩阵相乘后的结果
 */
private int[][] multiplyMatrix(int[][] m1, int[][] m2) {
    int[][] res = new int[m1.length][m2[0].length];
    for (int i = 0; i < m1.length; i++) {
        for (int j = 0; j < m2[0].length; j++) {
            for (int k = 0; k < m2.length; k++) {
                res[i][j] += m1[i][k] * m2[k][j];
            }
        }
    }
    return res;
}

  1. 首先定义矩阵base为{{1, 1}, {1, 0}},根据斐波那契数列的定义 Fn = F(n-1) + F(n-2),矩阵base的幂次即可得到Fn和F(n-1)的关系。
  2. 利用矩阵快速幂的方法,实现方法matrixPower来计算矩阵m的p次幂,其中将p拆解为二进制形式,可节省计算量。
  3. 矩阵的乘法在方法multiplyMatrix中实现,按照矩阵乘法的规则进行相乘操作。
  4. 最终在getNum3方法中返回计算得到的Fn数列的第n个元素。

十七   拼接所有字符串产生字典顺序最小的大写字符串

【题目】 给定一个字符串类型的数组strs,请找到一种拼接顺序,使得将所有的宇符串拼接起来组成的大写字符串是所有可能性中字典顺序最 * 小的,并返回这个大写字符串。

/**
 * 拼接所有字符串产生字典顺序最小的大写字符串
 *
 * 解题思路:
 * 使用自定义的比较器 MyComparator 对字符串数组进行排序,按照字典顺序得到一个字典序最小的大写字符串。
 * 排序规则为:若str1+str2在字典序中应在str2+str1之前,则str1排在str2之前。
 *
 * @param strs 字符串数组
 * @return 字典顺序最小的大写字符串
 */
public String lowestString(String[] strs) {
    if (strs == null || strs.length == 0) {
        return "";
    }
    
    Arrays.sort(strs, new MyComparator()); // 使用自定义比较器对字符串数组进行排序
    
    StringBuilder sb = new StringBuilder();
    for (String str : strs) {
        sb.append(str);
    }

    return sb.toString().toUpperCase(); // 将拼接后的字符串转换为大写并返回
}

/**
 * 自定义比较器实现 Comparator 接口,定义字符串比较规则
 */
public class MyComparator implements Comparator<String> {
    @Override
    public int compare(String a, String b) {
        return (a + b).compareTo(b + a); // 将字符串a和b拼接后进行比较,返回比较结果
    }
}

十八  找到字符串的最长无重复字符子串

【题目】给定一个字符串str,返回str的最长无重复字符子串的长度。

【举例】str="abcd",返回4。str="aabcb",最长无重复字符子串为"abc",返回3。

【要求】如果str的长度为N,请实现时间复杂度为O(N)的方法。

/**
 * 查找字符串的最长无重复字符子串的长度
 *
 * 解题思路:
 * 使用滑动窗口算法,通过维护两个指针pre和cur,以及一个长度变量len来记录最长无重复字符子串的长度。
 * 使用一个长度为256的数组map来记录字符出现的最后位置。
 * 初始时将map数组所有元素初始化为-1,pre和cur初始化为-1,len初始化为0。
 * 遍历字符串中的字符,更新pre指向当前字符上次出现位置,更新cur为当前字符位置与pre之间的距离,更新len为当前最大无重复字符子串的长度。
 * 将字符最后出现位置更新到map数组中,继续遍历直到字符串结束。
 * 返回最长无重复字符子串的长度len。
 *
 * @param str 输入字符串
 * @return 最长无重复字符子串的长度
 */
public int maxUnique(String str) {
    if (str == null || str.equals("")) {
        return 0;
    }
    
    char[] chas = str.toCharArray();
    int[] map = new int[256]; // 用于记录字符出现的最后位置
    for (int i = 0; i < 256; i++) {
        map[i] = -1;
    }
    
    int len = 0; // 最长无重复字符子串的长度
    int pre = -1; // 前一个重复字符的位置
    int cur = 0; // 当前无重复字符子串的长度
    
    for (int i = 0; i < chas.length; i++) {
        // 更新pre指向当前字符上次出现位置
        pre = Math.max(pre, map[chas[i]]);
        // 更新cur为当前字符位置与pre之间的距离
        cur = i - pre;
        // 更新len为当前最大无重复字符子串的长度
        len = Math.max(len, cur);
        // 将字符最后出现位置更新到map数组中
        map[chas[i]] = i;
    }

    return len; // 返回最长无重复字符子串的长度
}

十九  找到指定的新类型字符

【题目】

新类型字符的定义如下:

1.新类型字符是长度为1或者2的字符串。2.表现形式可以仅是小写字母,例如,"e";也可以是大写字母+小写字母,例如,"Ab";还可以是大写字母+大写字母,例如,"DC"。现在给定一个字符串str,str一定是若干新类型字符正确组合的结果。比如"eaCCBi",由新类型字符"e"、"a"、"CC"和"Bi"拼成。再给定一个整数k,代表str中的位置。请返回被k位置指定的新类型字符。

举例

/**
 * 找到指定位置的新类型字符
 *
 * 解题思路:
 * 1. 首先对输入的参数进行必要的判断,如果字符串为空、指定位置不在合法范围内,则直接返回空字符串。
 * 2. 将输入的字符串转换为字符数组chas,初始化uNum为0用于记录大写字母数量。
 * 3. 从指定位置k向左依次遍历字符数组chas,统计连续大写字母的个数uNum,直到不是大写字母或到达字符串起始位置。
 * 4. 判断连续大写字母的数量uNum,若为奇数,则返回从k位置开始的两个字符;若当前位置为大写字母,则返回从当前位置开始的两个字符;否则返回当前位置的字符。
 *
 * @param s 输入字符串
 * @param k 指定位置k
 * @return 指定位置的新类型字符
 */

public String pointNewchar(String s, int k) {
    // 如果输入字符串为空或者指定位置不合法,直接返回空字符串
    if (s == null || s.equals("") || k < 0 || k >= s.length()) {
        return "";
    }
    
    char[] chas = s.toCharArray(); // 将输入字符串转换为字符数组
    int uNum = 0; // 记录大写字母的数量

    // 从指定位置k向左遍历,统计连续大写字母的数量
    for (int i = k - 1; i >=0; i--) {
        if (!Character.isUpperCase(chas[i])) {
            break; // 如果遇到非大写字母,跳出循环
        }
        uNum++;
    }

    // 判断连续大写字母的数量,奇数则返回k位置的两个字符,偶数则根据k位置字符类型返回对应长度的字符串
    if ((uNum & 1) == 1) { 
        return s.substring(k - 1, k + 1);
    }
    if (Character.isUpperCase(chas[k])) {
        return s.substring(k, k + 2);
    } else {
        return String.valueOf(chas[k]);
    }
}

二十 旋变字符串问题

二十一  最小包含子串的长度

【题目】给定字符串str1和str2,求str1的子串中含有str2所有字符的最小子串长度。

【举例】str1="abcde",str2="ac"。因为"abc"包含str2所有的字符,并且在满足这一条件的str1的所有子串中,"abc"是最短的,返回3。str1="12345",str2="344"。最小包含子串不存在,返回0。

public int minLength(String str1, String str2) {
    // 如果输入字符串为空或者str2长度大于str1,无法找到最小包含子串,返回0
    if (str1 == null || str2 == null || str1.length() < str2.length()) {
        return 0;
    }
    
    char[] chas1 = str1.toCharArray();
    char[] chas2 = str2.toCharArray();
    int[] map = new int[256]; // 用于记录str2中字符出现的次数,ASCII码范围为0-255
    
    // 初始化map,统计str2中每个字符出现的次数
    for (int i = 0; i < chas2.length; i++) {
        map[chas2[i]]++;
    }
    
    int left = 0; // 滑动窗口左指针
    int right = 0; // 滑动窗口右指针
    int match = chas2.length; // 匹配字符数量
    int minLen = Integer.MAX_VALUE; // 最小子串长度

    while (right != chas1.length) {
        map[chas1[right]]--;
        if (map[chas1[right]] >= 0) {
            match--;
        }
        if (match == 0) {
            // 当滑动窗口包含了str2中所有字符时,移动左指针缩小窗口
            while (map[chas1[left]] < 0) {
                map[chas1[left++]++;
            }
            minLen = Math.min(minLen, right - left + 1); // 更新最小子串长度
            match++;
            map[chas1[left++]++;
        }
        right++;
    }
    
    return minLen == Integer.MAX_VALUE ? 0 : minLen;
}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值