Article / 文章

LeetCode 第267题:回文排列 II

给定一个字符串 s,返回其通过重新排列组合后所有可能的回文字符串,并去除重复的组合。 如不能形成任何回文排列时,则返回一个空列表。

📖 文章摘要

本文详细解析LeetCode第267题“回文排列 II”,这是一道回溯算法的中等难度问题。文章提供了从基础回文判断到高效回溯生成的完整解法思路,包含多种语言实现,配有详细的回溯过程图解和去重技巧分析。适合想要深入理解回溯算法和字符串处理的算法学习者。

核心知识点: 回溯算法、字符串处理、全排列生成、去重技巧
难度等级: 中等
推荐人群: 回溯算法学习者、字符串处理爱好者

题目描述

给定一个字符串 s,返回其通过重新排列组合后所有可能的回文字符串,并去除重复的组合。

如不能形成任何回文排列时,则返回一个空列表。

示例

示例 1:

输入: "aabb"
输出: ["abba", "baab"]

示例 2:

输入: "abc"
输出: []

提示

  • 字符串长度不会超过16。
  • 字符串仅包含小写英文字母。

解题思路

这道题是 LeetCode第266题:回文排列 的扩展,不仅需要判断字符串能否重排为回文串,还需要列出所有可能的回文排列。

核心分析

回文串的特性

  1. 偶数长度:所有字符都必须出现偶数次
  2. 奇数长度:最多只能有一个字符出现奇数次

解题策略

  1. 先判断能否形成回文排列
  2. 利用回文串的对称性,只需生成前半部分
  3. 使用回溯算法生成所有可能的前半部分排列
  4. 对每个前半部分构造完整的回文串

算法步骤

步骤1:预处理和可行性检查

  1. 统计每个字符出现的次数
  2. 确定是否可以形成回文排列(最多一个字符出现奇数次)
  3. 提取中间字符(如果有)和前半部分字符

步骤2:回溯生成排列

  1. 对前半部分字符进行全排列
  2. 使用回溯算法,注意去重处理
  3. 对每个排列构建完整的回文串

复杂度分析

  • 时间复杂度:O((n/2)!),主要由全排列的生成决定
  • 空间复杂度:O(n + (n/2)!),包括存储结果和递归栈空间

图解思路

算法步骤分析表

步骤 输入示例 操作 结果 说明
预处理 “aabb” 统计字符频率 {a:2, b:2} 所有字符出现偶数次
可行性 {a:2, b:2} 检查奇数字符数 0 ≤ 1 可以形成回文排列
提取字符 {a:2, b:2} 构建前半部分 [‘a’, ‘b’] 每个字符取一半
回溯排列 [‘a’, ‘b’] 生成全排列 [“ab”, “ba”] 去重的全排列
构建回文 [“ab”, “ba”] 前半+后半 [“abba”, “baab”] 镜像构建完整回文

回溯过程分析表

以输入“aabb”为例的回溯树:

层级 当前路径 可选字符 操作 结果
0 [] [‘a’, ‘b’] 开始回溯 选择第一个字符
1 [‘a’] [‘b’] 选择’a’ 继续深入
2 [‘a’, ‘b’] [] 选择’b’ 生成“abba”
1 [‘b’] [‘a’] 回溯选择’b’ 继续深入
2 [‘b’, ‘a’] [] 选择’a’ 生成“baab”

代码实现

C# 实现

using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;

public class Solution {
    public IList<string> GeneratePalindromes(string s) {
        var result = new List<string>();
        if (string.IsNullOrEmpty(s)) return result;
        
        // 统计字符出现次数
        int[] count = new int[128];
        foreach (char c in s) {
            count[c]++;
        }
        
        // 检查是否可以形成回文
        int oddCount = 0;
        char oddChar = '\0';
        for (int i = 0; i < 128; i++) {
            if (count[i] % 2 != 0) {
                oddCount++;
                oddChar = (char)i;
            }
        }
        if (oddCount > 1) return result;
        
        // 构建字符列表(前半部分)
        var chars = new List<char>();
        for (int i = 0; i < 128; i++) {
            for (int j = 0; j < count[i] / 2; j++) {
                chars.Add((char)i);
            }
        }
        
        // 使用回溯生成所有可能的排列
        bool[] used = new bool[chars.Count];
        var sb = new StringBuilder();
        Backtrack(chars, used, sb, oddChar, result);
        
        return result;
    }
    
    private void Backtrack(List<char> chars, bool[] used, 
                          StringBuilder sb, char oddChar, List<string> result) {
        if (sb.Length == chars.Count) {
            // 构建完整的回文串
            string half = sb.ToString();
            string palindrome = half + (oddChar != '\0' ? oddChar.ToString() : "") + 
                              new string(half.Reverse().ToArray());
            result.Add(palindrome);
            return;
        }
        
        for (int i = 0; i < chars.Count; i++) {
            // 跳过重复字符(去重关键)
            if (used[i] || (i > 0 && chars[i] == chars[i-1] && !used[i-1])) {
                continue;
            }
            
            used[i] = true;
            sb.Append(chars[i]);
            Backtrack(chars, used, sb, oddChar, result);
            sb.Length--;
            used[i] = false;
        }
    }
}

Python 实现

