👋 欢迎阅读

🎯 欢迎来到「距离相等的条形码」题解之旅! 本文将带你从“重新排列条形码使相邻元素不同”这一排列约束问题出发,深入理解贪心 + 频率优先放置的经典策略,并掌握如何通过先放最多的元素,再隔空填充其余元素来高效构造合法排列。
在开始之前,建议你先:
-
了解题目背景:这是 LeetCode 1054 题,给定一个条形码数组,要求重新排列使得任意两个相邻的条形码不能相同,题目保证有解。本质上,这是一个频率调度问题——出现次数最多的元素是瓶颈,只要把它每隔一个位置放一个,其余元素填满剩余空位,就能保证不相邻。
-
明确学习目标:掌握贪心构造法——先用哈希表统计每个数字的出现频率,并找出频率最高的元素;然后优先将该元素放在偶数位(0, 2, 4, ...),以最大程度拉开距离;接着将剩余元素按任意顺序填充到剩下的偶数位和奇数位(偶数位填满后填奇数位)。
-
准备好环境:建议在本地 IDE 或 LeetCode 在线编辑器中打开代码,边看边运行,亲手验证示例(如
barcodes = [1,1,1,2,2,2]输出[2,1,2,1,2,1]等合法结果)。
本文将从问题转化、贪心构造策略(频率统计 + 隔位放置)、代码模拟到复杂度分析,层层递进。即使你对贪心构造还不熟悉,我们也会从“先把最多的隔开放,剩下的随便塞”这一直觉出发,让你轻松抓住核心思想——只要最多的那个不挨着自己,其他元素就更容易被隔开,整体排列就一定合法。现在,让我们一起重新排列条形码,让相同的元素都“保持距离”吧! 📦🔀
一、题目

