常見算法思想算法

前言:本篇文章總結中用到很多其他博客內容,本來想附上原作鏈接,但很久了未找到,這裏關於原創性均來源於原作者。

分治法

分治策略的思想:

顧名思義,分治是將一個原始問題分解成多個子問題,而子問題的形式和原問題一樣,只是規模更小而已,通過子問題的求解,原問題也就自然出來了。總結一下,大致可以分爲這樣的三步:

分解:將原問題劃分成形式相同的子問題,規模可以不等,對半或2/3對1/3的劃分。
解決:對於子問題的解決,很明顯,採用的是遞歸求解的方式,如果子問題足夠小了,就停止遞歸,直接求解。
合併:將子問題的解合併成原問題的解。
這裏引出了一個如何求解子問題的問題,顯然是採用遞歸調用棧的方式。因此,遞歸式與分治法是緊密相連的,使用遞歸式可以很自然地刻畫分治法的運行時間。所以,如果你要問我分治與遞歸的關係,我會這樣回答:分治依託於遞歸,分治是一種思想,而遞歸是一種手段,遞歸式可以刻畫分治算法的時間複雜度。所以就引入本章的重點:如何解遞歸式?

分治法適用的情況

分治法所能解決的問題一般具有以下幾個特徵:

該問題的規模縮小到一定的程度就可以容易地解決
該問題可以分解爲若干個規模較小的相同問題,即該問題具有最優子結構性質。
利用該問題分解出的子問題的解可以合併爲該問題的解;
該問題所分解出的各個子問題是相互獨立的,即子問題之間不包含公共的子子問題。
第一條特徵是絕大多數問題都可以滿足的,因爲問題的計算複雜性一般是隨着問題規模的增加而增加;
第二條特徵是應用分治法的前提它也是大多數問題可以滿足的,此特徵反映了遞歸思想的應用;、
第三條特徵是關鍵,能否利用分治法完全取決於問題是否具有第三條特徵,如果具備了第一條和第二條特徵,而不具備第三條特徵,則可以考慮用貪心法或動態規劃法。
第四條特徵涉及到分治法的效率,如果各子問題是不獨立的則分治法要做許多不必要的工作,重複地解公共的子問題,此時雖然可用分治法,但一般用動態規劃法較好。

————————————————————————————————————————————

最大堆最小堆

1、堆

堆給人的感覺是一個二叉樹,但是其本質是一種數組對象,因爲對堆進行操作的時候將堆視爲一顆完全二叉樹,樹種每個節點與數組中的存放該節點值的那個元素對應。所以堆又稱爲二叉堆,堆與完全二叉樹的對應關係如下圖所示:

通常給定節點i,可以根據其在數組中的位置求出該節點的父親節點、左右孩子節點,這三個過程一般採用宏或者內聯函數實現。書上介紹的時候,數組的下標是從1開始的,所有可到: PARENT(i)=i/2  LEFT(i) = 2i   RIGHT(i) = 2i+1
  
根據節點數值滿足的條件,可以將分爲最大堆和最小堆。
最大堆的特性是:除了根節點以外的每個節點i,有A[PARENT(i)] >= A[i],最小堆的特性是:除了根節點以外的每個節點i,有A[PARENT(i)] >=A[i]。
  
把堆看成一個棵樹,有如下的特性:

(1)含有n個元素的堆的高度是lgn。
(2)當用數組表示存儲了n個元素的堆時,葉子節點的下標是n/2+1,n/2+2,……,n。
(3)在最大堆中,最大元素該子樹的根上;在最小堆中,最小元素在該子樹的根上。
2、保持堆的性質

堆個關鍵操作過程是如何保持堆的特有性質,給定一個節點i,要保證以i爲根的子樹滿足堆性質。書中以最大堆作爲例子進行講解,並給出了遞歸形式的保持最大堆性的操作過程MAX-HEAPIFY。先從看一個例子,操作過程如下圖所示:

從圖中可以看出,在節點i=2時,不滿足最大堆的要求,需要進行調整,選擇節點2的左右孩子中最大一個進行交換,然後檢查交換後的節點i=4是否滿足最大堆的要求,從圖看出不滿足,接着進行調整,直到沒有交換爲止。

3、建堆

建立最大堆的過程是自底向上地調用最大堆調整程序將一個數組A[1…N]變成一個最大堆。將數組視爲一顆完全二叉樹,從其最後一個非葉子節點(n/2)開始調整。調整過程如下圖所示:

4、堆排序算法

堆排序算法過程爲:先調用創建堆函數將輸入數組A[1…n]造成一個最大堆,使得最大的值存放在數組第一個位置A[1],然後用數組最後一個位置元素與第一個位置進行交換,並將堆的大小減少1,並調用最大堆調整函數從第一個位置調整最大堆。給出堆數組A={4,1,3,16,9,10,14,8,7}進行堆排序簡單的過程如下:
(1)創建最大堆,數組第一個元素最大,執行後結果下圖:

(2)進行循環,從length(a)到2,並不斷的調整最大堆,給出一個簡單過程如下:

5、問題

(1)在創建最大堆的過程中,爲什麼從最後一個非葉子節點(n/2)開始到第一個非葉子結束,而不是從第一個非葉子節點(1)到最後一個非葉子節點(n/2)結束呢?

我的想法是:如果是從第一個非葉子節點開始創建堆,有可能導致創建的堆不滿足堆的性質,使得第一個元素不是最大的。這樣做只是使得該節點的和其左右孩子節點滿足堆性質,不能確保整個樹滿足堆的性質。如果最大的節點在葉子節點,那麼將可能不會出現在根節點中。例如下面的例子:

從圖中可以看出,從第一個非葉子節點開始創建最大堆,最後得到的結果並不是最大堆。而從最後一個非葉子節點開始創建堆時候,能夠保證該節點的子樹都滿足堆的性質,從而自底向上進行調整堆,最終使得滿足最大堆的性質。

6、總結:

1.調整最大堆的時間複雜度:O(lgn)

2.建堆,由於每次比較的高度其實不大,所以對於一個無序的數組n,將其構造成爲最大堆的時間複雜度是O(n),是一個線性的時間複雜度

3.利用最大堆的方式去排序一個無序的數據,時間複雜度是O(n+n*lgn)=O(nlgn)

4.對於最大堆建立的時候,需要主要的是,開始建立的時候都是從最後一個非葉子節點向上(數組向前)進行建立堆,這樣做的目的是,可以讓葉子節點的最大數據通過調整到根節點上。
如果從第一個根節點開始建立堆的話,那麼如果最大節點在葉子節點,這樣調整將不能導致最大數據調整到根節點上,因爲只能保證當前的節點和葉子節點的大小調整,調整完根節點這個節點,之後就沒有調整了。

————————————————————————————————————————————

優先隊列

隊列是一種滿足先進先出(FIFO)的數據結構,數據從隊列頭部取出,新的數據從隊列尾部插入,數據之間是平等的,不存在優先級的。這個就類似於普通老百姓到火車站排隊買票,先來的先買票,每個人之間是平等的,不存在優先的權利,整個過程是固定不變的。而優先級隊列可以理解爲在隊列的基礎上給每個數據賦一個權值,代表數據的優先級。與隊列類似,優先級隊列也是從頭部取出數據,從尾部插入數據,但是這個過程根據數據的優先級而變化的,總是優先級高的先出來,所以不一定是先進先出的。這個過就類似於買火車票時候軍人比普通人優先買,雖然軍人來的晚,但是軍人的優先級比普通人高,總是能夠先買到票。通常優先級隊列用在操作系統中的多任務調度,任務優先級越高,任務優先執行(類似於出隊列),後來的任務如果優先級比以前的高,則需要調整該任務到合適的位置,以便於優先執行,整個過程總是使得隊列中的任務的第一任務的優先級最高。

優先級隊列有兩種:最大優先級隊列和最小優先級隊列,這兩種類別分別可以用最大堆和最小堆實現。書中介紹了基於最大堆實現的最大優先級隊列。一個最大優先級隊列支持的操作如下操作:

INSERT(S,x):把元素x插入到集合S
MAXIMUM(S):返回S中具有最大關鍵字的元素
EXTRACT_MAX(S):去掉並返回S中的具有最大關鍵字的元素
INCREASE_KEY(S,x,k):將元素x的關鍵字的值增加到k,這裏k值不能小於x的原關鍵字的值。
問題

如何使用優先級隊列實現一個先進先出的隊列和先進後出的棧?

我的想法是:隊列中的元素是先進先出(FIFO)的,因此可以藉助最小優先級隊列實現隊列。具體思想是,給隊列中的每個元素賦予一個權值,權值從第一個元素到最後一個依次遞增(如果採用數組實現的話,可以用元素所在的下標作爲優先級,優先級小的先出隊列),元素出隊列操作每次取優先級隊列第一個元素,取完之後需要堆最小優先級隊列進行調整,使得第一個元素的優先級最小。棧中的元素與隊列剛好相反,元素是先進後出(FILO),因此可以採用最大優先級隊列進行實現,與用最小優先級隊列實現隊列思想類似,按照元素出現的順序進行標記元素的優先級,數據越是靠後,優先級越高。
  
  舉例說明採用最小優先級隊列實現先進先出隊列,現在有一組數A={24,15,27,5,43,87,34}共六個數,假設數組下標從1開始,以元素所在數組中的下標爲優先級創建優先級隊列,隊列中元素出入時候調整最小優先級隊列。操作過程如下圖所示:

《算法導論課後習題》

