剑指offer 字符串的排列

1.题目

题目描述
输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab,cba
输入描述:

输入一个字符串,长度不超过9(可能有字符重复),字符只包括大小写字母。

来源:剑指offer
链接:https://www.nowcoder.com/practice/fe6b651b66ae47d7acce78ffdd9a96c7?tpId=13&tqId=11180&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking

2.我的题解

本题需要实现以下几个功能:

  • 全排列:若干字符的全排列;
  • 去重:有重复字符时,需要去掉重复的结果;
  • 有序:结果需要按照字典序输出;

这里使用递归法(交换)实现,具体思路是:

  • 给定初始状态,确定字符个数len
  • 以交换的方式确定当前位置的字符,并保证了不遗漏。然后将后续部分整体看做一个子问题进行递归,直到最后一个位置。
  • 假设当前位置为pos,考察位置i(pos<i<len)以确定ipos位置是否可以交换(直接交换的话可能会导致排列重复):如果[pos,i)位置上没有与i位置上相同的值,那么就可以交换。
  • 该方法并不保证有序性,但通过判断可以去重复。

举个例子:求1,2,3的全排列

初始状态递归一层(交换)递归两层(交换)
1,2,31,2,3
1,3,2
1,2,32,1,32,1,3
2,3,1
3,2,13,2,1
3,1,2

举例说明是否可交换的判断:1,2,3,4,2,5,6

  • 假设当前位置为第一个2。当第一个2想和第二个2交换时,可以看到二者之间的位置(前开后闭)存在一个2,那么就不应该交换;
  • 一个直观的理解。假设在5这个位置上,令一个不等于5的数与之交换,即可得到一个新的排列,显然25交换可以得到一个新的排列。但是这里有两个2,两个25交换将得到相同的排列,为了避免重复我们仅想让一个25交换,于是上述判断的依据实质上就是保证一个位置仅被重复的数字交换一次,说白了也就是设置“管辖区域”,在本例中,1,3,4由第一个2交换,5,6由第二个2交换;
  • 判断的依据不仅可以是上述说的[pos,i),还可以是[i,len),这样就是让第一个25,6,第二个21,3,4;

2.1递归法(交换)

class Solution {
    vector<string> res_;
    void swap(string &s, int i, int j) {
        char tmp = s[i];
        s[i] = s[j];
        s[j] = tmp;
    }
    void MyPermutation(string &s, int n, int pos) {
        if (pos == n)res_.push_back(s);
        for (int i = pos; i < n; i++) {
            bool flag = true;
            for (int j = i+1; j < n; j++)//for (int j = pos; j < i; j++)
                if (s[j] == s[i])flag = false;
            if (flag) {
                swap(s, i, pos);
                MyPermutation(s, n, pos + 1);
                swap(s, i, pos);
            }
        }
    }
public:
    vector<string> Permutation(string str) {
        res_.clear();
        if(str.size()==0)return res_;
        MyPermutation(str,str.size(),0);
        sort(res_.begin(),res_.end());
        return res_;
    }
};

3.别人的题解

3.1 字典序法

字典序之间可有关系?如找到一个排列的下一个字典序?例如我们都知道123的下一个字典序是132,那么有什么普适的寻找方法呢?
寻找下一个字典序排列的方法:

  • 举例:1532(下标从0起)
  • 从后向前找到第一个相邻的正序对的位置i:即array[i]<array[i+1],这里i=0;
  • i开始向右搜索,找到比array[i]大的当中最小的那个位置j:这里j=3;
  • 交换i,j位置上的数:得到2531;
  • i位置后的字符串反转2135;

优点: 该方法可去重读、有序、高效。

class Solution {
	vector<string> res_;
	void swap(char *ch1, char *ch2) {
		char tmp = *ch1;
		*ch1 = *ch2;
		*ch2 = tmp;
	}
	string nextPermutation(string s) {
		if (s.size()<2)return "";
		int i = s.size() - 2, j = 0, k = 0;
		//正序对
		while (i >= 0 && s[i]>=s[i + 1])i--;
		if (i<0)return "";
		//大于s[i]的最小的数
		j = i + 1;
		for (int k = i + 2; k<s.size(); k++)
			if (s[k]>s[i] && s[k]<s[j])j = k;
		//swap
		swap(&s[i], &s[j]);
		//reverse
		j = i + 1, k = s.size() - 1;
		while (j<k) {
			swap(&s[j++], &s[k--]);
		}
		return s;
	}
public:
	vector<string> Permutation(string str) {
		if (str.size() == 0)return res_;
		res_.clear();
		sort(str.begin(), str.end());
		do {
			//cout << str << endl;
			res_.push_back(str);
		} while ((str = nextPermutation(str)) != "");
		return res_;
	}
};