二、做题思路
一、问题分析(前置分析)
给定一个条形码数组,要求重新排列,使得任意两个相邻的条形码不相等。题目保证存在答案。
核心观察:这个问题等价于 “间隔排列” —— 将出现次数最多的元素优先插入到偶数位(0, 2, 4, ...),能最大限度地将其隔开,避免相邻。
鸽巢原理保证有解:在长度为 n 的数组中,若某种元素出现次数为 maxCount,则要使其不相邻,必须满足 maxCount ≤ (n + 1) / 2。因为最极端的情况是将该元素放在所有偶数位(共 (n+1)/2 个),若超过这个数,则必然会有两个该元素相邻。题目保证有解,即该条件成立。
二、贪心策略(核心决策规则)
-
第一步:使用哈希表统计每个数字出现的频率,并找出 出现次数最多的数字
maxVal及其次数maxCount。 -
第二步:将
maxVal优先放置在结果数组的 偶数位(下标 0, 2, 4, ...),这样能最大程度地将其分隔开。 -
第三步:从哈希表中移除
maxVal,然后将其余数字 任意顺序 填充到剩余的空位中(继续按偶数位填充,若偶数位已满则转向奇数位)。 -
第四步:返回重排后的数组。
三、正确性说明(简单版本)
鸽巢原理指出:要避免相同元素相邻,出现次数最多的元素必须不超过 (n+1)/2。题目保证有解,因此该条件天然成立。
贪心策略的核心是 “先放最多的”:将出现次数最多的数字放在所有偶数位,能保证它们之间至少隔一个其他数字(因为偶数位之间至少相差 2)。如果偶数位不够放,则说明最大次数 ≤ (n+1)/2,偶数位足够容纳所有该数字。
其余数字无论放在剩余的任何位置,都不会与 maxVal 相邻(因为 maxVal 只占偶数位,剩余位置均为奇数位,相邻位置中不会出现两个 maxVal)。因此这种放置方式必然能构造出合法排列,且算法一定能得到正确答案。
四、实现细节(边界防护)
-
使用
unordered_map<int, int>统计频率,同时记录maxVal和maxCount。 -
结果数组
ret初始化为n个 0。 -
放置
maxVal:用index = 0开始,每次index += 2,循环maxCount次。 -
移除
maxVal后,遍历剩余元素,对于每个元素x和其次数y,循环y次:-
若
index >= n,则切换到奇数位:index = 1。 -
将
x放在ret[index],然后index += 2。
-
-
最后返回
ret。 -
时间复杂度 O(n),空间复杂度 O(n)。
五、返回值(目标映射)
返回重排后的数组 ret,即 满足相邻元素不相等的一种排列。
四、代码
class Solution
{
public:
vector<int> rearrangeBarcodes(vector<int>& barcodes)
{
// 1. 统计每个数字出现的频率,并找出出现次数最多的数字(maxVal)及其次数(maxCount)
// 贪心策略:将出现次数最多的数字优先放置,且每隔一个位置放一个,避免相邻。
unordered_map<int, int> hash;
int maxVal = 0, maxCount = 0;
for (auto x : barcodes)
{
hash[x]++;
if (maxCount < hash[x])
{
maxVal = x;
maxCount = hash[x];
}
}
int n = barcodes.size();
vector<int> ret(n); // 结果数组
// 2. 将出现次数最多的数字放在偶数位(0, 2, 4, ...)
// 这样能最大程度地将它们分隔开,避免相邻。
int index = 0;
for (int i = 0; i < maxCount; i++)
{
ret[index] = maxVal;
index += 2; // 每次跳到下一个偶数位
}
// 3. 从哈希表中移除已放置最多的数字,然后将其余数字按任意顺序填充到剩余位置
hash.erase(maxVal);
// 遍历剩余的数字及其次数
for (auto& [x, y] : hash) // 结构化绑定(C++17),需要编译支持
{
for (int i = 0; i < y; i++)
{
// 如果当前偶数位已经填满(index >= n),则切换到奇数位(从1开始)
if (index >= n)
{
index = 1;
}
ret[index] = x;
index += 2; // 继续跳到下一个偶数位(或奇数位)
}
}
// 4. 返回重排后的数组(保证相同数字不相邻)
return ret;
}
};
五、流程图
六、正确性说明(详细版)
步骤 1:符号与问题建模
+----------------------------------------------------+
| 输入:条形码数组 barcodes,长度 n |
| 目标:重新排列,使任意相邻两个元素不相同 |
| 保证存在解 |
+----------------------------------------------------+
|
v
+----------------------------------------------------+
| 贪心策略: |
| ① 统计每个数字出现频率,找到出现次数最多的数 x, |
| 记其次数为 maxCount。 |
| ② 将 x 优先放置在 **偶数位置**(0, 2, 4, ...), |
| 每隔一个位置放一个,以最大程度隔开它们。 |
| ③ 其余数字任意填充剩余位置(继续隔位填充)。 |
| 贪心实质:**出现次数最多的数是最难处理的**, |
| 优先安排它们并间隔放置,能保证后续填充不冲突。 |
| 鸽巢原理保证:因为 maxCount ≤ (n+1)/2, |
| 所以偶数位(共 (n+1)/2 个)足够容纳所有 x。 |
+----------------------------------------------------+
-
设数组长度为
n,某个数字的出现次数为cnt。 -
鸽巢原理:若某数字出现次数超过
(n+1)/2,则无论怎样排列,都会有两个相邻(因为 n 个位置最多能容纳(n+1)/2个不相邻的相同元素)。题目保证有解,因此所有数字的出现次数都不超过(n+1)/2,尤其是最大次数maxCount ≤ (n+1)/2。 -
贪心核心:将出现次数最多的数字先放在偶数索引上,因为偶数位数量恰好是
(n+1)/2个(即ceil(n/2)),足以容纳它们,且相邻位置(i 和 i+1)不可能同时是偶数,从而避免了相邻。
步骤 2:关键性质 —— 鸽巢原理与贪心优先性的正确性(反证法)
+----------------------------------------------------+
| 性质 1(鸽巢原理可行性):若 maxCount > (n+1)/2, |
| 则不可能存在解。反证:若有超过一半的元素相同, |
| 根据抽屉原理,n 个位置中最多只能容纳 (n+1)/2 |
| 个互不相邻的位置(即所有偶数位),超过这个数量 |
| 必然有至少两个相同元素相邻,矛盾。 |
| 因此题目保证有解 ⇒ maxCount ≤ (n+1)/2。 |
+----------------------------------------------------+
|
v
+----------------------------------------------------+
| 性质 2(优先放置最多的数):若存在一个解,则 |
| 出现次数最多的数 x 必然可以(且应当)优先放置。 |
| 证明:若 x 不优先放置,则其可能被挤压到连续位置, |
| 但 x 的数量最多,最“危险”,因此优先分配它们到 |
| 偶数位是安全的,且不会影响其他数字的放置。 |
| 反证:若优先放置 x 导致无法完成,则任何其他顺序 |
| 也不可能完成(因为 x 的数量没有减少),故优先 |
| 放置 x 是最优的。 |
+----------------------------------------------------+
|
v
+----------------------------------------------------+
| 性质 3(间隔放置的有效性):将 x 放在偶数位后, |
| 剩余位置(奇数和未使用的偶数)形成若干连续段, |
| 每个剩余数字的出现次数 ≤ 段长的一半(由鸽巢原理 |
| 的递推保证),因此递归填充仍可成功。 |
| 证明:每次填充后,剩余问题与原问题同构, |
| 贪心策略可继续应用。 |
+----------------------------------------------------+
详细论证:
-
性质1(鸽巢原理):在一个长度为
n的数组中,要避免相邻相同,相同元素最多可以出现在ceil(n/2) = (n+1)//2个位置(例如所有偶数索引)。若maxCount > (n+1)//2,则无论放在哪些位置,必然有两个落在相邻位置(因为互不相邻的位置数量不够)。题目保证有解,因此maxCount ≤ (n+1)//2。图片中的(n+1)/2正是这个上界。 -
性质2(优先放置最多的数):设
x是出现次数最多的数字。因为x的数量最多,它是“最难”处理的元素。若最优解存在,则把x放在偶数位一定不会导致失败。用反证法:若把x放在偶数位后,剩余数字无法完成排列,则说明剩余空间中某个数字的出现次数超过了剩余位置的一半。但这是不可能的,因为原始数组满足鸽巢原理,且x占用了最分散的位置,剩余空间只会更宽松,因此原解也不存在,矛盾。所以优先放置x是安全的。 -
性质3(递归可行性):放置完
x后,剩余位置总数为n - maxCount,其中偶数位可能还有剩余(如果maxCount小于偶数位总数),奇数位全部空余。剩余每个数字的出现次数仍不超过对应剩余位置数的一半(由鸽巢原理的全局约束可推出),因此问题递归地满足同一性质。贪心可以继续选择下一个出现最多的数字,重复间隔填充。
步骤 3:归纳证明 —— 间隔填充过程得到全局有效排列
+----------------------------------------------------+
| 初始:数组为空,所有元素待填充。 |
| 归纳假设:假设前 t 次放置(每次放置一个数字的 |
| 所有副本到当前剩余的隔位位置)后,已填充部分满足 |
| 不相邻约束,且剩余未填充数字的次数 ≤ 剩余位置数 |
| 的一半(即鸽巢条件仍满足)。 |
+----------------------------------------------------+
|
v
+----------------------------------------------------+
| 第 t+1 次:选择剩余数字中出现次数最多的 y, |
| 从当前可用位置中**每隔一个位置放置一个**(即 |
| 从上一个填充位置后跳一个),共放置 cnt[y] 个。 |
| 由性质2和性质3,该操作不会导致相邻冲突, |
| 且剩余数字仍满足鸽巢条件,归纳假设保持。 |
+----------------------------------------------------+
|
v
+----------------------------------------------------+
| 当所有数字放置完毕,数组满足任意相邻不相等。 |
| 因为每一步都保持鸽巢条件,且贪心选择保证了 |
| 冲突不会发生,所以最终排列有效。 |
+----------------------------------------------------+
详细论证:
-
归纳基础:初始没有放置任何数字,已填充部分为空(无相邻冲突)。剩余数字的计数满足全局的鸽巢条件:
maxCount ≤ (n+1)//2。由性质1,这是保证有解的必要条件,因此归纳基础成立。 -
归纳步骤:假设已放置了部分数字,已填充位置互不相邻(每个数字都隔开),且剩余数字仍满足鸽巢条件(即剩余任何数字的出现次数 ≤ 剩余位置数的一半)。现在选择剩余数字中出现次数最多的
y(记次数为c),将其放置在剩余位置中从当前指针开始,每跳一个位置放一个(如代码中的index += 2)。因为c ≤ (剩余位置数 + 1)//2(由鸽巢条件),所以这些位置一定够用,且不会相邻(因为放置间隔为2)。放置后,y被完全消耗,剩余数字的总数减少c,剩余位置数减少c,且剩余数字仍满足鸽巢条件(因为原条件保证了最坏情况)。因此归纳假设保持。 -
终止:当所有数字都被放置,每个数字在放置时都遵循间隔规则,且放置后没有出现相邻相同的情况(因为放置时总是跳过已占用位置,不会紧挨着放)。因此最终排列满足要求。由于每次选择都是当前最“危险”的数字优先放置,且每次放置都保证了不相邻,算法必然成功。
🎯 闭幕
🎉 恭喜你完成了「距离相等的条形码」问题的学习!
为了巩固知识并进一步拓展,建议你:
🚀 动手实践
在 LeetCode 上提交代码,尝试不同的测试用例。
💡 深入思考
-
本题采用 贪心 + 频次统计 策略:先找出出现次数最多的数字,将其 隔一个位置(即放在偶数下标)放置,然后再放置其他数字。为什么这样能保证任意相邻两个条形码不相等? 如果出现次数最多的数字数量超过总数的一半,还会成立吗?(提示:题目保证有解,所以不会超过一半)
-
代码中先占满偶数位(
0, 2, 4, ...),如果偶数位不够,再转到奇数位(1, 3, 5, ...)。为什么先放偶数位,而不是奇数位? 如果数组长度为奇数,偶数位比奇数位多一个,这种选择有什么好处? -
填充剩余数字时,只需按任意顺序放入剩余的空位(继续隔一个位置)。如果剩余数字中也有多个相同的,会不会出现相邻相等的情况? 为什么不会?
📚 延伸挑战
-
如果数组中存在 三种以上 且频率都较高,间隔放置仍然有效吗?试着用
[1,1,2,2,3,3]验证代码是否正确输出一个合法排列。
如果你觉得本文对你有所帮助,欢迎:
👍 点赞 / 收藏
👤 关注作者,获取更多题解
💬 留言交流你的疑问或优化思路
祝你在 算法之路 上越走越稳,早日攻克每一道难题!下次见 🚀✨
—— 题解&spm=1001.2101.3001.5002&articleId=163153659&d=1&t=3&u=116f2e11430d4d98bb574b00170b0efb)
1027

被折叠的 条评论
为什么被折叠?



