有关快排和归并的一些题目
一、分治模式的完美诠释:归并排序
1、归并排序主要要解决合并的问题
//归并排序
//将数组A的q到r进行归并排序
public static void part1(int[] A,int p,int r){
if(p<r){
int mid=p+((r-p)>>1);//加减的优先级高于左右移
part1(A,p,mid);//对左边进行归并排序
part1(A,mid+1,r);//对右边进行归并排序
merge(A,p,mid,r);//将两遍合并起来
}
}
//合并函数
private static void merge(int[] A,int p,int mid,int r){
//先把A中的数据拷贝到helper中
int[] helper=new int[A.length];
for(int i=0;i<A.length;i++){
helper[i]=A[i];
}
int left=p;//左侧队伍的头部指针,指向待比较元素
int right=mid+1;//右侧队伍头部指针,指向待比较元素
int current=p;//原数组的指针,指向待填入数据的位置
while(left<=mid&&right<=r){
if(helper[left]<helper[right]){
A[current++]=helper[left];
left++;
}else{
A[current++]=helper[right];
right++;
}
}
if(left<=mid){
while(left<=mid){
A[current++]=helper[left++];
}
}else if(right<=r){
while(right<=r){
A[current++]=helper[right++];
}
}
}
二、快排和归并排序的一些题目
1、调整数组顺序使奇数位于偶数前面(并没有说奇数偶数要有序)
算法思想:用归并的合并思想或者快排的双向扫描法
//调整数组顺序使奇数位于偶数前面(并没有说奇数偶数要有序)
public static void part2(int[] A){
//利用归并排序中合并的方法
int[] B=new int[A.length];
for(int i=0;i<A.length;i++){
B[i]=A[i];
}
int left=0;//指向A数组左端
int right=A.length-1;//指向A数组右端
int current=0;//指向B数组
while(left<=right){
if(B[current]%2==0){
A[right--]=B[current++];
}else{
A[left++]=B[current++];
}
}
}
2、最快速率求出乱序数组中第k小的元素
第一种解法:先排序再找
第二种解法:按照快排的分区思想,按照三点中值法找出主元吗,进行一次分区,此时主元所在的位置就是排序后主元的位置,将k主元的序号对比,如果k比较小说明所找的数在主元的左侧,如果k比较大,那么所找的数就在主元右侧。
//最快速率求出乱序数组中第k小的元素(注意小标和第几个之间的关系)
//期望时间复杂度为o(n),最差情况是o(n^2)
public static int part3(int[] A,int p,int r,int k){
int q=partition(A,p,r);//获取主元的下标
int qk=q-p+1;//计算主元排第几
if(qk==k) return A[q];
else if(qk<k){
return part3(A,q+1,r,k-qk);//注意这里排第k-qk个
}else{
return part3(A,p,q-1,k);
}
}
//快排中的双向扫描法
public static int partition(int[] A,int p,int r){
int mid=(p+r)/2;
int temp;
if(((A[p]>A[mid])&&(A[mid]>A[r]))||((A[p]<A[mid])&&(A[mid]<A[r]))){
temp=A[p];
A[p]=A[mid];
A[mid]=temp;
}else if(((A[p]>A[r])&&(A[mid]<A[r]))||((A[p]<A[r])&&(A[mid]>A[r]))){
temp=A[p];
A[p]=A[r];
A[r]=temp;
}
int left=p+1;
int right=r;
while(left<=right){
if((A[left]<=A[p])&&(left<=right)){
left++;
}
if((A[right]>A[p])&&(left<=right)){
right--;
}
if(left<right){
temp=A[left];
A[left]=A[right];
A[right]=temp;
}
}
temp=A[right];
A[right]=A[p];
A[p]=temp;
return right;
}
3、超过一半的数字:数组中有一个数字出现的次数超过了数组长度的一半,找出这个数字。
法一:先排序,所找的数字一定在中间出现。(改变了原数组)
法二:用第二题中的解法,将k设置为A.length/2(改变了原数组)
法三:从右向左扫描数组,记下扫描的数值和出现次数,如果下一个与上一个相同,则次数加一;若下一个与上一个数值不同,次数清零。最后剩下的数值就是出现次数最多的。(不改变原数组)(很重要的一种方法)
第三种方法的实践:寻找发帖水王
有一个人不仅喜欢自己发帖,还会回复其他ID 发的每一个帖子。传闻水王发帖数目已经超过了贴子总数的一半。如果你有一个当前论坛上所有帖子(包括回帖)的列表,其中帖子作者的ID也在表中,你能快速地找出这个传说中的水王吗。
//超过一半的数字(法一)
public static void part41(int[] A){
Arrays.sort(A);
System.out.print(A[A.length/2]);
}
//超过一半的数字(法二) 消除法(不同的元素相互抵消,最后肯定会留下出现次数最多的元素)
public static void part42(int[] A){
//候选数,先定位第一个元素
int candidate=A[0];
int times=1;
for(int i=1;i<A.length;i++){
if(times==0){
candidate=A[i];
times=1;
continue;//***
}
if(candidate==A[i]){
times++;
}else{
times--;
}
}
System.out.print(candidate);
}
4、正好占数组一半的数字
算法思想:仍然用消除法,先统计数组中最后一个元素出现的次数,如果等于一半,那么该数字就是要找的数字;如果不足一半,那么在消除的时候就不用带上这个数字,那么最后剩下的就是要找的数字。(去除最后一个元素后,问题就和3中的法三一样了)
5、最小可用ID:在非负数组(乱序)中找到最小的可分配 id(从1开始编号),数据量1000000(即找到最小的缺席ID)
法一:先进行排序,再找第一个缺席的ID
法二:借助辅助空间,出现过得ID的位置设置为1,未出现的是0,最后找到第一个0即可。
法三:比较位于中间位置的值与其应该对应的序号,如果相同的话,说明左边没有空缺的ID,那么就在右边重复相同的动作;如果不相同的话,那么左侧一定有缺失的ID,在左边进行相同的操作。
三、算法题的五种解法
方法一:举例法
先列举出一些具体的例子,看看是否能发现其中的一般规则。
方法二:模式匹配法(见多识广)
将现有问题与相似问题作类比,看看是否能通过修改相关问题的解法来解决新问题。
方法三:简化推广法
分多步走。首先,我们会修改某个约束条件,比如数据类型或数据量,从而简化这个问题。接着,我们转而处理这个问题的简化版本。最后,一旦找到解决简化问题的算法,我们就可以基于这个问题进行推广,并试着调整简化版问题的解决方案,让它适用于这个问题的复杂版本。
方法四:简单构造法
先从基本的情况(比如n=1)来解决问题,一般只需记下正确的结果。得到n=2的情况。接下来,有了n=1和n=2的结果,就可以试着解决n=3的情况了。(实际上是一种递归的算法)
方法五:数据结构头脑风暴
快速过一下数据结构的列表,然后逐一尝试各种数据结构,一旦找到合适的数据结构,很多问题就能迎刃而解了。
//找最小可用ID,nlogn
public static int part51(int[] A){
Arrays.sort(A);//nlogn
int i=0;
while(i<A.length){
if(i+1!=A[i])
return i+1;
i++;
}
return i+1;
}
//找最小可用方ID法二,借用辅助空间,n
public static int part52(int[] A){
int[] helper=new int[A.length+1];//这样让下表值与对应的值相同
for(int i=0;i<A.length;i++){
if(A[i]<helper.length){//大于的一定不是最小的,即舍弃掉对结果无用的部分
helper[A[i]]=1;//有这个ID
}
}
for(int i=1;i<helper.length;i++){
if(helper[i]==0){
return i;
}
}
return helper.length;
}
//找最小可用ID法三:(分区,递归) n
public static int part53(int[] A,int l,int r){
if(l>r){
return l+1;
}
int midIndex=l+(r-l)>>1;//中间下标
int q=part3(A,l,r,midIndex-l+1);//找到中间位置元素的值
int t=midIndex+1;//期望值
if(q==t){//左侧紧密
return part53(A,midIndex+1,r);
}else{
return part53(A,l,midIndex-1);
}
}
6、合并有序数组(不额外创建数组):给定两个排序后的数组A和B,其中A末端有足够的缓冲空间容纳B,编写一个方法,将B合并入A并排序。
算法思想:设置三个指针,其中两个分别指向两个数组的末尾,最后一个指针current指向加上B数组后A数组的最后一个元素的位置,将A数组指针的值与B数组指针的值比较,较大的移到current所指的位置上,然后该指针前移。
//合并有序数组(不用额外空间)
//n是A数组有效数的长度
public static void part6(int[] A,int[] B,int n){
int a=n-1;//指向A数组的末端
int b=B.length-1;//指向B数组的末端
int current=n+B.length-1;//指向最终数组的末端
while(a>=0&&b>=0){
if(A[a]>B[b]){
A[current--]=A[a--];
}else{
A[current--]=B[b--];
}
}
if(a<0){//将B数组中剩下的元素全部移到A数组中
while(b>=0){
A[current--]=B[b--];
}
}
}
7、逆序对的个数:一个数列,如果左边的数大,右边的数小,则称这两个数为一个逆序对(左边的大右边的小)。求出一个数列中有多少个逆序对。
法一:设置两个指针,一个指针从头到尾定位元素,另一个指针从第一个指针所指的位置开始向后扫描,发现比第一个指针指向的元素小的,逆序对的指数就加一。
法二:(较优化的算法)在归并排序的合并算法中,只要将右侧队伍中的元素放在当前的位置,逆序对的数量就增加左侧队列中数的个数。(交换不影响逆序对的数量)
//方法二的实现
static int number=0; //逆序对数,全局变量
public static void part1(int[] A,int p,int r){
if(p<r){
int mid=p+((r-p)>>1);//加减的优先级高于左右移
part1(A,p,mid);//对左边进行归并排序
part1(A,mid+1,r);//对右边进行归并排序
part7(A,p,mid,r);//将两遍合并起来
}
}
private static void part7(int[] A,int p,int mid,int r){
//先把A中的数据拷贝到helper中
int[] helper=new int[A.length];
for(int i=0;i<A.length;i++){
helper[i]=A[i];
}
int left=p;//左侧队伍的头部指针,指向待比较元素
int right=mid+1;//右侧队伍头部指针,指向待比较元素
int current=p;//原数组的指针,指向待填入数据的位置
while(left<=mid&&right<=r){
if(helper[left]<helper[right]){
A[current++]=helper[left++];
}else{//这种情况下,逆序对数要增加
A[current++]=helper[right++];
number+=mid-left+1;//加上左边队列的元素个数
}
}
//看左边有没有到头。右边没到头也没关系
while(left<=mid){
A[current++]=helper[left++];
}
}
三、算法题的五种解法
方法一:举例法
先列举出一些具体的例子,看看是否能发现其中的一般规则。
方法二:模式匹配法(见多识广)
将现有问题与相似问题作类比,看看是否能通过修改相关问题的解法来解决新问题。
方法三:简化推广法
分多步走。首先,我们会修改某个约束条件,比如数据类型或数据量,从而简化这个问题。接着,我们转而处理这个问题的简化版本。最后,一旦找到解决简化问题的算法,我们就可以基于这个问题进行推广,并试着调整简化版问题的解决方案,让它适用于这个问题的复杂版本。
方法四:简单构造法
先从基本的情况(比如n=1)来解决问题,一般只需记下正确的结果。得到n=2的情况。接下来,有了n=1和n=2的结果,就可以试着解决n=3的情况了。(实际上是一种递归的算法)
方法五:数据结构头脑风暴
快速过一下数据结构的列表,然后逐一尝试各种数据结构,一旦找到合适的数据结构,很多问题就能迎刃而解了。