導師計劃--資料結構和演算法系列(下)

call_me_R發表於2019-04-07

banner

資料結構和演算法系列的課程分為上下兩篇文章,上篇文章主要是講解資料結構,可以戳導師計劃--資料結構和演算法系列(上)進行了解。本篇文章主要講解的是基本演算法,輔助的語言依舊是JavaScript。POST的本篇文章主要是擴充套件下我們在開發中的方式,發散下思維~

排序演算法

排序介紹:

  • 一旦我們將資料放置在某個資料結構(比如陣列)中儲存起來後,就可以根據需求對資料進行不同方式的排序:
    • 比如對姓名按字母排序
    • 對商品按照價格排序
    • etc.

排序演算法又分為簡單排序高階排序。其中簡單排序包括氣泡排序、選擇排序和插入排序。高階排序包括希爾排序、歸併排序和快速排序。【⚠️這裡僅介紹了六種排序演算法】

下面我們逐個排序演算法來講解下:

氣泡排序

之所以叫氣泡排序,是因為使用這種排序演算法時,資料值就會像氣泡那樣從陣列的一端漂浮到另一端。假設正在將一組數字按照升序排列,較大的值會浮動在陣列的右側,而較小的值則會浮動到陣列的左側。產生這種冒泡的現象是因為演算法會多次在陣列中移動過,比較相鄰的資料,當左側值大於右側值的時候將它們互換。

⚠️ 後面講到的排序演算法如無說明,則預設為升序

比如下面的簡單列表的例子。

E A D B H

經過第一次的排序後,列表會變成:

A E D B H

前面兩個元素進行了互動。接下來再次排序:

A D E B H

第二個元素和第三個元素進行了互動。繼續進行排序:

A D B E H

第三個元素和第四個元素進行了交換。這一輪最後進行排序:

A D B E H

因為第四個元素比最後一個元素小,所以比較後保持所在位置。反覆對第一個元素執行上面的操作(已經固定的值不參與排序,如第一輪固定的H不參與第二輪的比較了),得到如下的最終結果:

A B D E H

相關的動效圖如下:

bubble-sort-gif

關鍵程式碼如下:

bubbleSort(){
    let numElements = this.arr.length;
    for(let outer = numElements-1; outer >= 2; --outer){
        for(let inner = 0; inner <= outer-1; ++inner){
            if(this.arr[inner] > this.arr[inner+1]){
                this.swap(inner, inner+1); // 交換陣列兩個元素
            }
        }
    }
}
複製程式碼

選擇排序

選擇排序從陣列的開頭開始,將第一個元素和其它元素進行比較。檢查完所有的元素之後,最小的元素會被放在陣列的第一個位置,然後演算法會從第二個位置繼續。這個過程進行到陣列的倒數第二個位置時,所有的資料便完成了排序。

原理:

選擇排序用到雙層巢狀迴圈。外迴圈從陣列的第一個元素移動到倒數第二個元素;內迴圈從當前外迴圈所指元素的第二個元素開始移動到最後一個元素,查詢比當前外迴圈所指元素的元素。每次內迴圈迭代後,陣列中最小的值都會被賦值到合適的位置。

下面是對五個元素的列表進行選擇排序的簡單例子。初始列表為:

E A D H B

第一次排序會找到最小值,並將它和列表的第一個元素進行交換:

A E D H B

接下查詢第一個元素後面的最小值(第一個元素此時已經就位),並對它們進行交換:

A B D H E

D已經就位,因此下一步會對E H進行互換,列表已按順序排列好如下:

A B D E H

通過gif圖可能容易理解:

selection-sort-gif

關鍵程式碼如下:

selectionSort(){
    let min,
        numElements = this.arr.length;
    for(let outer = 0; outer <= numElements-2; outer++){
        min = outer;
        for(let inner = outer+1; inner <= numElements-1; inner++){
            if(this.arr[inner] < this.arr[min]){
                min = inner;
            }
        }
        this.swap(outer, min);
    }
}
複製程式碼

插入排序

插入排序類似我們按照數字或字母的順序對資料進行降序或升序排序整理~

原理:

插入排序也用了雙層的巢狀迴圈。外迴圈將陣列挨個移動,而內迴圈則對外迴圈中選中的元素以及內迴圈陣列後面的那個元素進行比較。如果外迴圈中選中的元素比內迴圈中選中的元素要小,那麼內迴圈的陣列元素會向右移動,騰出一個位置給外迴圈選定的元素。

