Q1、[中等] 山脉数组的峰值索引
1、题目描述
给定一个长度为 n
的整数 山脉 数组 arr
,其中的值递增到一个 峰值元素 然后递减。
返回峰值元素的下标。
你必须设计并实现时间复杂度为 O(log(n))
的解决方案。
示例 1:
输入:arr = [0,1,0] 输出:1
示例 2:
输入:arr = [0,2,1,0] 输出:1
示例 3:
输入:arr = [0,10,5,2] 输出:1
提示:
3 <= arr.length <= 105
0 <= arr[i] <= 106
- 题目数据 保证
arr
是一个山脉数组
2、解题思路
-
山脉数组特性:
- 存在一个峰值元素,其左侧所有元素严格递增,右侧所有元素严格递减。
- 峰值元素是数组中最大的元素。
-
二分查找应用:
- 由于数组是有序的(先增后减),可以利用二分查找来快速定位峰值。
- 比较中间元素与其相邻元素,判断当前处于上升段还是下降段:
- 如果
arr[mid] > arr[mid-1]
,说明处于上升段,峰值在右侧。 - 否则,处于下降段,峰值在左侧。
- 如果
-
边界处理:
- 初始化
left
和right
时,跳过首尾元素(它们不可能是峰值)。 - 循环条件确保找到唯一的峰值。
- 初始化
3、代码实现
C++
class Solution {
public:
int peakIndexInMountainArray(vector<int>& arr) {
int left = 1; // 左边界从第二个元素开始(第一个不可能是峰值)
int right = arr.size() - 2; // 右边界到倒数第二个元素(最后一个不可能是峰值)
while (left < right) {
// 计算中间位置,向上取整避免死循环
int mid = left + (right - left + 1) / 2;
if (arr[mid] > arr[mid - 1]) {
// 处于上升段,峰值在右侧
left = mid;
} else {
// 处于下降段,峰值在左侧
right = mid - 1;
}
}
// 最终 left 和 right 重合,即为峰值下标
return left;
}
};
Java
class Solution {
public int peakIndexInMountainArray(int[] arr) {
int left = 1;
int right = arr.length - 2;
while (left < right) {
int mid = left + (right - left + 1) / 2;
if (arr[mid] > arr[mid - 1]) {
left = mid;
} else {
right = mid - 1;
}
}
return left;
}
}
Python
class Solution:
def peakIndexInMountainArray(self, arr: List[int]) -> int:
left, right = 1, len(arr) - 2
while left < right:
mid = (left + right + 1) // 2 # 向上取整
if arr[mid] > arr[mid - 1]:
left = mid
else:
right = mid - 1
return left
4、复杂度分析
- 时间复杂度:O(log n),标准的二分查找,每次将搜索范围减半。
- 空间复杂度:O(1),只使用了常数级别的额外空间。
Q2、[中等] 车队
1、题目描述
在一条单行道上,有 n
辆车开往同一目的地。目的地是几英里以外的 target
。
给定两个整数数组 position
和 speed
,长度都是 n
,其中 position[i]
是第 i
辆车的位置, speed[i]
是第 i
辆车的速度(单位是英里/小时)。
一辆车永远不会超过前面的另一辆车,但它可以追上去,并以较慢车的速度在另一辆车旁边行驶。
车队 是指并排行驶的一辆或几辆汽车。车队的速度是车队中 最慢 的车的速度。
即便一辆车在 target
才赶上了一个车队,它们仍然会被视作是同一个车队。
返回到达目的地的车队数量 。
示例 1:
**输入:**target = 12, position = [10,8,0,5,3], speed = [2,4,1,1,3]
**输出:**3
解释:
- 从 10(速度为 2)和 8(速度为 4)开始的车会组成一个车队,它们在 12 相遇。车队在
target
形成。- 从 0(速度为 1)开始的车不会追上其它任何车,所以它自己是一个车队。
- 从 5(速度为 1) 和 3(速度为 3)开始的车组成一个车队,在 6 相遇。车队以速度 1 移动直到它到达
target
。示例 2:
**输入:**target = 10, position = [3], speed = [3]
**输出:**1
解释:
只有一辆车,因此只有一个车队。
示例 3:
**输入:**target = 100, position = [0,2,4], speed = [4,2,1]
**输出:**1
解释:
- 从 0(速度为 4) 和 2(速度为 2)开始的车组成一个车队,在 4 相遇。从 4 开始的车(速度为 1)移动到了 5。
- 然后,在 4(速度为 2)的车队和在 5(速度为 1)的车成为一个车队,在 6 相遇。车队以速度 1 移动直到它到达
target
。提示:
n == position.length == speed.length
1 <= n <= 105
0 < target <= 106
0 <= position[i] < target
position
中每个值都 不同0 < speed[i] <= 106
2、解题思路
-
问题分析:
- 车辆按位置和速度行驶,不能超车,只能追上并形成车队。
- 车队的速度是其最慢车辆的速度。
- 车辆按位置排序后,从后向前检查是否能形成车队。
-
关键观察:
- 如果一辆车到达目的地的时间比它前面的车短,它们不会形成车队。
- 否则,前面的车会被追上,形成车队,且车队的时间为当前车的到达时间。
-
算法选择:
-
计算每辆车到达目的地的时间。
-
按位置排序车辆(从近到远)。
-
从后向前遍历,比较相邻车辆的到达时间:
- 如果后车时间更短,则不能形成车队,车队数量加1。
- 否则,前车会被后车追上,更新前车的到达时间为后车的到达时间。
-
3、代码实现
C++
// 车辆类,记录位置和到达时间
class Car {
public:
int position;
double time;
Car(int p, double t) : position(p), time(t) {}
};
class Solution {
public:
int carFleet(int target, vector<int>& position, vector<int>& speed) {
int n = position.size();
if (n == 0) {
return 0;
}
vector<Car> cars;
// 计算每辆车到达目的地的时间
for (int i = 0; i < n; ++i) {
double time = static_cast<double>(target - position[i]) / speed[i];
cars.emplace_back(position[i], time);
}
// 按位置从近到远排序
sort(cars.begin(), cars.end(), [](const Car& a, const Car& b) {
return a.position < b.position;
});
int fleetCount = 0;
int i = n - 1; // 从最远的车开始检查
while (i > 0) {
if (cars[i].time < cars[i - 1].time) {
// 前车无法被追上,车队数量加1
fleetCount++;
} else {
// 前车会被追上,保持后车的到达时间
cars[i - 1].time = cars[i].time;
}
i--;
}
// 最后一辆车单独为一个车队
return fleetCount + 1;
}
};
Java
class Car {
int position;
double time;
Car(int p, double t) {
position = p;
time = t;
}
}
class Solution {
public int carFleet(int target, int[] position, int[] speed) {
int n = position.length;
if (n == 0)
return 0;
Car[] cars = new Car[n];
for (int i = 0; i < n; ++i) {
double time = (double) (target - position[i]) / speed[i];
cars[i] = new Car(position[i], time);
}
Arrays.sort(cars, (a, b) -> a.position - b.position);
int fleetCount = 0;
int i = n - 1;
while (i > 0) {
if (cars[i].time < cars[i - 1].time) {
fleetCount++;
} else {
cars[i - 1].time = cars[i].time;
}
i--;
}
return fleetCount + 1;
}
}
Python
class Solution:
def carFleet(self, target: int, position: List[int], speed: List[int]) -> int:
n = len(position)
if n == 0:
return 0
cars = []
for i in range(n):
time = (target - position[i]) / speed[i]
cars.append((position[i], time))
# 按位置排序
cars.sort()
fleet_count = 0
i = n - 1
while i > 0:
if cars[i][1] < cars[i - 1][1]:
fleet_count += 1
else:
cars[i - 1] = (cars[i - 1][0], cars[i][1])
i -= 1
return fleet_count + 1
4、复杂度分析
- 时间复杂度:O(n log n),主要是排序的时间复杂度。
- 空间复杂度:O(n),存储车辆信息所需空间。
Q3、[中等] 考场就坐
1、题目描述
在考场里,有 n
个座位排成一行,编号为 0
到 n - 1
。
当学生进入考场后,他必须坐在离最近的人最远的座位上。如果有多个这样的座位,他会坐在编号最小的座位上。(另外,如果考场里没有人,那么学生就坐在 0
号座位上。)
设计一个模拟所述考场的类。
实现 ExamRoom
类:
ExamRoom(int n)
用座位的数量n
初始化考场对象。int seat()
返回下一个学生将会入座的座位编号。void leave(int p)
指定坐在座位p
的学生将离开教室。保证座位p
上会有一位学生。
示例 1:
输入: ["ExamRoom", "seat", "seat", "seat", "seat", "leave", "seat"] [[10], [], [], [], [], [4], []] 输出: [null, 0, 9, 4, 2, null, 5] 解释: ExamRoom examRoom = new ExamRoom(10); examRoom.seat(); // 返回 0,房间里没有人,学生坐在 0 号座位。 examRoom.seat(); // 返回 9,学生最后坐在 9 号座位。 examRoom.seat(); // 返回 4,学生最后坐在 4 号座位。 examRoom.seat(); // 返回 2,学生最后坐在 2 号座位。 examRoom.leave(4); examRoom.seat(); // 返回 5,学生最后坐在 5 号座位。
提示:
1 <= n <= 109
- 保证有学生正坐在座位
p
上。seat
和leave
最多被调用104
次。
2、解题思路
-
数据结构选择:
- 有序集合
seats
:维护当前被占用的座位,便于快速查找相邻座位。 - 优先队列
pq
:存储所有可能的空位区间,按区间长度排序,最长的优先处理。
- 有序集合
-
座位选择策略:
- 新学生应坐的座位是现有区间中离最近的人最远的位置。
- 若区间
[a, b]
,最优座位是(a + b) // 2
,距离为(b - a) // 2
。 - 比较所有区间的可能座位,选择最优。
-
处理离开座位:
- 学生离开后,合并左右相邻的空位区间,并更新优先队列。
3、代码实现
C++
// 自定义比较器, 用于优先队列中的区间排序
// 优先选择长度较大的区间, 若长度相同则选择起始点较小的区间
struct Comp {
bool operator()(const pair<int, int>& p1, const pair<int, int>& p2) {
// 计算两个区间的半长度 (即距离)
int d1 = p1.second - p1.first;
int d2 = p2.second - p2.first;
// 比较半长度, 较大的优先: 若相同起始点小的优先
return d1 / 2 < d2 / 2 || (d1 / 2 == d2 / 2 && p1.first > p2.first);
}
};
class ExamRoom {
private:
int n; // 考场座位总数
set<int> seats; // 当前被占用的座位 (自动排序)
priority_queue<pair<int, int>, vector<pair<int, int>>, Comp> pq; // 可用区间优化队列
public:
ExamRoom(int n) : n(n) {}
// 安排下一个学生就坐
int seat() {
// 情况 1: 考场无人, 直接坐 0 号座位
if (seats.empty()) {
seats.insert(0);
return 0;
}
// 计算最左和最右的空位区间长度
int left = *seats.begin(); // 最左被占用座位到 0 的距离
int right = n - 1 - *seats.rbegin(); // 最右被占用座位到 n-1 的距离
// 检查优先队列中的区间是否有效
while (!pq.empty() && seats.size() >= 2) {
auto p = pq.top(); // 获取当前最优区间
// 验证区间是否未被分割且端点仍被占用
if (seats.count(p.first) && seats.count(p.second) &&
*next(seats.find(p.first)) == p.second) {
int d = p.second - p.first; // 区间长度
// 如果当前区间不是最优 (比左右边界短), 则退出循环
if (d / 2 < right || (d / 2 <= left && p.first != 0)) {
break;
}
// 分割区间并插入新座位
pq.pop();
int new_seat = p.first + d / 2; // 计算最优座位
pq.push({p.first, new_seat}); // 插入左半区间
pq.push({new_seat, p.second}); // 插入右半区间
seats.insert(new_seat); // 占用新座位
return new_seat;
}
// 无效区间,弹出队列
pq.pop();
}
// 处理最左或最右的区间(当队列中没有更优区间时)
if (right > left) { // 最右区间更优
pq.push({*seats.rbegin(), n - 1}); // 插入最右区间
seats.insert(n - 1); // 占用最右座位
return n - 1;
} else { // 最左区间更优
pq.push({0, *seats.begin()}); // 插入最左区间
seats.insert(0); // 占用最左座位
return 0;
}
}
// 学生离开座位p
void leave(int p) {
auto it = seats.find(p);
int left = -1, right = -1;
// 获取左右相邻座位(如果存在)
if (it != seats.begin()) {
left = *prev(it);
}
if (next(it) != seats.end()) {
right = *next(it);
}
seats.erase(p); // 移除该座位
// 如果左右都有相邻座位,则合并为新区间加入队列
if (left != -1 && right != -1) {
pq.push({left, right});
}
}
};
Java
class ExamRoom {
private int n; // 考场座位总数
private TreeSet<Integer> seats; // 当前被占用的座位 (自动排序)
private PriorityQueue<int[]> pq; // 可用区间优先队列
// 构造函数,初始化考场
public ExamRoom(int n) {
this.n = n;
seats = new TreeSet<>();
// 优先队列排序规则:区间长度大的优先,长度相同则起始点小的优先
pq = new PriorityQueue<>((a, b) -> {
int d1 = a[1] - a[0], d2 = b[1] - b[0];
if (d1 / 2 != d2 / 2) {
return d2 / 2 - d1 / 2; // 长度大的优先
} else {
return a[0] - b[0]; // 长度相同则起始点小的优先
}
});
}
// 安排下一个学生就座
public int seat() {
// 情况1:考场无人,直接坐0号座位
if (seats.isEmpty()) {
seats.add(0);
return 0;
}
// 计算最左和最右的空位区间长度
int left = seats.first(); // 最左被占用座位到0的距离
int right = n - 1 - seats.last(); // 最右被占用座位到n-1的距离
// 检查优先队列中的区间是否有效
while (!pq.isEmpty() && seats.size() >= 2) {
int[] p = pq.peek(); // 获取当前最优区间
// 验证区间是否未被分割且端点仍被占用
if (!seats.contains(p[0]) || !seats.contains(p[1]) || seats.higher(p[0]) != p[1]) {
pq.poll(); // 无效区间,弹出队列
continue;
}
int d = p[1] - p[0]; // 区间长度
// 如果当前区间不是最优(比左右边界短),则退出循环
if (d / 2 < right && (d / 2 <= left && p[0] != 0)) {
break;
}
// 分割区间并插入新座位
pq.poll(); // 移除当前区间
int newSeat = p[0] + d / 2; // 计算最优座位
pq.offer(new int[] { p[0], newSeat }); // 插入左半区间
pq.offer(new int[] { newSeat, p[1] }); // 插入右半区间
seats.add(newSeat); // 占用新座位
return newSeat;
}
// 处理最左或最右的区间(当队列中没有更优区间时)
if (right > left) { // 最右区间更优
pq.offer(new int[] { seats.last(), n - 1 }); // 插入最右区间
seats.add(n - 1); // 占用最右座位
return n - 1;
} else { // 最左区间更优
pq.offer(new int[] { 0, seats.first() }); // 插入最左区间
seats.add(0); // 占用最左座位
return 0;
}
}
// 学生离开座位p
public void leave(int p) {
// 获取左右相邻座位(如果存在)
Integer left = seats.lower(p);
Integer right = seats.higher(p);
seats.remove(p); // 移除该座位
// 如果左右都有相邻座位,则合并为新区间加入队列
if (left != null && right != null) {
pq.offer(new int[] { left, right });
}
}
}
4、复杂度分析
- 时间复杂度:
seat()
:平均 O(log n),最坏 O(n)。leave()
:平均 O(log n),最坏 O(n)。
- 空间复杂度:O(n),存储座位和区间信息。
Q4、[困难] 相似度为 K 的字符串
1、题目描述
对于某些非负整数 k
,如果交换 s1
中两个字母的位置恰好 k
次,能够使结果字符串等于 s2
,则认为字符串 s1
和 s2
的 相似度为 k
。
给你两个字母异位词 s1
和 s2
,返回 s1
和 s2
的相似度 k
的最小值。
示例 1:
输入:s1 = "ab", s2 = "ba" 输出:1
示例 2:
输入:s1 = "abc", s2 = "bca" 输出:2
提示:
1 <= s1.length <= 20
s2.length == s1.length
s1
和s2
只包含集合{'a', 'b', 'c', 'd', 'e', 'f'}
中的小写字母s2
是s1
的一个字母异位词
2、解题思路
这是一个典型的状态空间搜索问题,可以使用广度优先搜索(BFS)来解决。由于直接交换两个字符可以快速接近目标字符串,BFS 能够保证在最短的交换次数内找到解。
- 状态表示:每个状态是当前的字符串
cur
和下一个需要处理的字符位置pos
。 - 初始状态:
s1
和位置0
。 - 状态转移:对于当前字符串
cur
,从pos
开始,找到第一个cur[pos] != s2[pos]
的位置。然后,遍历pos
之后的字符,找到可以交换的字符cur[j] == s2[pos]
,交换cur[pos]
和cur[j]
,生成新状态。 - 终止条件:当前字符串
cur
等于s2
时,返回当前的交换次数(即 BFS 的层数)。 - 去重:使用哈希集合
visit
记录已经访问过的状态,避免重复处理。
3、代码实现
C++
class Solution {
public:
int kSimilarity(string s1, string s2) {
int n = s1.size();
queue<pair<string, int>> q; // 队列存储当前字符串和下一个处理位置
unordered_set<string> visit; // 记录已访问的字符串
q.emplace(s1, 0); // 初始状态:s1 和位置 0
visit.emplace(s1); // 标记 s1 为已访问
// step 表示当前的交换次数
for (int step = 0;; ++step) {
int size = q.size(); // 当前层的状态数
for (int i = 0; i < size; ++i) {
auto [cur, pos] = q.front(); // 取出队首状态
q.pop();
// 找到目标字符串,返回当前交换次数
if (cur == s2) {
return step;
}
// 跳过已经匹配的位置
while (pos < n && cur[pos] == s2[pos]) {
pos++;
}
// 从 pos + 1 开始寻找可以交换的字符
for (int j = pos + 1; j < n; ++j) {
// 找到可以交换的字符:cur[j] 需要等于 s2[pos] 且不等于 s2[j]
if (cur[j] != s2[j] && cur[j] == s2[pos]) {
swap(cur[pos], cur[j]); // 交换字符
// 如果新字符串未被访问
if (!visit.count(cur)) {
visit.emplace(cur); // 标记为已访问
q.emplace(cur, pos + 1); // 加入队列,处理下一个位置
}
swap(cur[pos], cur[j]); // 恢复字符,继续尝试其他交换
}
}
}
}
}
};
Java
class Solution {
public int kSimilarity(String s1, String s2) {
int n = s1.length();
Queue<Pair<String, Integer>> q = new LinkedList<>(); // 队列存储当前字符串和下一个处理位置
Set<String> visit = new HashSet<>(); // 记录已访问的字符串
q.offer(new Pair<>(s1, 0)); // 初始状态:s1 和位置 0
visit.add(s1); // 标记 s1 为已访问
// step 表示当前的交换次数
for (int step = 0;; ++step) {
int size = q.size(); // 当前层的状态数
for (int i = 0; i < size; ++i) {
Pair<String, Integer> p = q.poll();
String cur = p.getKey();
int pos = p.getValue();
// 找到目标字符串,返回当前交换次数
if (cur.equals(s2)) {
return step;
}
// 跳过已经匹配的位置
while (pos < n && cur.charAt(pos) == s2.charAt(pos)) {
pos++;
}
// 从 pos + 1 开始寻找可以交换的字符
for (int j = pos + 1; j < n; ++j) {
// 找到可以交换的字符:cur[j] 需要等于 s2[pos] 且不等于 s2[j]
if (cur.charAt(j) != s2.charAt(j) && cur.charAt(j) == s2.charAt(pos)) {
char[] arr = cur.toCharArray();
swap(arr, pos, j); // 交换字符
String next = new String(arr);
if (!visit.contains(next)) { // 如果新字符串未被访问
visit.add(next); // 标记为已访问
q.offer(new Pair<>(next, pos + 1)); // 加入队列,处理下一个位置
}
}
}
}
}
}
private void swap(char[] arr, int i, int j) {
char tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}
Python
class Solution:
def kSimilarity(self, s1: str, s2: str) -> int:
n = len(s1)
q = deque([(s1, 0)]) # 队列存储当前字符串和下一个处理位置
visit = {s1} # 记录已访问的字符串
step = 0
while q:
size = len(q) # 当前层的状态数
for _ in range(size):
cur, pos = q.popleft()
if cur == s2: # 找到目标字符串,返回当前交换次数
return step
# 跳过已经匹配的位置
while pos < n and cur[pos] == s2[pos]:
pos += 1
# 从 pos + 1 开始寻找可以交换的字符
for j in range(pos + 1, n):
# 找到可以交换的字符:cur[j] 需要等于 s2[pos] 且不等于 s2[j]
if cur[j] != s2[j] and cur[j] == s2[pos]:
# 交换字符
arr = list(cur)
arr[pos], arr[j] = arr[j], arr[pos]
next_str = "".join(arr)
if next_str not in visit: # 如果新字符串未被访问
visit.add(next_str) # 标记为已访问
q.append((next_str, pos + 1)) # 加入队列,处理下一个位置
step += 1
4、复杂度分析
- 时间复杂度:最坏情况下为 O(n^2),其中
n
是字符串长度。每个状态可能生成 O(n) 个新状态,最多有 O(n!) 种排列,但在实际问题中由于去重和剪枝,实际运行时间会远小于理论最坏情况。 - 空间复杂度:O(n!),用于存储访问过的字符串状态。但在实际问题中,由于去重和剪枝,实际空间消耗会远小于理论最坏情况。