面試總結之-排序算法分析

第一篇博客,把近段時間來準備面試的心得,碰到的題(題目以後再補充),分類總結在一起,方便以後自己查看。

    一系列博客主要面向有意應聘國外碼農的童鞋(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

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