題目如下:請給出一個時間爲O(nlgk)、用來將k個已排序鏈表合併爲一個排序鏈表的算法。此處n爲所有輸入鏈表中元素的總數。(提示:用一個最小堆來做k路合併)。

看到題目第個想到的是歸併排序過程中的歸併操作子過程,從頭開始兩兩比較,找出最小的,然後接着往後比較,常用的是2路歸併。而題目給的是k個已排好序的鏈表(k>=2)。如果沒有提示,我半天不知道如何去實現,幸好提示說用最小堆來做k路合併,於是我想到可以這樣做:創建一個大小爲k的數組,將k個鏈表中的第一個元素依次存放到數組中,然後將數組調整爲最小堆,這樣保證數組的第一個元素是最小的,假設爲min,將min從最小堆取出並存放到最終結果的鏈表中,此時將min所在鏈表的下一個元素到插入的最小堆中,繼續上面的操作,直到堆中沒有元素爲止。舉個例子如下圖所示(只給出不部分操作):

最終結果如下圖所示:

總結:

對於一個有優先級的無序事件,按照優先級的順序進行隊列的進出,實現的時間複雜度是O(lgn)
————————————————————————————————————————————

線性時間的排序

算法線性時間的排序算法主要是有3種,計數排序,基數排序,桶排序。他們不需要比較操作,是通過元素本身的位置來進行排序的,因而需要額外的存儲空閒,但是可以有線性的時間複雜度O(n)。同時這三種排序算法都是穩定的。

計數排序

就是用一個額外的數組記錄每個元素出現的次數,同時利用斐波那契思想將順序的時間相加起來,那麼就得出了每個元素前面有多少個比它小的元素,最後按照這個額外的數組,直接將數據放置在對應的位置即可。

計數排序假設n個輸入元素中的每一個都介於0和k之間的整數,k爲n個數中最大的元素。當k=O(n)時,計數排序的運行時間爲θ(n)。計數排序的基本思想是:對n個輸入元素中每一個元素x,統計出小於等於x的元素個數,根據x的個數可以確定x在輸出數組中的最終位置。此過程需要引入兩個輔助存放空間,存放結果的B[1…n],用於確定每個元素個數的數組C[0…k]。

算法的具體步驟如下:

(1)根據輸入數組A中元素的值確定k的值,並初始化C[1…k]= 0;
(2)遍歷輸入數組A中的元素,確定每個元素的出現的次數,並將A中第i個元素出現的次數存放在C[A[i]]中,然後C[i]=C[i]+C[i-1],在C中確定A中每個元素前面有多個元素;
(3)逆序遍歷數組A中的元素,在C中查找A中出現的次數,並結果數組B中確定其位置,然後將其在C中對應的次數減少1。
舉個例子說明其過程,假設輸入數組A=<2,5,3,0,2,3,0,3>,計數排序過程如下:

基數排序

就是按照基數的大小,從低位到高位來進行排序,首先按照元素的低位進行排序,在排序後的序列上,按照元素的高位在排序,一次從低到高排序,完成最後的排序。低位的排序是按照計數排序來執行的。

基數排序排序過程無須比較關鍵字,而是通過“分配”和“收集”過程來實現排序,它的時間複雜度可達到線性階:O(n)。對於十進制數來說,每一位的在[0,9]之中,d位的數,則有d列。基數排序首先按低位有效數字進行排序,然後逐次向上一位進行排序,直到最高位排序結束。

舉例說明基數排序過程,如下圖所示:

基數排序算法很直觀,假設長度爲n的數組A中,每個元素都有d位數字,其中第1位是最低位,第d位是最高位。

桶排序

比較簡單,就是將待序列分爲不同的區間,每個區間當成一個桶,在自己桶裏面先排序好,然後把各個桶的數據連在一起就可以了。但是這個需要有額外的通的開銷。
計數排序假設輸入是由一個小範圍內的整數構成,而桶排序則假設輸入由一個隨機過程產生的,該過程將元素均勻而獨立地分佈在區間[0,1)上。當桶排序的輸入符合均勻分佈時,即可以線性期望時間運行。桶排序的思想是:把區間[0,1)劃分成n個相同大小的子區間,成爲桶(bucket),然後將n個輸入數分佈到各個桶中去,對各個桶中的數進行排序,然後按照次序把各個桶中的元素列出來即可。

總結

線性時間的排序方法相對於比較排序在時間複雜度上有提高,但是同時需要犧牲額外的空間開銷,這也是正常的。
————————————————————————————————————————————

中位數和順序統計學

本章所討論的問題是在一個由n個不同數值構成的集合中選擇第i個順序統計量問題。主要講的內容是如何在線性時間內O(n)時間內在集合S中選擇第i小的元素,最基本的是選擇集合的最大值和最小值。一般情況下選擇的元素是隨機的,最大值和最小值是特殊情況,書中重點介紹瞭如何採用分治算法來實現選擇第i小的元素,並藉助中位數進行優化處理,保證最壞保證運行時間是線性的O(n)。

1、基本概念

順序統計量:在一個由n個元素組成的集合中,第i個順序統計量是值該集合中第i小的元素。例如最小值是第1個順序統計量,最大值是第n個順序統計量。
  
中位數:一般來說,中位數是指它所在集合的“中間元素”,當n爲奇數時,中位數是唯一的,出現位置爲n/2;當n爲偶數時候,存在兩個中位數,位置分別爲n/2(上中位數)和n/2+1(下中位數)。

2、選擇問題描述

輸入:一個包含n個(不同的)數的集合A和一個數i,1≤i≤n。
輸出:元素x∈A,它恰大於A中其他的i-1個元素。
最直接的辦法就是採用一種排序算法先對集合A進行排序,然後輸出第i個元素即可。可以採用前面講到的歸併排序、堆排序和快速排序,運行時間爲O(nlgn)。接下來書中由淺入深的講如何在線性時間內解決這個問題。

一般的選擇問題似乎要比選擇最大值和最小值要難,但是這兩種問題的運行時間是相同的,都是θ(n)。書中介紹了採用分治算法解決一般的選擇問題,其過程與快速排序過程中劃分類似。每次劃分集合可以確定一個元素的最終位置,根據這個位置可以判斷是否是我們要求的第i小的元素。如果不是,那麼我們只關心劃分產出兩個子部分中的其中一個,根據i的值來判斷是前一個還是後一個,然後接着對子數組進行劃分,重複此過程,直到找到第i個小的元素。劃分可以採用隨機劃分,這樣能夠保證期望時間是θ(n)(假設所有元素是不同的)。

給個例子說明此過程,假設現有集合A={32,23,12,67,45,78,10,39,9,58},要求其第5小的元素,假設在劃分過程中以總是以最後一個元素爲主元素進行劃分。執行過程如下圖所示:

本章中的選擇算法之所以具有線性運行時間,是因爲這些算法沒有進行排序,線性時間的行爲並不是因爲對輸入做假設所得到的結果。

————————————————————————————————————————————

散列表

本章介紹了散列表(hash table)的概念、散列函數的設計及散列衝突的處理。散列表類似與字典的目錄,查找的元素都有一個key與之對應,在實踐當中,散列技術的效率是很高的,合理的設計散函數和衝突處理方法,可以使得在散列表中查找一個元素的期望時間爲O(1)。散列表是普通數組概念的推廣,在散列表中,不是直接把關鍵字用作數組下標,而是根據關鍵字通過散列函數計算出來的。在STL中map容器的功能就是散列表的功能,但是map採用的是紅黑樹實現的,後面接着學習

1、直接尋址表

當關鍵字的的全域(範圍)U比較小的時,直接尋址是簡單有效的技術,一般可以採用數組實現直接尋址表,數組下標對應的就是關鍵字的值,即具有關鍵字k的元素被放在直接尋址表的槽k中。直接尋址表的字典操作實現比較簡單,直接操作數組即可以,只需O(1)的時間。

2、散列表

直接尋址表的不足之處在於當關鍵字的範圍U很大時,在計算機內存容量的限制下,構造一個存儲|U|大小的表不太實際。當存儲在字典中的關鍵字集合K比所有可能的關鍵字域U要小的多時,散列表需要的存儲空間要比直接尋址表少的很多。散列表通過散列函數h計算出關鍵字k在槽的位置。散列函數h將關鍵字域U映射到散列表T[0…m-1]的槽位上。即h:U->{0,1…,m-1}。採用散列函數的目的在於縮小需要處理的小標範圍,從而降低了空間的開銷。
  
  散列表存在的問題:兩個關鍵字可能映射到同一個槽上,即碰撞(collision)。需要找到有效的辦法來解決碰撞。

3、散列函數

好的散列函數的特點是每個關鍵字都等可能的散列到m個槽位上的任何一箇中去,並與其他的關鍵字已被散列到哪一個槽位無關。多數散列函數都是假定關鍵字域爲自然數N={0,1,2,…},如果給的關鍵字不是自然數,則必須有一種方法將它們解釋爲自然數。例如對關鍵字爲字符串時,可以通過將字符串中每個字符的ASCII碼相加,轉換爲自然數。

書中介紹了三種設計方案:除法散列法、乘法散法和全域散列法。

(1)除法散列法

通過取k除以m的餘數,將關鍵字k映射到m個槽的某一箇中去。散列函數爲:h(k)=k mod m 。m不應是2的冪,通常m的值是與2的整數冪不太接近的質數。

(2)乘法散列法

這個方法看的時候不是很明白,沒有搞清楚什麼意思,先將基本的思想記錄下來,日後好好消化一下。乘法散列法構造散列函數需要兩個步驟。第一步,用關鍵字k乘上常數A(0<A<1),並抽取kA的小數部分。然後,用m乘以這個值,再取結果的底。散列函數如下:h(k) = m(kA mod 1)。

