单调队列 POJ2823

七月 8th, 2011 No Comments »

看这个问题:An array of size n ≤ 106 is given to you. There is a sliding window of size k which is moving from the very left of the array to the very right. You can only see the k numbers in the window. Each time the sliding window moves rightwards by one position.Your task is to determine the maximum and minimum values in the sliding window at each position.

也就是有一个数列a,要求你求数列b和c,b[i]是a[i]…a[i+w-1]中的最小值,c[i]是最大值。如果a是1,3,-1,-3,5,3,6,7,则b为-1,-3,-3,-3,3,3,c为3,3,5,5,6,7。

这个问题相当于一个数据流(数列a)在不断地到来,而数据是不断过期的,相当于我们只能保存有限的数据(sliding window中的数据,此题中就是窗口的宽度w),对于到来的查询(此题中查询是每时刻都有的),我们要返回当前滑动窗口中的最大值\最小值。注意,元素是不断过期的。

解决这个问题可以使用一种叫做单调队列的数据结构,它维护这样一种队列:

a)从队头到队尾,元素在我们所关注的指标下是递减的(严格递减,而不是非递增),比如查询如果每次问的是窗口内的最小值,那么队列中元素从左至右就应该递增,如果每次问的是窗口内的最大值,则应该递减,依此类推。这是为了保证每次查询只需要取队头元素。

b)从队头到队尾,元素对应的时刻(此题中是该元素在数列a中的下标)是递增的,但不要求连续,这是为了保证最左面的元素总是最先过期,且每当有新元素来临的时候一定是插入队尾。

满足以上两点的队列就是单调队列,首先,只有第一个元素的序列一定是单调队列。

那么怎么维护这个单调队列呢?无非是处理插入和查询两个操作。

对于插入,由于性质b,因此来的新元素插入到队列的最后就能维持b)继续成立。但是为了维护a)的成立,即元素在我们关注的指标下递减,从队尾插入新元素的时候可能要删除队尾的一些元素,具体说来就是,找到第一个大于(在所关注指标下)新元素的元素,删除其后所有元素,并将新元素插于其后。因为所有被删除的元素都比新元素要小,而且比新元素要旧,因此在以后的任何查询中都不可能成为答案,所以可以放心删除。

对于查询,由于性质b,因此所有该时刻过期的元素一定都集中在队头,因此利用查询的时机删除队头所有过期的元素,在不含过期元素后,队头得元素就是查询的答案(性质a),将其返回即可。

由于每个元素都进队出队一次,因此摊销复杂度为O(n)。

POJ2823就是上面描述的那道题。

下面是代码,很无聊的是这道题竟然卡scanf和printf,如果用cin,cout就会超时,觉得有点无聊。

   1:  #include <iostream>
   2:  #include <vector>
   3:   
   4:  using namespace std;
   5:   
   6:  struct elem 
   7:  {
   8:      int v;
   9:      int p;
  10:  };
  11:   
  12:  int main()
  13:  {
  14:      int l,w;
  15:      cin>>l>>w;
  16:   
  17:      elem* q1 = new elem[l+w];
  18:      elem* q2 = new elem[l+w];
  19:   
  20:      int num,max;
  21:   
  22:      int head1 = 0,tail1 = 0;
  23:      int head2 = 0,tail2 = 0;
  24:      cin>>num;
  25:      q1[tail1].v = num;
  26:      q1[tail1].p = 0;
  27:   
  28:      q2[tail2].v = num;
  29:      q2[tail2].p = 0;
  30:   
  31:      vector<int> ans1,ans2;
  32:   
  33:      for (int t = 1;t < w;t++)
  34:      {
  35:          scanf("%d",&num);
  36:   
  37:          while (head1 <= tail1 && q1[tail1].v >= num)
  38:              --tail1;
  39:          q1[++tail1].v = num;
  40:          q1[tail1].p = t;
  41:   
  42:          while (head2 <= tail2 && q2[tail2].v <= num)
  43:              --tail2;
  44:          q2[++tail2].v = num;
  45:          q2[tail2].p = t;
  46:      }
  47:   
  48:      ans1.push_back(q1[head1].v);
  49:      ans2.push_back(q2[head2].v);
  50:   
  51:      for (int t = w;t < l;t++)
  52:      {
  53:          scanf("%d",&num);
  54:   
  55:          {
  56:   
  57:              while (head1 <= tail1 && q1[tail1].v >= num)
  58:                  --tail1;
  59:              q1[++tail1].v = num;
  60:              q1[tail1].p = t;
  61:   
  62:              while (t - q1[head1].p >= w)
  63:                  ++head1;
  64:   
  65:              ans1.push_back(q1[head1].v);
  66:          }
  67:   
  68:          {
  69:              while (head2 <= tail2 && q2[tail2].v <= num)
  70:                  --tail2;
  71:              q2[++tail2].v = num;
  72:              q2[tail2].p = t;
  73:   
  74:              while (t - q2[head2].p >= w)
  75:                  ++head2;
  76:   
  77:              ans2.push_back(q2[head2].v);
  78:          }    
  79:      }
  80:   
  81:      delete []q1;
  82:      delete []q2;
  83:   
  84:      for (vector<int>::iterator it = ans1.begin();it != ans1.end();it++)
  85:      {
  86:          printf("%d",*it);
  87:          if (it != ans1.end()-1)
  88:              printf(" ");
  89:          else
  90:              printf("\n");
  91:      }
  92:   
  93:      for (vector<int>::iterator it = ans2.begin();it != ans2.end();it++)
  94:      {
  95:          printf("%d",*it);
  96:          if (it != ans2.end()-1)
  97:              printf(" ");
  98:          else
  99:              printf("\n");
 100:      }
 101:  }

