完美洗牌问题系列文章:
提示:很重要的思想
1.完美洗牌问题——整体交换数组的左右2部分
2.完美洗牌问题——计算位置i下次要被挤到哪个位置j?
这2个文章,都是为本文做铺垫的,循序渐进理解什么是完美洗牌问题,如何解决完美洗牌问题?
完美洗牌问题的定义
完美洗牌问题的定义:
给你一个arr,长度是偶数长度S,左边部分长度为N,右边部分长度为N
请将左边右边部分交叉组合,保证时间复杂度不会超过o(n),空间复杂度不会超过o(1)——即完美洗牌
一、通过循环挤兑i到j,实现完美洗牌问题
根据文章:2.完美洗牌问题——计算位置i下次要被挤到哪个位置j?
可以知道,我们从start=1位置出发,可以不断地调用f(arr,i,S)函数【i位置应该去哪里?j,要求j,arr长度是偶数长S】,去挤兑另一个位置j,然后当j回到start位置时,全部挤兑完成,看图:
经过一圈循环,将i的数,挤兑到j处,完成了完美洗牌问题
二、但是一个数组arr,它可能存在多个挤兑环
比如,当长度S=6时
发现了没?S=6时,N=3,而从1挤兑到2,2挤兑到4,4很快挤兑到了1,形成了一个环,另外的那些还搞不定呢!!!
所以到这里,本题的重要记忆结论来了!!!!,直接背
记结论!
记结论!
记结论!
当长度S=3^^k - 1 时:即2,8,26,80这种特定的长度时,
他们的环起点有这些:1,3,9,……,S=3^^k - 1
而且,我们求解的时候,一定要找长度最长那个环开始挤兑,这就要求,我们需要找到<=S的3^^k - 1
比如S=14这种,显然不符合特定长
那要找到哪个3^^k - 1<=14
显然k=2时,3^^k - 1=8,它死最近的<=4的特定长度。
故我们要先扭转一部分数据到左边,凑合为左边能挤兑的8长度(S=8,N=4)【然后就能用环起点挤兑了】,
剩下的再看看他们能组成的最长特定长是多少?再去挤兑合适的数组。——这就是破解完美洗牌问题的方法,下面细说
三、通过扭转、挤兑最大那个环,破解完美洗牌问题
二中的举例:
(1)S=14,N=7,所以呢,咱先把那个能挤兑的整体长度8挤兑了,要求,右边有half=8/2=4这么多,要和左边的N-half那部分,相互扭转交换。
——这就是上面所说的文章1做的事情:1.完美洗牌问题——整体交换数组的左右2部分
(2)剩下的S=6长度的呢,还是不符合特定长,没法挤兑【看下图粉色部分】
一样的,我们还需要找到哪个3^^k - 1<=6
显然,k=1时,到哪个3^^k - 1 = 2<=6
2这个特定长度,先挤兑再说:也就是把L5R5放一起,
即把右边R5R6R7中,half=2/2=1这么多数,跟左边N-half这么多扭转交换,再挤兑
(3)还剩4个,仍然不是特定的长度【看上面这个图:橘黄色部分】
一样的,我们还需要找到哪个3^^k - 1<=4
显然,k=1时,到哪个3^^k - 1 = 2<=4
2这个特定长度,先挤兑再说:也就是把L6R6放一起,
即把右边R6R7中,half=2/2=1这么多数,跟左边N-half这么多扭转交换,再挤兑
(4)还剩下2个,恰好满足了特定的长度,直接挤兑即可;
最终,从最长的那个环开始挤兑,直到全部环都挤兑玩,完美洗牌问题敲定!!!!
我们先单独写
一个合规特定长度为S,循环挤兑各个环,达成这种特定长度S下的完美洗牌问题的代码【比如S=2,8,28,80这种】:
从arr的start环点开始,合规特定长度为S,最大合规满足3^^k - 1=S的k跟着来,k意味着有k个环需要挤兑
因为我不用0坐标,所有i=0开始要抠清楚arr的那些边界,
i控制最大到k个环,全部环都要挤兑完成
每次出发点从trigger=1出发,依次寻找这些环触发点:1,3,9,27,81……
cur=j就是位置x下一次需要去的地方
整体代码好好细品:
//给你一个起点,给你一个3^k-1的长度S,给你一个环的个数k,让你挨个挤兑到相应的位置
public static void cycle(int[] arr, int start, int S, int k){
//S=3^k-1,即长度为2,8,26,80的长度,一定有k个环需要挤兑
//环有k个,每个的出发点trigger是1,3,9,27,81,,,3^k-1,共k个触发点
for (int i = 0, trigger = 1; i < k; i++, trigger *= 3) {
//挤兑出去之前,我是preValue
int preValue = arr[start + trigger - 1];//-1是因为起点用1表示
//即将去哪个位置?
int cur = findNextIndex(trigger, S);//用S这个长,S/2==N去推下一个去的位置,先拿变量记住
while (cur != trigger){
int nextValue = arr[start + cur - 1];//tmp保存我
arr[start + cur - 1] = preValue;//被挤进来
preValue = nextValue;//刚刚cur处的值就作为我,要去挤兑别人的数值了
cur = findNextIndex(cur, S);//nextValue去挤兑谁呢,cur开始推理
}
//直到最后一个元素,此时cur==trigger时,回到了原点,则最后那个nextValue放回原点
arr[start + cur - 1] = preValue;//最后挤兑,本环结束
}
}
大流程: —— 完美洗牌问题的代码
就是不断寻找,满足满足3^^k - 1<=S的特定长度,然后扭转half这么多,凑成左边合规特定长度,再利用上面cycle函数循环去挤兑
代码细细品味:
public static void shuffle(int[] arr, int L, int R){
//当洗牌长度为0就完成了所有任务
while (R - L + 1 > 0){
int S = R - L + 1;//区间长度
int base = 3;//找一个3^k方够近S的值
int k = 1;//从k==1次方开始,统计环的个数
while (base < (S + 1) / 3){
base *= 3;
k++;//每次统计k个环
}
//此时3^k-1已经最接近S了,那我们可以先挤兑3^k-1这么长的的区间,
//首先,需要我们拿右边的half=base-1/2这么多扭到左边,也就是把L+hanf---mid---mid+half区间调换位置
//让左边组合层俩half,一个base-1这么长
int half = (base - 1) >> 1;
int mid = L + ((R - L) >> 1);//取数组中点,左边右边界,上中点
rotate(arr, L + half, mid, mid + half);
//然后洗牌左边的base-1长度
cycle(arr, L, base - 1, k);//参数,L是起点,S==base-1==3^k-1这么长,k个环需要触发挤兑去
L = L + base - 1;//切换我左边的L,已经搞定了base-1这么长了,剩下的右边部分继续分
}
}
测试一波:
public static void test(){
int[] arr = {1,2,3,4,5,6,7,8};
perfectShuffle(arr);
for(Integer i:arr) System.out.print(i +" ");
}
public static void main(String[] args) {
test();
}
总结
提示:重要经验:
1)理解清楚完美洗牌问题的定义
2)记忆一个结论:当长度S=3^^k - 1 时:即2,8,26,80这种特定的长度时,
他们的环起点有这些:1,3,9,……,S=3^^k - 1 ,也就是说,整到一个合规特定的长度S时,要依次把这k个环全部挤兑完,就是上面说的cycle函数
3)完美洗牌问题中由于arr可能不止一个环,而且还有先搞定最大的合规特定长度S那个部分,就需要先将右边部分的half扭转交换到左边,这里就需要扭转交换函数,3次逆序,一定要掌握;
4)破解完美洗牌问题,先不断寻找,满足满足3^^k - 1<=S的特定长度,然后扭转half这么多,凑成左边合规特定长度,再利用上面cycle函数循环去挤兑
5)这个题目,笔试时只能抄写代码,面试时尽力写吧,先给面试官讲清楚思想,差不多了就行。