3.2 递归回溯法(填空)

上面的递归法(交换)是在给定初始状态(即某个排列)的基础上,通过交换确定某个位置上的值,从而获取全部排列的方法。本方法中换一个思路,不再进行交换,而是进行填空,即在某个位置上填写值,这需要维护一个剩余字符集合,具体如下:

  • 初始化长度为len的空排列,剩余字符集合包含所有字符;
  • 向当前位置填值,值可以是剩余字符结合中的任意字符,同时剩余字符结合要去掉该字符。后续部分通过递归继续处理。
  • 该方法不能去除重复、不能保证有序性,需要借助set中转。

记录一下实现时的坑:

  • 这个思路在评论区看的,java版本实现,使用ArrayList作为递归时的参数维护剩余字符集合,该数据结构实现了通过下标的读取和删除(get(i),remove(i))。我用C++实现,该用什么呢?list?
  • 入坑了:list似乎不支持通过下标的读取和删除,我用的较少,没搞出来。循环时放弃使用下标,用迭代器吧!读是可以读了,删除呢?
  • remove:它接收值作为参数,返回值为空,删除容器中所有等于该值的节点。
  • erase:它接收迭代器为参数,返回值为下一个迭代器,删除当前迭代器指向的节点。erase删除后会丢失迭代器,要用it = list.erase(it);list.erase(it++);
  • 坑1:直接使用erase删除当前参数中的某个位置,并当做参数传入进行下一次递归。我是傻的吗?递归回来进行下一次循环的时候,我又要删除一个,那么下一次循环时传入的参数被我删了两个,这不合适吧?
  • 坑2:咱新建一个list,把新建的当做参数传递,这样就不会影响当前层的参数。可是没有下标,新建的list我不知道该删哪个地方啊!没有迭代器也不能通过下标。
  • 坑3:remove更不行,有重复字符的话删除必多删,必出错。
  • 坑4:不新建list,删除当前层参数的某个位置,并当作参数传入下一层递归,递归回来后,利用insert还原当前层的参数。。。坑,兜了一圈还不如用vector呢。
  • 使用vector实现,主要vector可以方便的地实现下标操作。本题中合法的字符是字母,那么0作为删除的标记,遇到就跳过该位置即可。
//vector实现
class Solution {
	vector<string> res_;
	set<string> set_;
	string array_;
	vector<char> left_;
	int len = 0;
	void solve(int pos) {
		if (pos == len) {
			if (set_.find(array_) == set_.end()){
				set_.insert(array_);
				//cout << array_ << endl;
			}
			return;
		}
		for (int i = 0; i < left_.size();i++) {
			if (left_[i] == '0')continue;
			array_[pos] = left_[i];
			left_[i] = '0';
			solve(pos + 1);
			left_[i] = array_[pos];
		}
	}
public:
	vector<string> Permutation(string str) {
		//init
		len = str.size();
		if (len == 0)return res_;
		res_.clear();
		set_.clear();
		left_.clear();
		array_ = str;
		for (int i = 0; i < len; i++)left_.insert(left_.end(), str[i]);
		//solve
		solve(0);
		//res
		res_.assign(set_.begin(), set_.end());
		return res_;
	}
};
//list实现
class Solution {
	vector<string> res_;
	set<string> set_;
	string array_;
	list<char> left_;
	int len = 0;
	void solve(int pos,list<char> left) {
		if (pos == len) {
			if (set_.find(array_) == set_.end()){
				set_.insert(array_);
				//cout << array_ << endl;
			}
			return;
		}
		for (auto it = left.begin(); it != left.end();) {
			array_[pos] = *it;
			it = left.erase(it);
			solve(pos + 1,left);
			left.insert(it, array_[pos]);
		}
	}
public:
	vector<string> Permutation(string str) {
		//init
		len = str.size();
		if (len == 0)return res_;
		res_.clear();
		set_.clear();
		left_.clear();
		array_ = str;
		for (int i = 0; i < len; i++)left_.insert(left_.end(), str[i]);
		//solve
		solve(0,left_);
		//res
		res_.assign(set_.begin(), set_.end());
		return res_;
	}
};

4.总结与反思

(1)递归一看就会,一写就废,仍要多温习。
(2)全排列、去重、有序。
(3)STL list的使用,remove(),erase()

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值