编程之美 3.8 求二叉树中节点的最大距离 3.9 重建二叉树

六月 11th, 2011 No Comments »

3.8 题意为:将一棵二叉树看成一个图,求树上两点之间的距离。距离定义为两点之间边的条数,如下图的二叉树中,节点8和5之间的距离为3。

3.9题意为已知二叉树的前序遍历结果和中序遍历结果,要求重建二叉树。之所以在这里说3.9,是因为解第八题的时候为了处理输入建成二叉树,实现了一个重建二叉树的函数。
Continue reading »

编程之美2.21 什么样的数不能写成连续N个自然数之和

六月 9th, 2011 No Comments »

有些数可以写成连续N(>1)个自然数之和,比如14=2+3+4+5;有些不能,比如8.那么如何判断一个数是否可以写成连续N个自然数之和呢?这是这一节的基本问题。

一个数M若可以写成以a开头的连续n个自然数之和,则M=a+(a+1)+(a+2)+…+(a+n-1)=n*a+n*(n-1)/2,要求a!=0,否则就是以a+1开头的连续n-1个整数了,也就是要求(M-(n+n*(n-1)/2))%n==0,即(M-(n*(n+1)/2))%n==0,这样就很容易判断一个数可不可以写成连续n个自然数的形式了,遍历n=2…sqrt(M)*2,还可以输出所有解。

第二个问题是什么样的数可以写成连续n个自然数之和,什么样的数不能?

通过编程实验发现,除了2^n以外,其余所有数都可以写成该形式。下面说明为什么。

若数M符合条件,则有M=a+(a+1)+(a+2)+…+(a+n-1)=(2*a+n-1)*n/2,而2*a+n-1与n肯定一个为奇数一个为偶数,即M一定要有一个奇数因子,而所有2^n都没有奇数因子,因此肯定不符合条件。

再证明只有M有一个奇数因子,即M!=2^n,M就可以写成连续n个自然数之和。假设M有一个奇数因子a,则M=a*b。

1)若b也是奇数,只要b-(a-1)/2>0,M就可以写成以b-(a-1)/2开头的连续a个自然数;将这条结论里的a和b调换,仍然成立。15=3*5=1+2+3+4+5=4+5+6.

2)若b是偶数,则我们有一个奇数a和一个偶数b。

2.1)若b-(a-1)/2>0,M就可以写成以b-(a-1)/2开头的连续a个自然数。24=3*8=7+8+9.

2.2)若(a+1)/2-b>0,M就可以写成以(a+1)/2-b开头的连续2*b个自然数。38=19*2=8+9+10+11.

上述两个不等式必然至少有一个成立,所以可以证明,只要M有一个奇数因子,就一定可以写成连续n个自然数之和。

