Google面试一道排序题
2015-09-05 23:33
375 查看
今年有同学在面试Google实习生的时候碰到一道题目,大半年过去了,等到现在才发出来,原意也是不希望泄露题库,可以互相交流。
题目是:把一组不重复的数组排序,希望最后达到计数项都小于相邻的偶数项,也就是说要达到a(2i - 1) > a(2i) < a(2i + 1)的情况。
思路一:乍一看,这个题目可以用很简单的普通排序做出来,用快排或其他排序算法可以达到O(nlgn)的时间复杂度,然后再把大的那一部分全部都填入偶数项,小的那一部分全都填入奇数项,即可达到目标。
代码如下:
虽然这样可以,但是这样肯定不能满足面试官要求。
如何才能用更好更快的方法呢?
思路二:
可以不用先全部排序吗,其实从上面的例子中,我们可以看到,我们是要把数据分成大的和小的两部分,相当于找到中位数,那么我们只要找到第k大个数,其中k为n / 2,把小于第k大的数全都放在奇数项,把大于等于第k大的数全都放在偶数项。就能达到目标。
其中用到找到第k大数的算法了,是很经典的算法了,我参考了http://www.cnblogs.com/luxiaoxun/archive/2012/08/06/2624799.html)
其实它利用了快排的思想(更准确得说快排和找第k大数都用了相同的思想),用前面一个数把数组分成小于它和大于它的两部分,当我们计数后发现前一部分或后一部分少了的时候,再递归对后一部分和前一部分找第k'(一个换算过程)大个数。
虽然,找第k大数算法的时间复杂度是O(n),不过还是略微麻烦,思路三会说到。
代码如下:
思路三:
这里,就有一个“浪费”的思想,其实,题目中只要求我们排成一小一大一小就好,没有必要把大的位置是最大的,就是说1 3 2 6 5 8 4 9 0 7也是可以的,其中3不是最大的大一部分,但是同样可以在偶数项,所以可以知道先全局排序是一种“浪费”,无论是思路1还是思路2,相当于我们先用比较大的力气使它达到了更有序的状态,然后再把它调整回相对无须的状态,得不偿失。
最好是直接就奔着那个目标去,杀鸡不用宰牛刀,把力气花的恰到好处,刚好跑到山顶最好。
所以,我的思路是
(1)先把数组分成奇数项-偶数项,奇数项-偶数项……两个两个分,相当于a奇-a偶,a奇-a偶,……
(2)先对其进行一次排列,按照递增的顺序,使其达到所有的a奇<a偶。
(3)然后在对其分成a奇,a偶-a奇,a偶-a奇……这样的顺序,
(4)对其进行一次排序,按照递减的顺序,使其达到所有a偶>a奇的效果,
这样我们就能满足所有的a奇都小于相邻a偶的效果。
证明如下:
假如有a奇1,a偶1,a奇2,a偶2这四个数,当经过(1)(2)步骤后,会有a奇1<a偶1,a奇2<a偶2,(如果不满足对应的奇和偶会交换)
然后当经过(3)和(4)时,
(a)假设a偶1>a奇2成立,那么不要动,就有a奇1<a偶1>a奇2<a偶2
(b)假设a偶1<a奇2成立,就形成了a奇1<a偶1<a奇2<a偶2,(注意到这里a奇1<a奇2)那么当我们把a偶1和a奇2调换后,肯定有a奇1<a偶1>a奇2<a偶2
所以,我们没有要求所有的奇数项一定是数组中绝对小的那一部分,所以是符合不“浪费”的思想的,也就是其无序状态最大。
相当于思路一是全局排序,思路三是局部排序,思路二是介于两者之间的。
代码如下:
P.S
从这里可以看到,即使是很简单的一道排序题,都需要花费心思去思考,到底什么样的算法才是最适合它的,八面玲珑的普适算法虽然可以信手拈来,但是,不一定是针对具体一道题的利器。对每一道题的特点分析,才是王道。
——Apie陈小旭
题目是:把一组不重复的数组排序,希望最后达到计数项都小于相邻的偶数项,也就是说要达到a(2i - 1) > a(2i) < a(2i + 1)的情况。
思路一:乍一看,这个题目可以用很简单的普通排序做出来,用快排或其他排序算法可以达到O(nlgn)的时间复杂度,然后再把大的那一部分全部都填入偶数项,小的那一部分全都填入奇数项,即可达到目标。
代码如下:
#include<iostream> #include<vector> #include<algorithm> using namespace std; vector<int> googleSort(vector<int> vv){ sort(vv.begin(), vv.end());//用标准库中排序递增排序vv,时间复杂度O(nlgn) vector<int>ret(vv.size()); int idx = 0;//从前向后取vv中的值 for (int i = 0; i < ret.size(); i += 2){//把小的部分全都放在奇数项,时间复杂度O(n) ret[i] = vv[idx ++]; } idx = vv.size() - 1;//从后向前取vv中的值 for (int i = 1; i < ret.size(); i += 2){//把大的部分全都放在偶数项 ret[i] = vv[idx--]; } return ret; } void print(vector<int> vv){//格式化输出 for (int i = 0; i < vv.size(); ++i){ if (i)cout << " " << vv[i]; else cout << vv[i]; } cout << endl; } int main(){ vector<int>V = { 1, 7, 9, 0, 5, 3, 2, 6, 8, 4 };//假设有这些数据 vector<int>res = googleSort(V); print(res); return 0; }运行如下:
虽然这样可以,但是这样肯定不能满足面试官要求。
如何才能用更好更快的方法呢?
思路二:
可以不用先全部排序吗,其实从上面的例子中,我们可以看到,我们是要把数据分成大的和小的两部分,相当于找到中位数,那么我们只要找到第k大个数,其中k为n / 2,把小于第k大的数全都放在奇数项,把大于等于第k大的数全都放在偶数项。就能达到目标。
其中用到找到第k大数的算法了,是很经典的算法了,我参考了http://www.cnblogs.com/luxiaoxun/archive/2012/08/06/2624799.html)
其实它利用了快排的思想(更准确得说快排和找第k大数都用了相同的思想),用前面一个数把数组分成小于它和大于它的两部分,当我们计数后发现前一部分或后一部分少了的时候,再递归对后一部分和前一部分找第k'(一个换算过程)大个数。
虽然,找第k大数算法的时间复杂度是O(n),不过还是略微麻烦,思路三会说到。
代码如下:
#include<iostream> #include<vector> #include<algorithm> using namespace std; int partition(vector<int>& vv,int start, int end){//为简化算法,这里没有使用随机挑选k //划分函数,其中start表示开始的位置,end表示结束位置 int idx_s = start; int x = vv[start]; end = end + 1; while (1){ while (vv[++start] > x); while (vv[--end] < x); if (start >= end)break; int tmp = vv[start]; vv[start] = vv[end]; vv[end] = tmp; } vv[idx_s] = vv[end]; vv[end] = x; return end; } int findKth(vector<int>& vv,int start,int end,int k){//这里多传递一个k的值 if (start == end) return vv[start]; int i = partition(vv, start, end); int j = i - start + 1; if (k == j)return vv[i]; if (k < j) return findKth(vv, start, i - 1, k); else return findKth(vv, i + 1, end, k - j); } vector<int> googleSort(vector<int> vv){ int kth = findKth(vv, 0, vv.size() - 1, vv.size() / 2); vector<int>ret(vv.size()); int i = 0, j = 1;//奇数项和偶数项的下标 for (int idx = 0; idx < vv.size(); ++idx){//遍历vv if (vv[idx] < kth){//把小于kth的数全都放在奇数项,时间复杂度O(n) ret[i] = vv[idx]; i += 2; } else{//把大于kth的数全都放在偶数项 ret[j] = vv[idx]; j += 2; } } return ret; } void print(vector<int> vv){//格式化输出 for (int i = 0; i < vv.size(); ++i){ if (i)cout << " " << vv[i]; else cout << vv[i]; } cout << endl; } int main(){ vector<int>V = { 1, 7, 9, 0, 5, 3, 2, 6, 8, 4 };//假设有这些数据 vector<int>res = googleSort(V); print(res); return 0; }运行结果如下:
思路三:
这里,就有一个“浪费”的思想,其实,题目中只要求我们排成一小一大一小就好,没有必要把大的位置是最大的,就是说1 3 2 6 5 8 4 9 0 7也是可以的,其中3不是最大的大一部分,但是同样可以在偶数项,所以可以知道先全局排序是一种“浪费”,无论是思路1还是思路2,相当于我们先用比较大的力气使它达到了更有序的状态,然后再把它调整回相对无须的状态,得不偿失。
最好是直接就奔着那个目标去,杀鸡不用宰牛刀,把力气花的恰到好处,刚好跑到山顶最好。
所以,我的思路是
(1)先把数组分成奇数项-偶数项,奇数项-偶数项……两个两个分,相当于a奇-a偶,a奇-a偶,……
(2)先对其进行一次排列,按照递增的顺序,使其达到所有的a奇<a偶。
(3)然后在对其分成a奇,a偶-a奇,a偶-a奇……这样的顺序,
(4)对其进行一次排序,按照递减的顺序,使其达到所有a偶>a奇的效果,
这样我们就能满足所有的a奇都小于相邻a偶的效果。
证明如下:
假如有a奇1,a偶1,a奇2,a偶2这四个数,当经过(1)(2)步骤后,会有a奇1<a偶1,a奇2<a偶2,(如果不满足对应的奇和偶会交换)
然后当经过(3)和(4)时,
(a)假设a偶1>a奇2成立,那么不要动,就有a奇1<a偶1>a奇2<a偶2
(b)假设a偶1<a奇2成立,就形成了a奇1<a偶1<a奇2<a偶2,(注意到这里a奇1<a奇2)那么当我们把a偶1和a奇2调换后,肯定有a奇1<a偶1>a奇2<a偶2
所以,我们没有要求所有的奇数项一定是数组中绝对小的那一部分,所以是符合不“浪费”的思想的,也就是其无序状态最大。
相当于思路一是全局排序,思路三是局部排序,思路二是介于两者之间的。
代码如下:
#include<iostream> #include<vector> #include<algorithm> using namespace std; vector<int> googleSort(vector<int> vv){ for (int i = 0, j = 1; i < vv.size() && j < vv.size(); i += 2, j += 2){//奇-偶,奇-偶比较 if (vv[i] > vv[j]) swap(vv[i], vv[j]); } for (int i = 1, j = 2; i < vv.size() && j < vv.size(); i += 2, j += 2){//偶-奇,偶-奇比较 if (vv[i] < vv[j]) swap(vv[i], vv[j]); } return vv; } void print(vector<int> vv){//格式化输出 for (int i = 0; i < vv.size(); ++i){ if (i)cout << " " << vv[i]; else cout << vv[i]; } cout << endl; } int main(){ vector<int>V = { 1, 7, 9, 0, 5, 3, 2, 6, 8, 4 };//假设有这些数据 vector<int>res = googleSort(V); print(res); return 0; }运行结果如下:
P.S
从这里可以看到,即使是很简单的一道排序题,都需要花费心思去思考,到底什么样的算法才是最适合它的,八面玲珑的普适算法虽然可以信手拈来,但是,不一定是针对具体一道题的利器。对每一道题的特点分析,才是王道。
——Apie陈小旭
相关文章推荐
- 黑马程序员--OC多态
- 程序员常去的14个顶级开发社区
- 程序员常去的14个顶级开发社区
- 黑马程序员-JAVA基础学习日记八——IO流的学习总结
- 黑马程序员----oc加强笔记----分类(Gategory)
- Java开发工程师职业发展图
- SQL面试积累
- 黑马程序员-Java基础:集合(Collection)
- 数组中只出现1次的两个数字(面试题)
- 常见的链表面试题大汇总:
- 管理层必学!刘备如何面试诸葛亮?
- Java面试题之一---------字符串截取(字节分配)(编码)
- 黑马程序员之IO字符流及缓冲器
- 找到一个重复元素 - 面试题
- 黑马程序员——Java中的面向对象
- 黑马程序员之Collection类
- 黑马程序员之Map集合以及Collections静态方法
- BAT面试
- 临睡前的十分钟,决定未来职场的高度
- 程序员的十个层次