找工作知識儲備(3)A---從頭說12種排序算法:原理、圖解、動畫視頻演示、代碼以及筆試面試題目中的應(yīng)用

作者:寒小陽 時間:2013年9月。
出處:http://blog.csdn.net/han_xiaoyang/article/details/11596001。
聲明:版權(quán)所有,轉(zhuǎn)載請注明出處,謝謝。

0、前言

從這一部分開始直接切入我們計算機互聯(lián)網(wǎng)筆試面試中的重頭戲算法了,初始的想法是找一條主線,比如數(shù)據(jù)結(jié)構(gòu)或者解題思路方法,將博主見過做過整理過的算法題逐個分析一遍(博主當年自己學算法就是用這種比較笨的刷題學的,囧),不過又想了想,算法這東西,博主自己學的過程中一直深感,基礎(chǔ)還是非常重要的,很多難題是基礎(chǔ)類數(shù)據(jù)結(jié)構(gòu)和題目的思想綜合發(fā)散而來。比如說作為最基本的排序算法就種類很多,而事實上筆試面試過程中發(fā)現(xiàn)掌握的程度很一般,有很多題目,包括很多算法難題,其母題或者基本思想就是基于這些經(jīng)典算法的,比如說快排的partition算法,比如說歸并排序中的思想,比如說桶排序中桶的思想。
這里對筆試面試最常涉及到的12種排序算法(包括插入排序、二分插入排序、希爾排序、選擇排序、冒泡排序、雞尾酒排序、快速排序、堆排序、歸并排序、桶排序、計數(shù)排序和基數(shù)排序)進行了詳解。每一種算法都有\color{red}{基本介紹、算法原理分析、圖解/視頻演示、算法代碼、筆試面試重點分析、筆試面試題}等板塊,希望能幫助大家真正理解這些排序算法,并能使用這些算法的思想解決一些題。不多說了,下面就進入正題了。

一、插入排序

1)算法簡介
插入排序(Insertion Sort)的算法描述是一種簡單直觀的排序算法。它的工作原理是通過\color{red}{構(gòu)建有序序列,對于未排序數(shù)據(jù),在已排序序列中從后向前掃描,找到相應(yīng)位置并插入}。插入排序在實現(xiàn)上,通常采用in-place排序(即只需用到O(1)的額外空間的排序),因而在從后向前掃描過程中,\color{red}{需要反復(fù)把已排序元素逐步向后挪位,為最新元素提供插入空間}。

2)算法描述和分析
一般來說,插入排序都采用in-place在數(shù)組上實現(xiàn)。具體算法描述如下:

\color{blue}{1、從第一個元素開始,該元素可以認為已經(jīng)被排序}

\color{blue}{2、取出下一個元素,在已經(jīng)排序的元素序列中從后向前掃描}

\color{blue}{3、如果該元素(已排序)大于新元素,將該元素移到下一位置}

\color{blue}{4、重復(fù)步驟3,直到找到已排序的元素小于或者等于新元素的位置}

\color{blue}{5、將新元素插入到該位置后}

\color{blue}{6、重復(fù)步驟2~5}

如果目標是把n個元素的序列升序排列,那么采用插入排序存在最好情況和最壞情況。最好情況就是,序列已經(jīng)是升序排列了,在這種情況下,需要進行的比較操作需(n-1)次即可。最壞情況就是,序列是降序排列,那么此時需要進行的比較共有n(n-1)/2次。插入排序的賦值操作是比較操作的次數(shù)減去(n-1)次。平均來說\color{red}{插入排序算法復(fù)雜度為O(n^2)}。因而,插入排序不適合對于數(shù)據(jù)量比較大的排序應(yīng)用。但是,如果需要排序的數(shù)據(jù)量很小,例如,量級小于千,那么插入排序還是一個不錯的選擇。 插入排序在工業(yè)級庫中也有著廣泛的應(yīng)用,在STL的sort算法和stdlib的qsort算法中,都將插入排序作為快速排序的補充,用于少量元素的排序(通常為8個或以下)。

3)算法圖解、視頻演示

圖解:

視頻:插入排序舞蹈

)

4)算法代碼