第三个问题是,在64位整数范围内,能写成最多种连续n个自然数之和的数是哪个?

对于数M,考察其所有奇数因子,是否满足上述的三个不等式,每满足一个不等式,就可以有一种用连续n个自然数相加将其表示的方法,当然不可能三个都满足,因为b不可能同时为奇数和偶数。

卡特兰数

一月 1st, 2011 3 Comments »

算法课最后一节讲到了卡特兰数,总结和学到了很多以前不知道的东西。

卡特兰数的递推公式是F(n)=∑(k=1…n){F(k-1)*F(n-k)}=∑(k=0…n-1){F(k)*F(n-k-1)}
一般性公式为F(n)=C(2n,n)/(n+1)

可以描述的问题有
1、n个元素的二叉查找树有多少种。
2、n*n棋盘从左下角走到右上角而不穿过主对角线的走法。
3、2n个人排队买票问题,票价50,n个人拿50元,n个人拿100元,售票处无零钱,能顺利卖票的所有排队方式。
4、n个元素全部入栈并且全部出栈的所有可能顺序。

这些问题的答案都是卡特兰数F(n)。但是很明显可以看出后三个问题是同质的。
都可以抽象成2n个操作组成的操作链,其中A操作和B操作各n个,且要求截断到操作链的任何位置都有:A操作(向右走一步、收到50元、元素入栈)的个数不少于B操作(向上走一步、收到100元找出50元、元素出栈)的个数。故问题2、3、4其实是同一个问题。

下面先证明问题2、3、4和问题1同解,再证明问题2、3、4的解是F(n)=C(2n,n)/(n+1),从而证明问题1的解也是F(n)=C(2n,n)/(n+1)。 Continue reading »

理解动态规划

十二月 25th, 2010 No Comments »

看了《算法导论》上对动态规划的讲解,觉得自己对动态规划的理解又进了一步,之前在读到《算法之道》相关章节时就有这感觉,但是仍然不敢说自己已经完全掌握了动态规划,只是比以前又透彻了一些,说说自己新的理解,其实就是复述一下算法导论上的内容而已。

0.两个例子

装配线调度问题

一个产品要经过N道工序,有两条装配链提供着N道工序,在任何一道工序i时产品都可以选择在两条线上的一条进行加工,在装配线1上加工工序i的时间 为a[i][1],装配线2上类似。在同一条装配线上前进不花时间,跳转到另一条装配线上需要一定的时间t[1][i](从装配线1的工序i跳转到装配线 2的工序i+1所花费的时间)和t[2][i](类似)。

问题是求解一个产品最快完成所有工序所需的时间。

动态规划解法:

我们用dp[i][j]存放在第j条装配线完成第i到工序所用的最少时间,其中i=1…N,j=1,2。因为要么是从同条装配线的上一道工序而 来,要么是从另一条装配线的上一道工序跳转而来,而最少时间应该是这两种情况里较少的那种。注意应该认为dp[i-1][1]和dp[i-1][2]已经 求解完毕,或者说不要关心它们,只要认为它们存放的是正确答案就可以了,为何可以如此,见后面。则有:

dp[i][1] = min(dp[i-1][1]+a[i][1],dp[i-1][2]+t[i-1][2]+a[i][1])

dp[i][2] = min(dp[i-1][1]+t[i-1][1]+a[i][2],dp[i-1][2]+a[i][2])

这样min(dp[N][1],dp[N][2])就是想要的答案。

矩阵链乘法问题

两个矩阵A(lxm)*B(mxn)相乘所需要的代价是O(lmn),而相乘得到的矩阵式C(lxn)型的,l和n作为矩阵的行列数量仍然保存在了 矩阵链中,因此对于一个矩阵链的乘法来说,做乘法的顺序不同,所花费的时间就可能相差很多,矩阵链乘法问题就是对于一个给定的矩阵链相乘,求解最少的计算 时间(如何安排乘法的计算顺序)。

动态规划解法:

我们用dp[i][j]表示矩阵链中从第i个乘到第j个所需的最少时间,用r[i]和c[i]分别表示第i个矩阵的行数和列数。有:

dp[i][j]=min(k)(dp[i][k]+dp[k+1][j]+r[i]*c[k]*c[j])

注意c[k]==r[k+1]

即,i到j这一部分矩阵链的最优计算一定是在某处k(i<=k<j)将矩阵链分成两部分,通过先计算i到k和k+1到j这两条矩阵链再将这两个矩阵相乘得到的。

1.什么是动态规划

dynamic programming这里的programming指的是查表,表里存放的是之前求解过的子问题的解。动态规划就是动态查表,即在求解的过程中的每一步 都需要查表来得到对当前子问题求解所需的信息。如在装配线问题上dp[i][1]就分别查表找到了dp[i-1][1]和dp[i-1][2];矩阵乘法 链问题上dp[i][j]的求解查表找到了对应于不同k值的dp[i][k]和dp[k+1][j]。

正因为存放在了表里,所以不用再次求解,才达到了较高的效率,将传统回溯解法指数的复杂度降低为多项式程度,关于复杂度后面还会说到。

2.最优子结构

一个问题要想用动态规划的方法来解决,必须具有最优子结构,至于为什么具有最优子结构的问题就可以用动态规划来解决,后面说。现在说说什么叫最优子结构。

最优子结构是指一个问题的解是一个选择过程,可能是二选一(装配线问题选择从前一道工序的哪条线上过来)、可能是多选一(矩阵链决定从哪里断开,即 先做哪一处乘法)。假设作出了正确的选择后(这其实是需要子问题信息来做选择依据的),原问题就退化成一个或多个子问题(在装配线问题中,退化成所选择的 那条装配线上完成第i-1道工序需要的最少时间,这是一个子问题;在矩阵乘法链问题中,退化成两条矩阵链i到k和k+1到j的最优求解问题,这是两个子问 题),所谓子问题指的是与原问题同构但规模较小的问题。

光有这个性质还不叫具有最优子结构,最优子结构的另一个要求是原问题的最优解必须包含子问题的最优解。在验证这一点时,可以通过剪贴+反证法的思 路,即如果该解法用到的子问题的解不是最优的,那么以最优的解代替之一定可以得到该问题的一个更好的解。还是通过例子说,装配线问题中,不管加工工序i的 最优选择是从哪条线过来,所用到的在该线上加工工序i-1的时间一定需要是最优的,否则用最优的替换之可得到对于工序i的一个更优的解。

有了这两条性质——1.做选择后问题退化为一个或几个子问题、2.问题的最优解包含子问题的最优解,一个问题就具备了最优子结构。

3.为什么高效、如何求解

如果一个问题具有最优子结构的第一条性质,就可以使用分而治之的思想求解,比如递归。通常做法是将问题分解,然后对于分解出现的各个子问题分别递归求解,如果这种分解是多分枝的,那么复杂度应该是指数级的。

同递归思想一样,动态规划也需要对所有子问题都全部求解才能得到最终的答案,但是动态规划不愿意单纯地像递归那样做,而希望能够通过一些手段只做必要的工作,多余的一点都不想做。

动态规划是基于如下两点观察:

1)某个子问题的求解只和规模更小的子问题求解有关,而与与其同规模或者更大规模的子问题求解无关。

2)某个子问题的求解只用到其子问题的最优解,也就是说对于一个子问题来说,只有其最优解是有价值的。

通过观察1)我们知道只要在求解所有子问题时控制子问题的求解顺序按照子问题规模递增来进行,就能保证求解任意子问题时,它所依赖的规模较小的子问题都已经求解完毕。

通过观察2)我们知道只有最优解是有意义的,因此我们将最优解存在表里以供后面用到时查找,就可以保证某个子问题只被求解一遍,而且有了上一个观察,在求解某个子问题时,可以保证它所需要的表格内容都已经准备好了。

所以动态规划的解法已经呼之欲出了。

首先证明一个问题具有最优子结构,然后发现描述该问题的子问题应该是什么形式的(装配线问题中的dp[i][1]、dp[i][2]和矩阵链乘法问题中的dp[i][j]),按照子问题规模递增的顺序求解所有子问题,最后得到整个问题的解。

而,这里的关键就是找出子问题的形式,和子问题在经过选择后如何向更小规模的子问题退化。

下面给出两个例子的伪代码