(3)全域散列

給定一組散列函數H,每次進行散列時候從H中隨機的選擇一個散列函數h,使得h獨立於要存儲的關鍵字。全域散列函數類的平均性能是比較好的。

4、碰撞處理

通常有兩類方法處理碰撞:開放尋址(Open Addressing)法和鏈接(Chaining)法。前者是將所有結點均存放在散列表T[0…m-1]中;後者通常是把散列到同一槽中的所有元素放在一個鏈表中,而將此鏈表的頭指針放在散列表T[0…m-1]中。

(1)開放尋址法

所有的元素都在散列表中,每一個表項或包含動態集合的一個元素,或包含NIL。這種方法中散列表可能被填滿,以致於不能插入任何新的元素。在開放尋址法中,當要插入一個元素時,可以連續地檢查或探測散列表的各項,直到有一個空槽來放置待插入的關鍵字爲止。有三種技術用於開放尋址法:線性探測、二次探測以及雙重探測。

<1>線性探測

給定一個普通的散列函數h’:U —>{0,1,…,m-1},線性探測方法採用的散列函數爲:h(k,i) = (h’(k)+i)mod m,i=0,1,…,m-1
  
探測時從i=0開始,首先探查T[h’(k)],然後依次探測T[h’(k)+1],…,直到T[h’(k)+m-1],此後又循環到T[0],T[1],…,直到探測到T[h’(k)-1]爲止。探測過程終止於三種情況:
  (1)若當前探測的單元爲空,則表示查找失敗(若是插入則將key寫入其中);
  (2)若當前探測的單元中含有key,則查找成功,但對於插入意味着失敗;
  (3)若探測到T[h’(k)-1]時仍未發現空單元也未找到key,則無論是查找還是插入均意味着失敗(此時表滿)。
 
線性探測方法較容易實現,但是存在一次羣集問題,即連續被佔用的槽的序列變的越來越長。採用例子進行說明線性探測過程,已知一組關鍵字爲(26,36,41,38,44,15,68,12,6,51),用除餘法構造散列函數,初始情況如下圖所示:

散列過程如下圖所示:

<2>二次探測

二次探測法的探查序列是:h(k,i) =(h’(k)+i*i)%m ,0≤i≤m-1 。初次的探測位置爲T[h’(k)],後序的探測位置在次基礎上加一個偏移量,該偏移量以二次的方式依賴於i。該方法的缺陷是不易探查到整個散列空間。

<3>雙重散列

該方法是開放尋址的最好方法之一,因爲其產生的排列具有隨機選擇的排列的許多特性。採用的散列函數爲:h(k,i)=(h1(k)+ih2(k)) mod m。其中h1和h2爲輔助散列函數。初始探測位置爲T[h1(k)],後續的探測位置在此基礎上加上偏移量h2(k)模m。

(2)鏈接法

將所有關鍵字爲同義詞的結點鏈接在同一個鏈表中。若選定的散列表長度爲m,則可將散列表定義爲一個由m個頭指針組成的指針數組T[0…m-1]。凡是散列地址爲i的結點,均插入到以T[i]爲頭指針的單鏈表中。T中各分量的初值均應爲空指針。在拉鍊法中,裝填因子α可以大於1,但一般均取α≤1。

舉例說明鏈接法的執行過程,設有一組關鍵字爲(26,36,41,38,44,15,68,12,6,51),用除餘法構造散列函數,初始情況如下圖所示:

最終結果如下圖所示:

5、字符串散列

通常都是將元素的key轉換爲數字進行散列,如果key本身就是整數,那麼散列函數可以採用keymod tablesize(要保證tablesize是質數)。而在實際工作中經常用字符串作爲關鍵字,例如身姓名、職位等等。這個時候需要設計一個好的散列函數進程處理關鍵字爲字符串的元素。

有以下幾種處理方法:

方法1:將字符串的所有的字符的ASCII碼值進行相加,將所得和作爲元素的關鍵字。設計的散列函數如下所示:

1 int hash(const string& key,int tablesize)
2 {
3 int hashVal = 0;
4 for(int i=0;i<key.length();i++)
5 hashVal += key[i];
6 return hashVal % tableSize;
7 }
1
2
3
4
5
6
7
此方法的缺點是不能有效的分佈元素,例如假設關鍵字是有8個字母構成的字符串,散列表的長度爲10007。字母最大的ASCII碼爲127,按照方法1可得到關鍵字對應的最大數值爲127×8=1016,也就是說通過散列函數映射時只能映射到散列表的槽0-1016之間,這樣導致大部分槽沒有用到,分佈不均勻,從而效率低下。
  
方法2:假設關鍵字至少有三個字母構成,散列函數只是取前三個字母進行散列。設計的散列函數如下所示:

1 int hash(const string& key,int tablesize)
2 {
3 //27 represents the number of letters plus the blank
4 return (key[0]+27key[1]+729key[2])%tablesize;
5 }
1
2
3
4
5
該方法只是取字符串的前三個字符的ASCII碼進行散列,最大的得到的數值是2851,如果散列的長度爲10007,那麼只有28%的空間被用到,大部分空間沒有用到。因此如果散列表太大,就不太適用。

方法3:藉助Horner’s 規則,構造一個質數(通常是37)的多項式,(非常的巧妙,不知道爲何是37)。計算公式爲:key[keysize-i-1]37^i,0<=i<keysize求和。設計的散列函數如下所示:

1 int hash(const string & key,int tablesize)
2 {
3 int hashVal = 0;
4 for(int i =0;i<key.length();i++)
5 hashVal = 37*hashVal + key[i];
6 hashVal %= tableSize;
7 if(hashVal<0) //計算的hashVal溢出
8 hashVal += tableSize;
9 return hashVal;
10 }
1
2
3
4
5
6
7
8
9
10
該方法存在的問題是如果字符串關鍵字比較長,散列函數的計算過程就變長,有可能導致計算的hashVal溢出。針對這種情況可以採取字符串的部分字符進行計算,例如計算偶數或者奇數位的字符。

6、再散列(rehashing)——再散列可以保證平均的查找複雜度是不會變得

如果散列表滿了,再往散列表中插入新的元素時候就會失敗。這個時候可以通過創建另外一個散列表,使得新的散列表的長度是當前散列表的2倍多一些,重新計算各個元素的hash值,插入到新的散列表中。再散列的問題是在什麼時候進行最好,有三種情況可以判斷是否該進行再散列:

(1)當散列表將快要滿了,給定一個範圍,例如散列被中已經被用到了80%,這個時候進行再散列。
(2)當插入一個新元素失敗時候,進行再散列。
(3)根據裝載因子(存放n個元素的、具有m個槽位的散列表T,裝載因子α=n/m,即每個鏈子中的平均存儲的元素數目)進行判斷,當裝載因子達到一定的閾值時候,進行在散列。
  
在採用鏈接法處理碰撞問題時,採用第三種方法進行在散列效率最好。
————————————————————————————————————————————

紅黑樹

紅黑樹是一種二叉查找樹,但在每個結點上增加了一個存儲位表示結點的顏色,可以是RED或者BLACK。通過對任何一條從根到葉子的路徑上各個着色方式的限制,紅黑樹確保沒有一條路徑會比其他路徑長出兩倍,因而是接近平衡的。本章主要介紹了紅黑樹的性質、左右旋轉、插入和刪除。重點分析了在紅黑樹中插入和刪除元素的過程,分情況進行詳細討論。一棵高度爲h的二叉查找樹可以實現任何一種基本的動態集合操作,如SEARCH、PREDECESSOR、SUCCESSOR、MIMMUM、MAXMUM、INSERT、DELETE等。當二叉查找樹的高度較低時,這些操作執行的比較快,但是當樹的高度較高時,這些操作的性能可能不比用鏈表好。紅黑樹(red-black tree)是一種平衡的二叉查找樹,它能保證在最壞情況下,基本的動態操作集合運行時間爲O(lgn)。本章內容有些複雜,看了兩天,才大概清楚其插入和刪除過程,日後需要經常回顧,爭取完全消化掉。紅黑樹的用途非常廣泛,例如STL中的map就是採用紅黑樹實現的,效率非常之高,有機會可以研究一下STL的源代碼。

1、紅黑樹的性質

紅黑樹中的每個結點包含五個域:color、key、left、right和parent。如果某結點沒有一個子結點或父結點,則該結點相應的指針parent域包含值爲NIL(NIL並是是空指針,此處有些迷惑,一會解釋)。把NIL視爲指向紅黑樹的外結點(葉子)的指針,而把帶關鍵字的結點視爲紅黑樹的內結點。紅黑樹結點結構如下所示:

1 #define RED 0
2 #define BLACK 1
3 struct RedBlackTreeNode
4 {
5 T key;
6 struct RedBlackTreeNode * parent;
7 struct RedBlackTreeNode * left;
8 struct RedBlackTreeNode * right;
9 int color;
10 };
1
2
3
4
5
6
7
8
9
10
紅黑樹的性質如下:

(1)每個結點或是紅色,或是黑色。
(2)根結點是黑色。
(3)每個葉子結點(NIL)是黑色。
(4)如果有一個結點是紅色,則它的兩個兒子都是黑色。
(5)對每個結點,從該結點到其孫子結點的所有路徑上包含相同數目的黑色結點。
如下圖是一棵紅黑樹:

從圖可以看出NIL不是空指針,而是一個葉子結點。實際操作的時候可以將NIL視爲哨兵,這樣便於對黑紅色進行操作。紅黑樹的操作主要是對內部結點操作,因爲內部結點存儲了關鍵字的值。書中爲了便於討論,忽略了葉子結點的,如是上圖紅黑樹變成如下圖所示:

書中給出了黑高度的概念:從某個結點x出發(不包含該結點)到達一個葉子結點的任意一條路徑上,黑色結點的個數稱爲該結點的黑高度。由紅黑樹的性質(5)可知,從該結點出發的所有下降路徑都有相同的黑色結點個數。紅黑樹的黑高度定義爲其根結點的黑高度。
  書中給出了一個引理來說明爲什麼紅黑樹是一種好的查找樹,並對引理進行了證明(採用歸納法進行證明的,需要很強的歸納推理知識,正是我的不足之處,看書的痛苦在於此)。
引理:一棵有n個內結點的紅黑樹的高度之多爲2lg(n+1)。

比較與疑問:

1.紅黑樹和平衡二叉樹(AVL樹)的區別與聯繫
2.紅黑樹爲啥增加插入的節點要置爲紅節點
3.紅黑樹在哪裏用的比較多,它相對其他的平衡搜索樹有何優點?

解答:

1, 紅黑樹並不追求“完全平衡”——它只要求部分地達到平衡要求,降低了對旋轉的要求,從而提高了性能。

紅黑樹能夠以O(log2 n) 的時間複雜度進行搜索、插入、刪除操作。此外,由於它的設計,任何不平衡都會在三次旋轉之內解決。當然,還有一些更好的,但實現起來更復雜的數據結構 能夠做到一步旋轉之內達到平衡,但紅黑樹能夠給我們一個比較“便宜”的解決方案。紅黑樹的算法時間複雜度和AVL相同,但統計性能比AVL樹更高。

平衡二叉樹嚴格的高度控制,左右子樹的高度差距不能大於1,這回導致插入刪除有較多的旋轉調整的步驟,並且其樹的高度一定是lgn,也就是說這種樹的平均時間複雜度就是O(lgn)
但是紅黑樹,只用保證任何節點到葉子節點均包含相同數目的黑色節點,通過顏色來約束樹的形狀,主要有以下幾個特徵:
1.紅黑樹的高度總是低於2lg(n+1),n爲節點的個數
2.紅黑樹的時間複雜度爲O(h(x))= O(2lg(n+1))=O(lgn)
3.任何插入刪除導致的不平衡都可以在三次旋轉操作內完成平衡,降低了實現的複雜度。

查找較多的可以選擇用avltree、插入刪除較多的可以用rbtree

1、黑色,如果是黑色,那麼不管原來的紅黑樹是什麼樣的,這樣一定會破壞平衡,因爲原來的樹是平衡的,現在在這一條路徑上多了一個黑色,必然違反了性質5(不記得的時候多看幾遍性質,並理解是最好的)。

2、紅色,如果新插入的點是紅色的,那麼也有可能會破壞平衡,主要可能是違反了性質4,比如上圖中,新插入的點21的父節點22爲紅色。但是卻有一種特殊情況,比如上圖中,如果我插入一個key=0的節點。把0這個節點置爲紅色,並不會影響原來樹的平衡,因爲0的父節點是黑色。
如下圖:

好歹也有不需要調整的情況,所以還是選擇把新插入的節點顏色置爲紅色。

AVL樹

平衡二叉樹,一般是用平衡因子差值決定並通過旋轉來實現,左右子樹樹高差不超過1,那麼和紅黑樹比較它是嚴格的平衡二叉樹,平衡條件非常嚴格(樹高差只有1),只要插入或刪除不滿足上面的條件就要通過旋轉來保持平衡。由於旋轉是非常耗費時間的。我們可以推出AVL樹適合用於插入刪除次數比較少,但查找多的情況。
應用相對其他數據結構比較少。windows對進程地址空間的管理用到了AVL樹。

紅黑樹:平衡二叉樹,通過對任何一條從根到葉子的簡單路徑上各個節點的顏色進行約束,確保沒有一條路徑會比其他路徑長2倍,因而是近似平衡的。所以相對於嚴格要求平衡的AVL樹來說,它的旋轉保持平衡次數較少。用於搜索時,插入刪除次數多的情況下我們就用紅黑樹來取代AVL。

紅黑樹應用比較廣泛:

· 廣泛用在C++的STL中。map和set都是用紅黑樹實現的。
· 著名的linux進程調度Completely Fair Scheduler,用紅黑樹管理進程控制塊。
· epoll在內核中的實現,用紅黑樹管理事件塊
· nginx中,用紅黑樹管理timer等
· Java的TreeMap實現
B樹,B+樹:它們特點是一樣的,是多路查找樹,一般用於數據庫中做索引,因爲它們分支多層數少,因爲磁盤IO是非常耗時的,而像大量數據存儲在磁盤中所以我們要有效的減少磁盤IO次數避免磁盤頻繁的查找。
1
B+樹是B樹的變種樹,有n棵子樹的節點中含有n個關鍵字,每個關鍵字不保存數據,只用來索引,數據都保存在葉子節點。是爲文件系統而生的。

B+樹相對B樹磁盤讀寫代價更低:因爲B+樹非葉子結點只存儲鍵值,單個節點佔空間小,索引塊能夠存儲更多的節點,從磁盤讀索引時所需的索引塊更少,所以索引查找時I/O次數較B-Tree索引少,效率更高。而且B+Tree在葉子節點存放的記錄以鏈表的形式鏈接,範圍查找或遍歷效率更高。Mysql InnoDB用的就是B+Tree索引。

Trie樹:
又名單詞查找樹,一種樹形結構,常用來操作字符串。它是不同字符串的相同前綴只保存一份。

相對直接保存字符串肯定是節省空間的,但是它保存大量字符串時會很耗費內存(是內存)。
類似的有:前綴樹(prefix tree),後綴樹(suffix tree),radix tree(patricia tree, compactprefix tree),crit-bit tree(解決耗費內存問題),以及前面說的double array trie。

前綴樹:字符串快速檢索,字符串排序,最長公共前綴,自動匹配前綴顯示後綴。
後綴樹:查找字符串s1在s2中,字符串s1在s2中出現的次數,字符串s1,s2最長公共部分,最長迴文串。

trie 樹的一個典型應用是前綴匹配,比如下面這個很常見的場景,在我們輸入時,搜索引擎會給予提示。還有比如IP選路,也是前綴匹配,一定程度會用到trie。

面試中紅黑樹常考問題

1.stl中的set底層用的什麼數據結構?
2.紅黑樹的數據結構怎麼定義的?
3.紅黑樹有哪些性質?
4.紅黑樹的各種操作的時間複雜度是多少?
5.紅黑樹相比於BST和AVL樹有什麼優點?
6.紅黑樹相對於哈希表,在選擇使用的時候有什麼依據?
7.如何擴展紅黑樹來獲得比某個結點小的元素有多少個?
8.擴展數據結構有什麼步驟?
9 爲什麼一般hashtable的桶數會取一個素數
詳細解答

1.stl中的set底層用的什麼數據結構?

紅黑樹

2.紅黑樹的數據結構怎麼定義?

  1. enum Color
  2. {
  3.       RED = 0,  
    
  4.       BLACK = 1  
    
  5. };
  6. struct RBTreeNode
  7. {
  8.        struct RBTreeNode*left, *right, *parent;  
    
  9.        int   key;  
    
  10.        int data;  
    
  11.        Color color;  
    
  12. };
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    3.紅黑樹有哪些性質?

一般的,紅黑樹,滿足以下性質,即只有滿足以下全部性質的樹,我們才稱之爲紅黑樹:

1)每個結點要麼是紅的,要麼是黑的。
2)根結點是黑的。
3)每個葉結點(葉結點即指樹尾端NIL指針或NULL結點)是黑的。
4)如果一個結點是紅的,那麼它的倆個兒子都是黑的。
5)對於任一結點而言,其到葉結點樹尾端NIL指針的每一條路徑都包含相同數目的黑結點。
4.紅黑樹的各種操作的時間複雜度是多少?

能保證在最壞情況下,基本的動態幾何操作的時間均爲O(lgn)

5.紅黑樹相比於BST和AVL樹有什麼優點?

紅黑樹是犧牲了嚴格的高度平衡的優越條件爲代價,它只要求部分地達到平衡要求,降低了對旋轉的要求,從而提高了性能。紅黑樹能夠以O(log2 n)的時間複雜度進行搜索、插入、刪除操作。此外,由於它的設計,任何不平衡都會在三次旋轉之內解決。當然,還有一些更好的,但實現起來更復雜的數據結構能夠做到一步旋轉之內達到平衡,但紅黑樹能夠給我們一個比較“便宜”的解決方案。

相比於BST,因爲紅黑樹可以能確保樹的最長路徑不大於兩倍的最短路徑的長度,所以可以看出它的查找效果是有最低保證的。在最壞的情況下也可以保證O(logN)的,這是要好於二叉查找樹的。因爲二叉查找樹最壞情況可以讓查找達到O(N)。

紅黑樹的算法時間複雜度和AVL相同,但統計性能比AVL樹更高,所以在插入和刪除中所做的後期維護操作肯定會比紅黑樹要耗時好多,但是他們的查找效率都是O(logN),所以紅黑樹應用還是高於AVL樹的. 實際上插入 AVL 樹和紅黑樹的速度取決於你所插入的數據.如果你的數據分佈較好,則比較宜於採用 AVL樹(例如隨機產生系列數),但是如果你想處理比較雜亂的情況,則紅黑樹是比較快的

6.紅黑樹相對於哈希表,在選擇使用的時候有什麼依據?

權衡三個因素:== 查找速度, 數據量, 內存使用,可擴展性。==

總體來說,hash查找速度會比map快,而且查找速度基本和數據量大小無關,屬於常數級別;而map的查找速度是log(n)級別。並不一定常數就比log(n) 小,hash還有hash函數的耗時,明白了吧,如果你考慮效率,特別是在元素達到一定數量級時,考慮考慮hash。但若你對內存使用特別嚴格, 希望程序儘可能少消耗內存,那麼一定要小心,hash可能會讓你陷入尷尬,特別是當你的hash對象特別多時,你就更無法控制了,而且 hash的構造速度較慢。

紅黑樹並不適應所有應用樹的領域。如果數據基本上是靜態的,那麼讓他們待在他們能夠插入,並且不影響平衡的地方會具有更好的性能。如果數據完全是靜態的,例如,做一個哈希表,性能可能會更好一些。

在實際的系統中,例如,需要使用動態規則的防火牆系統,使用紅黑樹而不是散列表被實踐證明具有更好的伸縮性。Linux內核在管理vm_area_struct時就是採用了紅黑樹來維護內存塊的。
紅黑樹通過擴展節點域可以在不改變時間複雜度的情況下得到結點的秩。

7.如何擴展紅黑樹來獲得比某個結點小的元素有多少個?

這其實就是求節點元素的順序統計量,當然任意的順序統計量都可以需要在O(lgn)時間內確定。
在每個節點添加一個size域,表示以結點 x 爲根的子樹的結點樹的大小
則有size[x] = size[[left[x]] + size [right[x]] + 1;,這時候紅黑樹就變成了一棵順序統計樹。

利用size域可以做兩件事:

1). 找到樹中第i小的結點;

  1. OS-SELECT(x;,i)
  2. r = size[left[x]] + 1;
  3. if i == r
  4.  return x  
    
  5. elseif i < r
  6.  return OS-SELECT(left[x], i)  
    
  7. else return OS-SELECT(right[x], i)
    1
    2
    3
    4
    5
    6
    7
    思路:size[left[x]]表示在對x爲根的子樹進行中序遍歷時排在x之前的個數,遞歸調用的深度不會超過O(lgn);

2).確定某個結點之前有多少個結點,也就是我們要解決的問題;

  1. OS-RANK(T,x)
  2. r = x.left.size + 1;
  3. y = x;
  4. while y != T.root
  5.      if y == y.p.right  
    
  6.              r = r + y.p.left.size +1  
    
  7.      y = y.p  
    
  8. return r
    1
    2
    3
    4
    5
    6
    7
    8
    思路:x的秩可以視爲在對樹的中序遍歷種,排在x之前的結點個數加上一。最壞情況下,OS-RANK運行時間與樹高成正比,所以爲O (lgn).

8.擴展數據結構有什麼步驟?

1).選擇基礎數據結構;
2).確定要在基礎數據結構種添加哪些信息;
3).驗證可用基礎數據結構上的基本修改操作來維護這些新添加的信息;
4).設計新的操作。
9 爲什麼一般hashtable的桶數會取一個素數

設有一個哈希函數
H( c ) = c % N;
當N取一個合數時,最簡單的例子是取2n,比如說取23=8,這時候
H( 11100(二進制) ) = H( 28 ) = 4
H( 10100(二進制) ) = H( 20 )= 4

這時候c的二進制第4位(從右向左數)就”失效”了,也就是說,無論第c的4位取什麼值,都會導致H( c )的值一樣.這時候c的第四位就根本不參與H( c )的運算,這樣H( c )就無法完整地反映c的特性,增大了導致衝突的機率.

取其他合數時,都會不同程度的導致c的某些位”失效”,從而在一些常見應用中導致衝突.
但是取質數,基本可以保證c的每一位都參與H( c )的運算,從而在常見應用中減小衝突機率
———————————————————————————————————————————

動態規劃(dynamic programming)

通過組合子問題的解而解決整個問題的。分治算法是指將問題劃分爲一些獨立的子問題,遞歸的求解各個問題,然後合併子問題的解而得到原問題的解。例如歸併排序,快速排序都是採用分治算法思想。

而動態規劃與此不同,適用於子問題不是獨立的情況,也就是說各個子問題包含有公共的子問題。如在這種情況下,用分治算法則會重複做不必要的工作。採用動態規劃算法對每個子問題只求解一次,將其結果存放到一張表中,以供後面的子問題參考,從而避免每次遇到各個子問題時重新計算答案。

動態規劃與分治法之間的區別:

(1)分治法是指將問題分成一些獨立的子問題,遞歸的求解各子問題
(2)動態規劃適用於這些子問題不是獨立的情況,也就是各子問題包含公共子問題
動態規劃通常用於最優化問題(此類問題一般有很多可行解,我們希望從這些解中找出一個具有最優(最大或最小)值的解)。動態規劃算法的設計分爲以下四個步驟:

(1)描述最優解的結構
(2)遞歸定義最優解的值
(3)按自低向上的方式計算最優解的值
(4)由計算出的結果構造一個最優解
1、基本概念

動態規劃是通過組合子問題的解而解決整個問題的,通過將問題分解爲相互不獨立(各個子問題包含有公共的子問題,也叫重疊子問題)的子問題,對每個子問題求解一次,將其結果保存到一張輔助表中,避免每次遇到各個子問題時重新計算。動態規劃通常用於解決最優化問題,

其設計步驟如下:

(1)描述最優解的結構。
(2)遞歸定義最優解的值。
(3)按自底向上的方式計算最優解的值。
(4)由計算出的結果構造出一個最優解。
第一步是選擇問題的在什麼時候會出現最優解,通過分析子問題的最優解而達到整個問題的最優解。在第二步,根據第一步得到的最優解描述,將整個問題分成小問題,直到問題不可再分爲止,層層選擇最優,構成整個問題的最優解,給出最優解的遞歸公式。第三步根據第二步給的遞歸公式,採用自底向上的策略,計算每個問題的最優解,並將結果保存到輔助表中。第四步驟是根據第三步中的最優解,藉助保存在表中的值,給出最優解的構造過程。

動態規劃與分治法之間的區別:

(1) 分治法是指將問題分成一些獨立的子問題,遞歸的求解各子問題。
(2) 動態規劃適用於這些子問題不是獨立的情況,也就是各子問題包含公共子問題。
2、動態規劃基礎

什麼時候可以使用動態規範方法解決問題呢?這個問題需要討論一下,書中給出了採用動態規範方法的最優化問題中的兩個要素:最優子結構和重疊子結構。

1)最優子結構

最優子結構是指問題的一個最優解中包含了其子問題的最優解。在動態規劃中,每次採用子問題的最優解來構造問題的一個最優解。尋找最優子結構,遵循的共同的模式:

(1)問題的一個解可以是做一個選擇,得到一個或者多個有待解決的子問題。
(2)假設對一個給定的問題,已知的是一個可以導致最優解的選擇,不必關心如何確定這個選擇。
(3)在已知這個選擇後,要確定哪些子問題會隨之發生,如何最好地描述所得到的子問題空間。
(4)利用“剪貼”技術,來證明問題的一個最優解中,使用的子問題的解本身也是最優的。
最優子結構在問題域中以兩種方式變化:

(1)有多少個子問題被使用在原問題的一個最優解中。
(2)在決定一個最優解中使用哪些子問題時有多少個選擇。
動態規劃按照自底向上的策略利用最優子結構,即:首先找到子問題的最優解,解決子問題,然後逐步向上找到問題的一個最優解。爲了描述子問題空間,可以遵循這樣一條有效的經驗規則,就是儘量保持這個空間簡單,然後在需要時再擴充它。

注意:在不能應用最優子結構的時候,就一定不能假設它能夠應用。 警惕使用動態規劃去解決缺乏最優子結構的問題!

使用動態規劃時,子問題之間必須是相互獨立的!可以這樣理解,N個子問題域互不相干,屬於完全不同的空間。

2)重疊子問題

用來解決原問題的遞歸算法可以反覆地解同樣的子問題,而不是總是產生新的子問題。重疊子問題是指當一個遞歸算法不斷地調用同一個問題。動態規劃算法總是充分利用重疊子問題,通過每個子問題只解一次,把解保存在一個需要時就可以查看的表中,每次查表的時間爲常數。

由計算出的結果反向構造一個最優解:把動態規劃或者是遞歸過程中作出的每一次選擇(記住:保存的是每次作出的選擇)都保存下來,在最後就一定可以通過這些保存的選擇來反向構造出最優解。

做備忘錄的遞歸方法:這種方法是動態規劃的一個變形,它本質上與動態規劃是一樣的,但是比動態規劃更好理解!

(1) 使用普通的遞歸結構,自上而下的解決問題。
(2) 當在遞歸算法的執行中每一次遇到一個子問題時,就計算它的解並填入一個表中。以後每次遇到該子問題時,只要查看並返回表中先前填入的值即可。
3、總結

動態規劃的核心就是找到問題的最優子結構,在找到最優子結構之後的消除重複子問題。最終無論是採用動態規劃的自底向上的遞推,還是備忘錄,或者是備忘錄的變型,都可以輕鬆的找出最優解的構造過程。

4、思考

使用動態規劃首先確定解決的問題是否具有最優子結構,那麼隨之而來的是,什麼是最優子結構?

最優子結構就是說,原文題的最優解也是子問題的最優解,例如算導上面的鋼條切割的問題,因爲原問題是n長度鋼條切割的最優解,那麼子問題是n-i長度的鋼條的最優解,這樣的問題是最優子結構的問題,我們可以認爲存在一個最優的切割方法導致了出現了n-i長度的鋼條,那麼就是i長度和n-i長度鋼條的最優切割方法決定了n長度鋼條的最優切割方法,也就是說子問題的最優解就是原問題的最優解。

那麼什麼樣的問題,不滿足最優子結構呢?

有一個網友給出的例子;
給定一個n*m的矩陣,每個格子裏有一個正整數,從左上角開始到右下角,每次只能向下或向右走,問經過的數之和模k後最大能是多少?

定義狀態f(i,j)表示從左上角走到i,j的最優解(這裏最優解指經過的數之和模k最大),顯然從f(i,j)轉移到f(i+1,j)或f(i,j+1)顯然是不對的。 也就是說,這裏一個問題的最優解不一定包含其子問題的最優解,所以不滿足最優子結構。。

這個例子的重點就是在從f(i,j)轉移到f(i+1,j)或f(i,j+1),即f(i,j)的最優解並不代表是f(i+1,j)的最優解,因爲f(i+1,j)的最優解包含路徑很有可能並不會經過f(i,j),即不滿足最優子結構!!!!

那麼這個問題該如何解決呢????暴力破解,從頭開始枚舉向下向右的每一個路徑,計算路徑的最小值。

———————————————————————————————————————————

貪心算法

貪心算法是通過一系列的選擇來給出某一個問題的最優解,每次選擇一個當前(看起來是)最佳的選擇。

貪心算法解決問題的步驟爲:

(1)決定問題的最優子結構
(2)設計出一個遞歸解
(3)證明在遞歸的任一階段,最優選擇之一總是貪心選擇。保證貪心選擇總是安全的。
(4)證明通過貪心選擇,所有子問題(除一個意外)都爲空。
(5)設計出一個實現貪心策略的遞歸算法。
(6)將遞歸算法轉換成迭代算法。
什麼時候才能使用貪心算法的呢?書中給出了貪心算法的兩個性質,只有最優化問題滿足這些性質,就可採用貪心算法解決問題。

(1)貪心選擇性質:一個全局最優解可以通過舉辦最優解(貪心)選擇來達到。即:當考慮做選擇時,只考慮對當前問題最佳的選擇而不考慮子問題的結果。而在動態規劃中,每一步都要做出選擇,這些選擇依賴於子問題的解。動態規劃一般是自底向上,從小問題到大問題。貪心算法通常是自上而下,一個一個地做貪心選擇,不斷地將給定的問題實例規約爲更小的子問題。
(2)最優子結構:問題的一個最優解包含了其子問題的最優解。
動態規劃與貪心的區別:

貪心算法:

(1)貪心算法中,作出的每步貪心決策都無法改變,因爲貪心策略是由上一步的最優解推導下一步的最優解,而上一部之前的最優解則不作保留;
(2)由(1)中的介紹,可以知道貪心法正確的條件是:每一步的最優解一定包含上一步的最優解。
動態規劃算法:

(1)全局最優解中一定包含某個局部最優解,但不一定包含前一個局部最優解,因此需要記錄之前的所有最優解 ;
(2)動態規劃的關鍵是狀態轉移方程,即如何由以求出的局部最優解來推導全局最優解 ;
(3)邊界條件:即最簡單的,可以直接得出的局部最優解。

0-1揹包問題描述

有一個竊賊在偷竊一家商店時發現有n件物品,第i件物品價值爲vi元,重量爲wi,假設vi和wi都爲整數。他希望帶走的東西越值錢越好,但他的揹包中之多隻能裝下W磅的東西,W爲一整數。他應該帶走哪幾樣東西?

0-1揹包問題中:每件物品或被帶走,或被留下,(需要做出0-1選擇)。小偷不能只帶走某個物品的一部分或帶走兩次以上同一個物品。

部分揹包問題:小偷可以只帶走某個物品的一部分,不必做出0-1選擇。

0-1揹包問題解決方法

0-1揹包問題是個典型舉辦子結構的問題,但是隻能採用動態規劃來解決,而不能採用貪心算法。因爲在0-1揹包問題中,在選擇是否要把一個物品加到揹包中,必須把該物品加進去的子問題的解與不取該物品的子問題的解進行比較。這種方式形成的問題導致了許多重疊子問題,滿足動態規劃的特徵。動態規劃解決0-1揹包問題步驟如下:

0-1揹包問題子結構:選擇一個給定物品i,則需要比較選擇i的形成的子問題的最優解與不選擇i的子問題的最優解。分成兩個子問題,進行選擇比較,選擇最優的。

0-1揹包問題遞歸過程:設有n個物品,揹包的重量爲w,C[i][w]爲最優解。即:

———————————————————————————————————————————

圖的搜索

一、深度優先搜索和廣度優先搜索的深入討論

深度優先搜索的特點是:

(1)無論問題的內容和性質以及求解要求如何不同,它們的程序結構都是相同的,即都是深度優先算法(一)和深度優先算法(二)中描述的算法結構,不相同的僅僅是存儲結點數據結構和產生規則以及輸出要求。

(2)深度優先搜索法有遞歸以及非遞歸兩種設計方法。一般的,當搜索深度較小、問題遞歸方式比較明顯時,用遞歸方法設計好,它可以使得程序結構更簡捷易懂。當搜索深度較大時,當數據量較大時,由於系統堆棧容量的限制,遞歸容易產生溢出,用非遞歸方法設計比較好。

(3)深度優先搜索方法有廣義和狹義兩種理解。廣義的理解是,只要最新產生的結點(即深度最大的結點)先進行擴展的方法,就稱爲深度優先搜索方法。在這種理解情況下,深度優先搜索算法有全部保留和不全部保留產生的結點的兩種情況。而狹義的理解是,僅僅只保留全部產生結點的算法。本書取前一種廣義的理解。不保留全部結點的算法屬於一般的回溯算法範疇。保留全部結點的算法,實際上是在數據庫中產生一個結點之間的搜索樹,因此也屬於圖搜索算法的範疇。

(4)不保留全部結點的深度優先搜索法,由於把擴展望的結點從數據庫中彈出刪除,這樣,一般在數據庫中存儲的結點數就是深度值,因此它佔用的空間較少,所以,當搜索樹的結點較多,用其他方法易產生內存溢出時,深度優先搜索不失爲一種有效的算法。

(5)從輸出結果可看出,深度優先搜索找到的第一個解並不一定是最優解。
如果要求出最優解的話,一種方法將是後面要介紹的動態規劃法,另一種方法是修改原算法:把原輸出過程的地方改爲記錄過程,即記錄達到當前目標的路徑和相應的路程值,並與前面已記錄的值進行比較,保留其中最優的,等全部搜索完成後,才把保留的最優解輸出。

廣度優先搜索法的顯著特點是:

(1)在產生新的子結點時,深度越小的結點越先得到擴展,即先產生它的子結點。爲使算法便於實現,存放結點的數據庫一般用隊列的結構。

(2)無論問題性質如何不同,利用廣度優先搜索法解題的基本算法是相同的,但數據庫中每一結點內容,產生式規則,根據不同的問題,有不同的內容和結構,就是同一問題也可以有不同的表示方法。

(3)當結點到跟結點的費用(有的書稱爲耗散值)和結點的深度成正比時,特別是當每一結點到根結點的費用等於深度時,用廣度優先法得到的解是最優解,但如果不成正比,則得到的解不一定是最優解。這一類問題要求出最優解,一種方法是使用後面要介紹的其他方法求解,另外一種方法是改進前面深度(或廣度)優先搜索算法:找到一個目標後,不是立即退出,而是記錄下目標結點的路徑和費用,如果有多個目標結點,就加以比較,留下較優的結點。把所有可能的路徑都搜索完後,才輸出記錄的最優路徑。

(4)廣度優先搜索算法,一般需要存儲產生的所有結點,佔的存儲空間要比深度優先大得多,因此程序設計中,必須考慮溢出和節省內存空間得問題。

(5)比較深度優先和廣度優先兩種搜索法,廣度優先搜索法一般無回溯操作,即入棧和出棧的操作,所以運行速度比深度優先搜索算法法要快些。

總之,一般情況下,深度優先搜索法佔內存少但速度較慢,廣度優先搜索算法佔內存多但速度較快,在距離和深度成正比的情況下能較快地求出最優解。因此在選擇用哪種算法時,要綜合考慮。決定取捨。

深度優先搜索的圖文介紹

  1. 深度優先搜索介紹

圖的深度優先搜索(Depth First Search),和樹的先序遍歷比較類似。

它的思想:假設初始狀態是圖中所有頂點均未被訪問,則從某個頂點v出發,首先訪問該頂點,然後依次從它的各個未被訪問的鄰接點出發深度優先搜索遍歷圖,直至圖中所有和v有路徑相通的頂點都被訪問到。 若此時尚有其他頂點未被訪問到,則另選一個未被訪問的頂點作起始點,重複上述過程,直至圖中所有頂點都被訪問到爲止。

顯然,深度優先搜索是一個遞歸的過程。

  1. 深度優先搜索圖解

2.1 無向圖的深度優先搜索

下面以"無向圖"爲例,來對深度優先搜索進行演示。

對上面的圖G1進行深度優先遍歷,從頂點A開始。

第1步:訪問A。
第2步:訪問(A的鄰接點)C。 在第1步訪問A之後,接下來應該訪問的是A的鄰接點,即"C,D,F"中的一個。但在本文的實現中,頂點ABCDEFG是按照順序存儲,C在"D和F"的前面,因此,先訪問C。
第3步:訪問(C的鄰接點)B。 在第2步訪問C之後,接下來應該訪問C的鄰接點,即"B和D"中一個(A已經被訪問過,就不算在內)。而由於B在D之前,先訪問B。
第4步:訪問(C的鄰接點)D。 在第3步訪問了C的鄰接點B之後,B沒有未被訪問的鄰接點;因此,返回到訪問C的另一個鄰接點D。
第5步:訪問(A的鄰接點)F。 前面已經訪問了A,並且訪問完了"A的鄰接點B的所有鄰接點(包括遞歸的鄰接點在內)";因此,此時返回到訪問A的另一個鄰接點F。
第6步:訪問(F的鄰接點)G。
第7步:訪問(G的鄰接點)E。
因此訪問順序是:A -> C -> B -> D -> F -> G -> E
2.2 有向圖的深度優先搜索

下面以"有向圖"爲例,來對深度優先搜索進行演示。

對上面的圖G2進行深度優先遍歷,從頂點A開始。

第1步:訪問A。
第2步:訪問B。 在訪問了A之後,接下來應該訪問的是A的出邊的另一個頂點,即頂點B。
第3步:訪問C。 在訪問了B之後,接下來應該訪問的是B的出邊的另一個頂點,即頂點C,E,F。在本文實現的圖中,頂點ABCDEFG按照順序存儲,因此先訪問C。
第4步:訪問E。 接下來訪問C的出邊的另一個頂點,即頂點E。
第5步:訪問D。 接下來訪問E的出邊的另一個頂點,即頂點B,D。頂點B已經被訪問過,因此訪問頂點D。
第6步:訪問F。 接下應該回溯"訪問A的出邊的另一個頂點F"。
第7步:訪問G。
因此訪問順序是:A -> B -> C -> E -> D -> F -> G
廣度優先搜索的圖文介紹

  1. 廣度優先搜索介紹

廣度優先搜索算法(Breadth First Search),又稱爲"寬度優先搜索"或"橫向優先搜索",簡稱BFS。
它的思想是:從圖中某頂點v出發,在訪問了v之後依次訪問v的各個未曾訪問過的鄰接點,然後分別從這些鄰接點出發依次訪問它們的鄰接點,並使得“先被訪問的頂點的鄰接點先於後被訪問的頂點的鄰接點被訪問,直至圖中所有已被訪問的頂點的鄰接點都被訪問到。如果此時圖中尚有頂點未被訪問,則需要另選一個未曾被訪問過的頂點作爲新的起始點,重複上述過程,直至圖中所有頂點都被訪問到爲止。

換句話說,廣度優先搜索遍歷圖的過程是以v爲起點,由近至遠,依次訪問和v有路徑相通且路徑長度爲1,2…的頂點。

  1. 廣度優先搜索圖解

2.1 無向圖的廣度優先搜索

下面以"無向圖"爲例,來對廣度優先搜索進行演示。還是以上面的圖G1爲例進行說明。

第1步:訪問A。
第2步:依次訪問C,D,F。 在訪問了A之後,接下來訪問A的鄰接點。前面已經說過,在本文實現中,頂點ABCDEFG按照順序存儲的,C在"D和F"的前面,因此,先訪問C。再訪問完C之後,再依次訪問D,F。
第3步:依次訪問B,G。 在第2步訪問完C,D,F之後,再依次訪問它們的鄰接點。首先訪問C的鄰接點B,再訪問F的鄰接點G。
第4步:訪問E。 在第3步訪問完B,G之後,再依次訪問它們的鄰接點。只有G有鄰接點E,因此訪問G的鄰接點E。
因此訪問順序是:A -> C -> D -> F -> B -> G -> E
2.2 有向圖的廣度優先搜索

下面以"有向圖"爲例,來對廣度優先搜索進行演示。還是以上面的圖G2爲例進行說明。

第1步:訪問A。
第2步:訪問B。
第3步:依次訪問C,E,F。 在訪問了B之後,接下來訪問B的出邊的另一個頂點,即C,E,F。前面已經說過,在本文實現中,頂點ABCDEFG按照順序存儲的,因此會先訪問C,再依次訪問E,F。
第4步:依次訪問D,G。 在訪問完C,E,F之後,再依次訪問它們的出邊的另一個頂點。還是按照C,E,F的順序訪問,C的已經全部訪問過了,那麼就只剩下E,F;先訪問E的鄰接點D,再訪問F的鄰接點G。
因此訪問順序是:A -> B -> C -> E -> F -> D -> G
———————————————————————————————————————————

圖算法之最小生成樹

圖算法裏面有一個問題就是關於所有路徑中的最小加權路徑,它對應的實際例子類似電路板裏面如何走線效率最高,用的線的長度最小?

這一類的問題歸根到底就是最小生成樹的問題,最小生成樹即是圖中最佳的路徑選擇,最小生成樹的問題是一個典型的貪心算法,每一步都選擇權重最小的邊。

求圖中的最小生成樹的問題主要有兩個經典的算法,Kruskal算法和Prim算法下面就這兩個算法進行詳細的論述:

Kruskal算法

最小生成樹

在含有n個頂點的連通圖中選擇n-1條邊,構成一棵極小連通子圖,並使該連通子圖中n-1條邊上權值之和達到最小,則稱其爲連通網的最小生成樹。

例如,對於如上圖G4所示的連通網可以有多棵權值總和不相同的生成樹。

克魯斯卡爾算法介紹

克魯斯卡爾(Kruskal)算法,是用來求加權連通圖的最小生成樹的算法。

基本思想:按照權值從小到大的順序選擇n-1條邊,並保證這n-1條邊不構成迴路。

具體做法:首先構造一個只含n個頂點的森林,然後依權值從小到大從連通網中選擇邊加入到森林中,並使森林中不產生迴路,直至森林變成一棵樹爲止。

克魯斯卡爾算法圖解
以上圖G4爲例,來對克魯斯卡爾進行演示(假設,用數組R保存最小生成樹結果)。

第1步:將邊<E,F>加入R中。
邊<E,F>的權值最小,因此將它加入到最小生成樹結果R中。
第2步:將邊<C,D>加入R中。
上一步操作之後,邊<C,D>的權值最小,因此將它加入到最小生成樹結果R中。
第3步:將邊<D,E>加入R中。
上一步操作之後,邊<D,E>的權值最小,因此將它加入到最小生成樹結果R中。
第4步:將邊<B,F>加入R中。
上一步操作之後,邊<C,E>的權值最小,但<C,E>會和已有的邊構成迴路;因此,跳過邊<C,E>。同理,跳過邊<C,F>。將邊<B,F>加入到最小生成樹結果R中。
第5步:將邊<E,G>加入R中。
上一步操作之後,邊<E,G>的權值最小,因此將它加入到最小生成樹結果R中。
第6步:將邊<A,B>加入R中。
上一步操作之後,邊<F,G>的權值最小,但<F,G>會和已有的邊構成迴路;因此,跳過邊<F,G>。同理,跳過邊<B,C>。將邊<A,B>加入到最小生成樹結果R中。
此時,最小生成樹構造完成!它包括的邊依次是:<E,F> <C,D> <D,E> <B,F> <E,G> <A,B>。
克魯斯卡爾算法分析

根據前面介紹的克魯斯卡爾算法的基本思想和做法,我們能夠瞭解到,克魯斯卡爾算法重點需要解決的以下兩個問題:

問題一 對圖的所有邊按照權值大小進行排序。
問題二 將邊添加到最小生成樹中時,怎麼樣判斷是否形成了迴路。

問題一很好解決,採用排序算法進行排序即可。
問題二,處理方式是:記錄頂點在"最小生成樹"中的終點,頂點的終點是"在最小生成樹中與它連通的最大頂點"(關於這一點,後面會通過圖片給出說明)。然後每次需要將一條邊添加到最小生存樹時,判斷該邊的兩個頂點的終點是否重合,重合的話則會構成迴路。 以下圖來進行說明:

在將<E,F> <C,D> <D,E>加入到最小生成樹R中之後,這幾條邊的頂點就都有了終點:
(01) C的終點是F。
(02) D的終點是F。
(03) E的終點是F。
(04) F的終點是F。
關於終點,就是將所有頂點按照從小到大的順序排列好之後;某個頂點的終點就是"與它連通的最大頂點"。 因此,接下來,雖然<C,E>是權值最小的邊。但是C和E的重點都是F,即它們的終點相同,因此,將<C,E>加入最小生成樹的話,會形成迴路。這就是判斷迴路的方式。
————————————————————————————————————————————

Prim算法

普里姆算法介紹

普里姆(Prim)算法,和克魯斯卡爾算法一樣,是用來求加權連通圖的最小生成樹的算法。

基本思想
對於圖G而言,V是所有頂點的集合;現在,設置兩個新的集合U和T,其中U用於存放G的最小生成樹中的頂點,T存放G的最小生成樹中的邊。 從所有uЄU,vЄ(V-U) (V-U表示出去U的所有頂點)的邊中選取權值最小的邊(u, v),將頂點v加入集合U中,將邊(u, v)加入集合T中,如此不斷重複,直到U=V爲止,最小生成樹構造完畢,這時集合T中包含了最小生成樹中的所有邊。

普里姆算法圖解

以上圖G4爲例,來對普里姆進行演示(從第一個頂點A開始通過普里姆算法生成最小生成樹)。

初始狀態:V是所有頂點的集合,即V={A,B,C,D,E,F,G};U和T都是空!
第1步:將頂點A加入到U中。
此時,U={A}。
第2步:將頂點B加入到U中。
上一步操作之後,U={A}, V-U={B,C,D,E,F,G};因此,邊(A,B)的權值最小。將頂點B添加到U中;此時,U={A,B}。
第3步:將頂點F加入到U中。
上一步操作之後,U={A,B}, V-U={C,D,E,F,G};因此,邊(B,F)的權值最小。將頂點F添加到U中;此時,U={A,B,F}。
第4步:將頂點E加入到U中。
上一步操作之後,U={A,B,F}, V-U={C,D,E,G};因此,邊(F,E)的權值最小。將頂點E添加到U中;此時,U={A,B,F,E}。
第5步:將頂點D加入到U中。
上一步操作之後,U={A,B,F,E}, V-U={C,D,G};因此,邊(E,D)的權值最小。將頂點D添加到U中;此時,U={A,B,F,E,D}。
第6步:將頂點C加入到U中。
上一步操作之後,U={A,B,F,E,D}, V-U={C,G};因此,邊(D,C)的權值最小。將頂點C添加到U中;此時,U={A,B,F,E,D,C}。
第7步:將頂點G加入到U中。
上一步操作之後,U={A,B,F,E,D,C}, V-U={G};因此,邊(F,G)的權值最小。將頂點G添加到U中;此時,U=V。
此時,最小生成樹構造完成!它包括的頂點依次是:A B F E D C G。
————————————————————————————————————————————

圖算法之單源最短路徑

Dijkstra 算法

(中文名:迪傑斯特拉算法)是由荷蘭計算機科學家 Edsger Wybe Dijkstra 提出。該算法常用於路由算法或者作爲其他圖算法的一個子模塊。舉例來說,如果圖中的頂點表示城市,而邊上的權重表示城市間開車行經的距離,該算法可以用來找到兩個城市之間的最短路徑。

迪傑斯特拉算法介紹

迪傑斯特拉(Dijkstra)算法是典型最短路徑算法,用於計算一個節點到其他節點的最短路徑。
它的主要特點是以起始點爲中心向外層層擴展(廣度優先搜索思想),直到擴展到終點爲止。

基本思想

通過Dijkstra計算圖G中的最短路徑時,需要指定起點s(即從頂點s開始計算)。此外,引進兩個集合S和U。S的作用是記錄已求出最短路徑的頂點(以及相應的最短路徑長度),而U則是記錄還未求出最短路徑的頂點(以及該頂點到起點s的距離)。

初始時,S中只有起點s;U中是除s之外的頂點,並且U中頂點的路徑是"起點s到該頂點的路徑"。然後,從U中找出路徑最短的頂點,並將其加入到S中;接着,更新U中的頂點和頂點對應的路徑。 然後,再從U中找出路徑最短的頂點,並將其加入到S中;接着,更新U中的頂點和頂點對應的路徑。 … 重複該操作,直到遍歷完所有頂點。

操作步驟

(1) 初始時,S只包含起點s;U包含除s外的其他頂點,且U中頂點的距離爲"起點s到該頂點的距離"[例如,U中頂點v的距離爲(s,v)的長度,然後s和v不相鄰,則v的距離爲∞]。
(2) 從U中選出"距離最短的頂點k",並將頂點k加入到S中;同時,從U中移除頂點k。
(3) 更新U中各個頂點到起點s的距離。之所以更新U中頂點的距離,是由於上一步中確定了k是求出最短路徑的頂點,從而可以利用k來更新其它頂點的距離;例如,(s,v)的距離可能大於(s,k)+(k,v)的距離。
(4) 重複步驟(2)和(3),直到遍歷完所有頂點。
單純的看上面的理論可能比較難以理解,下面通過實例來對該算法進行說明。
迪傑斯特拉算法圖解

以上圖G4爲例,來對迪傑斯特拉進行算法演示(以第4個頂點D爲起點)。

初始狀態:S是已計算出最短路徑的頂點集合,U是未計算除最短路徑的頂點的集合!
第1步:將頂點D加入到S中。
此時,S={D(0)}, U={A(∞),B(∞),C(3),E(4),F(∞),G(∞)}。 注:C(3)表示C到起點D的距離是3。
第2步:將頂點C加入到S中。
上一步操作之後,U中頂點C到起點D的距離最短;因此,將C加入到S中,同時更新U中頂點的距離。以頂點F爲例,之前F到D的距離爲∞;但是將C加入到S之後,F到D的距離爲9=(F,C)+(C,D)。
此時,S={D(0),C(3)}, U={A(∞),B(23),E(4),F(9),G(∞)}。
第3步:將頂點E加入到S中。
上一步操作之後,U中頂點E到起點D的距離最短;因此,將E加入到S中,同時更新U中頂點的距離。還是以頂點F爲例,之前F到D的距離爲9;但是將E加入到S之後,F到D的距離爲6=(F,E)+(E,D)。
此時,S={D(0),C(3),E(4)}, U={A(∞),B(23),F(6),G(12)}。
第4步:將頂點F加入到S中。
此時,S={D(0),C(3),E(4),F(6)}, U={A(22),B(13),G(12)}。
第5步:將頂點G加入到S中。
此時,S={D(0),C(3),E(4),F(6),G(12)}, U={A(22),B(13)}。
第6步:將頂點B加入到S中。
此時,S={D(0),C(3),E(4),F(6),G(12),B(13)}, U={A(22)}。
第7步:將頂點A加入到S中。
此時,S={D(0),C(3),E(4),F(6),G(12),B(13),A(22)}。
此時,起點D到各個頂點的最短距離就計算出來了:A(22) B(13) C(3) D(0) E(4) F(6) G(12)。
————————————————————————————————————————————
字符串匹配KMP算法

KMP算法是一種改進的字符串匹配算法。KMP算法的關鍵是利用匹配失敗後的信息,儘量減少模式串與主串的匹配次數以達到快速匹配的目的。具體實現就是實現一個next()函數,函數本身包含了模式串的局部匹配信息。時間複雜度O(m+n)。

下面先直接給出KMP的算法流程(如果感到一點點不適,沒關係,堅持下,稍後會有具體步驟及解釋):

假設現在文本串S匹配到 i 位置,模式串P匹配到 j 位置 如果j = -1,或者當前字符匹配成功(即S[i] == P[j]),都令i++,j++,繼續匹配下一個字符;如果j != -1,且當前字符匹配失敗(即S[i] != P[j]),則令 i 不變,j = next[j]。此舉意味着失敗時,模式串P相對於文本串S向右移動了j - next [j] 位。 換言之,當匹配失敗時,模式串向右移動的位數爲:失敗字符所在位置 - 失敗字符對應的next 值(next 數組的求解會在下文的3.3.3節中詳細闡述),即移動的實際位數爲:j - next[j],且此值大於等於1。 很快,你也會意識到next 數組各值的含義:若k=next[j],代表模式串P中當前字符之前的字符串中,最前面的k個字符和j之前的最後k個字符是一樣的。
如果用數學公式來表示是這樣的:
P[0 ~ k-1] == P[j-k ~ j-1]

此也意味着在某個字符匹配失敗時,該字符對應的next 值會告訴你下一步匹配中,模式串應該跳到哪個位置(跳到next [j] 的位置)。如果next [j] 等於0或-1,則跳到模式串的開頭字符,若next [j] = k 且 k > 0,代表下次匹配跳到j 之前的某個字符,而不是跳到開頭,且具體跳過了k 個字符。

繼續拿之前的例子來說,當S[10]跟P[6]匹配失敗時,KMP不是跟暴力匹配那樣簡單的把模式串右移一位,而是執行第②條指令:“如果j != -1,且當前字符匹配失敗(即S[i] != P[j]),則令 i 不變,j = next[j]”,即j 從6變到2(後面我們將求得P[6],即字符D對應的next 值爲2),所以相當於模式串向右移動的位數爲j - next[j](j - next[j] =6-2 = 4)。

向右移動4位後,S[10]跟P[2]繼續匹配。爲什麼要向右移動4位呢,因爲移動4位後,模式串中又有個“AB”可以繼續跟S[8]S[9]對應着,從而不用讓i 回溯。相當於在除去字符D的模式串子串中尋找相同的前綴和後綴,然後根據前綴後綴求出next 數組,最後基於next 數組進行匹配(不關心next 數組是怎麼求來的,只想看匹配過程是咋樣的,可直接跳到下文3.3.4節)。

如何求next數組
由上文,我們已經知道,字符串“ABCDABD”各個前綴後綴的最大公共元素長度分別爲:

而且,根據這個表可以得出下述結論:

失配時,模式串向右移動的位數爲:已匹配字符數- 失配字符的上一位字符所對應的最大長度值

上文利用這個表和結論進行匹配時,我們發現,當匹配到一個字符失配時,其實沒必要考慮當前失配的字符,更何況我們每次失配時,都是看的失配字符的上一位字符對應的最大長度值。如此,便引出了next 數組。

給定字符串“ABCDABD”,可求得它的next 數組如下:

把next 數組跟之前求得的最大長度表對比後,不難發現,next 數組相當於“最大長度值” 整體向右移動一位,然後初始值賦爲-1。意識到了這一點,你會驚呼原來next 數組的求解竟然如此簡單:就是找最大對稱長度的前綴後綴,然後整體右移一位,初值賦爲-1(當然,你也可以直接計算某個字符對應的next值,就是看這個字符之前的字符串中有多大長度的相同前綴後綴)。
換言之,對於給定的模式串:ABCDABD,它的最大長度表及next 數組分別如下:


作者:祚兒瘋
來源:CSDN
原文:https://blog.csdn.net/u012414189/article/details/83861402
版權聲明:本文爲博主原創文章,轉載請附上博文鏈接!

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