算法筆記

最近在閱讀關於算法書,想把算法在過一遍,畢竟自己不是科班出身,網上有人說我們java攻城獅不懂算法一樣可以寫代碼,我個人的觀點是你要想當碼農,是可以不需要懂底層算法,要是你想研究底層,看底層源碼實現,算法是必須要懂的,沒見過那個真正的大佬是不懂算法的,懂算法能加深自己對底層的理解,純屬個人觀點.

關於算法的標準:大O表示法

算法複雜度大體分類
O(1)    最優秀的算法 不論處理多少數據都是常數級時間 
O(logN) 
O(N)
O(N²)

一.簡單算法 O(N²)

先從最簡單的算法說起,也是我們最熟悉的算法,他們的複雜度都是O(N²),說他們簡單是因爲好理解,而且你完全可以很快的手敲出一個他們的實現

1.冒泡排序

原理:舉個例子來說 在一個有十個數字的數組中,拿到左邊第一個數字開始和相鄰右邊的數字開始比較,如果比右邊大則換位,如果右邊的比較大,則拿右邊數字和下一個做比較,每次都能把最大的一個移動到最右邊.

2.順序排序

原理:從左邊開始標記第一個數字,和後面的數字做比較,發現比之前標記小的數字則標記它,繼續拿後面的數字和他作比較,知道全部比較一遍,找到最小的數字,並且將最小的數字放到最左邊,然後在從第二個數字開始反覆循環.

比較:相對於冒泡,順序排序減少了數字換位的次數,每循環一次只有一個元素被換位置.

3.插入排序

原理:拿出一個數字,找到比這個數字n-1位置上大,比n位置小的位置,將這個數字插入到n-1位置後面,n位置以後的數字都向後移動.

這三種排序插入排序效率都是最高的,但是他們的複雜度都是O(N²).在算法中複雜度是O(N²)就是很差的算法,但是在數據量很少的情況下有時候這三種算法反而更加適合.

二.關於歸併排序和遞歸

1.歸併排序

歸併排序的複雜度,用大O表示法來表示是:O(N*logN).

一句話總結的話,就是把一個麻煩的問題分解,而且是一層一層的分解,分解若干個小碎片,從小的碎片開始逐層解決,解決完成一層後,在返回到上一層,依次類推,其實這也是算法比較常用的解決問題的思路,舉個例子吧,如下面這個數組,使用歸併排序實現圖:

在歸併算法實現中使用了遞歸來處理,但是實際的過程中遞歸的實際效率並不是很高.

歸併排序能做到複雜度到O(N*logN),也已經算是比較優秀的算法了,但是和後面同樣是O(N*logN)算法相比(例如快排),他有一個劣勢他在處理時需要附加的內存空間輔助它將這個排序碎片化.

2.關於遞歸和棧

我這也是想到那寫到哪,其實遞歸的實現是和棧這種數據結構又緊密的聯繫的,當你調用一個方法的時候,編譯器會把這個方法所有的參數及其返回地址(這個方法能夠到達的地方)都壓入棧裏,然後將棧的控制權交給這個方法,當方法返回數據是,這些值開始退棧,參數消失了,並且控制權重新回到地址處.
遞歸也是利用了棧這種後進先出的原則來實現的.

三.高級排序

1.希爾排序(shell)

希爾排序其實是前面提到的簡單排序中插入排序的改進,因爲插入排序是從左邊開始依次拿數據做比較插入到對應位置的,如果排序已經進行到了最後,前面已經有N個數據了,這時你取到一個最小值,這時你需要把它插入到最左邊,這以爲着你之前排好的數據都要移動一遍,即:n次,當然這是極端情況,平均下來就是移動 n/2次,有n個元素,所以要移動 n²/2次,用大O表示法,插入排序他的複雜度就是O(N²).

1.1希爾排序的具體實現

希爾排序是通過加大插入排序中元素之間的間隔,並且在有間隔的數據進行插入排序,同時實現數據項能大跨度的移動.

上圖是跨度爲4時

在這個過程中,可能數組會很大,我們會選擇不同的跨度,而跨度的有相對應的計算公式:

最大間隔:h=3*h+1
最小間隔:(h-1)/3
這個h初始值是1.
h最常見的跨度值:1 4 13 40 121 363 1093

當跨度間隔越來越小的時候,排序也就越來越接近最終的結果.

1.2希爾排序的複雜度

剛纔說了,希爾排序是插入排序的一種改進,確實在加入跨度值後,希爾排序的複雜度O(N)(3/2)方 -O(N)(7/6)方

這裏統一使用O(log²N)

2.快速排序

2.1 關於劃分算法

將一個數據根據一個標準值(一般是平均值)劃分成兩組的操作.

劃分是快速排序的基礎.
劃分算法是兩個指針分別衝兩端相向而行,直到兩個指針相遇爲止,劃分算法的O(N)

2.2 快速排序

毫無疑問,快速排序是目前最流行的排序算法,大多數情況下快速排序都是最快的,他的時間複雜度是:O(N*logN).
快速排序的本質,是通過劃分將數據劃分成兩個數組,在通過兩個數組自己調用遞歸,對自己的內部元素進行排序.當然這個實現中還要加一些其他的東西,對算法本身進行加工.

2.3 選擇按鈕

按鈕的選擇:

1.在數據中選擇一個具體的數據做按鈕.

2.可以任意選擇一個數據項做按鈕,一般爲了簡便,都是選擇數組最右邊的>元素.

3.要保證按鈕左邊的元素都小於該元素,元素右邊都大於該元素,也就是說按鈕放入的位置就是他最終的位置.