class Solution:
    def generatePalindromes(self, s: str) -> List[str]:
        if not s:
            return []
            
        # 统计字符出现次数
        count = {}
        for c in s:
            count[c] = count.get(c, 0) + 1
            
        # 检查是否可以形成回文
        odd_chars = [c for c, v in count.items() if v % 2 != 0]
        if len(odd_chars) > 1:
            return []
            
        # 构建字符列表(前半部分)
        chars = []
        for c, v in count.items():
            chars.extend([c] * (v // 2))
        
        # 排序以便去重
        chars.sort()
            
        # 使用回溯生成所有可能的排列
        result = []
        used = [False] * len(chars)
        
        def backtrack(curr):
            if len(curr) == len(chars):
                # 构建完整的回文串
                half = ''.join(curr)
                palindrome = half + (odd_chars[0] if odd_chars else '') + half[::-1]
                result.append(palindrome)
                return
                
            for i in range(len(chars)):
                # 跳过重复字符(去重关键)
                if used[i] or (i > 0 and chars[i] == chars[i-1] and not used[i-1]):
                    continue
                    
                used[i] = True
                curr.append(chars[i])
                backtrack(curr)
                curr.pop()
                used[i] = False
                
        backtrack([])
        return result

C++ 实现

class Solution {
public:
    vector<string> generatePalindromes(string s) {
        vector<string> result;
        if (s.empty()) return result;
        
        // 统计字符出现次数
        vector<int> count(128, 0);
        for (char c : s) {
            count[c]++;
        }
        
        // 检查是否可以形成回文
        int oddCount = 0;
        char oddChar = 0;
        for (int i = 0; i < 128; i++) {
            if (count[i] % 2 != 0) {
                oddCount++;
                oddChar = (char)i;
            }
        }
        if (oddCount > 1) return result;
        
        // 构建字符列表(前半部分)
        vector<char> chars;
        for (int i = 0; i < 128; i++) {
            for (int j = 0; j < count[i] / 2; j++) {
                chars.push_back((char)i);
            }
        }
        
        // 使用回溯生成所有可能的排列
        vector<bool> used(chars.size(), false);
        string curr;
        backtrack(chars, used, curr, oddChar, result);
        
        return result;
    }
    
private:
    void backtrack(vector<char>& chars, vector<bool>& used, string& curr,
                  char oddChar, vector<string>& result) {
        if (curr.length() == chars.size()) {
            // 构建完整的回文串
            string half = curr;
            string palindrome = half + (oddChar ? string(1, oddChar) : "") + 
                              string(half.rbegin(), half.rend());
            result.push_back(palindrome);
            return;
        }
        
        for (int i = 0; i < chars.size(); i++) {
            // 跳过重复字符(去重关键)
            if (used[i] || (i > 0 && chars[i] == chars[i-1] && !used[i-1])) {
                continue;
            }
            
            used[i] = true;
            curr.push_back(chars[i]);
            backtrack(chars, used, curr, oddChar, result);
            curr.pop_back();
            used[i] = false;
        }
    }
};

执行结果

C# 实现

  • 执行用时:88 ms
  • 内存消耗:39.8 MB

Python 实现

  • 执行用时:56 ms
  • 内存消耗:16.4 MB

C++ 实现

  • 执行用时:4 ms
  • 内存消耗:8.2 MB

性能对比

语言 执行用时 内存消耗 特点
C++ 4 ms 8.2 MB 性能最佳,内存占用最小
Python 56 ms 16.4 MB 代码简洁,内存占用适中
C# 88 ms 39.8 MB 代码结构清晰,但性能相对较差

代码亮点

  1. 🎯 巧妙地将问题分为两部分:可行性判断 + 回溯生成,降低问题复杂度
  2. 💡 利用回文串的对称性,只需生成前半部分,大大减少计算量
  3. 🔍 使用经典的去重技巧:排序+跳过重复,确保结果无重复
  4. 🎨 回溯模板清晰,选择-递归-撤销的三步骤结构完整

常见错误分析

  1. 🚫 忘记检查字符串是否能形成回文排列,直接开始生成排列
  2. 🚫 没有正确处理出现奇数次的字符,导致回文构建错误
  3. 🚫 全排列过程中没有去重,导致结果中有重复的回文串
  4. 🚫 构建回文串时,前半部分和后半部分的镜像关系处理错误

解法对比

解法 时间复杂度 空间复杂度 优点 缺点
回溯算法 O((n/2)!) O(n + (n/2)!) 利用对称性优化,结果无重复 实现复杂度较高
暴力全排列 O(n!) O(n + n!) 思路直观 时间复杂度过高,需要额外去重
字符组合 O((n/2)!) O(n + (n/2)!) 直接生成,避免中间步骤 代码复杂,难以理解

相关题目


📖 系列导航

🔥 算法专题合集 - 查看完整合集

📢 关注合集更新:点击上方合集链接,关注获取最新题解!目前已更新第267题。


💬 互动交流

感谢大家耐心阅读到这里!希望这篇题解能够帮助你更好地理解和掌握这道算法题。

如果这篇文章对你有帮助,请:

  • 👍 点个赞,让更多人看到这篇文章
  • 📁 收藏文章,方便后续查阅复习
  • 🔔 关注作者,获取更多高质量算法题解
  • 💭 评论区留言,分享你的解题思路或提出疑问

你的支持是我持续分享的动力!

💡 一起进步:算法学习路上不孤单,欢迎一起交流学习!