2018.9.3 《剑指Offer》从零单刷个人笔记整理(66题全)目录传送门
这道题比书上的题目还要复杂一些,解法总体思路差不多。花的时间比较长,因为是第一次接触全排列问题,初步实现+调试+改进花了不少时间,做完参考其他优秀的解法,又花时间进行了理解和学习,同时又从网上了解了其他很多有意思的全排列问题的解决方法,算法真是博大精深啊。题目主要复杂在哪里呢?1.字符重复,2.字典排序。因此这题可以简称为去重字典序全排列问题。
顾名思义,全排列就是一串字符的所有有序组合。生成全排列的算法有很多,这里给出三种比较常用的实现方法。解题思路:
方法一:交换递归法
将每次递归分为序列的第一个结点与后面其他结点,每次递归将第一个结点依次与其他结点交换,交换至末尾时对本次递归结果进行打印,每次执行算法后需要对交换进行复位以便进行下一次交换。
例如对于序列abc,第一层第一次交换为a与a交换,第二层第一次为b与b交换,第三层第一次为c与c交换,交换结束,打印abc;倒退一层,第二层第二次为b与c交换,第三层第一次为b与b交换,交换结束,打印acb;倒退两层,第一层第二次为a与b交换……最后打印出来的序列为abc acb bac bca cba cab。这就是原书题目的标准解答,也是一次全排列的实现过程了。
全排列是打印出来了,可是注意到后两次打印并不满足字典序,事实上因为是隔位交换,也不可能保证字典序。同时也无法保证字符重复造成的排列重复。因此还需要进行改进。
改进方案1:
对于字典序:可在排列之后利用Collections.sort对容器进行排序,字符串默认字典序。
对于字符重复:可在每次打印前对容器用contains进行字符重复的判断,如重复则不打印。
改进方案2:
先用TreeSet临时保存,可以保证排序和去重。(TreeSet可以理解成数组实现的二叉搜索树的容器,可以直接用addAll添加到ArrayList中)
方法二:字典序排列算法
一个全排列可看做一个字符串,字符串可有前缀、后缀。 生成给定全排列的下一个排列。所谓一个的下一个就是这一个与下一个之间没有其他的。这就要求这一个与下一个有尽可能长的共同前缀,也即变化限制在尽可能短的后缀上。例如839647521是1—9的排列。1—9的排列最前面的是123456789,最后面的987654321,从右向左扫描若都是增的,就到了987654321,也就没有下一个了。否则找出第一次出现下降的位置。
字典序排列算法分步如下,例如:
1.先对字符数组进行初始排序并打印初始序列
2.从尾部向前找第一个由大变小的数字(较大者)的位置lIndex
3.从i开始向后找到第一个小于lIndex-1的值的位置rIndex
4.将lIndex-1与rIndex-1位置对应的值进行交换,也即将从后往前第一个由大变小的数字(较小者)与从前往后最后一个大于前者的值
5.将lIndex-1后的序列进行翻转
6.打印这个序列,继续进行循环
如果直接理解起来晦涩难懂,可以参考一个例子——如何得到346987521的下一个 :
1.从尾部往前找第一个P(i-1) < P(i)的位置 3 4 6 <- 9 <- 8 <- 7 <-5 <- 2 <- 1 最终找到6是第一个变小的数字&