void insertion_sort(int array[], int first, int last)
 {
        int i,j;
        int temp;
        for (i = first+1; i<=last;i++)
        {
                temp = array[i];
                j=i-1;
 
                //與已排序的數(shù)逐一比較,大于temp時,該數(shù)后移
                while((j>=first) && (array[j] > temp))  //當first=0,j循環(huán)到-1時,由于[[短路求值]],不會運算array[-1]
                {
                        array[j+1] = array[j];
                        j--;
                }
                array[j+1] = temp;      //被排序數(shù)放到正確的位置
 
        }
 }

5)考察點,重點和頻度分析
把插入排序放在第一個的原因是因為其出現(xiàn)的頻度不高,尤其是這里提到的直接排序算法,基本在筆試的選擇填空問時間空間復(fù)雜度的時候才可能出現(xiàn)。畢竟排序速度比較慢,因此算法大題中考察的次數(shù)比較比較少。

6)筆試面試例題

例題1、

請寫出鏈表的插入排序程序

template<typename T>
struct list_node
{
    struct list_node<T> *next;
    T value;
};
template<typename T>
struct _list
{
    struct list_node<T> *head;
    int size;
};
template<typename T>
void SortLink(struct _list<T> * link) {
    struct list_node<T> *pHead,*pRear,*p,*tp;
    if (!link) return;
    for (pHead=link->head,pRear=0;pHead;pHead=pHead->next) {
        for (tp=pHead,p=pHead->next;p;tp=p,p=p->next)
            if (pHead->value>=p->value)
                tp->next=p->next,p->next=pHead,pHead=p,p=tp;
        if (!pRear) link->head=pHead;
        else pRear->next=pHead;
        pRear=pHead;
    }
}

例題2、

\color{red}{下列排序算法中最壞復(fù)雜度不是n(n-1)/2的是?}

A.快速排序 B.冒泡排序 C.直接插入排序 \color{blue}{D.堆排序}

二、二分插入排序

1)算法簡介
二分(折半)插入(Binary insert sort)排序是一種在直接插入排序算法上進行小改動的排序算法。其與直接排序算法最大的區(qū)別在于\color{red}{查找插入位置時使用的是二分查找的方式},在速度上有一定提升。

2)算法描述和分析
一般來說,插入排序都采用in-place在數(shù)組上實現(xiàn)。具體算法描述如下:

\color{blue}{1、從第一個元素開始,該元素可以認為已經(jīng)被排序}

\color{blue}{2、取出下一個元素,在已經(jīng)排序的元素序列中二分查找到第一個比它大的數(shù)的位置}

\color{blue}{3、將新元素插入到該位置后}

\color{blue}{4、重復(fù)上述兩步}

    1)穩(wěn)定

    2)空間代價:O(1)

    3)時間代價:插入每個記錄需要O(log i)比較,最多移動i+1次,最少2次。最佳情況O(n log n),最差和平均情況O(n^2)。

二分插入排序是一種穩(wěn)定的排序。\color{red}{當n較大時,總排序碼比較次數(shù)比直接插入排序的最差情況好得多},\color{red}{但比最好情況要差,所元素初始序列已經(jīng)按排序碼接近有序時,直接插入排序比二分插入排序比較次數(shù)少}。二分插入排序元素移動次數(shù)與直接插入排序相同,依賴于元素初始序列。

3)算法圖解、視頻演示

圖解:

視頻:二分插入排序

4)算法代碼

void BinInsertSort(int a[], int n) 
{ 
        int key, left, right, middle; 
        for (int i=1; i<n; i++) 
        { 
                key = a[i]; 
                left = 0; 
                right = i-1; 
                while (left<=right) 
                { 
                        middle = (left+right)/2; 
                        if (a[middle]>key) 
                                right = middle-1; 
                        else 
                                left = middle+1; 
                } 
                 
                for(int j=i-1; j>=left; j--) 
                { 
                        a[j+1] = a[j]; 
                } 
                 
                a[left] = key;        
        } 
}

5)考察點,重點和頻度分析
這個排序算法在筆試面試中出現(xiàn)的頻度也不高,但畢竟是直接排序算法的一個小改進算法,同時二分查找又是很好的思想,有可能會在面試的時候提到,算法不難,留心一下就會了。