装配线问题

dp[1][1] = a[1][1];
dp[1][2] = a[1][2];
for (int i = 2;i <= n;i++)
{
	dp[i][1] = min(dp[i-1][1]+a[i][1],dp[i-1][2]+t[i-1][2]+a[i][1]);
	dp[i][2] = min(dp[i-1][1]+t[i-1][1]+a[i][2],dp[i-1][2]+a[i][2]);
}

矩阵链乘法

for (int i = 1;i <= n;i++)
{
	dp[i][i] = 0;
}
for (int l = 2;l <= n;l++)
{
	for (int i = 1;i <= n-l;i++)
	{
		int j = i+l-1;
		for (int k = i;k < j;k++)
		{
			dp[i][j] = min(dp[i][j],dp[i][k]+dp[k+1][j]+r[i]*c[k]*c[j]);
		}
	}
}

4.动态规划的时间复杂度由什么决定

动态规划的过程就是求解所有子问题的过程,因此首先复杂度由子问题的个数决定,而子问题的个数又有子问题的形式所决定。在装配线问题中子问题形式是 dp[i][1 or 2],其中只有i是变化的,因此子问题个数为O(n);在矩阵链乘法问题中,子问题形式是dp[i][j],i和j都在变化,因此子问题个数是 O(n^2)。另外由于前面所提到的每一个子问题都是一个选择,可能是二选一,可能是多选一,而要解答此子问题就要考察所有的选项,因此每一个子问题的求 解时间还和面临的选择个数有关,因此复杂度应该是子问题个数*每个子问题面临选择数。这样,装配线问题的时间复杂度O(n),矩阵链乘法问题的时间复杂度 为O(n^3),正如代码中所示。

网络最大流、二分图最大匹配、POJ2536

十二月 14th, 2010 No Comments »

通常我们把一个有向无环图边上的权值看做两个节点之间的距离,在这个模型下的问题有各种最短路问题。如果将边上的权值不看做距离而看做两点间的容量(比如两个城市之间一天的运输能力),这样的有向无环图就叫做流网络,对应的问题就是求最大流——单位时间内通过网络的最大容量(比如工厂所在城市一天最多可以生产多少才能全部经过一个流网络运到仓库所在城市而不造成中途的淤积)。

流网络只有一个源点和一个汇点。关于定义就不说了,算法导论上很清楚。有一点要说的是,如果一个流网络有多个源点和多个汇点,可以通过增加一个到所有现有源点的容量都无穷的超级源点和一个所有汇点到其容量都无穷的超级汇点转化成只有一个源点和一个汇点的流网络。

求流网络的最大流算法是Ford-Fulkerson算法。首先定义一条边的残留容量是容量减去现有流,由残留容量标示的网络称为残留网络。再定义增广路径是残留网络中从源点s到汇点t的一条路径。Ford-Fulkerson算法是一个迭代过程。每次在残留网络中查找一条增广路径,然后把这条增广路径中最小的一段残留容量加到增广路径各条边的流上,更新残留网络继续迭代,直到找不到增广路径为止。此时退出迭代,现有流就是最大流。

二分图有两部分节点L和R,各部分内部节点之间没有边,即每条边的两个节点都一定分属这两部分,二分图的一个匹配是找到这样一组边,使得每个节点都只有至多一条边与其相连。

二分图的最大匹配问题可以转化为网络最大流问题。增加一个到所有L中顶点容量均为1的源点s和一个所有R中顶点到其容量均为1的汇点t,所有L到R中的边容量也设置为1,现在查找此网络流的最大流就等同于求此二分图的最大匹配。证明过程见算法导论。

POJ2536就是一道二分图最大匹配的应用。题目给出n个老鼠的坐标和m个鼠洞的坐标,如果一只老鼠能在一定时间内到达一个鼠洞,就能躲过老鹰。所以如果某个老鼠与某个鼠洞之间距离小于某值,它俩之间就存在一条边,否则不存在,这是一个明显的二分图。而要求有多少老鼠能躲过老鹰,自然是求此二分图的最大匹配。

代码如下: Continue reading »

一道面试题,关于在数组里找一些数

十一月 23rd, 2010 2 Comments »

