744. 寻找比目标字母大的最小字母
问题描述
给你一个字符数组 letters
,该数组按非递减顺序排序,以及一个字符 target
。letters
里至少有两个不同的字符。
返回 letters
中大于 target
的最小字符。如果不存在这样的字符,则返回 letters
中的第一个字符。
注意:字母是循环的。例如,如果 target == 'z'
且 letters == ['a', 'b']
,则返回 'a'
。
示例:
输入: letters = ["c", "f", "j"], target = "a"
输出: "c"
输入: letters = ["c", "f", "j"], target = "c"
输出: "f"
输入: letters = ["c", "f", "j"], target = "d"
输出: "f"
输入: letters = ["c", "f", "j"], target = "g"
输出: "j"
输入: letters = ["c", "f", "j"], target = "j"
输出: "c"
解释: 循环到开头
算法思路
二分查找法:
- 由于数组已排序,使用二分查找寻找第一个大于
target
的字符 - 如果找到,返回该字符
- 如果未找到(即所有字符都 ≤ target),根据循环规则返回第一个字符
- 利用字符的ASCII值进行比较
核心思想:这是一个"寻找上界"的问题,可以使用二分查找在O(log n)时间内解决。
代码实现
方法一:二分查找(推荐解法)
class Solution {
/**
* 寻找比目标字母大的最小字母
*
* @param letters 按非递减顺序排序的字符数组
* @param target 目标字符
* @return 大于target的最小字符,循环到开头
*/
public char nextGreatestLetter(char[] letters, char target) {
// 输入校验
if (letters == null || letters.length == 0) {
return '\0';
}
int n = letters.length;
// 特殊情况:如果target大于等于最后一个字符
// 根据循环规则,返回第一个字符
if (target >= letters[n - 1]) {
return letters[0];
}
// 二分查找:寻找第一个大于target的字符
int left = 0;
int right = n - 1;
while (left < right) {
int mid = left + (right - left) / 2;
if (letters[mid] <= target) {
// mid位置的字符小于等于target
// 第一个大于target的字符在右半部分
left = mid + 1;
} else {
// mid位置的字符大于target
// 可能是答案,但需要检查更左边是否有更小的
right = mid;
}
}
// 循环结束时left == right
// 此时letters[left]就是第一个大于target的字符
return letters[left];
}
}
方法二:线性搜索
class Solution {
/**
* 线性搜索解法
* 时间复杂度O(n),用于理解问题
*
* @param letters 输入数组
* @param target 目标字符
* @return 结果字符
*/
public char nextGreatestLetter(char[] letters, char target) {
if (letters == null || letters.length == 0) {
return '\0';
}
int n = letters.length;
// 遍历数组寻找第一个大于target的字符
for (char letter : letters) {
if (letter > target) {
return letter;
}
}
// 如果没有找到,根据循环规则返回第一个字符
return letters[0];
}
}
方法三:优化的二分查找
class Solution {
/**
* 优化:处理边界情况
*
* @param letters 输入数组
* @param target 目标字符
* @return 结果字符
*/
public char nextGreatestLetter(char[] letters, char target) {
if (letters == null || letters.length == 0) {
return '\0';
}
int n = letters.length;
// 使用二分查找
int left = 0;
int right = n; // 注意:right初始化为n
while (left < right) {
int mid = left + (right - left) / 2;
if (letters[mid] <= target) {
left = mid + 1;
} else {
right = mid;
}
}
// 如果left == n,说明所有字符都 <= target
// 根据循环规则返回第一个字符
return letters[left % n];
}
}
算法分析
-
时间复杂度:
- 方法一(二分查找):O(log n)
- 方法二(线性搜索):O(n)
- 方法三(优化二分):O(log n)
-
空间复杂度:O(1)
- 只使用常数额外空间
-
方法对比:
- 方法一:效率最高,推荐使用
- 方法二:逻辑最直观,适合理解
- 方法三:代码更简洁,处理循环更优雅
算法过程
letters = ['c','f','j'], target = 'd'
:
- 初始:
left=0, right=2
- 第1轮:
mid = 0 + (2-0)/2 = 1
letters[1]='f' > 'd'
right = 1
- 第2轮:
left=0, right=1
mid = 0 + (1-0)/2 = 0
letters[0]='c' <= 'd'
left = 1
- 循环结束:
left == right == 1
- 返回:
letters[1] = 'f'
测试用例
public static void main(String[] args) {
Solution solution = new Solution();
// 测试用例1:目标在开头
char[] letters1 = {'c', 'f', 'j'};
System.out.println("Test 1: " + solution.nextGreatestLetter(letters1, 'a')); // 'c'
// 测试用例2:目标等于某个字符
System.out.println("Test 2: " + solution.nextGreatestLetter(letters1, 'c')); // 'f'
// 测试用例3:目标在中间
System.out.println("Test 3: " + solution.nextGreatestLetter(letters1, 'd')); // 'f'
// 测试用例4:目标接近末尾
System.out.println("Test 4: " + solution.nextGreatestLetter(letters1, 'g')); // 'j'
// 测试用例5:目标等于最后一个
System.out.println("Test 5: " + solution.nextGreatestLetter(letters1, 'j')); // 'c' (循环)
// 测试用例6:目标大于所有
System.out.println("Test 6: " + solution.nextGreatestLetter(letters1, 'k')); // 'c' (循环)
// 测试用例7:两个字符
char[] letters2 = {'a', 'b'};
System.out.println("Test 7: " + solution.nextGreatestLetter(letters2, 'z')); // 'a' (循环)
// 测试用例8:重复字符
char[] letters3 = {'a', 'a', 'b', 'b'};
System.out.println("Test 8: " + solution.nextGreatestLetter(letters3, 'b')); // 'a' (循环)
}
关键点
-
循环规则:
- 如果没有大于target的字符,返回第一个
- 这是题目特殊要求
-
二分查找边界:
- 使用
left < right
而不是left <= right
- 避免
死循环
- 使用
-
比较操作:
- 字符可以直接用
>
、<
比较 - 基于ASCII值
- 字符可以直接用
-
边界处理:
- target大于等于最后一个字符
- 数组长度为1的情况
常见问题
-
为什么用二分查找?
- 数组已排序,二分查找是最优解
- 时间复杂度从O(n)降到O(log n)
-
如何处理大小写?
- 算法同样适用
- ASCII值自然排序
-
能否用Arrays.binarySearch?
- 可以,但需要额外处理
-
最大数组长度?
- LeetCode测试用例中长度合理
-
如何返回索引?
- 修改返回值为索引