[剑指-Offer] 38. 字符串的排列(全排列、递归、回溯、巧妙解法)

1. 题目来源

链接:字符串的排列
来源:LeetCode——《剑指-Offer》专项

2. 题目说明

输入一个字符串,打印出该字符串中字符的所有排列。

你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。

示例:

输入:s = “abc”
输出:[“abc”,“acb”,“bac”,“bca”,“cab”,“cba”]

限制:

  • 1 <= s 的长度 <= 8

3. 题目解析

方法一:STL next_permutation()+巧妙解法

下面对于 STL next_permutation() 的介绍转自:C++语言基础 —— STL —— 算法 —— 排列组合算法


首先要了解什么是 “下一个” 排列组合,什么是 “上一个” 排列组合。

假设有三个数字组成的序列:{a,b,c}

则这个序列有 6 种可能的排列组合:abc、acb、bac、bca、cab、cba

上述的排列组合是根据 less-than 操作符做字典顺序的排序,即:abc 处于第一,每一个元素都小于其后的元素,而 acb 是次一个排列组合,它是固定了序列内最小元素( a )之后所做的新组合。

同理,序列中次小元素( b )而做的排列组合,在次序上将先于那些固定最小元素( c )而做的排列组合,以 bac、bca 为例,bacbca 之前,因为次序 ac 小于序列 ca ,因此,对于 bca,可以说其前一个排列组合是 bac,其后一个排列组合是 cab

要注意的是,处于排列首的序列 abc 没有 “前一个” 排列组合,处于排列尾的序列 cba 没有 “后一个” 排列组合。

STL 中提供的算法

STL 提供了两个用来计算排列组合关系的算法,分别是 next_permutation()prev_permutation()

其中,next_permutation() 是取出当前范围内的排列,并重新排序为下一个排列,prev_permutation() 是取出指定范围内的序列并将它重新排序为上一个序列。如果不存在下一个序列或上一个序列则返回 false,否则返回 true

这个算法有两个版本,其一使用元素类别所提供的操作符来决定下一个或上一个排列组合,其二是以仿函数 comp 来决定

next_permutation() 为例:

  • 从最尾端开始向前寻找两个相邻的元素,令第一元素为 *i,第二元素为 *ii,且满足 *i<*ii
  • 找到上述的一组相邻元素后,从最尾端向前检验,找出第一个大于 *i 的元素,令其为 *j,然后将 i、j 元素交换
  • 再将 ii 之后的所有元素颠倒排序

假设存在序列 {0,1,2,3,4},下图即为寻找全排列的过程
在这里插入图片描述
next_permutation() 的用法

对于给定的任意一种排列组合,如果能求出下一个排列的情况,那么求得所有全排列情况就容易了。

利用 next_permutation() 的返回值,通过判断排列是否结束,即可求出全排列。

int a[N];
void all_permutation(int n)
{
    sort(a,a+n);
    do{
        for(int i=0; i<n; i++)
            printf("%d ",a[i]);
        printf("\n");
    }while(next_permutation(a,a+n));
}

next_permutation() 与 prev_permutation() 的区别

next_permutation() 函数默认的是从小到大的顺序,而 prev_permutation() 函数默认的是从大到小的顺序。

例如:对于序列 {3,1,2}

next_permutation() 函数得到的结果是:312、321
prev_permutation() 函数得到的结果是:312、231、213、132、123


大佬给总结的很明白了,使用 next_permutation() 的话,需要原序列是非严格增序列,就能够直接使用该函数得到全排列,在这个问题上得到了完美的呈现。但是若不进行 sort() 排序而使用该函数,则会导致输出不全。

参见代码如下:

// 执行用时 :24 ms, 在所有 C++ 提交中击败了93.02%的用户
// 内存消耗 :20.1 MB, 在所有 C++ 提交中击败了100.00%的用户

class Solution {
public:
    vector<string> permutation(string s) {
        vector<string> vt;
        sort(s.begin(), s.end());
        vt.emplace_back(s);
        while (next_permutation(s.begin(), s.end())) 
            vt.emplace_back(s);
        return vt;
    }
};

方法二:递归+回溯+通用解法

全排列问题就离不开回溯和递归,再需要注意的点就是 set 去重以及回溯剪枝问题了。主要几点思路如下:

  • 首先求所有可能出现在第一个位置的字符,即把第一个字符和后面的所有字符交换

  • 然后固定第一个字符,求后面所有字符的排列

  • 这时候仍然把后面的所有字符分成两部分:

    • 后面字符的第一个字符
    • 以及这个字符之后的所有字符。
  • 然后把第一个字符逐一和它后面的字符交换。

即以字符串 "abc" 为例

在逛题解时发现的图,挺不错的,链接入下:

在这里插入图片描述

参见代码如下:

// 执行用时 :212 ms, 在所有 C++ 提交中击败了22.38%的用户
// 内存消耗 :30.9 MB, 在所有 C++ 提交中击败了100.00%的用户炫耀一下:

class Solution {
public:
	vector<string> res;
	set<string> tempRes;
	vector<string> permutation(string s) {
		if (s.size() == 0) return res;  
		help(s, &s[0]);
		for (set<string>::iterator it=tempRes.begin();it!=tempRes.end();it++) 
			res.push_back(*it);
		return res;
	}
	void help(string& s, char* sBegin)
	{
		if (*sBegin == '\0') tempRes.insert(s);     // sBegin所指字符为空'\0',将当前字符串s存储起来
		else {
			for (char* sCh = sBegin; *sCh != '\0'; sCh++) { // sBegin所指字符为空'\0',将当前字符串s存储起来
				char temp = *sBegin;    // 交换位置
				*sBegin = *sCh;
				*sCh = temp;
                
				help(s, sBegin + 1); // 交换完后继续全排列
				
				temp = *sBegin; // 回溯
				*sBegin = *sCh;
				*sCh = temp;
			}
		}
	}
};
发布了332 篇原创文章 · 获赞 167 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/yl_puyu/article/details/104644312