上面表達的晦澀難懂。簡單來說,插入排序就是未排序的元素對已經排序好的序列資料進行合適位置的插入。如果還是不懂,結合下面的排序示例來理解下:

下面對五個元素進行插入排序。初始列表如下:

E B A H D

第一次插入排序,第二個元素挪動到第一位:

B E A H D

第二次插入排序是對A進行操作:

B A E H D

A B E H D

第三次是對H進行操作,因為它比之前的元素都大,所以保持位置。最後一次是對D元素進行插入排序了,過程和最後結果如下:

A B E D H

A B D E H

相關的gif圖瞭解一下:

gif

相關程式碼如下:

insertionSort(){
    let temp,
        inner,
        numElements = this.arr.length;
    for(let outer = 1; outer <= numElements-1; outer++){
        temp = this.arr[outer];
        inner = outer;
        while(inner > 0 && this.arr[inner-1] >= temp){
            this.arr[inner] = this.arr[inner-1];
            inner--;
        }
        this.arr[inner] = temp;
    }
}
複製程式碼

希爾排序

希爾排序是插入排序的優化版,但是,其核心理念與插入排序不同,希爾排序會首先比較距離較遠的元素,而非相鄰的元素。

原理:

希爾排序通過定義一個間隔序列來表示資料在排序過程中進行比較的元素之間有多遠的間隔。我們可以動態定義間隔序列,不過對於大部分的實際應用場景,演算法用到的間隔序列可以提前定義好

如下演示希爾排序中,間隔序列是如何執行的:

how-hash-sort-run

通過下面的gif圖你也許會更好理解:

hash-sort-gif

實現的程式碼:

shellSort(){
    let temp,
        j,
        numElements = this.arr.length;
    for(let g = 0; g < this.gaps.length; ++g){
        for(let i = this.gaps[g]; i < numElements; ++i){
            temp = this.arr[i];
            for(j = i; j >= this.gaps[g] && this.arr[j - this.gaps[g]] > temp; j -= this.gaps[g]){ // 之前的已經拍好序的了
                this.arr[j] = this.arr[j - this.gaps[g]];
            }
            this.arr[j] = temp; // 這裡和上面的for迴圈是互換兩個資料位置
        }
    }
}
複製程式碼

?思考:[6, 0, 2, 9, 3, 5, 8, 0, 5, 4] 間隔為3的排序結果是什麼呢?

歸併排序

原理:

把一系列的排好序的子序列合併成一個大的有序序列。從理論上講,這個演算法很容易實現。我們需要兩個排好序的子陣列,然後通過比較資料的大小,先從最小的資料開始插入,最後合併得到第三個陣列。然而,實際上操作的相當大的資料的時候,使用歸併排序是很耗記憶體的,這裡我們瞭解一下就行。

merge-sort-gif

實現歸併排序一般有兩種方法,一種是自頂向下自底向上的方法。

上面的gif圖是自頂向下的方法,那麼何為自頂向下呢?

自頂向下的歸併排序演算法就是把陣列元素不斷的二分,直到子陣列的元素個數為一個,因為這個時候子陣列必定是有序的,然後再將兩個有序的序列合併成一個新的有序序列,連個有序序列又可以合併成另一個新的有序序列,以此類推,直到合併一個有序的陣列。如下圖:

merge-sort-demo1

自底向上的歸併排序演算法的思想是將陣列先一個一個歸併成兩兩有序的序列,兩兩有序的序列歸併成四個四個有序的序列,以此類推,直到歸併的長度大於整個陣列的長度,此時整個陣列有序。

⚠️注意:陣列按照歸併長度劃分,最後一個子陣列可能不滿足長度要求,這種情況就要特殊處理了。

merge-sort-demo2

快速排序

快速排序是處理大資料集最快的排序演算法之一,時間複雜度 最好的情況也也是和歸併排序一樣,為O(nlogn)。

原理:

快速排序是一種**分而治之(分治)**的演算法,通過遞迴的方式將資料依次分解為包含較小元素和較大元素的不同子序列,然後不斷重複這個步驟,直到所有的資料都是有序的。

可以更清晰的表達快速排序演算法步驟如下:

  1. 選擇一個基準元素(pivot,樞紐),將列表分隔成兩個子序列;
  2. 對列表重新排序,將所有小於基準值的元素放在基準值的前面,將所有大於基準值的元素放在基準值的後面;
  3. 分別對較小元素的子序列和較大元素的子序列重複步驟1 和 2

