字符串的排列及解决思路的总结

题目描述

输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串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个方法能够帮助我们解决复杂问题。

  • 图形,能够使抽象的问题形象化。当面试题涉及链表、二叉树等数据结构时,如果在纸上画几张草图,则题目中隐藏的规律就有可能变得很直观了。
  • 举例,能够使抽象问题具体化。很多与算法有关的问题都很抽象,未必一眼就能看出他们的规律。这个时候多举几个例子,一步一步模拟运行的过程,说不定就能发现其中的规律,从而找到解决问题的窍门。
  • 分解,使解决复杂问题的有效方法。如果我们遇到的问题很大,则可以尝试把大问题分解成若干个小问题,然后再递归解决这些小问题。分治法、动态规划等方法应用的都是分解复杂问题的思路。

猜你喜欢

转载自blog.csdn.net/qq_33575542/article/details/80825221