继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

全排列及相关扩展算法(六)——全排列最蛋疼的算法:邻位对换法

九日王朝
关注TA
已关注
手记 180
粉丝 42
获赞 185

1.引入原因:在此之前我们实现全排列本质上都是采用单向交换的思路,当交换到末端便要回溯至上一层面,如果我们采用双向的交换,便可以不断地交换下去,于是产生了邻位对换法。邻位对换法在找下一个排列的方法上在很多情况下要比字典序算法要快上许多,因为每次的下一个排列只是交换两个相邻的元素,当然缺点就是到左端或者右端时要进行找最大可移动数的计算,故最终整体效率也没什么提升,所以称之为最蛋疼的全排列算法。

https://img3.mukewang.com/5b3e2b5b00011b0801140106.jpg

2.定义:

定义1:在一个序列中任意两个元素对如果满足a[k] < a[m](k < m),则称为正序,否则称为逆序。
定义2:如果一个排列中逆序的数目为奇数,则称为奇排列,若为偶数则称为偶排列。
初始的时候,我们假设这个序列是一个升序的序列,而且最小元素至少为1,升序的序列总是偶排列,并且我们设定初始所有数的移动(其实就是交换)的方向均从右向左。我们给出可移动的概念:如果一个数沿着它的方向的邻位比它小,则称这个数是可移动的。由这个概念可以知道,1是永远不可移动的,最大的数除非是在两端而且方向指向序列外面,要不一直是可移动的。我们再规定一个性质:如果一个可移动的数发生了移动,那么比它大的所有数的方向都反过来。对于一个排列而言,它的邻位对换法的下一个排列是最大的可移动的数移动后的结果。

以{1,2,3,4}为例:
1,2,3,4.[很显然,这是一个偶排列,因为它是升序序列。1<2,2<3,3<4,都是正序]。为了得到下一个排列,我们取最大的数4,它是最大的可移动数,并把它向左移动:
1,2,4,3.实际上是3和4的交换,这便是它的下一个排列。再移动:
1,4,2,3.再移动,
4,1,2,3.
由此我们得到了四个排列,每次都是通过交换相邻元素实现的。当4到头了之后,无法移动了,此时我们找到可移动的最大的数3,并把它向左移动1次,得到
4,1,3,2.
此时由于4的移动方向已经反过来了,所以最大可移动数为4,把4依次向右移动:
1,4,3,2
1,3,4,2
1,3,2,4.
4到了头,再次无法移动了,此时最大的可移动的数变成了3,把3向左移动一次,得到
3,1,2,4.
此时4的移动方向再反过来向左,得到
3,1,4,2.
3,4,1,2.
4,3,1,2.
此时3也到头了,此时我们找可移动的最大的数,2.得到
4,3,2,1.
4和3的移动方向再反向右,
3,4,2,1
3,2,4,1
3,2,1,4.
4到头后,由于此时3的移动方向向右,得到
2,3,1,4.
则4的方向又反,向左移动
2,3,4,1
2,4,3,1.     
4,2,3,1.
此时3再向右移动,得到
4,2,1,3.
此时4再向右移动
2,4,1,3.
2,1,4,3.
2,1,3,4.
由此,我们从一个升序排列得到了全排列。


3.总结:

邻位对换法的过程:

1.找到最大可移动数并移动至端点
2.找到现存的最大可移动数移动一次
3.回到原最大可移动数并移动至另一端点
4.找到现存的最大可移动数移动一次
......
5.找不到最大可移动数,循环结束,遍历结束。


邻位对换法的下一个排列:

首先找到最大数的位置,然后判断方向。每当我们的最大的数从一端移到另一端时,就要进行最大可移动数的交换,这个过程过后,在不考虑最大数的数列中便会增加一个逆序。而初始的逆序为0,最大的数在移动的过程中不会改变除去最大数后的数列的顺序,所以,不考虑最大数后的数列的逆序为偶数个时(偶排列),最大数在向左移动,逆序为奇数个时(奇排列),最大数向右移动。同样地,如果最大数此时在某一端点,我们可以通过上述性质判断是移动最大数还是找到最大可移动数并进行交换。非最大数方向也是如此:由所有比它小的数构成的序列的逆序决定的,如果是偶数个时,向左,奇数个时向右。

