题目描述
输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。
输入描述
输入一个字符串,长度不超过9(可能有字符重复),字符只包括大小写字母。
思路
我们可以向前面的几个问题一样,先把问题分解成多个小问题再一一解决,比如:我们把一个字符串看成由两部分组成:第一部分为第一个字符,第二部分为剩下的字符串。
因此我们可以把字符串的排列看成两步:
1、求所有可能出现在第一个位置的字符,即把第一个字符和后面的所有字符都交换一遍;
2、固定第一个字符,求后面字符的排列。
这样一看,是不是立马想到,我们可以通过递归来实现这个问题的解决方案?我们只需要通过递归,每次把第一部分固定,然后把第二部分递归进入下一个排列,这样第二部分的第一个字符又将被固定,以此类推,就能遍历出所有的排列了。
因此不难得出下列C#代码:
//递归方法
class Solution1
{
/// <summary>
/// 把传入的字符串转化成StringBuild类型,然后把所有排列加入集合,并排序
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
public List<string> Permutation(string str)
{
List<string> list = new List<string>();
int len = str.Length;
//如果字符串为空或不为空但是无元素,返回一个空集合
if (str == null || len < 1)
return list;
StringBuilder stringBuilder = new StringBuilder(str);
Permutation(ref list, stringBuilder, 0);
//排列集合,按字典序排序(字符串从小到大的顺序)
list.Sort();
return list;
}
/// <summary>
/// 排列字符,并把满足条件的字符串添加进集合
/// </summary>
/// <param name="list">储存所有排列的集合</param>
/// <param name="str">排列的字符所组成的字符串</param>
/// <param name="begin">字符串可排列开始的位置</param>
private void Permutation(ref List<string> list, StringBuilder str, int begin)
{
//如果排列到最后一个字符了,把这个字符串添加到集合,返回到上一个函数中继续
if (begin == str.Length - 1)
{
list.Add(str.ToString());
return;
}
//从当前的位置开始,与后面的每个字符交换位置,并固定当前位置的字符,为后面的字符串重排列
for (int i = begin; i < str.Length; i++)
{
//如果与当前字符重复,则匹配下一个字符
if (i != begin && str[i] == str[begin])
continue;
//把begin位置的字符与i位置的字符交换位置
char ch = str[i];
str[i] = str[begin];
str[begin] = ch;
//固定当前位置的字符,排列后面的字符
Permutation(ref list, str, begin + 1);
//当排列完成之后,把交换位置的字符换回原来的位置
ch = str[i];
str[i] = str[begin];
str[begin] = ch;
/* 举例来说“abca”,为什么使用了两次swap函数
交换时是a与b交换,遍历;
交换时是a与c交换,遍历;(使用一次swap时,是b与c交换)
交换时是a与a不交换;
*/
}
}
}
//非递归方法
class Solution2
{
//排列字符串
public List<string> Permutation(string str)
{
List<string> list = new List<string>();
if (str == null || str.Length < 1)
return list;
StringBuilder sb = Sort(str);
int right = 0, left = sb.Length - 1;
//在该循环中,每次交换一个字符,且当固定字符右边的值已经排列完所有的可能,则把left指针往前移一位,然后重排当前可排列的字符串
while (true)
{
list.Add(sb.ToString());
//令左指针从最左边开始遍历,直到左边的字符小于当前左指针指的值
left = sb.Length - 1;
//如果左边的字符小于左指针指的值,说明该字符可以交换位置,则继续执行
while (left > 0 && sb[left - 1] >= sb[left])
{
left--;
}
//如果左指针已经移动到字符串最左边,说明没有需要固定的字符了,则退出循环,表示所有的排列已经得出
if (left == 0) break;
//令右指针指向当前左指针指向的值
right = left;
//遍历,找到比左指针指的值小的值,如果左指针指的值是当前可排列的字符中最大的字符,那么把最后一个字符与固定字符的最后一个字符交换位置
while (right < sb.Length - 1 && sb[right + 1] > sb[left - 1])
{
right++;
}
Swap(sb, left - 1, right);
//重新排列,当左指针指的不是最后一个值,那么每当改变一次前面的字符,后面的所有字符都要从小到大再排列一次
Invert(sb, left, sb.Length - 1);
}
return list;
}
/// <summary>
/// 从固定的字符右边的第一个字符开始,交换已排列的值,因为每次更改左边的字符之前,右边的字符串都是从大到小排列的了,因此只需一个从左一个从右往中间开始交换,当交换到中间的值的时候,说明固定字符串的右边部分已经排序完成,字符的排列顺序为从小到达完成的。
/// </summary>
/// <param name="sb">传入的字符串</param>
/// <param name="fromIndex">开始的下标</param>
/// <param name="toIndex">结束的下标</param>
private void Invert(StringBuilder sb, int fromIndex, int toIndex)
{
while (fromIndex < toIndex)
{
Swap(sb, fromIndex, toIndex);
fromIndex++;
toIndex--;
}
}
/// <summary>
/// 交换两个字符在字符串中的位置
/// </summary>
/// <param name="sb">传入的字符串</param>
/// <param name="index1">第一个字符的下标</param>
/// <param name="index2">第二个字符的下标</param>
private void Swap(StringBuilder sb, int index1, int index2)
{
char temp = sb[index1];
sb[index1] = sb[index2];
sb[index2] = temp;
}
/// <summary>
/// 排序,从小到大的顺序排列字符串
/// </summary>
/// <param name="str">传入的字符串</param>
/// <returns>把传入的字符串排序后已StringBuild的格式输出</returns>
private StringBuilder Sort(string str)
{
StringBuilder sb = new StringBuilder(str);
char c;
//从第二个字符开始排序
for (int i = 1; i < sb.Length; i++)
{
c = sb[i];
int j = i - 1;
//当前一个字符大于当前字符,把大的字符的位置往前移,如果前面的字符小于当前字符,说明前面所有的字符都小于当前字符,跳出执行下一次循环
for (; j >= 0; j--)
{
if (c < sb[j])
{
sb[j + 1] = sb[j];
}
else
{
break;
}
}
sb[j + 1] = c;
}
return sb;
}
}
总结
其实,通过前面很多问题的解决方法可以看出,当遇到难题时,画图、举例和分解3个方法能够帮助我们解决复杂问题。
- 图形,能够使抽象的问题形象化。当面试题涉及链表、二叉树等数据结构时,如果在纸上画几张草图,则题目中隐藏的规律就有可能变得很直观了。
- 举例,能够使抽象问题具体化。很多与算法有关的问题都很抽象,未必一眼就能看出他们的规律。这个时候多举几个例子,一步一步模拟运行的过程,说不定就能发现其中的规律,从而找到解决问题的窍门。
- 分解,使解决复杂问题的有效方法。如果我们遇到的问题很大,则可以尝试把大问题分解成若干个小问题,然后再递归解决这些小问题。分治法、动态规划等方法应用的都是分解复杂问题的思路。