quicky-sort-gif

我們來用程式碼實現下:

// 快速排序
    quickSort(){
        this.arr = this.quickAux(this.arr);
    }

// aux函式 - 快排的輔助函式
quickAux(arr){
    let numElements = arr.length;
    if(numElements == 0){
        return [];
    }
    let left = [],
        right = [],
        pivot = arr[0]; // 取陣列的第一個元素作為基準值
    for(let i = 1; i < numElements; i++){
        if(arr[i] < pivot){
            left.push(arr[i]);
        }else{
            right.push(arr[i]);
        }
    }
    return this.quickAux(left).concat(pivot, this.quickAux(right));
}
複製程式碼

以上介紹了六種排序的演算法,當然還有很多其它的排序演算法,你可以到視訊 | 手撕九大經典排序演算法,看我就夠了!文章中檢視。

搜尋演算法

在列表中查詢資料又兩種方式:順序查詢二分查詢。順序查詢適用於元素隨機排列的列表;而二分查詢適用於元素已排序的列表。二分查詢效率更高,但是我們必須在進行查詢之前花費額外的時間將列表中的元素進行排序。

順序查詢

對於查詢資料來說,最簡單的就是從列表中的第一個元素開始對列表元素逐個進行判斷,直到找到了想要的元素,或者直到列表結尾也沒有找到。這種方法稱為順序查詢或者線性查詢

這種查詢的程式碼實現方式很簡單,如下:

/*
* @param { Array } arr 目標陣列
* @param { Number } data 要查詢的陣列
* @return { Boolean } 是否查詢成功
**/
function seqSearch(arr, data){
	for(let i = 0; i < arr.length; i++){
		if(arr[i] === data){
			return true;
		}
	}
	return false;
}
複製程式碼

當然,看到上面的程式碼,你也許會簡化成下面的這樣的程式碼:

function seqSearch(arr, data){
	return arr.indexOf(data) >= 0 ? true : false;
}
複製程式碼

實現的方式有多種,但是原理都是一樣的,要從第一個元素開始遍歷,有可能會遍歷到最後一個元素都找不到要查詢的元素。所以,這是一種暴力查詢技巧的一種。

那麼,有什麼更加高效的查詢方法嘛?這就是我們接下來要講的了。

二分查詢演算法

在開始之前,我們來玩一個猜數字遊戲

  • 規則:在數字1-100之間,你朋友選擇要猜的數字之後,由你來猜數字。你每猜一個數字,你的朋友將會作出下面三種迴應之一:
    • 猜對了
    • 猜大了
    • 猜小了

這個遊戲很簡單,如果我們使用二分查詢的策略進行的話,我們只需要經過短短的幾次就確定我們要查詢的資料了。

那麼二分查詢的原理是什麼呢?

二分查詢又稱為折半查詢,對有序的列表每次進行對半查詢。就是這麼簡單@~@!

程式碼實現走一波:

/*
* @param { Array } arr 有序的陣列 ⚠️注意:是有序的有序的有序的
* @param { Number } data 要查詢的資料
* @return { Number } 返回查詢到的位置,未查詢到放回-1值
**/
function binSearch(arr, data){
	let upperBound = arr.length -1,
		 lowerBound = 0;
	while(lowerBound <= upperBound){
		let mid = Math.floor((upperBound + lowerBound) / 2);
		if(arr[mid] < data){
			lowerBound = mid + 1;
		}else if(arr[mid] > data){
			upperBound = mid + 1;
		}else{
			return mid;
		}
	}
	return -1; // 你朋友選要猜的資料在1-100範圍之外
}
複製程式碼

至此,導師計劃--資料結構和演算法已經完結。後期的話會在另一篇文章中補充一下各個演算法的時間複雜度的比較(不作為課程講解,要動筆算算的,而且也就是總結一個表而已~),當然你可以檢視文章演算法的時間複雜度並結合實際編寫的程式碼來自行理解,並去總結。

後話

文章中的一些案例來自coderwhy的資料結構和演算法系列文章,感謝其授權

author_wechat_permission

繪圖軟體 Numbers,本篇文章用到的圖片繪圖稿感興趣可以下載。

課程程式碼可以戳相關演算法來獲取

部分圖片來自網路,侵刪

文章首發:github.com/reng99/blog…

更多內容:github.com/reng99/blog…

參考

相關文章