6)筆試面試例題

例題1、

\color{red}{下面的排序算法中,初始數(shù)據(jù)集的排列順序?qū)λ惴ǖ男阅軣o影響的是( )}

A、二分插入排序 \color{blue}{B、堆排序} C、冒泡排序 D、快速排序

例題2、

\color{red}{寫出下列算法的時間復(fù)雜度。}

(1)冒泡排序;(2)選擇排序;(3)插入排序;(4)二分插入排序;(5)快速排序;(6)堆排序;(7)歸并排序;

三、希爾排序

1)算法簡介
希爾排序,也稱遞減增量排序算法,因DL.Shell于1959年提出而得名,是插入排序的一種高速而穩(wěn)定的改進版本。

2)算法描述
\color{blue}{1、先取一個小于n的整數(shù)d1作為第一個增量,把文件的全部記錄分成d1個組。}

\color{blue}{2、所有距離為d1的倍數(shù)的記錄放在同一個組中,在各組內(nèi)進行直接插入排序。}

\color{blue}{3、取第二個增量d2<d1重復(fù)上述的分組和排序,}

\color{blue}{4、直至所取的增量dt=1(dt<dt-l<…<d2<d1),即所有記錄放在同一組中進行直接插入排序為止。}

希爾排序的時間復(fù)雜度與增量序列的選取有關(guān),例如希爾增量時間復(fù)雜度為O(n2),而Hibbard增量的希爾排序的時間復(fù)雜度為O(N(5/4)),但是現(xiàn)今仍然沒有人能找出希爾排序的精確下界。

3)算法圖解、視頻演示

圖解:




視頻:希爾排序Shell Sort 舞蹈

4)算法代碼

#include <stdio.h>
 
int main()
{
     const int n = 5;
     int i, j, temp; 
     int gap = 0;
     int a[] = {5, 4, 3, 2, 1}; 
     while (gap<=n)
     {
          gap = gap * 3 + 1;
     } 
     while (gap > 0) 
     {
         for ( i = gap; i < n; i++ )
         {
             j = i - gap;
             temp = a[i];             
             while (( j >= 0 ) && ( a[j] > temp ))
             {
                 a[j + gap] = a[j];
                 j = j - gap;
             }
             a[j + gap] = temp;
         }
         gap = ( gap - 1 ) / 3;
     }    
 }

5)考察點,重點和頻度分析
事實上希爾排序算法在筆試面試中出現(xiàn)的頻度也不比直接插入排序高,但它的時間復(fù)雜度并不是一個定值,所以偶爾會被面試官問到選擇的步長和時間復(fù)雜度的關(guān)系,要稍微有點了解吧。算法大題中使用該方法或者其思想的題也不多。

6)筆試面試例題

例題1、

\color{red}{寫出希爾排序算法程序,并說明最壞的情況下需要進行多少次的比較和交換。}

程序略,需要O(n^2)次的比較

例題2、

\color{red}{設(shè)要將序列(Q, H, C, Y, P, A, M, S, R, D, F, X)中的關(guān)鍵碼按字母序的升序重新排列},則:

冒泡排序一趟掃描的結(jié)果是 \color{blue}{ H, C, Q, P, A, M, S, R, D, F, X ,Y } ;

初始步長為4的希爾(shell)排序一趟的結(jié)果是 \color{blue}{P, A, C, S, Q, D, F, X , R, H,M, Y} ;

二路歸并排序一趟掃描的結(jié)果是 \color{blue}{H, Q, C, Y,A, P, M, S, D, R, F, X} ;

快速排序一趟掃描的結(jié)果是 \color{blue}{F, H, C, D, P, A, M, Q, R, S, Y,X} ;

堆排序初始建堆的結(jié)果是 \color{blue}{A, D, C, R, F, Q, M, S, Y,P, H, X} 。

四、選擇排序

1)算法簡介
選擇排序(Selection sort)是一種簡單直觀的排序算法。它的工作原理如下。\color{red}{首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置},\color{red}{然后,再從剩余未排序元素中繼續(xù)尋找最?。ù螅┰?,然后放到已排序序列的末尾}。以此類推,直到所有元素均排序完畢。