在一个int数组里查找这样的数,它大于等于左侧所有数,小于等于右侧所有数。
直观想法是用两个数组a、b。a[i]、b[i]分别保存从前到i的最大的数和从后到i的最小的数,这需要两次遍历,然后再遍历一次原数组,将所有data[i]>=a[i-1]&&data[i]<=b[i]的data[i]找出即可。

给出这个解答后,面试官有要求只能用一个辅助数组,且要求少用一次遍历。想了半天没想出来,后来在面试官的提示下得出解,今天实现了一下。

见代码:

#include <iostream>
 
using namespace std;
 
#define max(a,b) a>b?a:b
#define min(a,b) a<b?a:b
 
void findNum(int data[],int len)
{
	int* a = new int[len];
	int* b = new int[len];
	a[len-1] = data[len-1];
	for (int i = len-2;i > 0;i--)
	{
		a[i] = min(data[i],a[i+1]);
	}
 
	if (data[0] <= a[1])
	{
		cout<<"0:"<<data[0]<<endl;
	}
 
	a[0] = data[0];
	if (data[1] >= a[0] && data[1] <= a[2])
	{
		cout<<"1:"<<data[1]<<endl;
	}
 
	for (int i = 1;i < len-2;i++)
	{
		a[i] = max(data[i],a[i-1]);
		if (data[i+1] >= a[i] && data[i+1] <= a[i+2])
		{
			cout<<i+1<<":"<<data[i+1]<<endl;
		}
	}
	a[len-2] = max(data[len-2],a[len-3]);
	if (data[len-1] >= a[len-2])
	{
		cout<<len-1<<":"<<data[len-1]<<endl;
	}
}
 
int main()
{
	int data[10] = {1,3,2,4,6,7,5,9,11,10};
	findNum(data,10);
}

二叉树先序、中序、后序遍历的非递归实现

十一月 23rd, 2010 2 Comments »

在网上看了一些用非递归实现先序中序后序遍历二叉树的代码,都很混乱,while、if各种组合嵌套使用,逻辑十分不清晰,把我也搞懵了。想了大半天,写了大半天,突然开了窍,实际上二叉树的这三种遍历在逻辑上是十分清晰的,所以才可以用递归实现的那么简洁。既然逻辑如此清晰,那么用非递归实现也应该是清晰的。

自认为自己的代码比网上搜到的那些都清晰得多,好理解得多。

稍微解释一下:

先序遍历。将根节点入栈,考察当前节点(即栈顶节点),先访问当前节点,然后将其出栈(已经访问过,不再需要保留),然后先将其右孩子入栈,再将其左孩子入栈(这个顺序是为了让左孩子位于右孩子上面,以便左孩子的访问先于右孩子;当然如果某个孩子为空,就不用入栈了)。如果栈非空就重复上述过程直到栈空为止,结束算法。

中序遍历。将根节点入栈,考察当前节点(即栈顶节点),如果其左孩子未被访问过(有标记),则将其左孩子入栈,否则访问当前节点并将其出栈,再将右孩子入栈。如果栈非空就重复上述过程直到栈空为止,结束算法。

后序遍历。将根节点入栈,考察当前节点(即栈顶节点),如果其左孩子未被访问过,则将其左孩子入栈,否则如果其右孩子未被访问过,则将其右孩子入栈,如果都已经访问过,则访问其自身,并将其出栈。如果栈非空就重复上述过程直到栈空为止,结束算法。

其实,这只不过是保证了先序中序后序三种遍历的定义。对于先序,保证任意一个节点先于其左右孩子被访问,还要保证其左孩子先于右孩子被访问。对于中序,保证任意一个节点,其左孩子先于它被访问,右孩子晚于它被访问。对于后序,保证任意一个节点的左孩子右孩子都先于它被访问,其中左孩子先于右孩子被访问。如是而已。

代码里应该体现得比较清楚。这里不光给出了非递归版本,也给出了递归版本。
Continue reading »

快速排序的几种分割策略

十月 23rd, 2010 No Comments »

快速排序的基本思想是在待排序数据中选取一个pivot数据,将剩余数据按照与pivot的比较结果分割成两份,小的放在pivot左边,大的放在pivot右边(假设是升序排序),这样pivot已经在正确的位置上了。然后对pivot的左右待排数据继续递归调用上述过程。到待排数据长度<=1的时候递归返回,当所有递归都返回,就完成了整个数据的排序。

