我們?cè)?a href="http://www.itdecent.cn/p/04d986590ad8" target="_blank">面向?qū)ο蟮难葸M(jìn)過(guò)程一文中介紹了面向?qū)ο蟀l(fā)展的幾個(gè)階段,其中第一個(gè)階段遠(yuǎn)古時(shí)期的程序由數(shù)據(jù)結(jié)構(gòu)和算法組成的。其中,數(shù)據(jù)結(jié)構(gòu)表示數(shù)據(jù)的組織形式,基本的數(shù)據(jù)結(jié)構(gòu)包括數(shù)組、鏈表、棧、隊(duì)列、樹(shù)、哈希表、圖、堆等。而算法表示對(duì)數(shù)據(jù)結(jié)構(gòu)中的數(shù)據(jù)進(jìn)行處理的方式或過(guò)程,換句話說(shuō),就是解決問(wèn)題的方法。它們倆之間的關(guān)系:數(shù)據(jù)結(jié)構(gòu)為算法服務(wù),很多算法依賴于特定的數(shù)據(jù)結(jié)構(gòu),但不是全部算法,算法可以和數(shù)據(jù)結(jié)構(gòu)沒(méi)有關(guān)系。本期我們就來(lái)聊一聊算法。
學(xué)習(xí)算法的重要性
在介紹具體算法之前,我先談一下個(gè)人對(duì)學(xué)習(xí)算法的初心。我的初心無(wú)非有兩點(diǎn):一,BAT等互聯(lián)網(wǎng)公司招聘面試時(shí)要問(wèn)算法知識(shí),如果想要進(jìn)入互聯(lián)網(wǎng)公司,我就必須學(xué)好算法;二,通過(guò)學(xué)習(xí)算法提升個(gè)人開(kāi)發(fā)的基本功,這樣一來(lái),對(duì)于不同場(chǎng)景我就可以正確選擇對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu)和算法,使得程序更健壯,提高程序的運(yùn)行效率。
應(yīng)用領(lǐng)域
目前計(jì)算機(jī)各個(gè)細(xì)分領(lǐng)域涉及到不同的算法。比如說(shuō)搜索引擎,平時(shí)我們使用google、百度等瀏覽器,只要我們輸入一個(gè)關(guān)鍵字,瀏覽器就會(huì)快速地返回相關(guān)的集合,這個(gè)集合的背后就隱藏著許多算法。如果沒(méi)有這些算法,我們是不可能這么快速地得到想要的結(jié)果。再比如說(shuō)人工智能,通過(guò)計(jì)算模型算法實(shí)現(xiàn)人體識(shí)別、語(yǔ)音識(shí)別等各應(yīng)用場(chǎng)景。
算法分析
上文我們已經(jīng)介紹到算法就是解決問(wèn)題的方法,而對(duì)于同一個(gè)問(wèn)題,可能存在不同的解決方法。因此,為了衡量一個(gè)算法的優(yōu)劣,提出了時(shí)間復(fù)雜度與空間復(fù)雜度這兩個(gè)概念。
時(shí)間復(fù)雜度
一般情況下,算法中基本操作重復(fù)執(zhí)行的次數(shù)是問(wèn)題規(guī)模n的某個(gè)函數(shù)f(n),算法的時(shí)間度量記為 T(n) = O(f(n)),它表示隨問(wèn)題規(guī)模n的增大,算法執(zhí)行時(shí)間的增長(zhǎng)率和f(n)的增長(zhǎng)率相同,稱作算法的漸近時(shí)間復(fù)雜度,簡(jiǎn)稱時(shí)間復(fù)雜度。
空間復(fù)雜度
空間復(fù)雜度是對(duì)一個(gè)算法在運(yùn)行過(guò)程中臨時(shí)占用存儲(chǔ)空間大小的量度,記做S(n)=O(f(n))。一個(gè)算法的優(yōu)劣主要從算法的執(zhí)行時(shí)間和所需要占用的存儲(chǔ)空間兩個(gè)方面衡量。
排序算法
根據(jù)時(shí)間復(fù)雜度我們大體可以將排序算法分為兩類,一類是以選擇排序?yàn)榇淼?code>O(n^2)的算法,另一類是以快速排序?yàn)榇淼?code>O(nlogn)的算法??吹竭@里我們不禁會(huì)問(wèn):既然有O(nlogn)的排序算法,那些O(n^2)的算法還有存在的必要嗎?要回答這個(gè)問(wèn)題,先來(lái)看下O(n^2)的排序算法的特點(diǎn):首先,它相對(duì)是比較基礎(chǔ)的,編碼簡(jiǎn)單,易于實(shí)現(xiàn),在一些特定場(chǎng)景下O(n^2)更適合 ,譬如在機(jī)器語(yǔ)言中O(n^2)更容易實(shí)現(xiàn);其次,簡(jiǎn)單的排序算法思路衍生出復(fù)雜的排序算法,比如說(shuō)希爾排序是對(duì)插入排序的優(yōu)化;最后,對(duì)于一些簡(jiǎn)單的算法,由于它們本身的一些性質(zhì),可以被用作改進(jìn)更復(fù)雜排序算法的子過(guò)程中。基于此,本文O(n^2)排序算法中兩個(gè)代表性的算法即選擇算法和插入算法。