2)算法描述和分析
n個記錄的文件的直接選擇排序可經(jīng)過n-1趟直接選擇排序得到有序結(jié)果:

\color{blue}{1、初始狀態(tài):無序區(qū)為R[1..n],有序區(qū)為空。}

\color{blue}{2、第i趟排序(i=1,2,3...n-1)}

\color{blue}{第i趟排序開始時,當前有序區(qū)和無序區(qū)分別為R[1..i-1]和R(i..n)}\color{blue}{該趟排序從當前無序區(qū)中選出關(guān)鍵字最小的記錄 R[k],將它與無序區(qū)的第1個記錄R交換}\color{blue}{使R[1..i]和R分別變?yōu)橛涗泜€數(shù)增加1個的新有序區(qū)和記錄個數(shù)減少1個的新無序區(qū)}。

\color{blue}{3、前n-1趟結(jié)束,數(shù)組有序化了}

選擇排序的交換操作介于0和(n-1)次之間。選擇排序的比較操作為n(n-1)/2次之間。選擇排序的賦值操作介于0和3(n-1)次之間。比較次數(shù)O(n^2),比較次數(shù)與關(guān)鍵字的初始狀態(tài)無關(guān),總的比較次數(shù)N=(n-1)+(n-2)+...+1=n*(n-1)/2。 交換次數(shù)O(n),最好情況是,已經(jīng)有序,交換0次;最壞情況是,逆序,交換n-1次。 交換次數(shù)比冒泡排序少多了,由于交換所需CPU時間比比較所需的CPU時間多,n值較小時,選擇排序比冒泡排序快。

最差時間復(fù)雜度 О(n2)
最優(yōu)時間復(fù)雜度 О(n2)
平均時間復(fù)雜度 О(n2)
最差空間復(fù)雜度 О(n) total, O(1)

3)算法圖解、視頻演示

圖解:

視頻:選擇排序Select Sort排序舞蹈

4)算法代碼

void selection_sort(int *a, int len)
{
    register int i, j, min, t;
    for(i = 0; i < len - 1; i ++)
    {
        min = i;
        //查找最小值
        for(j = i + 1; j < len; j ++)
            if(a[min] > a[j])
                min = j;
        //交換
        if(min != i)
        {
            t = a[min];
            a[min] = a[i];
            a[i] = t;
        }
    }
}

5)考察點,重點和頻度分析
就博主看過的筆試面試題而言,選擇算法也大多出現(xiàn)在選擇填空中,要熟悉其時間和空間復(fù)雜度,最好最壞的情況分別是什么,以及在那種情況下,每一輪的比較次數(shù)等。

6)筆試面試例題

例題1、

\color{red}{在插入和選擇排序中,若初始數(shù)據(jù)基本正序,則選用}:\color{blue}{ 插入排序}(到尾部) ;\color{red}{若初始數(shù)據(jù)基本反序,則選用}:\color{blue}{ 選擇排序} 。

例題2、

\color{red}{下述幾種排序方法中,平均查找長度()最小的是}
A. 插入排序 \color{blue}{B.快速排序 } C. 歸并排序 D. 選擇排序

五、冒泡排序

1)算法簡介
冒泡排序是一種簡單的排序算法。它重復(fù)地走訪過要排序的數(shù)列,一次比較兩個元素,如果他們的順序錯誤就把他們交換過來。走訪數(shù)列的工作是重復(fù)地進行直到?jīng)]有再需要交換,也就是說該數(shù)列已經(jīng)排序完成。這個算法的名字由來是因為越小的元素會經(jīng)由交換慢慢“浮”到數(shù)列的頂端。

2)算法描述
\color{blue}{1、比較相鄰的元素。如果第一個比第二個大,就交換他們兩個。}

\color{blue}{2、對每一對相鄰元素作同樣的工作,從開始第一對到結(jié)尾的最后一對。在這一點,最后的元素應(yīng)該會是最大的數(shù)。}

\color{blue}{3、針對所有的元素重復(fù)以上的步驟,除了最后一個。}

\color{blue}{4、持續(xù)每次對越來越少的元素重復(fù)上面的步驟,直到?jīng)]有任何一對數(shù)字需要比較。}