這裏涉及到一個問題,你選擇一個按鈕,然後把它插入到數組內某個位置時,需要刪除他原來所在的位置的元素,並且將在他插入位置右邊的數據都移動一遍.這個是沒有必要的,常見做法是:36和處在第四個位置上的63交換一下位置.

快排就是利用了按鈕的位置就是其最終位置的特性,當遇到較長數組時,可以將數組多次分割成小的數組,每次分割的過程中都會產生一個按鈕.但是這樣也是有弊端的,因爲理想狀態下按鈕是正好能夠將數組分割成對等的兩個更小的數組,但是實際情況並非如此,有些按鈕將數組分割的過度不均勻時,極端情況就是每次選的按鈕都是將數組分成了,1和N-1這個兩個數組,這樣就會使快排的算法複雜度從原來的:

O(N*logN) => O(N²)

這樣還帶來了一個潛在的問題,隨着算法複雜度的上升,也增加了遞歸運算的次數,相對應的棧的壓力也會變大,有溢出的風險.

綜上所述,可以看出快速排序中按鈕的選擇尤爲重要,比較常用的方式是,從數組兩端和數組中間分別拿到三個數字,取他們的中間值做按鈕.這樣有效的避免了取到最大值,或者最小值,做按鈕的可能性.同時當面對較小的數組的時候,可以使用插入排序.

四.關於二叉樹

1.關於二叉樹我的一點理解

對於二叉樹這種結構來說,作爲一個程序員一定不陌生,他在特定的場景下有效的結合了,數組和鏈表的優點,這裏稍微說一下數組和鏈表:

數組:查詢,修改快,但是它的插入和刪除性能相對比較的底,因爲不管是新增還是刪除產生的位置變動都需要移動後面的對象,這是多餘的性能開銷.

鏈表:插入刪除快,查找修改慢,鏈表當你需要插入一個元素的時候,只需要將插入新的元素指向,插入點前一個元素,並且讓插入點後一個元素指向自己就可以,刪除同理,但是它想要查找一個元素時,就需要將整個鏈表遍歷才能找到這個元素.

二叉樹的作用就是集合這兩個數據結構的有點而誕生的;

總體上來說,這種數據結構是將數組和鏈表這種一維數據結構(都是線性的),提升到了二維,包含層數,左右子節點等參數來定位這個數據的位置,提升一個維度有效的結合二者的優點.這也是數據結構設計的一個解決問題的思路吧,個人的一點感想.

2.二叉樹的時間複雜度

二叉樹:查找,插入,刪除等操作

時間複雜度:O(logN)

3.關於二叉樹的缺點

二叉樹的理想狀態是每一個節點都掛着兩個子節點,最好每一層都能如此,這樣才能發揮二叉樹的最佳性能,但是這也是一廂情願的,如果拿一個排列好的數列使用二叉樹去直接插入的話,就會出現最差情況,二叉樹就會出現只有單一路徑,一直向左或向右,說白了這就使二叉樹變成了鏈表,這樣他們的效率就會變成鏈表,爲了避免這種情況後來又出現了會旋轉的紅黑樹,2,3,4樹.他們使用了一些特殊的算法去處理以上所說的樹不平衡的問題.

五.關於堆

1.堆的實現

堆是使用二叉樹實現的,他是一種完全二叉樹,看圖

他是一種除了最後一層其他層每個節點都掛了兩個子節點的書,當時他不同於一般的樹的是,它左右兩邊並不是像二叉樹一樣,遵循左小右大的原則,也就是說他是一種’弱’關係的樹,

2.堆的插入

當刪除樹的根節點時,堆是這樣處理的,層層比較,將較大的節點移動到上一層.

六.哈希表

1.關於哈希(hash)

哈希(hash)是計算機編程中最常見的,他不但速度快,而且也很簡單,他是java編程語言最常見,也是最基礎的算法(Object.hashcode()),結構(哈希表),他的時間複雜度是O(1),不管數據量有多大,複雜度都是常數級別的,這也是算法中最優秀的.

在Java中通過hash算法直接將一個對象經過hashCode(),算出一個數字,在java中這是一個16進制的數字,他直接表示的是,這個對象在堆中的地址,也就是說你在不重寫toString()方法是輸出的地址就是hash算出來的.

2.關於哈希的弊端與解決

使用哈希雖然很快,但是有一個最大的問題,他對空間的利用率可能並不高,而且對象相同時計算的結果相同,這些問題就需要一些其他的機制去解決,如果解決不當也會出現內存利用的浪費.

總結

1.數據結構的橫向對比

在最近的開發中我就遇到了使用到數據結構問題

List<User> userList = new arrayList<>();
`for(Long userId:userIdList){
    User user = userDao.getById(userId);
    userList.put(user);
 }`

這裏當時並不知道會有多少個user信息放進去,debug的時候發現將近有3000人的信息放到userList中,(當然我這是舉個例子,現實中不會一次一次掉數據庫JDBC每次獲取一個user信息,這裏一般都要批量處理),後來看了一下ArrayList的源碼實現,它的初始化大小是10,當超過最大容量時,他會擴容成原來的1.5倍, 10 –> 3000 每次擴容1.5倍!!,這要擴容多少次!所以速度慢,這裏不應該使用ArrayList,應該使用LinkList,即List的基於鏈表的實現.數組是查找快,鏈表是新增刪除快.當然這也是我舉個例子,想必大家可能不會像我一樣這麼簡單的東西都不會用.

 2.算法的橫向對比

這裏算法並不是都要使用性能最好的,每一個算法都有自己試用的範圍,例如
當數據量小於1000條時,完全可以使用插入排序,當數據量上升在5000條以內的時候,可以採用希爾排序,當數據量再上升時,可以採用更高級的排序,並且這些排序也都有自己的缺陷,快速排序在不隨機的情況下,複雜度也會提升到O(N²).

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