邻位对换法的中介数:

对于某个排列如2341,从1开始看起(1没有方向,不用算),如果它是向左的,则右边,所有比它小的数的个数为中介数的最高位,如果是向右的,则左边所有比它小的数的个数为中介数的最高位,位逐次降低,如2对应k[1] = 1,最高位为1,3对应k[2] = 1,第三位为1,4是向左的,第二位为k[3] = 1。得到111为2341的中介数。

其对应的序号的求法为:

(1 * 3 + 1) * 4 + 2 = 17,和递减进位的公式一样,所以通过序号求中介数也是和递减进位、递增进位类似,除以n取余,除以n-1除余,。。。。。。

邻位对换法由中介数求原排列的方法:

首先要判断最大数的方向,这个和上面一样,要由其它数构成的序列的奇偶性决定,所以要求其它数构成序列的奇偶性,这个奇偶性通过求序号的公式来得到,由性质原排列中取所有小于等于k的数构成的数列(顺序不变)的奇偶性和对应的序号的奇偶性相同。我举简单的例子:2341的中介数为112.那么231的中介数为11(舍掉低相应位数即可得到相应的中介数),则它的序号为1*3+1 = 4是偶数,所以最高位的方向4的方向向左。以此类推即可。


4.算法代码:

bool Movable(int A[], bool direct[], int n) //direct参数用于接收每个元素移动方向的数组。
{
int max = 1;//初始化最大可移动数为1,因为规定1是最小的数,可以自己设定。
int pos = -1;//初始化最大可移动数的位置为-1.
/*下面先找到最大可移动数位置*/
for (int i = 0; i<n; i++)
{
if (A[i] < max)
continue;
if ((i < n - 1 && A[i] > A[i + 1] && direct[i]) || (i> 0 && A[i] >A[i - 1] && !direct[i]))
{
max = A[i];
pos = i;
}
}
/*下面对它进行移动*/
if (pos == -1)
return false;
if (direct[pos])
{
swap(A[pos], A[pos + 1]);
swap(direct[pos], direct[pos + 1]);
}
else
{
swap(A[pos], A[pos - 1]);
swap(direct[pos], direct[pos - 1]);
}
 
/*最后调整所有比最大可移动数大的数的方向*/
for (int i = 0; i<n; i++)
{
if (A[i] > max)
direct[i] = !direct[i];
}
return true;
}
void Full_Array(int A[], int n)
{
bool* direct = new bool[n]; //产生一个记录每个元素移动方向的数组
sort(A, A + n); //将原序列变成一个升序
for (int i = 0; i<n; i++)
    direct[i] = false;//初始化移动方向为false,表示从右向左。
do
{
Print(A, n);
if (A[n - 1] == n)
for (int i = n - 1; i>0; i--)
{
swap(A[i], A[i - 1]);
swap(direct[i], direct[i - 1]); 
Print(A, n);
}
else
for (int i = 0; i < n-1; i++)
{
swap(A[i], A[i + 1]);
swap(direct[i], direct[i + 1]);
Print(A, n);
}
} while (Movable(A, direct, n));
delete[]direct;
}

5.运行截图:

https://img3.mukewang.com/5b3e2b380001223506590403.jpg

6.几种全排列算法效率对比:

递增(递减)通过中介数增长逆推全排列的效率就明显不如其他几种,就不参与对比了。

这里对比了字典序法、对比邻位对换法、普通回溯递归法以及字典序法STL模版next_permutation算法。

数据选择了{ 1,2,3,4,5,6,7,8,9,10,11,12},毕竟是阶乘太大了时间会很久,有兴趣的可以多加几位试试

https://img2.mukewang.com/5b3e2b6f0001ee8a01090100.jpg

运行截图:

https://img4.mukewang.com/5b3e2b8f00017b1708540554.jpg

对于模版……我可能用了假的<algorithm>

https://img2.mukewang.com/5b3e2ba1000106b801010099.jpg

注:用Count计数来保证函数执行的正确性,用 while (next_permutation(A, n))这种方法的话注意保证输入数据是递增的,且运行数会比其他的少1,因为不会计算第一组数据。


7.参考文档

https://wenku.baidu.com/view/8c79a2facc17552706220880.html


打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP