文章目录
- 摘要
- 描述
- 解决方法
- 分析问题和解决代码
- 代码要点详解
- 示例测试和结果
- 时间复杂度
- 空间复杂度
- 总结
摘要
这道题的核心是:把字符串里的字符重新排一下顺序,让相同字符之间至少隔开 k
个位置。如果做不到,就返回空串。看上去像“排座位”,其实是一道标准的“贪心 + 堆 + 冷却队列”题。本文用 Swift 写出一份可直接运行的 Demo,顺便把实现细节和常见坑讲清楚。
描述
给定一个字符串 s
和整数 k
,要求把 s
里的字符重新排列,使得任意两个相同字符在新字符串中的位置至少相隔 k
。
- 如果存在这样的重排,返回重排后的字符串。
- 如果不存在,返回
""
。
直觉:频率高的字符更“难安置”,理应优先被安排;但又不能连续塞,必须“冷却”至少 k-1
个位置。于是我们需要:
- 一个**最大堆(优先队列)**按剩余次数挑选字符。
- 一个冷却队列存放刚刚用过的字符,等走过
k
步再重新投入堆。
解决方法
思路分三步:
-
统计每个字符的出现次数。
-
用最大堆按“剩余次数”挑一个当前最应该放的字符。
-
将每轮最多取
k
个不同字符依次放入结果,同时把它们的剩余次数减一并放入“等待区”。当这轮结束,再把等待区里还剩次数的字符放回堆,进入下一轮。- 如果某轮拿不到足够的不同字符,但还有字符没用完,说明无法满足间隔要求,直接返回
""
。
- 如果某轮拿不到足够的不同字符,但还有字符没用完,说明无法满足间隔要求,直接返回
这个分轮的方式,简单直观,也好写对。
分析问题和解决代码
下面是完整的 Swift 实现,包含一个通用的最大堆(优先队列)实现、解题函数,以及可运行的测试 Demo。
import Foundation// MARK: - Generic Max Heap
public struct PriorityQueue<Element> {private var elements: [Element] = []private let areSorted: (Element, Element) -> Bool // max-heap: return lhs > rhspublic init(sort: @escaping (Element, Element) -> Bool) {self.areSorted = sort}public var isEmpty: Bool { elements.isEmpty }public var count: Int { elements.count }public mutating func push(_ value: Element) {elements.append(value)siftUp(from: elements.count - 1)}public mutating func pop() -> Element? {guard !elements.isEmpty else { return nil }elements.swapAt(0, elements.count - 1)let item = elements.removeLast()siftDown(from: 0)return item}public func peek() -> Element? {elements.first}private mutating func siftUp(from index: Int) {var child = indexvar parent = (child - 1) / 2while child > 0 && areSorted(elements[child], elements[parent]) {elements.swapAt(child, parent)child = parentparent = (child - 1) / 2}}private mutating func siftDown(from index: Int) {var parent = indexwhile true {let left = parent * 2 + 1let right = left + 1var candidate = parentif left < elements.count && areSorted(elements[left], elements[candidate]) {candidate = left}if right < elements.count && areSorted(elements[right], elements[candidate]) {candidate = right}if candidate == parent { return }elements.swapAt(parent, candidate)parent = candidate}}
}// MARK: - Solution
class Solution {/// Rearrange string so that same chars are at least k distance apart./// - Returns: rearranged string or "" if impossiblefunc rearrangeString(_ s: String, _ k: Int) -> String {// 快速返回:k <= 1 不需要限制if k <= 1 { return s }if s.isEmpty { return "" }// 1) 统计频率var freq = [Character: Int]()for ch in s {freq[ch, default: 0] += 1}// 2) 最大堆,按剩余次数排序(次数相同可按字母排序,方便稳定)// 元组:(char, count)var maxHeap = PriorityQueue<(Character, Int)>(sort: { (a, b) inif a.1 == b.1 {return a.0 < b.0 // 次数相等时,字典序更小的优先(不必须,但稳定)}return a.1 > b.1})for (ch, c) in freq {maxHeap.push((ch, c))}// 3) 分轮放置,每轮最多放 k 个不同字符var result: [Character] = []// 临时存放本轮用过的字符(减1后如果还有余量,等轮末再压回堆)while !maxHeap.isEmpty {var usedInThisRound: [(Character, Int)] = []var take = min(k, s.count - result.count) // 剩余位数可能不足 kfor _ in 0..<take {guard let (ch, c) = maxHeap.pop() else {// 堆空但还没填满(说明还有待放字符被卡住),失败// 注意:只有当结果未完成且堆空时,才是失败return ""}result.append(ch)if c - 1 > 0 {usedInThisRound.append((ch, c - 1))}// 若已经填满,提前结束if result.count == s.count { break }}// 轮末:把本轮用过仍有余量的字符压回堆for item in usedInThisRound {maxHeap.push(item)}// 如果堆仍有元素,但是这一轮没凑够 k 个且结果还没填满,说明必须留空位才能满足距离要求,但字符串不允许空位 => 不可能if !maxHeap.isEmpty && result.count < s.count && usedInThisRound.count < k {// 这个判断等价:本轮拿的不同字符数 < k,但后面还有字符没安排// 说明“冷却距离”要求挡住了return ""}}return String(result)}
}// MARK: - Demo (Runnable)
func demo() {let sol = Solution()let cases: [(String, Int)] = [("aabbcc", 3), // 可行,典型示例("aaabc", 3), // 不可行,返回 ""("aaadbbcc", 2), // 可行("a", 2), // 只有一个字符,k>1 也可行(本身就满足)("", 2), // 空串("aa", 1), // k=1 总是原样即可("aa", 2), // 需要至少间隔 2,两个 a 无法满足 => ""]for (s, k) in cases {let ans = sol.rearrangeString(s, k)print("Input: s=\(s), k=\(k) -> Output: \(ans.isEmpty ? "\"\"" : ans)")}
}// Run demo
demo()
代码要点详解
-
最大堆设计
- 我们用泛型
PriorityQueue
,构造时传sort
比较函数,让它成为“最大堆”。 - 堆里的元素是
(Character, Int)
,其中Int
是剩余频次,按频次降序;为了稳定性,频次相同按字符字典序小的优先(不必须,仅让结果可预期一点)。
- 我们用泛型
-
分轮取 k 个不同字符
- 每一轮最多取
k
个不同字符,确保相同字符之间天然相隔至少k
。 - 本轮用过的字符,频次减一后暂存到
usedInThisRound
,等这一轮结束再放回堆,避免本轮再次被取到。
- 每一轮最多取
-
失败条件
- 如果堆提前空了但还没填满字符串,说明被“冷却”规则卡住了,不可行。
- 或者本轮没拿满
k
个(意味着后面位置距离不够了)且堆里还有字符,也是不可能的情况,返回空串。
-
为什么不是用“时间戳冷却”
- 另一种写法是给每个字符打“readyTime”,当
i >= readyTime
才能重新入堆。这也行,但分轮更直观:一轮拿k
个不同字符,本轮结束再回堆,自然保证间隔。
- 另一种写法是给每个字符打“readyTime”,当
示例测试和结果
运行上面的 demo()
,输出大致如下(具体顺序可能因为同频字符的 tie-breaker 有细微差别,但满足约束即可):
Input: s=aabbcc, k=3 -> Output: abcabc
Input: s=aaabc, k=3 -> Output: ""
Input: s=aaadbbcc, k=2 -> Output: abacabcd
Input: s=a, k=2 -> Output: a
Input: s=, k=2 -> Output: ""
Input: s=aa, k=1 -> Output: aa
Input: s=aa, k=2 -> Output: ""
简单解释:
aabbcc
、k=3
:可以按abcabc
摆,符合任意同字母之间至少隔 3。aaabc
、k=3
:a
太多,中间插不进足够的“垫片”,失败。aaadbbcc
、k=2
:可以,比如abacabcd
就行。- 单字符或
k=1
都是“没有硬性间隔限制”,基本能过。
时间复杂度
- 设字符串长度为
n
,不同字符种类数为σ
(最多 26 个小写,或 128/256 ASCII)。 - 每个字符都会被插入/弹出堆多次(与它的频次成正比),堆操作是
O(log σ)
。 - 总体复杂度:O(n log σ)。在英文小写场景基本可视为 O(n)。
空间复杂度
- 频率表
O(σ)
,堆O(σ)
,结果O(n)
。 - 额外空间:O(n + σ),在字符集有限时接近 O(n)。
总结
这题看似是“重排字符串”,其实就是优先安排最难放的字符,再用冷却队列保证间隔。实现上:
- 用最大堆挑剩余次数最多的字符;
- 分轮放置,每轮最多
k
个不同字符,轮末再把有余量的字符放回堆; - 如果某轮拿不到
k
个,但还有字符没放完,说明不可能。
这种模式在很多“调度/任务安排”的题里都能复用,比如 CPU 任务调度、订单/工位分配等。写成 Swift 后,借助一个干净的优先队列模板,整件事就顺了。欢迎把这份 Demo 拿去直接跑,也可以按你业务里的字符集和限制微调。