冒泡排序是與插入排序擁有相等的執(zhí)行時間,但是兩種法在需要的交換次數(shù)卻很大地不同。在最壞的情況,冒泡排序需要O(n2)次交換,而插入排序只要最多O(n)交換。冒泡排序的實現(xiàn)(類似下面)通常會對已經(jīng)排序好的數(shù)列拙劣地執(zhí)行(O(n2)),而插入排序在這個例子只需要O(n)個運算。因此很多現(xiàn)代的算法教科書避免使用冒泡排序,而用插入排序取代之。冒泡排序如果能在內(nèi)部循環(huán)第一次執(zhí)行時,使用一個旗標來表示有無需要交換的可能,也有可能把最好的復(fù)雜度降低到O(n)。在這個情況,在已經(jīng)排序好的數(shù)列就無交換的需要。若在每次走訪數(shù)列時,把走訪順序和比較大小反過來,也可以稍微地改進效率。有時候稱為往返排序,因為算法會從數(shù)列的一端到另一端之間穿梭往返。

最差時間復(fù)雜度 O(n^2)
最優(yōu)時間復(fù)雜度 O(n)
平均時間復(fù)雜度 O(n^2)
最差空間復(fù)雜度 總共O(n),需要輔助空間O(1)

3)算法圖解、視頻演示

圖解:

視頻:舞動的排序算法 冒泡排序

4)算法代碼

 #include <stdio.h>
 void bubbleSort(int arr[], int count)
   {
       int i = count, j;
       int temp;
 
       while(i > 0)
       {
          for(j = 0; j < i - 1; j++)
          {
              if(arr[j] > arr[j + 1])
              {   temp = arr[j];
                  arr[j] = arr[j + 1];
                  arr[j + 1] = temp;
              }
          }
          i--;
      }
 
  }
 
  int main()
  {
      //測試數(shù)據(jù)
      int arr[] = {5, 4, 1, 3, 6};
      //冒泡排序
      bubbleSort(arr, 5);
      //打印排序結(jié)果
      int i;
      for(i = 0; i < 5; i++)
          printf("%4d", arr[i]);
 }

5)考察點,重點和頻度分析
一般我們學到的第一個排序算法就是冒泡排序,不得不說,這個還真是一個很常見的考點,平均時間空間復(fù)雜度,最好最壞情況下的時間空間復(fù)雜度,在不同情況下每一趟的比較次數(shù),以及加標志位減少比較次數(shù)等,都是需要注意的地方。

6)筆試面試例題

例題1、

\color{red}{對于整數(shù)序列100,99,98,…3,2,1,如果將它完全倒過來,分別用冒泡排序,它們的比較次數(shù)和交換次數(shù)各是多少?}
\color{blue}{ 答:冒泡排序的比較和交換次數(shù)將最大,都是1+2+…+n-1=n(n-1)/2=50×99=4545次。}

例題2、

\color{red}{把一個字符串的大寫字母放到字符串的后面,各個字符的相對位置不變,不能申請額外的空間。}

事實上,這道題放到冒泡排序這里不知道是不是特別合適,只是有一種解法是類似冒泡的思想,如下解法一

\color{blue}{解法一、}
每次遇到大寫字母就往后冒,最后結(jié)果即為所求

#include <stdio.h>
#include <string.h>
//題目以及要求:把一個字符串的大寫字母放到字符串的后面,
//各個字符的相對位置不變,不能申請額外的空間。 
//判斷是不是大寫字母 
int isUpperAlpha(char c){
if(c >= 'A' && c <= 'Z'){
return 1;
}
return 0; 
}
//交換兩個字母 
void swap(char *a, char *b){
char temp = *a;
*a = *b;
*b = temp;
} 
char * mySort(char *arr, int len){
if(arr == NULL || len <= 0){
return NULL;
}
int i = 0, j = 0, k = 0;
for(i = 0; i < len; i++){
for(j = len - 1 - i; j >= 0; j--){
if(isUpperAlpha(arr[j])){
for(k = j; k < len - i - 1; k++){
swap(&arr[k], &arr[k + 1]);
}
break;
}
//遍歷完了字符數(shù)組,但是沒發(fā)現(xiàn)大寫字母,所以沒必要再遍歷下去
if(j == 0 && !isUpperAlpha(arr[j])){
//結(jié)束;
                           return arr;
}
}
}
//over: 
return arr;
}
int main(){
char arr[] = "aaaaaaaaaaaaaaaaaaaaaaaAbcAdeBbDc";
printf("%s\n", mySort(arr, strlen(arr)));
return 0;
}

