數據結構篇--------算法

一、貪心算法

  1. 貪心算法的經典應用有:霍夫曼編碼、Prim和Kruskal最小生成樹算法、Dijkstra單源最短路徑算法。
  2. 貪心算法思想:針對一組數據,我們定義了限制值和期望值,希望從中選出幾個數據,在滿足限制值的的情況下,期望值最大。
    嚴格證明貪心算法的正確性是非常複雜的,需要涉及較多的數學推理。而且從實踐的角度來看,大部分能用貪心算法解決的問題正確性是顯而易見的,不需要進行嚴格的數學證明。
    實際上,用貪心算法解決問題的思路,不一定總能給出最優解。
    經典案例:
    1.分糖果
    我們有m個糖果和n個孩子,現在要把糖果分給這些孩子吃,但是糖果少,孩子多(m<n),所以糖果只能分給一部分孩子吃,每個糖果的大小不等,這m個糖果的大小分別是s1,s2,s3,…,sm。除此以外,每個孩子對糖果大小的需求也不一樣,只有糖果的大小大於等於孩子對糖果的需求時孩子才能得到滿足,假設這n個孩子對糖果的需求分別是g1,g2,g3,…,gn。求如何分配糖果能儘可能的滿足最多數量的孩子?
    我們可以把這個問題抽象成,從n個孩子中,抽取一部分孩子分配糖果,讓滿足的孩子個數(期望值)是最大的,限制值是糖果個數m。對於一個孩子來說,如果小的糖果可以滿足,就沒必要用大的糖果,這樣更大的就可以留給其他的對糖果需求更大的孩子。另一方面,對糖果的大小需求小的孩子更容易被滿足,所以可以從需求小的孩子開始分配糖果因爲滿足一個對糖果需求大的孩子和需求小的孩子對我們的期望值的貢獻是一樣的。
    每次從剩下的孩子中找出對糖果的大小需求最小的,然後發給他剩下的糖果中能滿足他的最小的糖果,這樣得到的分配方案也就是滿足最多孩子需求的個數最多的方案
    2.區間覆蓋
    假設我們有n個區間,區間的起始端點和結束端點分別是[l1,r1],[l2,r2],[l3,r3],…,[ln,rn]。我們從這n個區間中選出一部分區間,這部分區間滿足兩兩不相交(端點相交的地方不算相交),最多能選出幾個區間呢?
    解決思路:假設這n個區間中最左端點是lmin,最右端點是rmax,這個問題就相當於選擇幾個不相交的區間,從左到右將[lmin,rmax]覆蓋上,我們按照起始端點從小到大的順序對這n個區間排序。
    每次選擇的時候,左端點和前面的區間是不重合的,右端點又儘可能小的,這樣可以讓剩下的未覆蓋區間儘可能的大,就可以放更多的區間,這實際上就是一種貪心的選擇方法。

在這裏插入圖片描述在這裏插入圖片描述3.跳躍遊戲
給定一個非負整數數組,你最初位於數組的第一個位置,數組中的每個元素代表你可以在該位置跳躍的最大長度,判斷你能否到達最後一個位置?
思路1.從右到左遍歷數組,如果遇到的元素可以到達最後一個元素,則截斷後面的元素,否則繼續向前,若最後剩下的元素大於1個,則判斷爲假,否則爲真,時間複雜度爲O(n)

class Solution {
    public boolean canJump(int[] nums) {
        int n=1;
        for(int i=nums.length-2;i>=0;i--){
            if(nums[i]>=n){
                n=1;
            }else{
                n++;
            }
            if(i==0&&n>1){
                return false;
            }
        }

        return true;
    }
}

思路2:可達性分析:影響是否可達的關鍵是數組中值爲0的節點
A:如果數組中只包含正整數,則一定可以到達最後一個位置,因爲每個節點都可以往後跳,即使一次只跳一次也可以到達最後一個位置。
B:數組中除了正整數,還包含節點值爲0的點,此時就需要對0節點進行可達性判斷。
若節點值爲0,則表示到達該節點,就不能往後跳。
所以爲了避開這個節點,就需要從0節點開始向前尋找任意一個可以跳過該節點的節點,如果不存在這樣一個節點,說明不管怎麼跳,都會落到該0節點上。

public boolean canJump(int[] nums) {
		//兩種特殊情況
        //nums長度爲1,初始位置就是目標位置,直接返回true
        if(nums.length == 1) {
            return true;
        //如果長度不爲1,但第一個位置值爲0,返回false
        } else if(nums[0] == 0) {
            return false;
        }
        //從倒數第二個位置開始遍歷
        for(int i = nums.length - 2; i > 0; i--) {
			//若值爲0,則需要判斷前面是否存在可以跳過該節點的結點
            if(nums[i] == 0) {
                for(int j = i; j >= 0; j-- ) {
                    if(nums[j] > (i - j)) {
                        //說明從nums[j]結點可以直接跳到“0”結點之後,那麼直接就從nums[j]開始向前繼續判斷
                        i = j;
                        break;
                    //一直遍歷到第一個位置還找不到可以跳過“0”結點的結點,說明不可達
                    } else if(j == 0) {
                        return false;
                    }
                }
            }
        }
        return true;
    }

