最近看到这道经典的算法题,开始也没怎么想,就看了看别人写的,但是当我把他们的结果,用自己的想法画出来过后,就发现了不对,然后我看了看其它写的都是类似的写法,千篇一律的相同,然后我就自己重新写了一个算法,这也是我第一次写文章,如果有什么写的不好或者在这道算法理解上有什么问题,希望大神多多指教。
算法描述:
历史:三色旗的问题最早由E.W.Dijkstra(荷兰人)所提出,他所使用的用语为Dutch Nation Flag(荷兰国家国旗),而多数的作者则使用Three-Color Flag(三色国旗)来称之。
说明:假设有一条绳子,上面有红、白、蓝三种颜色的旗子,起初绳子上的旗子颜色并没有顺序,您希望将之分类,并排列为蓝、白、红的顺序,要如何移动次数才会最少,注意您只能在绳子上进行这个动作,而且一次只能调换两个旗子。
提示:是不是刚读完一点想法都没有,就想去csdn一下,没错是我刚开始看到这题的想法了。这时候我们在读一次题,整理一下思路。
一、解题步骤
我们先不要着急忙慌的直接就上手去看代码,先和我一起分析。
1、解读题目
从题目中我们可以大致理解到,就是对顺序不一样的三种颜色的旗子,进行分类,每一次只能交换两面旗子(可以是相邻的也可以是不相邻),我们可以把旗子理解成对1,2,3三个数的归类,把相同的数放到对应的区间。如果还是不理解我们可以画个图。
原数据:2 1 1 3 1 2
分类后:1 1 1 2 2 3
上面就是我们分类后最终达到的效果
2、解题思路
是不是感觉也还好呀,仔细一读 so easy 呐,不要忘了还有一个我们没有提到的条件,要如何移动次数才会最少
.
原数据:2 1 1 3 1 2
第一次:1 2 1 3 1 2
第二次:1 1 2 3 1 2
第三次:1 1 1 3 2 2
第四次:1 1 1 2 3 2
第五次:1 1 1 2 2 3
这种是我们最常想到的但是这是步骤最少吗?显然不是。
我们想一想三个区间如果第一个区间和第三个区间排好了,不就排序完成了
原数据:2 1 1 3 1 2
第一次:1 2 1 3 1 2
第二次:1 1 2 3 1 2
第三次:1 1 1 3 2 2
第四次:1 1 1 2 2 3
很多人到这里估计就感觉最少了,但真的这样吗?显然也不是。
原数据:2 1 1 3 1 2
第一次:1 1 1 3 2 2
第二次:1 1 1 2 2 3
是不是突然步骤一下就减少了。
为什么会减少这么多?这也是我解决这道算法的核心,诸位别急,我慢慢道来。
既然我们都想到了,第一个区间和第三个区间排序完成就达到目的了,我们不妨在深入一点,置换的时候如果是第一区间的数我们就跳过,不是再去置换,第三区间也一样。
可以看到我们置换的是第二区间的 1 ,而没有去改变本来已经在第一区间的 1,这样就减少了在 本区间置换产生的"无用"步骤。
这样我们就可以清楚的对比,本来一步就可以达到的目的却用了三步,这样写出来的代码部分情况,可以达到步骤最少了,但是我们在细一点,如果刚好第一区间存在第三区间的值,第三区间存在第一区间的值,我们是不是可以先对这两个区间进行替换,再去第二区间查找,这样不就又可能减少一些步骤了。
原数据:3 2 1 1
第一次:1 2 3 1
第二次:1 1 3 2
第三次:1 1 2 3
这是我们刚刚算法的思路排序
原数据:3 2 1 1
第一次:1 2 1 3
第二次:1 1 2 3
这是后面的细节操作
可以看出来又少了一些步骤,这就是我们的解题思路,看到这的可以先写一写,把思路转为代码。
二、代码
代码不难,一行一行看下去就可以明白。
public class Test {
public static void main(String[] args) {
//三色旗
//有一条绳子上面挂有白、红、蓝三种颜色的多面旗子,这些旗子的排列是无序的。
//现在要将绳子上的旗子按蓝、白、红三种颜色进行归类排列,
//但是只能在绳子上进行旗子的移动,并且每次只能调换两个旗子。
//问如何采用最少的步骤来完成三色旗的排列呢?
int []a = {3,2,1,1,2,2,1,2,1,3,1,2,3};
//来记录调换次数
int sum = 0;
// todo 1就往前交换 3就往后交换 解题的最基本的想法
//输出原始数据
System.out.print("原始数据:");
for(int i = 0 ; i < a.length ; i ++){
System.out.print(a[i] + " ");
}
System.out.println();
//记录每个数出现的次数,方便划分区间
int one = 0 , two = 0 , three = 0;
for(int i = 0 ; i < a.length ; i++){
if(a[i] == 1){
one ++;
}else if(a[i] == 2){
two ++;
}else {
three ++;
}
}
System.out.println("出现1的次数:"+ one + " 出现2的次数:" + two + " 出现三的次数:" + three);
// todo 思路
// 划分出三个的区间界限 只需要排好第一个区间 和 最后一个区间 就可以完成排序
// 第一个区间排序
// 循环 one 如果不等于 1 先判断是否属于第三区间的数,如果是在判断第三区间有没有第一区间的值,有进行置换
for(int i = 0 ; i < one ; i++){
//循环前五个数看是否为 1 不为1就调换
if(a[i] != 1){
//判断是否等于3 如果等于先从第三区间找 是否有等于 这样又可以减少步骤
if(a[i] == 3){
for(int k = one + two ; k < a.length ; k++){
int temp;
if(a[k] == 1){
temp = a[i];
a[i] = a[k];
a[k] = temp;
break;
}
}
}else {
for (int j = one; j < a.length ; j++) {
int temp;
if (a[j] == 1) {
temp = a[i];
a[i] = a[j];
a[j] = temp;
break;
}
}
}
//输出每一行调换后的结果
for(int k = 0 ; k < a.length; k++){
System.out.print(a[k] + " ");
}
System.out.println();
//成功调换一次就记录一次
sum ++;
}
}
// 给最后一个区间进行排序
for(int i = a.length - 1 ; i >= a.length - three ; i --){
if(a[i] != 3){
//方便理解选择从后面往前找,也可以选择从 1 区间结束开始查找到 3 区间之前
for(int k = a.length - three - 1 ; k > a.length - three - two -1 ; k --){
int temp;
if(a[k] == 3){
temp = a[i];
a[i] = a[k];
a[k] = temp;
break;
}
}
for(int j = 0 ; j < a.length; j++){
System.out.print(a[j] + " ");
}
System.out.println();
sum ++;
}
}
System.out.println("进行调换的总次数: " + sum);
}
}
结果
原始数据:3 2 1 1 2 2 1 2 1 3 1 2 3
出现1的次数:5 出现2的次数:5 出现三的次数:3
1 2 1 1 2 2 1 2 1 3 3 2 3
1 1 1 1 2 2 2 2 1 3 3 2 3
1 1 1 1 1 2 2 2 2 3 3 2 3
1 1 1 1 1 2 2 2 2 2 3 3 3
进行调换的总次数: 4
小结
刚开始写文章,估计有很多不足的地方,欢迎大家留言或者私信我,我们共同进步,最后以上算法为作者自己的想法,可能哪里有问题或者bug,可以提出来我们大家一起优化。