\color{blue}{解法二}
步驟如下

1、兩個指針p1和p2,從后往前掃描

2、p1遇到一個小寫字母時停下, p2遇到大寫字母時停下,兩者所指向的char交換

3、p1, p2同時往前一格

代碼如下:

#include <stdio.h>
#include <string.h>
//判斷是不是大寫字母 
int isUpperAlpha(char c){
if(c >= 'A' && c <= 'Z'){
return 1;
}
return 0; 
}
//交換兩個字母 
void swap(char *a, char *b){
char temp = *a;
*a = *b;
*b = temp;
} 
char * Reorder(char *arr, int len){
if(arr == NULL || len <= 0){
return NULL;
}
int *p1 = arr;
int *p2 = arr;
While(p1<arr+len && p2<arr+len){
While( isUpperAlpha(*p1) ){
P1++;
}
While( !isUpperAlpha(*p2) ){
P2++;
}
swap(p1, p2)
}
//結(jié)束
return arr;
}
int main(){
char arr[] = "aaaaaaaaaaaaaaaaaaaaaaaAbcAdeBbDc";
printf("%s\n", Reorder(arr, strlen(arr)));
return 0;
}

六、雞尾酒排序/雙向冒泡排序

1)算法簡介
雞尾酒排序等于是冒泡排序的輕微變形。不同的地方在于\color{red}{從低到高然后從高到低},而冒泡排序則僅從低到高去比較序列里的每個元素。他可以得到比冒泡排序稍微好一點的效能,原因是冒泡排序只從一個方向進行比對(由低到高),每次循環(huán)只移動一個項目。

2)算法描述和分析
\color{blue}{1、依次比較相鄰的兩個數(shù),將小數(shù)放在前面,大數(shù)放在后面};

\color{blue}{2、第一趟可得到:將最大數(shù)放到最后一位}。

\color{blue}{3、第二趟可得到:將第二大的數(shù)放到倒數(shù)第二位}。

\color{blue}{4、如此下去,重復(fù)以上過程,直至最終完成排序}。

雞尾酒排序最糟或是平均所花費的次數(shù)都是O(n^2),但如果序列在一開始已經(jīng)大部分排序過的話,會接近O(n)。

最差時間復(fù)雜度 O(n^2)
最優(yōu)時間復(fù)雜度 O(n)
平均時間復(fù)雜度 O(n^2)

3)算法圖解、視頻演示

圖解:


4)算法代碼

void CocktailSort(int *a,int nsize)
{
    int tail=nsize-1;
    for (int i=0;i<tail;)
    {
        for (int j=tail;j>i;--j) //第一輪,先將最小的數(shù)據(jù)排到前面
        {
            if (a[j]<a[j-1])
            {
                int temp=a[j];
                a[j]=a[j-1];
                a[j-1]=temp;
            }
        }
        ++i;                    //原來i處數(shù)據(jù)已排好序,加1
        for (j=i;j<tail;++j)    //第二輪,將最大的數(shù)據(jù)排到后面
        {
            if (a[j]>a[j+1])
            {
                int temp=a[j];
                a[j]=a[j+1];
                a[j+1]=temp;
            }    
        }
        tail--;                 //原tail處數(shù)據(jù)也已排好序,將其減1
    }
}

5)考察點,重點和頻度分析
雞尾酒排序在博主印象中出現(xiàn)的頻度也不高,用到它的算法題大題很少,選擇填空出現(xiàn)的話多以雙向冒泡排序的名稱出現(xiàn),注意注意時間空間復(fù)雜度,理解理解算法應(yīng)該問題就不大了。

6)筆試面試例題
考點基本類似冒泡排序,請參考上一節(jié)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容