漫画算法-小灰的算法之旅(24)
1. 寻找全排列的下一个数
Q: 给出一个正整数,找出这个正整数所有数字全排列的下一个数。
说通俗一点就是在一个整数所包含数字的全部组合中,找到一个大于且仅大于原数的新整数。例如:
如果输入12345;则返回12354
如果输入12354;则返回12435
如果输入12435;则返回12453
思路
对于长度为n的排列a:
首先从后向前查找第一个顺序对(i,i+1),满足a|i|<a|i+1|.这样「较小数」即为a|i|。此时[i+1,n)必然是下降序列。
如果找到了顺序对,那么在区间[i+1,n)中从后向前查找第一个元素j满足a|i|<a|j|。这样「较大数」即为a[j].
交换a|i|与a|j|,此时可以证明区间[i+1,n)必为降序。此时把逆序转为升序就好了。
例如:给出整数12354,它包含的数字是1,2,3,4,5;如何找到这些数字全排列之后仅大于原数的新整数呢?
为了和原数接近,我们需要尽量保持高位不变,低位在最小的范围内变换顺序。至于变换顺序的范围大小,则取决于当前整数的逆序区域。
如图所示:12354的逆序区域是最后两位,仅看这两位已经是当前的最大组合。若想最接近原数,又比原数更大,必须从倒数第3位开始改变。
如何改变?12345的倒数第3位是3,我们需要从后面的逆序区域中找到大于3的最小数字,让其和3的位置进行互换。
互换后的临时结果是12453,倒数第3位已经确定,这个时候最后两位仍然是逆序状态,我们需要把最后两位转变为顺序状态,以此保证在倒数第3位数字位4的情况下,后两位尽可能小。
排序后的结果为12435.
总结一下:
获得全排列下一个数的3个步骤:
- 从后向前查看逆序区域,找到逆序区域的前一位,也就是数字置换的边界
- 让逆序区域的前一位和逆序区域中大于它的最小的数字交换位置
- 把原来的逆序区域转为顺序区域。
时间复杂度
该算法3个步骤每一步的时间复杂度都是O(n),因此整体时间复杂度也为O(n)。
代码实现
public static int[] findNearestNumber(int[] numbers){
//1. 从后向前查看逆序区域,找到逆序区域的前一位,也就是数字置换的边界
int index = findTransferPoint(numbers);
// 如果数字置换边界是0,说明整个数组已经逆序,无法得到更大的相同数字组成的整数,返回null
if(index==0){
return null;
}
//2. 把逆序区域的前一位和逆序区域中刚刚大于它的数字交换位置复制并入参,避免直接修改入参
int[] numbersCopy =Arrays.copyOf(numbers,numbers.length);
exchangeHead(numbersCopy,index);
//3.把原来的逆序区域转为顺序
reverse(numbersCopy,index);
return numbersCopy;
}
private static int findTransferPoint(int[] numbers){
for(int i=numbers.length-1;i>0;i--){
if(numbers[i]>numbers[i-1]){
return i;
}
}
return 0;
}
private static int[] exchangehead(int[] numbers,int index){
int head=numbers[index-1];
for(int i=numbers.length-1;i>0;i--){
if(head<numbers[i]){
numbers[index-1]=numbers[i];
numbers[i]=head;
break;
}
}
return numbers;
}
private static int[] reverse(int[] num,int index){
for(int i=index,j=num.length-1;i<j;i++,j--){
int temp=num[i];
num[i]=num[j];
num[j]=temp;
}
return num;
}
private static void outputNumbers(int[] numbers){
for(int i:numbers){
System.out.print(i);
}
System.out.println();
}
public static void main(String[] args){
int[] numbers={1,2,3,4,5}
//打印12345之后的10个全排列整数
for(int i=0;i<10;i++){
numbers=findNearestNumber(numbers);
outputNumbers(numbers);
}
}
golang实现
func findNearestNumber(numbers []int) []int {
//1. 从后向前查看逆序区域,找到逆序区域的前一位,也就是数字置换的边界
index := findTransferPoint(numbers)
// 如果数字置换边界是0,说明整个数组已经是逆序,无法得到更大的相同数字组成的整数,返回null
if index==0 {
return nil
}
//2。 把逆序区域的前一位和逆序区域的中刚刚大于它的数字交换位置复制并入参,避免直接修改入参
changeArray := exchangehead(numbers, index)
//3. 把原来的逆序区域转为顺序
val := reverse(changeArray, index)
return val
}
func findTransferPoint(numbers []int) int {
for i:= len(numbers)-1;i>0;i-- {
if numbers[i]>numbers[i-1] {
return i
}
}
return 0
}
func exchangehead(numbers []int, index int) []int {
head :=numbers[index-1];
for i := len(numbers)-1;i>0;i--{
if head<numbers[i] {
numbers[index-1]=numbers[i]
numbers[i]=head
break
}
}
return numbers
}
func reverse(numbers []int, index int) []int {
for j:=len(numbers)-1;index<j;j--{
temp:=numbers[index]
numbers[index]=numbers[j]
numbers[j]=temp
index++
}
return numbers
}
func printArray(numbers []int) {
for _, number := range numbers {
fmt.Printf("%d ",number)
}
fmt.Println()
log.Println()
}
func main() {
numbers :=[]int{1,2,3,4,5}
log.Println(numbers)
for i :=0;i<10;i++ {
number := findNearestNumber(numbers)
printArray(number)
}
}