演算法初體驗

木可大大發表於2018-05-03

我們知道程式由資料結構和演算法組成的。其中,資料結構表示資料的組織形式,基本的資料結構包括陣列、連結串列、棧、佇列、樹、雜湊表、圖、堆等。而演算法表示對資料結構中的資料進行處理的方式或過程,換句話說,就是解決問題的方法。它們倆之間的關係:資料結構為演算法服務,很多演算法依賴於特定的資料結構,但不是全部演算法,演算法可以和資料結構沒有關係。本期我們就來聊一聊演算法。

學習演算法的重要性

在介紹具體演算法之前,我先談一下個人對學習演算法的初心。我的初心無非有兩點:一,BAT等網際網路公司招聘面試時要問演算法知識,如果想要進入網際網路公司,我就必須學好演算法;二,通過學習演算法提升個人開發的基本功,這樣一來,對於不同場景我就可以正確選擇對應的資料結構和演算法,使得程式更健壯,提高程式的執行效率。

應用領域

目前計算機各個細分領域涉及到不同的演算法。比如說搜尋引擎,平時我們使用google、百度等瀏覽器,只要我們輸入一個關鍵字,瀏覽器就會快速地返回相關的集合,這個集合的背後就隱藏著許多演算法。如果沒有這些演算法,我們是不可能這麼快速地得到想要的結果。再比如說人工智慧,通過計算模型演算法實現人體識別、語音識別等各應用場景。

演算法分析

上文我們已經介紹到演算法就是解決問題的方法,而對於同一個問題,可能存在不同的解決方法。因此,為了衡量一個演算法的優劣,提出了時間複雜度空間複雜度這兩個概念。

時間複雜度

一般情況下,演算法中基本操作重複執行的次數是問題規模n的某個函式f(n),演算法的時間度量記為 T(n) = O(f(n)),它表示隨問題規模n的增大,演算法執行時間的增長率和f(n)的增長率相同,稱作演算法的漸近時間複雜度,簡稱時間複雜度。

空間複雜度

空間複雜度是對一個演算法在執行過程中臨時佔用儲存空間大小的量度,記做S(n)=O(f(n))。一個演算法的優劣主要從演算法的執行時間和所需要佔用的儲存空間兩個方面衡量。

排序演算法

根據時間複雜度我們大體可以將排序演算法分為兩類,一類是以選擇排序為代表的O(n^2)的演算法,另一類是以快速排序為代表的O(nlogn)的演算法。看到這裡我們不禁會問:既然有O(nlogn)的排序演算法,那些O(n^2)的演算法還有存在的必要嗎?要回答這個問題,先來看下O(n^2)的排序演算法的特點:首先,它相對是比較基礎的,編碼簡單,易於實現,在一些特定場景下O(n^2)更適合 ,譬如在機器語言中O(n^2)更容易實現;其次,簡單的排序演算法思路衍生出複雜的排序演算法,比如說希爾排序是對插入排序的優化;最後,對於一些簡單的演算法,由於它們本身的一些性質,可以被用作改進更復雜排序演算法的子過程中。基於此,本文O(n^2)排序演算法中兩個代表性的演算法即選擇演算法和插入演算法。

image.png

選擇排序

思想:在整個待排序陣列裡找到最小的值,然後和待排序中的第一個元素進行交換,接著在剩下的元素裡找到最小的元素,接著將它和待排序中的第一個元素進行交換,以此類推。為了加深大家的理解,舉個具體例子,對8、6、2、3、1、5、7、4進行升序排序。

image.png

選擇排序的Java語言實現如下:

   /**
     * 思路:每次從待選陣列中選擇一個最小元素,然後和對應位置交換位置
     * @param arr
     * @param n
     */
    public void sort(int[] arr, int n) {
        for(int i=0;i<n;i++) {
            // 1. 尋找[i,n)區間裡的最小元素
            int minIndex = i;
            for(int j=i+1;j<n;j++ ) {
                if (arr[j] < arr[minIndex]) {
                    minIndex = j;
                }
            }
            // 2. 交換位置
            this.swap(arr,i,minIndex);

        }
    }
複製程式碼

插入排序

思路:插入排序是在一個已經有序的小序列的基礎上,一次插入一個元素。當然,剛開始這個有序的小序列只有1個元素,就是第一個元素。比較是從有序序列的末尾開始,也就是想要插入的元素和已經有序的最大者開始比起,如果比它大則直接插入在其後面,否則一直往前找直到找到它該插入的位置。如果碰見一個和插入元素相等的,那麼插入元素把想插入的元素放在相等元素的後面。所以,相等元素的前後順序沒有改變,從原無序序列出去的順序就是排好序後的順序,所以插入排序是穩定的。

image.png
插入排序的Java語言實現如下:

 public void sort(Comparable[] arr){
        int n = arr.length;
        for (int i = 0; i < n; i++) {
            // 尋找元素arr[i]合適的插入位置
            for( int j = i; j > 0 && arr[j].compareTo(arr[j-1]) < 0 ; j--)
                swap(arr, j, j-1);
        }
    }
複製程式碼

通過比較選擇排序和插入排序的程式碼實現,我們可以發現一旦有部分排序好之後,新插入一個數如果比排好序最大值還要大,則不用再和其他數字比較,減少了比較次數。但是,我們應該注意到插入排序在每次遍歷的時候都需要進行交換操作,這個交換操作包含三次賦值操作,導致插入排序的時間要比選擇排序的時間更長。針對這個問題,我們的先輩們想到了一個方法:先將待比較元素複製一份,然後依次和有序陣列中的元素進行比較,如果比有序陣列中的元素小,則將有序陣列中的元素覆蓋待比較元素,以此類推。如下圖所示,首先我們將元素6複製一份,接著驗證元素6是否應當放在當前位置,通過比較6和它之前的元素大小,發現元素8應該放在元素6的位置上,因此將元素8覆蓋元素6,然後我們考查元素6是否應該放在前一個元素位置上,此時,由於元素8在第0個位置上我們就不用比較直接覆蓋。它的Java程式碼實現如下:

image.png

 for (int i = 0; i < n; i++) {
            // 尋找元素arr[i]合適的插入位置
            Comparable e = arr[i];
            int j = i;
            for( ; j > 0 && arr[j-1].compareTo(e) > 0 ; j--)
                arr[j] = arr[j-1];
            arr[j] = e;
        }
複製程式碼

這樣一來,內迴圈只需要進行一次賦值操作,效率得到了大大優化,不僅超過了選擇排序,而且在待排序陣列是有序的情況下,時間複雜度可以達到O(n)


image

歡迎關注微信公眾號:木可大大,所有文章都將同步在公眾號上。