如何用貪心算法實現霍夫曼編碼?
假設我有一個包含1000個字符的文件,每個字符佔1個byte(1byte=8bits),存儲這1000個字符就一共需要8000bits,那麼也沒有更節約存儲空間的存儲方式呢?
假設我們通過統計分析發現,這1000個字符中包含6種不同的字符,假設他們分別是a,b,c,d,e,f。而3個二進制位(bit)就可以表示8個不同的字符,所以,爲了節約存儲空間,每個字符我們用3個二進制位來表示。那麼存儲這1000個字符只需要3000bits就可以了,比原來節約很多的空間,但是有沒有更節約的存儲方式呢?
此時,霍夫曼編碼就要出現了,霍夫曼編碼是一種十分有效的編碼方式,廣泛應用於數據壓縮中,其壓縮率通常在20%~90%之間。霍夫曼編碼不僅會考察文本中有多少個不同的字符,還會考察每個字符出現的頻率,根據頻率的不同,選擇不同長度的編碼,霍夫曼編碼試圖用這種不等長的編碼方式來進一步增加壓縮的效率。根據貪心的思想,可以把出現頻率較高的字符用稍微短一些的編碼,出現頻率較低的字符,用稍微長一些的編碼。
因爲霍夫曼編碼是不等長的,每次應該讀取1位還是2位等等來解壓縮呢?爲了避免在解壓縮的過程中的歧義,霍夫曼編碼要求各個字符的編碼之間,不會出現某個編碼是另一個編碼前綴的情況。
在霍夫曼編碼中如何根據字符出現的頻率的不同給不同的字符進行不同長度的編碼呢?
我們把每個字符看作一個節點,並且附帶着把頻率放到優先級隊列中。我們從隊列中取出頻率最小的兩個節點A、B,然後新建也該節點C,把頻率設置位兩個節點的頻率之和,並把這個新節點C作爲節點A、B的父節點,最後再把C節點放到優先級隊列中,重複這個過程,直到隊列中沒有數據。
在這裏插入圖片描述
現在,給每一條邊加上一個權值,指向左子節點的邊統統標記爲0,指向右子節點的邊標記爲1,那麼從根節點到葉子節點的路徑就是葉子節點對應字符的霍夫曼編碼
在這裏插入圖片描述
實際上,貪心算法適用的場景比較有限,這種算法思想更多的是指導設計基礎算法,比如最小生成樹算法、單源最短路徑算法。最難的是如何將要解決的問題抽象成貪心算法模型。
思考:
1.在一個非負整數a中,我們希望從中移除k個數字,讓剩下的數字值最小,如何選擇移除哪k個數字呢?
2.假設有n個人等待被服務,但是服務窗口只有一個,每個人需要被服務的時間長度是不一樣的,如何安排被服務的先後順序,才能讓這n個人總的等待時間最短?
由等待時間最短的先開始服務。

二、回溯算法

  1. 回溯算法的應用:深度優先算法、數獨、八皇后、0-1揹包、圖的着色、旅行商問題、全排列等等。

  2. 回溯的處理思想:有點類似於枚舉搜索,枚舉所有的解,找到滿足期望的解。爲了有規律的枚舉所有可能的解,避免遺漏和重複,我們把問題求解的過程分爲多個階段,每個階段都會面對一個岔路口,我們隨意選一條路走,當發現這條路走不通的時候(不符合期望的解),就回退到上一個岔路口,另選一條走法繼續走

  3. 八皇后問題:我們有一個8*8的棋盤,希望往裏放8個棋子(皇后),每個棋子所在的行、列、對角線都不能有另一個棋子。我們把這個問題劃分爲八個階段,依次i將8個棋子放到第一行、第二行、第三行…第八行,在放置的過程中,我們不停的檢查當前放法,是否滿足要求。如果滿足,則跳到下一行繼續放置棋子;如果不滿足,那就再換一種放法,繼續嘗試。(回溯算法非常適合用遞歸來實現)

三、動態規劃算法(一般解決最優解問題)

  • 動態規劃算法:一個模型和三個特徵
  • 一個模型:多階段決策最優解模型
  • 三個特徵:最優子結構、無後效性、重複子問題
    (1).最優子結構:最優子結構指的是問題的最優解包含子問題的最優解,反過來說就是,我們可以通過子問題的最優解推導出問題的最優解。
    (2).無後效性:第一層含義是,在推導後面階段的狀態時,我們之關心前面階段的狀態值,不關心這個狀態是怎麼一步一步推導出來的;第二層含義是,某個階段狀態一旦確定,就不受之後階段的決策影響。只要滿足前面提到的動態規劃問題模型,基本上都會滿足無後效性。
    (3).重複子問題:不同的決策序列,到達某個相同的階段時,可能會產生重複的狀態。
  • 動態規劃解題思路總結:有兩種思路:狀態轉移表法和狀態轉移方程法
    (1 )狀態轉移表法
    一般你用動態規劃解決的問題都可以用回溯算法的暴力搜索解決。所以,當我們拿到問題時,我們可以先用簡單的回溯算法解決,然後定義狀態,每個狀態表示一個節點,然後對應畫出遞歸樹,在遞歸樹中我們可以很容易的看出來,是否存在重複子問題,以及重複子問題是如何產生的,由此來看規律,看是否可用動態規劃解決。
    找到重複子問題後,有兩種辦法,一種是直接用回溯加備忘錄的方法,來避免重複子問題,從執行效率上來看,這跟動態規劃的解決思路沒有差別。第二種是使用動態規劃的解決辦法,狀態轉移表法 。
    (2). 狀態轉移方程法

