题目描述

✅ 面试题 01.04. 回文排列

image-20260929004810983

题意分析

可以重新排列全部字符,判断是否存在正反相同的排列,不必构造结果,也不要求原字符串已经是回文。每个字符都必须保留,不能为了凑回文删掉多余字符。

解法:统计奇数频次的字符种数

核心思路

[!blue]

回文中关于中心对称的两个位置必须是同一字符,因此除中心外,每种字符都成对出现。只有奇数长度回文能在中心多放一个字符,所以出现次数为奇数的字符种类最多只能有一种。这是能够组成回文的必要条件。

这个条件也足够:把每种字符的一半放到左侧,另一半按相反顺序放到右侧;若有一种字符剩余一个,就放在中心。所有字符恰好用完,得到的排列一定是回文,因此无需尝试具体排列。

用哈希表统计每个码点的频次,再累加 v & 1。偶数频次贡献 0,奇数频次贡献 1,所以 sum 是奇数频次的种类数,返回 sum < 2 即可。总长度与奇数项数量同奇偶,这个统一条件也已经覆盖了偶数长度全成对、奇数长度只留一个中心的区别。

代码按码点计数:Java 用 codePointAt 读取并按 Character.charCount 前进,Go 用 range 得到 rune。同一补充平面字符不会被拆成两个 UTF-16 单元或多个字节;大小写、空格仍是各自不同的字符,不额外做转换或删除。

解题步骤

  1. Java 逐码点读取并按 charCount 推进,Go 用 range 遍历 rune。
  2. 记录每个码点的总次数。
  3. 累加每个频次的最低位,奇数项至多一个则返回 true。

代码实现

class Solution {
    public boolean canPermutePalindrome(String s) {
        Map<Integer, Integer> cnt = new HashMap<>();

        for (int i = 0; i < s.length(); ) {
            int c = s.codePointAt(i);

            cnt.merge(c, 1, Integer::sum);
            i += Character.charCount(c);
        }

        int sum = 0;

        for (int v : cnt.values()) {
            // v & 1 取频次的最低位,落单的字符贡献 1。
            sum += v & 1;
        }

        return sum < 2;
    }
}
func canPermutePalindrome(s string) bool {
    cnt := map[rune]int{}
    for _, c := range s {
        cnt[c]++
    }

    sum := 0
    for _, v := range cnt {
        // v & 1 取频次的最低位,落单的字符贡献 1。
        sum += v & 1
    }

    return sum < 2
}

复杂度分析

  • 时间复杂度:期望 $O(n)$,n 为输入码点数,统计频次后再扫描至多 n 个不同码点。
  • 空间复杂度:$O(u)$,u 为不同码点数,保存每种字符的频次。

关键点总结

[!green]

大小写和空格按各自码点计数,不自动删空格或转换大小写;按码点处理不拆开补充平面字符。

易错点总结

[!yellow]

  • 统计奇数频次的种类,不是奇数字符总个数。
  • 原字符串不必已经回文,只需能重排。
  • Java char 是 UTF-16 代码单元,不能把它直接当任意 Unicode 码点。

相似题目

题目 难度 关联与区别
409. 最长回文串 简单 同样利用成对字符,本题要求用完全部字符,原题允许舍弃多余奇数字符求最大长度。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/67894291
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!