選擇排序
思想:在整個(gè)待排序數(shù)組里找到最小的值,然后和待排序中的第一個(gè)元素進(jìn)行交換,接著在剩下的元素里找到最小的元素,接著將它和待排序中的第一個(gè)元素進(jìn)行交換,以此類推。為了加深大家的理解,舉個(gè)具體例子,對(duì)8、6、2、3、1、5、7、4進(jìn)行升序排序。

選擇排序的Java語(yǔ)言實(shí)現(xiàn)如下:
/**
* 思路:每次從待選數(shù)組中選擇一個(gè)最小元素,然后和對(duì)應(yīng)位置交換位置
* @param arr
* @param n
*/
public void sort(int[] arr, int n) {
for(int i=0;i<n;i++) {
// 1. 尋找[i,n)區(qū)間里的最小元素
int minIndex = i;
for(int j=i+1;j<n;j++ ) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
// 2. 交換位置
this.swap(arr,i,minIndex);
}
}
插入排序
思路:插入排序是在一個(gè)已經(jīng)有序的小序列的基礎(chǔ)上,一次插入一個(gè)元素。當(dāng)然,剛開(kāi)始這個(gè)有序的小序列只有1個(gè)元素,就是第一個(gè)元素。比較是從有序序列的末尾開(kāi)始,也就是想要插入的元素和已經(jīng)有序的最大者開(kāi)始比起,如果比它大則直接插入在其后面,否則一直往前找直到找到它該插入的位置。如果碰見(jiàn)一個(gè)和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后順序沒(méi)有改變,從原無(wú)序序列出去的順序就是排好序后的順序,所以插入排序是穩(wěn)定的。

插入排序的Java語(yǔ)言實(shí)現(xiàn)如下:
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);
}
}
通過(guò)比較選擇排序和插入排序的代碼實(shí)現(xiàn),我們可以發(fā)現(xiàn)一旦有部分排序好之后,新插入一個(gè)數(shù)如果比排好序最大值還要大,則不用再和其他數(shù)字比較,減少了比較次數(shù)。但是,我們應(yīng)該注意到插入排序在每次遍歷的時(shí)候都需要進(jìn)行交換操作,這個(gè)交換操作包含三次賦值操作,導(dǎo)致插入排序的時(shí)間要比選擇排序的時(shí)間更長(zhǎng)。針對(duì)這個(gè)問(wèn)題,我們的先輩們想到了一個(gè)方法:先將待比較元素復(fù)制一份,然后依次和有序數(shù)組中的元素進(jìn)行比較,如果比有序數(shù)組中的元素小,則將有序數(shù)組中的元素覆蓋待比較元素,以此類推。如下圖所示,首先我們將元素6復(fù)制一份,接著驗(yàn)證元素6是否應(yīng)當(dāng)放在當(dāng)前位置,通過(guò)比較6和它之前的元素大小,發(fā)現(xiàn)元素8應(yīng)該放在元素6的位置上,因此將元素8覆蓋元素6,然后我們考查元素6是否應(yīng)該放在前一個(gè)元素位置上,此時(shí),由于元素8在第0個(gè)位置上我們就不用比較直接覆蓋。它的Java代碼實(shí)現(xiàn)如下:

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;
}
這樣一來(lái),內(nèi)循環(huán)只需要進(jìn)行一次賦值操作,效率得到了大大優(yōu)化,不僅超過(guò)了選擇排序,而且在待排序數(shù)組是有序的情況下,時(shí)間復(fù)雜度可以達(dá)到O(n)。