面试总结之-排序算法分析

第一篇博客,把近段时间来准备面试的心得,碰到的题(题目以后再补充),分类总结在一起,方便以后自己查看。

    一系列博客主要面向有意应聘国外码农的童鞋(Facebook,LinkedIn, Amazon, Google, 简称FLAG,当然Microsoft,Twitter等等也包括在内),实际上它们的面试风格也是大同小异,算法coding为主,中间夹杂design pattern,big data等的题目。由于自己大部分时间都是在准备主流题目(算法,coding),所以~先写这一块的总结吧。至于国内的大互联网公司(BAT)面试风格感觉形式各异,不一定会面的这么细,也许一本《编程之美》就可以解决大部分问题了。

    文章中提到的题目,大部分来自 http://leetcode.com/,这是一个准备面试的必刷之地,上面有132题(目前)面试的 高频题,请务必刷完!!!不过不要盲目的刷,一边刷一边总结吧,刷一次也不够,刷了重新刷吧。。。。。。

 

排序的分析

说到排序,大家应该可以列一堆出来:

  计数排序
         插入排序
           冒泡排序
           快速排序
          归并排序
           堆排序

然后还可以流利的讲出各种排序的时空复杂度,不过~这不是重点= = !面试常用的一般只有后三种,而且而且,后两种经常不是以排序的角色出现(当然了,排序的光环都被快排抢走了),所以,第一篇文章不会每种排序都讲一遍,这里主要分析的排序是 快速排序+归并排序

快速排序:

一开始先给一个code:

code[0]:
void qsort(int* a,int n){ 
  int i=0,j=n,mid=a[n/2];
  while(i<=j){ 
    while(a[i]<mid) i++;
    while(a[j]>mid) j--;
    if(i<=j)
      std::swap(a[i++],a[j--]); 
  }
  if(j>0) qsort(a,j);
  if(i<n) qsort(a+i,n-i);
}

这个是我常用的quicksort模板,代码来源已经不知道了N久之前就用,作了一些小改动。

这种代码有啥好分析的呢?要是你这样觉得,要不你就没认真看过代码(比赛贴模板的吧),要不你已经研究得很深入,忘了这个东西有很多陷阱还很多人不知道~~~有啥陷阱,还是代码说话吧,下面再给几个代码:

code[1]


void qsort(int* a,int n){ 
  int i=0,j=n,mid=a[n/2];
  while(i<=j){ 
    while(a[i]<mid) i++;
    while(a[j]>mid) j--;
    if(i<j)
      std::swap(a[i++],a[j--]); 
  }
  if(j>0) qsort(a,j);
  if(i<n) qsort(a+i,n-i);
}


code[2]

void qsort(int* a,int n){ 
  int i=0,j=n,mid=a[n/2];
  while(i<j){ 
    while(a[i]<mid) i++;
    while(a[j]>mid) j--;
    if(i<=j)
      std::swap(a[i++],a[j--]); 
  }
  if(j>0) qsort(a,j);
  if(i<n) qsort(a+i,n-i);
}



code[3]


void qsort(int* a,int n){ 
  int i=0,j=n,mid=a[n/2];
  while(i<=j){ 
    while(a[i]<=mid) i++;
    while(a[j]>=mid) j--;
    if(i<=j)
      std::swap(a[i++],a[j--]); 
  }
  if(j>0) qsort(a,j);
  if(i<n) qsort(a+i,n-i);
}


code[4]

void qsort(int* a,int n){ 
  int i=0,j=n,mid=a[n/2];
  while(i<j){ 
    while(a[i]<mid) i++;
    while(a[j]>mid) j--;
    if(i<j)
      std::swap(a[i++],a[j--]); 
  }
  if(j>0) qsort(a,j);
  if(i<n) qsort(a+i,n-i);
}


不要编译,不要看标准代码,看看这些代码都有什么问题呗。

 

=================分割线===========分割线=============分割线===========分割线=============分割线===========分割线=====================


 

实际上,除了code[0]和code[2]是没有问题的,其他code都会在某些情况下出bug。没有问题的code为什么没有问题就不说了,具体讲讲有问题的code为什么有问题,在什么情况下会出现什么问题吧。

code[1]: 这个代码会死循环。跟code[0]相比,我去掉了if(i<=j)中的等于号,这样的话,当i==j时,代码没有执行i++,j--,就没法跳出循环了,考虑只有一个元素的情况;

code[4]: 原谅我,先说code[4],因为code[4]跟code[1]很像。在去掉if的等于号之外,我再去掉了while(i<=j)的等于号,这样,貌似代码就可以跳出循环了--!但是别高兴太早,这个代码会无限递归导致栈溢出,因为它虽然跳出了循环,但是由于当i==j时没有做i++,j--的操作,下面递归时,程序无限重复对同一个子数组进行处理,每次又不处理就进入下一重递归~考虑这种输入情况[1,2];

code[3]: code[3] 在code[0]的基础上,在while(a[i]<mid])两个循环中加入了等号。这样的话,就相当于,如果某个值跟阈值相等,我也不打算把它跟其他元素swap,这个逻辑听起来好像也没啥问题,不过注意,当元素都一样时,悲剧就发生了,这个while循环是会越界的!考虑下只有一个元素的情况吧。

这几种代码基本涵括了常见的错误,当然,你还可以在最后递归条件的if里面加入个等号= =!但是你应该不会这么做——当只有一个元素时,为什么还要递归下去排序呢= =!

把它们都总结到代码里面,就是这么个情况:


void qsort(int* a,int n){ //n是最后一个元素下标
  int i=0,j=n,mid=a[n/2];
  //当后面用if(i<=j)时,这里有没有等号都是正确的,当后面面用if(i<j)时,这里不用等号会导致无限递归而栈溢出,用等号会死循环(考虑这种输入[1,2])
  while(i<=j){ 
    while(a[i]<mid) i++; //为什么没有等于号,有等于号的话当mid是数组最大或最小值时,会下标溢出
    while(a[j]>mid) j--; 
    if(i<=j) //有等于号主要为了i++,j--,当然可以分开判断,等于时只做i++,j--
      std::swap(a[i++],a[j--]); 
  }
  if(j>0) qsort(a,j); //这里不用等号就很明显了,没必要
  if(i<n) qsort(a+i,n-i);
}


为什么要把一个代码分析的这么细呢?因为面试官是不会满足于你能把代码bug free的写在纸上的,尤其是快排这种满街都是的代码,面试官为了看看你是不是有认真思考过,肯定会问你:这为什么这样写?不这样写有啥问题?提前想好了,有备无患。

搞定了快排的代码,再来看下它的其他用处。

关于qsort的使用,除了用来排序之外,经常见的就是topK查找,就是说从一个数组里面找到最小(大)的k个值。用heap来做的话复杂度是O(NlogK)用qsort的方法做的话平均复杂度是O(N)(当然,最坏情况下是O(n*max(n-k,k))了。

用heap很简单,用一个大小为K的maxheap,把元素一个个加到heap里,超过K之后把最大的扔掉,这个方法比题目要求做多了一点东西:实际上你已经把K个元素都排了序了,而题目只需要这K个元素,它们相对大小不需要。

然后就是quicksort方法了。quicksort做法的idea是,找到一个threshold把数组分成两部分之后,如果左边的元素个数大于K,那么我们只需要递归处理左边(因为K个最小的肯定不在数组的右半部分了),如果左边元素个数小于K,那么左边的元素全部符合要求,只需要处理右半部分。

Code:


voidTopK(int* a,int n,int k){
  int i = 0,j = n,mid = a[n/2];
  while(i<=j){
    while(a[i]<mid)i++;
    while(a[j]>mid)j--;
    if(i<=j)
      std::swap(a[i++],a[j--]);
  }
  if(k<i) TopK(a,j,k);  //最小的i个已经找到,如果k<i,前k小在数组左半部分
  if(k>i) TopK(a+i,n-i,k-i); //如果k>i,还需要从右半部分找k-i个最小值
  //如果k==i,刚好搞定,不用递归下去了
}


大体框架跟qsort一样,递归条件发生了变化。还有注意的是由于递归条件发生了变化,while循环需要<=(还记得前面的分析的话,qsort时,这个等于号是可要可不要的),至于这个等号去掉会发生什么问题,可以自己跑一下。

这个修改一下也可以变成找第k大元素的算法框架,找第K大的各种方法,可以参考下这里:http://www.cnblogs.com/zhjp11/archive/2010/02/26/1674227.html

 

归并排序:


void Merge(int* a,int* b,int n){
  int i=n/2, j=n-n/2-1;
  while(i>=0&&j>=0){
    //不要等号,不然不是稳定排序了
    if(a[i]>b[j])
      a[n--] = a[i--];
    else a[n--] = b[j--];
  }
  while(i>=0)
    a[n--] = a[i--];
  while(j>=0)
    a[n--] = b[j--];
}
void MergeSort(int* a,int n){
  if(n==0) return;
  int *p = new int[n-n/2];
  for(int i=n/2+1;i<=n;i++)//拷贝a数组后半部分
    p[i-1-n/2] = a[i];
  MergeSort(a,n/2);
  MergeSort(p,n-n/2-1);
  Merge(a,p,n);
  delete[] p;
}


MergeSort分成两部分,首先把数组平均分成两份,分别作MergeSort,做完之后再Merge到原数组中。代码的具体实现中,new了n-n/2的空间用来存储后半部分的数据(前面一半数据就不需要copy一次了),分别MergeSort之后,重新Merge回原数组。用两个数组做Merge(其中一个数组有足够空间存储所有数据),要从数组最后一个下标开始存,可以完全避免元素的覆盖(参看Merge的代码),这个有时候也可以独立作为一个面试题。

归并排序出错可能性比quicksort小。要注意的地方是:

1.      Merge时注意不要写成不稳定的排序;

2.      new的空间记得delete;

3.      跟quicksort比较,quicksort是先处理,再递归,mergesort是先递归,再处理;

MergeSort在面试时出现概率不高,因为凡是要sort的地方一般都是quicksort了,但是它主要以两种形式出现,一是考Merge,两个有序数组的merge,多个有序数组的merge,两个有序链表的merge,多个有序链表的merge(貌似还见过两棵binarysearch tree的merge?);

第二个是用MergeSort求逆序对(不知道逆序对google之)的数目,这种题当然不会直接说逆序对,但是idea就是逆序对,出现概率不高。

MergeSort要讲的东西不多,面试时,MergeSort考察的重点一般不在于sort,而在于Merge,要是说真的要用MergeSort的话,印象中就是单向链表的排序了,用MergeSort来做单向链表的排序,比较方便,虽然说用快排也行。

再提一下http://leetcode.com/,实际上这部分内容在leetcode上的题不错,原因不是上面的题目不需要sort,而是需要sort的题目都可以用系统函数解决了。关于Merge的题目倒是有几道,先给个链接,以后有机会我把我的code也附上来:

Merge Sorted Array

Merge Two Sorted Lists

Merge k Sorted Lists

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章