四、分治算法

  1. MapReduce是Google大數據處理的三駕馬車之一,另外兩個是GFS和Bigtable,他在倒排索引、PageRank計算、網頁分析等搜索引擎相關的技術中都有大量的應用。MapReduce的本質就是分治算法。
  2. 分治算法:核心思想就是分而治之。也就是將原問題劃分爲n個規模較小、並且結構與原問題相似的子問題,遞歸的解決這些子問題,然後再合併其結果,就得到原問題的解。
    分治算法是一種處理問題的思想,遞歸是一種編程技巧,實際上分治算法一般都比較適合用遞歸來實現。
  3. 分治算法可以解決的問題一般需要滿足下面幾個條件:
  • 原問題與分解成的小問題具有相同的模式;

  • 原問題分解成的子問題可以獨立求解,子問題之間沒有相關性,這也是分治算法和動態規劃算法的明顯區別;

  • 具有分解終止條件,即當問題足夠小時,可以直接求解;

  • 可以將子問題合併爲原問題,而這個合併操作的複雜度不能太高,不然就起不到減小算法總體複雜的效果了。
    4.經典題型:

  • 二維平面上有n個點,如何快速的計算出兩個距離最接近的點對?

  • 有兩個nn的矩陣A、B,如何快速求解兩個矩陣的乘積C=AB?

分治思想在海量數據處理中的應用

分治思想不僅僅應用於指導編程和算法設計,還經常用在海量數據的處理。我們之前講的數據機構和算法,大部分都是基於內存存儲和單機處理,但是當要處理的數據量非常大,沒法一次性放到內存中,此時需要分治思想。
比如,給10GB的訂單文件按照金額排序這個需求,因爲機器的內存有限,無法一次加載到內存,也就無法單純的通過歸併排序、快排等基礎算法來解決。
利用分治的思想將海量的數據通過某種方法,劃分爲幾個小的數據集合,每個小的數據集合單獨加載到內存來解決,然後再將小數據集合合併爲大數據集合。先掃描一遍訂單,根據訂單的金額,將10GB的文件劃分爲幾個金額區間,比如將訂單金額爲1到100的放到一個小文件,101到200的放一個小文件,以此類推,每個小文件都能單獨加載到內存排序,最後將這些有序的小文件合併就是最終有序的10GB訂單數據。
對於谷歌搜索引擎來說,網頁爬取、清洗、分析、分詞、計算權重、倒排索引等各個環節都會面對如此海量的數據,所以,利用集羣並行處理是大勢所趨。實際上,Map Reduce框架只是一個任務調度器,底層依賴於GFS來存儲數據,依賴Borg中的機器執行,並且時刻監視機器執行的速度,一旦出現機器宕機、進度卡殼等就會重新從Borg中調度一臺機器執行。
MapReduce提供了高可靠、高性能、高容錯的並行計算框架,並行的處理這幾十億、上百億的網頁。

四種算法思想的比較分析

如果對這四種算法思想進行分類的話,貪心、回溯、動態規劃爲一類,分治算法爲另一類。因爲前三個算法解決問題的模型,都可以抽象成多階段決策最優解模型,而分治算法解決的打本份問題也是最優解問題,但是大部分不能抽象爲多決策模型。
基本上能用動態規劃、貪心解決的問題都可以用回溯解決。回溯相當於窮舉搜索,窮舉所有的情況,然後對比得到最優解。但是,回溯的時間複雜度很高,是指數級別的,只能解決小規模數據的問題。
儘管動態規劃比回溯算法高效,但是不是所有的問題都可以用動態規劃來解決。能用動態規劃解決的問題,需滿足最優子結構、無後效性、重複子問題三個條件。在重複子問題上,動態規劃和分治算法的區別十分明顯。分治算法要求分割成的子問題,不能有重複子問題,而動態規劃正好相反,動態規劃之所以高效,就是因爲回溯算法實現中存在大量的重複子問題。
貪心算法實際上是動態規劃的一種特殊情況。他更高效,但是能解決的問題有限,它能解決的問題需要滿足最優子結構、無後效性、貪心選擇性。貪心選擇性是指通過局部的最優的選擇,能產生全局的最優選擇。每個階段,我們都選擇當前看起來最優的決策,所有階段的決策完成之後,最終由這些局部最優解構成全局最優解。

發佈了11 篇原創文章 · 獲贊 2 · 訪問量 573
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章