如何分割是快速排序的关键所在。这里提三种不同的分割方式。

所谓分割就是要对所有数据的位置进行重新安排,使得所有比pivot小的数据位于pivot左侧,所有比pivot大的数据位于pivot右侧。

第一种策略采用空位vacancy的概念。用low和high标记未检测区域的边界,起初将E[low]作为pivot保存在本地变量中,这样E[low]成为一个空位,用vacancy标记。为了找到一个元素放在这个空位里(应该是小于pivot的),而且为了尽可能远地移动它(通过一次比较而将元素移动尽可能远的距离可能争取到更高的排序效率),我们从high开始向左查找第一个大于pivot的数,找到后将它移动到vacancy里,这样该处留下了一个空位标记为highVac,同时更新high=highVac,因为右侧的数据都已经检查过了,都比pivot大,在接下来的过程中不需要在移动。接下来从low处开始向右查找第一个比pivot大的元素(同样,为了将它放在刚空出来的highVac中,而且希望移动尽可能远的距离),找到后将其移动到highVac中,将新空出来的空位用lowVac标记,并且更新low=lowVac。至此,我们完成了一次迭代,更新了low和high,使得未被检测的区域变窄了,而且low左侧的数据都比pivot小,high右侧的数据都比pivot大,low处为一个空位,这和迭代开始的条件是一致的,因此迭代可以进行下去。直到某一次查找大于或小于pivot元素的过程中一直到对面的空位都没有找到,则说明所有元素的位置都已经被安排好,这时候再把保存在本地变量中的pivot放到空位里,就完成了分割。

第二种策略与第一种类似,也是从两侧向中间推进,但是没有使用空位的概念,而是采用交换的方法。将pivot放在E[low]处,从E[low+1]和E[high]分别开始向中间查找第一个大于pivot的和第一个小于pivot的数据,找到后将两者进行交换,然后继续向中间推进,直到两个方向的查找相遇。然后交换E[low]和最后一个小于pivot的数据,就完成了分割。

第三种策略是单向的策略,基本思想是时刻更新标记小于pivot的最后一个数据位置m初始为low-1。在按照下标i遍历的过程中,如果E[i]>pivot那么一切正常,m不需要更新,E[i]在m之右;如果E[i]<pivot,那么说明m需要增一,然后需要将E[i]与E[m]交换,这样就维持了m标记最后一个比pivot小的数据这样一个不变式。最后遍历完成之后将E[m]与位于队头的pivot进行交换,即完成了分割。

在递减序列经过旋转得到的序列中查找某个数

十月 22nd, 2010 1 Comment »

在CSDN算法版看到的题目,据说是MS的笔试题。

题目是这样的:

一个数组是由一个递减数列左移若干位形成的,比如{4,3,2,1,6,5}是由{6,5,4,3,2,1}左移两位形成的,在这种数组中查找某一个数。

NASH说了个想法,很直观,用O(logN)找到原序列的起点,再用O(logN)找到要找的点(做一些下标控制)。应该可以,我没有实现,BAD说这叫化未知问题为已知问题,嗯,不错。

我自己写了另一个解法,单纯的二分查找,只不过判断该左转还是右转的时候需要谨慎一些,把所有情况都考虑到自然就没有问题了。

在此序列不断二分的过程中,由于原序列是一个递减序列经过旋转得到的,将它从任何位置分开,都会得到两个序列,其中一个是递减序列,另一个可以通过一个递减序列通过旋转得到。这样在不断地二分查找时,我们处理的序列子片段要么就是一个旋转后递减序列,要么就是一个纯递减序列,而无论是前者还是后者,在继续分成两个片段时,至少有一个纯递减序列(可能两个都是,如果之前的序列片段就是纯递减序列的话)。这样我们可以保证能找到一个片段是纯递减序列(if(data[i]>=data[j])),然后判断我们要找的数是否在这个片段中(这是很直观的,if(data[i]< =num&&num<=data[j])),如果在则继续在此片段中查找,否则说明在另一个序列中,则递归在其中查找。

代码如下:
Continue reading »