常用算法和思維歸納

在這裏插入圖片描述
最重要的複雜度問題:

時間複雜度: 並不表示代碼真正的執行時間 只表示代碼執行時間隨數據規模增長的變化趨勢(所以即使某段代碼常量1000000 雖然對這段代碼執行時間來說是有影響 但是隻要不涉及n 我們就忽略)
所有代碼的執行時間 T(n) 與每行代碼的執行次數成正比 		T(n) = O(f(n)) O,表示代碼的執行時間 T(n) 與 f(n) 表達式成正比。
 			低階、常量、係數 都就可以忽略 只需要記錄一個最大量級
 	分析方法:(1)只關注循環執行次數最多的那一段代碼就可以了
 					(2)總複雜度等於量級最大的那段代碼的複雜度
 					(3)嵌套代碼的複雜度等於嵌套內外代碼複雜度的乘積
 	多項式時間: O(1)   只要代碼的執行時間不隨 n 的增大而增長  算法中不存在循環語句、遞歸語句
 						O(logn)(循環不斷乘以一個數以至於達到n)  O(nlogn)(其實就是O(logn) 循環n次) 底數可以忽略 直接寫成O(logn)
 						O(n) :如果兩個變量m n 不知道哪個大 那就是O(m+n)
 						 O(n平方) O(n立方) O(nk次方)
 	非多項式時間:O(2的n次方) O(n!)    np問題 當數據規模 n 越來越大時,非多項式量級算法的執行時間會急劇增加,求解問題的執行時間會無限增長

---爲了表示代碼在不同情況下 n的不同時間複雜度(例如有時候n循環提前結束):
(1)最好情況時間複雜度:
		理想情況下執行這段代碼的時間複雜度
(2)最壞情況時間複雜度
(3)平均情況時間複雜度(加權平均時間複雜度)
(4)均攤時間複雜度:分析方法:平攤分析  
				對一個數據結構進行一組連續操作中,大部分情況下時間複雜度都很低,只有個別情況下時間複雜度比較高,而且這些操作之間存在前後連貫的時序關係,一般等於最好情況時間複雜度

空間複雜度:比較簡單 就是判斷佔用空間多少(也是以n計算)    一般是O(1)、O(n)、O(n2)

數據結構

(1)數組:一種線性表數據結構。它用一組連續的內存空間,來存儲一組具有相同類型的數據
			提一點 在數組中插入如果不理會順序 可以直接在該位置插入 把這個位置的數字放到末尾 ---快排
				  在數組中刪除也可以在該位置刪除然後把後末尾的移動過去 或者先標記好刪除的元素 在最後沒空間的時候一次性來一次刪除操作----jvm的清理機制
		arraylist 和 數組的選擇:arraylist只能用包裝類 拆包裝包是有性能消耗的  --開發底層代碼例如框架的時候可以選擇數組
												數據大小已知 並且不需要那麼多封裝好的api  可以直接用數組
		在實現上使用的是連續的內存空間,可以藉助 CPU 的緩存機制,預讀數組中的數據,所以訪問效率更高
(2)鏈表:通過指針將一組零散的內存塊串聯在一起
		鏈表在內存中並不是連續存儲,所以對 CPU 緩存不友好,沒辦法有效預讀。
		對鏈表進行頻繁的插入、刪除操作會導致頻繁的內存申請釋放===》內存碎片====》頻繁的GC
單鏈表  
雙向鏈表 (例如LinkedHashMap):每個結點不止有一個後繼指針 next 指向後面的結點,還有一個前驅指針 prev 指向前面的結點 ------比單鏈表優點: 如果需要在某個節點前插入一個節點或者刪除所在的節點 可以直接索引到他的前驅節點而不用重新遍歷   
循環鏈表:循環鏈表的尾結點指針是指向鏈表的頭結點
小應用:可以用  散列表+單鏈表 實現LRU緩存			
----------------怎麼寫好鏈表代碼:e.g 反轉 有序鏈表合併等--------------------------												
(1)理解好指針
將某個變量賦值給指針,實際上就是將這個變量的地址賦值給指針,或者反過來說,指針中存儲了這個變量的內存地址,指向了這個變量,通過指針就能找到這個變量
(2)警惕指針丟失(自己指向自己)和內存泄漏			
(3)利用哨兵(沒有值)簡化實現難度:  哨兵解決邊界問題
針對鏈表的插入、刪除操作,需要對插入第一個結點和刪除最後一個結點的情況進行特殊處理		
引入哨兵節點後 head指針會一直指向這個哨兵節點	 此時進行插入 刪除就不用特殊處理第一個和最後一個節點
(4)注意監測邊界條件:
如果鏈表爲空時,代碼是否能正常工作?
如果鏈表只包含一個結點時,代碼是否能正常工作?
如果鏈表只包含兩個結點時,代碼是否能正常工作?
代碼邏輯在處理頭結點和尾結點的時候,是否能正常工作?
	鏈表的幾個問題:
單鏈表反轉:  遞歸法 遍歷法
鏈表中環的檢測  :快慢指針法   足跡法
兩個有序的鏈表合併 :while循環法(跟合併兩個有序數組是一樣的)     遞歸法(專門針對鏈表的)
刪除鏈表倒數第 n 個結點: 遞歸計數法     還有一個牛逼的:雙指針法!
求鏈表的中間結點:	先遍歷一次記錄個數n 再遍歷n/2拿中間節點					仍然可以用雙指針法(兩倍速度的遍歷到末尾時 一倍速度的就是中間)!

(3)棧
順序棧:用數組實現的棧
鏈式棧:用鏈表實現的棧

(4)隊列:可以應用在任何有限資源池中,用於排隊請求
順序隊列:用數組實現的隊列
鏈式隊列:用鏈表實現的隊列
循環隊列:解決順序隊列入棧數據搬移導致時間複雜度o(n)的問題 
				循環隊列滿:規定爲尾指針的下一個指針到頭指針時 (也可以設置一個標識位來標誌是滿還是空 因爲兩個狀態都是head = tail)        :   (tail+1)%n=head  會浪費一個空間
阻塞隊列:
併發隊列:線程安全的阻塞隊列

(5)二叉樹
每個節點最多有兩個子節點(包括滿二叉樹和完全二叉樹)
	1鏈式存儲法  : 三個字段 數據  指向左右子節點的指針
	2數組存儲法:從1開始存儲 節點 X 存儲在數組中下標爲 i 的位置,下標爲 2 * i 的位置存儲的就是左子節點,下標爲 2 * i + 1 的位置存儲的就是右子節點 --------非完全二叉樹會浪費很多空間(所以完全二叉樹的規定是最後兩層 且都靠左  並且完全二叉樹用數組存儲最省空間)
	二叉樹的前、中、後序遍歷就是一個遞歸的過程
	二叉樹遍歷:每個節點最多訪問兩次,時間複雜度o(n)

(6)二叉查找樹(二叉搜索樹,二叉排序樹:因爲可以中序遍歷o(n)輸出一個有序的數據序列):支持快速查找 插入 刪除一個數據   類似o(1)的散列表
在樹中的任意一個節點,其左子樹中的每個節點的值,都要小於這個節點的值,而右子樹節點的值都大於這個節點的值
		時間複雜度:三個都是跟高度成正比 
		平衡二叉查找樹的高度接近 logn   :O(logn)       但是有的不平衡的二叉查找樹如果像鏈表一樣就退化成o(n)了 所以平衡很重要
		散列表插入刪除查找都是o(1),那即使是平衡二叉查找樹的優點在哪裏?
		1散列表數據是無序的, 要輸出有序的數據需要另外排序,而二叉查找樹只需要中序遍歷既可以o(n)輸出
		2散列表擴容耗時很多 而且容易因爲散列衝突而導致時間複雜度不穩定   但是平衡二叉樹使用起來穩定趨近於o(logn)  
		3哈希函數耗時 且 o(1)的常量查找時間並不一定比 o(logn)小
		4散列表需要考慮 散列因子 擴容 縮容 哈希衝突 哈希函數性能等      平衡二叉樹只需要考慮平衡(且這點的解決方法已很成熟)
(7)平衡二叉查找樹avl:目的只是爲了儘量保證左右子樹高度低一點 爲了解決二叉查找樹因爲動態更新導致的性能退化問題 時間複雜度就更穩定於o(logn)(紅黑樹不是完全定義上的平衡二叉查找樹 但是隻要在logn量級附近即可 )  且爲了維持高度的平衡 插入刪除都要做調整 所以比較複雜耗時 對於插入刪除操作多的不適合使用avl
		二叉查找樹  + 二叉樹中任意一個節點的左右子樹的高度相差不能大於 1
		紅黑樹:   高度只比avl樹最多大了一倍(不準確 實際上紅黑樹性能更好)
			根節點是黑色
			紅黑樹中的節點,一類被標記爲黑色,一類被標記爲紅色
		    每個葉子節點不存儲數據 都是黑色的空節點(NIL)
		    父子之間不會出現相連的紅色節點,被黑色節點分開
		    每個節點到達他可以到達的葉子節點中經過的路徑 每個路徑上的黑色節點數量相同
		     			紅黑樹只是近似平衡 以保證插入 刪除 查找各項性能都比較穩定 都是o(logn)

						比較一下對於插入刪除查找常用的算法:
							散列表:插入刪除查找都是O(1), 是最常用的,但其缺點是不能順序遍歷以及擴容縮容的性能損耗。適用於那些不需要順序遍歷,數據更新不那麼頻繁的。
							
							跳錶:插入刪除查找都是O(logn), 並且能順序遍歷。缺點是空間複雜度O(n)。適用於不那麼在意內存空間的,其順序遍歷和區間查找非常方便。
							
							紅黑樹:插入刪除查找都是O(logn), 中序遍歷即是順序遍歷,穩定。缺點是難以實現,去查找不方便。其實跳錶更佳,但紅黑樹已經用於很多地方了。
			--紅黑樹調整:   遇到哪種排布進行哪種調整  (紅黑樹規定最後葉子節點一定是黑色空節點是爲了讓插入刪除時候平衡操作更有規律 簡潔  實際中使用公用的一個黑色節點即可)
				插入操作:插入的節點必須是紅色的   +   新插入的節點都是放在葉子節點上 ,插入後破壞了紅黑樹 就需要 左旋轉或者右旋轉來重平衡 
				刪除操作:兩次調整 分別解決 紅黑樹定義的  第四個 和 第三個問題
		
(8)遞歸樹: 借用遞歸樹可以分析遞歸算法的時間複雜度
  (9)堆:   堆排序  空間複雜度o(1)   時間複雜度o(nlogn)
      堆 是一種特殊的樹 :  完全二叉樹  +   每一個節點的值都必須大於等於(或小於等於)其子樹中每個節點的值(大頂堆 小頂堆)
      1既然是完全二叉樹 那就可以使用數組來存儲 是比較節省空間的
      2 插入操作因爲都是插入在最後的節點 數組的末尾 所以肯定要進行調整
      		注意的是刪除堆頂操作的時候 可以移動最後一個節點 放到堆頂 然後往下比較看誰是小的 一個一個交換 來滿足完全二叉樹的要求
      		插入和刪除堆頂 都是 o(logn)  ,因爲不想二叉搜索樹 可以很快定位到特定節點 所以查找和根據指定數刪除比較慢
	----堆應用:
	1優先級隊列   因爲堆插入刪除o(logn)  比循環數據進行判斷o(n)強
	2基於優先隊列的高性能定時器 :定時器可以在最小堆頂元素在需要調用的時候再進行觸發任務 然後重新堆化拿到最小堆頂元素 計算下一次最近的任務
	3基於優先隊列的多路合併文件
	4計算 靜態 /  動態 top k 問題      建立好一個靜態數組前k大的堆 (k+1個元素的數組) ,動態的話在新插入數據的時候更新O(nlogK)
	5利用堆求動態數據中   中位數,可以推廣到求任何數據中n%位置的值   利用一個大頂堆存儲小的數 小頂堆存大的數(利用堆的目的就是動態數據的插入和刪除可以用堆來操作 o(logn))
	(10)圖:
	1利用二維數組存儲 無向圖 有向圖 值就是權重		
				缺點:容易浪費內存空間
				優點:存儲方式簡單直接 獲取節點關係方便直接高效
						運算方便
	2臨接表存儲方式:
	爲每個節點開闢一個數組的位置  位置上存儲該節點指向的鏈表(其實就是散列表)
			優點:省空間
			缺點:查詢不方便
	 可以利用臨接表和逆臨接表來存儲指向 和 被指向
	 	可以利用圖進行深度優先 和 廣度優先搜索
	(11)trie樹  字典樹(搜索引擎使用)  多叉樹  字符串前綴公用同一個節點
	專門處理字符串匹配的數據結構,用來解決在一組字符串集合中快速查找某個字符串的問題
	構建時間複雜度:o(n)所有字符串長度和
	構建好後的查詢時間複雜度:o(k)  k是要查詢的字符串的長度,顯而易見 很快速
	以內存換時間(每個節點維持26個空間的數組 雖然可以對數據結構進行變形,或者縮點優化)
	缺點:
		字符集不能太大 會浪費存儲空間 --即使優化也犧牲查詢 插入效率的代價
		要求字符串前綴重合比較多 不然空間消耗會變大 (看具體的空間策略)
		因爲用的指針 對緩存不友好 
	優點:更適合用來查找前綴匹配的字符串,例如百度搜索 通過前綴搜索可能存在的全文 或者自動補全功能,紅黑樹和散列表更適合精確查找

	單模式串匹配:每個模式串需要重新掃描一遍主串
	多模式串匹配:只需要掃描一遍主串 就可以知道多個模式串是否存在  例如trie

	ac自動機 改良版本的tire樹 類似加了next函數的KMP:
	(1)將多個模式串構建成 AC 自動機:
			模式串構建成 Trie 樹 + 在 Trie 樹上構建失敗指針
   (2)在ac自動機中匹配主串
	

算法

(1)遞歸:
*****一個問題的解可以分解爲幾個子問題的解:最重要的概念 問題都是通過這個概念來列出數學等式
這個問題與分解之後的子問題,除了數據規模不同,求解思路完全一樣
存在遞歸終止條件
 	怎麼寫?:寫出遞推公式,找到終止條件   (通過邏輯 列出遞歸公式 翻譯成代碼即可 不用思考更細節的每一次是怎麼調用的 代碼思維就是這麼簡單 只需要代碼的邏輯跟公式的邏輯相吻合就是成功的)
 	寫遞歸代碼的關鍵就是找到如何將大問題分解爲小問題的規律,並且基於此寫出遞推公式,然後再推敲終止條件,最後將遞推公式和終止條件翻譯成代碼,把它抽象成一個遞推公式,不用想一層層的調用關係,不要試圖用人腦去分解遞歸的每個步驟
	防止遞歸導致棧溢出:設計一個變量記錄遞歸層次 達到後跑出異常
	避免重複計算:在遞歸時候遇到多個分支的情況下 這個分支和另外一個分支的子分支可能會重複計算,可以通過數據結構(散列表或者其他)來保存求解過的f(k)
	缺點: 經常會有時間複雜度(遞歸調用相當於循環 O(n))+空間複雜度(每一次遞歸都壓棧至少O(n)) 都很大的情況(空間複雜度高、有堆棧溢出的風險、存在重複計算、過多的函數調用會耗時較多等問題)
	優點:遞歸代碼的表達力很強,寫起來非常簡潔
	---怎麼改成非遞歸----:可以用循環(動態規劃)                  !!!!搞清楚遞歸和動態規劃!!
(2)排序:
	怎麼分析一個算法:
 1 最好情況、最壞情況、平均情況時間複雜度 並且知道  排序算法在不同數據下的性能表現。
 2 時間複雜度的係數、常數 、低階 (實際的算法評估就不能省略了 因爲n也不是無窮大)
 3比較次數和交換(或移動)次數                  
 4算法的穩定性   (在實際業務中遍歷對象的時候可以簡化排序操作)
		開始學習實際算法:
		逆序度 = 滿有序度 - 有序度。我們排序的過程就是一種增加有序度,減少逆序度的過程,最後達到滿有序度,不管算法怎麼改進,交換次數總是確定的,即爲逆序度,也就是n*(n-1)/2–初始有序度   -----可以求解平均時間複雜度
		--------------------------------------------------O(n2)適合小規模的數據排序(這是理論 實際應用得看具體系數 數據量小的時候可能o(n2)更好)-------------------------------------------------------------------
1冒泡排序:冒泡排序只會操作相鄰的兩個數據。每次冒泡操作都會對相鄰的兩個元素進行比較,看是否滿足大小關係要求。如果不滿足就讓它倆互換。一次冒泡會讓至少一個元素移動到它應該在的位置,重複 n 次,就完成了 n 個數據的排序工作: 元素的比較+元素的對換
	空間複雜度 o(1)
	穩定的排序算法
	最好情況時間複雜度是 O(n)     最壞情況時間複雜度爲 O(n2)     通過有序度可以知道:平均情況下的時間複雜度就是 O(n2)
	
2插入排序:元素的比較+元素的移動
取未排序區間中的元素,在已排序區間中找到合適的插入位置將其插入,並保證已排序區間數據一直有序。重複這個過程,直到未排序區間中元素爲空,算法結束	
	對於一個給定的初始序列,移動操作的次數總是固定的,就等於逆序度
	空間複雜度:o(1)
	穩定
	時間複雜度: 最好o(n) 最差o(n2)
							平均:通過n*(n-1)/2 取平均數n*(n-1)/4    所以是o(n2)  (注意如果通過複雜度來計算 是包括了找到插入的位置+移位操作的 因爲表示的是最終的一種有序的狀態)
	*****時空複雜度都相同,但是插入比冒泡好 因爲就實際代碼來說 插入排序只需要一次賦值操作 而冒泡需要3次 	******					
3選擇排序:
每次會從未排序區間中找到最小的元素,將其放到已排序區間的末尾
	空間複雜度:o(1)
	時間複雜度 :最好最差平均都是o(n2)  
	不是穩定的!:在選擇最小的+交換的時候 可能會打亂被交換的元素在原本元素中跟他相等的值得順序
								判斷是否穩定 就是看這個算法改變位置的操作是否會改變原本相等數組元素的順序! 很大一個特點就是冒泡和插入在遍歷的過程中都是保持着順序的 而選擇排序直接選一個最小的進行交換 無視了原數組中的順序

--------------------------------------------------------------O(nlogn)適合大規模的數據排序----------------------------------------------------------------------
1歸併排序(分治思想 意味着可以用遞歸來實現)
歸併排序的核心思想還是蠻簡單的。如果要排序一個數組,我們先把數組從中間分成前後兩部分,然後對前後兩部分分別排序,再將排好序的兩部分合並在一起,這樣整個數組就都有序了
		可以利用哨兵簡化merge()
		穩定的算法:合併函數並沒有改變原本元素的順序
		最好情況,最壞情況,還是平均情況,時間複雜度都是 O(nlogn)   和有序度無關
		空間複雜度:O(n)  這就是快排好過歸併的地方  ,但是在數據量佔內存小的時候是可以使用的,時間複雜度穩定且優秀
2快速排序:
如果要排序數組中下標從 p 到 r 之間的一組數據,我們選擇 p 到 r 之間的任意一個數據作爲 pivot(分區點)。我們遍歷 p 到 r 之間的數據,將小於 pivot 的放到左邊,將大於 pivot 的放到右邊,將 pivot 放到中間。經過這一步驟之後,數組 p 到 r 之間的數據就被分成了三個部分,前面 p 到 q-1 之間都是小於 pivot 的,中間是 pivot,後面的 q+1 到 r 之間是大於 pivot 的
		時間複雜度:大部分情況下的時間複雜度都可以做到 O(nlogn),只有在極端情況下,纔會退化到 O(n2)
		空間複雜度:O(n2)
		如何解決快速排序最壞情況下o(n2)的情況?:因爲出現這種情況是分區選擇問題(數組原本就有序選擇了最後一個節點 最理想的分區點是:被分區點分開的兩個分區中,數據的數量差不多。)
      									 (1)可以選取 首尾中(看數據大小 可以選擇多) 取中位 當作分區點
      									 (2)隨機取數法 隨機----》間複雜度退化爲最糟糕的 O(n2) 的情況,出現的可能性不大
      	如何解決遞歸棧溢出?:
      	            (1)限制遞歸深度。一旦遞歸過深,超過了我們事先設定的閾值,就停止遞歸
      	            (2)在堆上模擬實現一個函數調用棧,手動模擬遞歸壓棧、出棧的過程,這樣就沒有了系統棧大小的限制  -》 把使用棧內存轉換爲使用堆內存
3 堆排序  空間複雜度o(1)   時間複雜度o(nlogn)   ,實際應用中快排比堆排好  爲什麼呢?
		1建堆o(n)     兩種方式  一個一個插入時候立馬調整        或者			全部元素插入完畢之後從下往上調整
		2排序 O(nlogn)    建堆成功後 把堆頂元素刪除(跟堆末尾交換)   然後重新調整  再刪除 就能每次都把最大值放在末尾了
				所以總的時間是 O(nlogn) 
							不穩定 ---- 因爲存在堆頂和尾的交換
	缺點: 爲什麼咩有快排好
	1數據訪問是跳着訪問的    例如建堆的時候 需要 1 2 4 8 16地訪問 對cpu緩存不好
	2因爲有o(n)的建堆  時間複雜度其實會比快排更高  逆序度可能會變高  交換次數更多   ---例如原本有序的建堆後卻變成無序的了


	---------------------------------------------------o(n)非基於比較的排序算法,都不涉及元素之間的比較操作---------------------------------------------------------------------------	
1桶排序:
將要排序的數據分到幾個有序的桶裏,每個桶裏的數據再單獨進行排序。桶內排完序之後,再把每個桶裏的數據按照順序依次取出,組成的序列就是有序的了
	時間複雜度:桶的個數 m 接近數據個數 n 時 爲O(n)
	比較適合在外排中,把大文件按順序桶在外存中分成幾個文件  讀取每個文件進行排序  然後按順序把文件寫入一個大的文件即可  ,如果不均勻就在特定的大小中進行分割
	不適合的情況:數據範圍k遠大於n
	穩定
2計數排序:
其實就是 n個元素 0 - k 每個桶只有一個元素 桶的大小是從0 - k  每個桶內都是相等的元素 所以o(n)
		一般可以通過數組完成計數排序,一個原始數組數量n 值0 - k      遍歷存儲在一個k大小的數組b 數組元素進行疊加處理 此時每個元素的值就是對應在排序好的數組的下標值+1  ,逆序遍歷原數組 發現一個 k 就找對應的b數組的對應下標裏的值 -1 存儲在最終的數組中(更新b數組對應的值 -1)
		試用條件:  如果k遠大於n 不合適
							只能給非負整數排序
		穩定
3基數排序:例子 10萬個 11位數的手機號進行排序
		從尾到頭每一位進行穩定的排序 例如桶排序   ->  o(n)
	適用範圍:
		(1)需要可以分割出獨立的“位”來比較,而且位之間有遞進的關係,如果 a 數據的高位比 b 數據大,那剩下的低位就不用比較了
		(2)每一位的數據範圍不能太大,要可以用線性排序算法來排序,否則,基數排序的時間複雜度就無法做到 O(n) 了
		   可以解決桶排序和計數排序 k 》》 n的問題
(3)查找算法:
(1)二分查找:
針對的是一個有序的數據集合,查找思想有點類似分治思想。每次都通過跟區間的中間元素對比,將待查找的區間縮小爲之前的一半,直到找到要查找的元素,或者區間被縮小爲 0,可以用循環或者遞歸實現(假設此時是不存在重複數據的)
		O(logn):有時候在實際算法中 O(logn)可能比o(1)更好  因爲o(1)可以是o(100000)  但是O(log2n)其實可以很小
		代碼編寫三個條件:
		1low<=high,而不是 low
		2mid = low+((high-low)>>1)
		3low=mid+1,high=mid-1  如果直接寫成 low=mid 或者 high=mid 在集合中並沒有這個元素的時候會死循環
		侷限性:
		1 不可以應用在鏈表上 只能是順序表(數組 因爲需要根據下標隨機訪問 否則複雜度太高)
		2 針對的是有序的數據
		3只能用在插入、刪除操作不頻繁,一次排序多次查找的場景中 
		4只有數據量比較大的時候,二分查找的優勢纔會比較明顯(但是如果即使數據量不大 但每次比較時間過長 想要減少比較次數 也可以使用二分)
		5太大的數據也不能用二分,因爲數組爲了支持隨機訪問的特性,要求內存空間連續,對內存的要求比較苛刻
---****最難的是二分查找的變形:  要注意三點: 終止條件、區間上下界更新方法、返回值選擇
	變體一:查找第一個值等於給定值的元素(存在重複的數據)
					找到一個相等的值後需要判斷是否在0上或者前一位是不是不是這個值了
	變體二:查找最後一個值等於給定值的元素
					找到一個相等的值後需要判斷是否在n-1上或者後一位是不是不是這個值了
	變體三:查找第一個大於等於給定值的元素
					在大於等於某個元素後判斷是否是0上的元素或者前一個元素是否小於給定值
	變體四:查找最後一個小於等於給定值的元素
求值等於給定值的二分查找確實不怎麼會被用到,在內存不緊張的情況下 可以用散列表、二叉樹實現(雖然二分查找不需要額外內存 但是二分查找畢竟是數組 對插入刪除等動態數據操作不友好),但是“近似”查找問題就不能用散列表 二叉樹了  只能用變形二分查找
	附:如果數組是循環數組,兩個遞增組合成的 二分查找照樣適用 只不過我們每次看劃分出來的遞增數組中是否存在你要的元素了 
	(2)跳錶(鏈表加多級索引的結構): 可以支持二分查找的鏈表(因爲改造過) redis的有序集合也是用跳錶
		一種各方面性能都比較優秀的動態數據結構,可以支持快速的插入、刪除、查找操作,寫起來也不復雜,甚至可以替代紅黑樹(也支持快速的插入、刪除、查找)
		最高等級索引只有兩個節點(間隔兩個取一個索引)情況下可達到 時間複雜度O(logn)  :即在單鏈表上實現了二分查找(空間換時間)
						 				              空間複雜度:O(n)
		插入、刪除操作:O(logn)     
		跳錶索引動態更新:通過    隨機函數    來維護,比如隨機函數生成了值 K,那我們就將這個結點添加到第一級到第 K 級這 K 級索引中
		redis有序集合使用跳錶而不是紅黑樹:
		插入、刪除、查找以及迭代輸出有序序列這幾個操作,紅黑樹也可以完成,時間複雜度跟跳錶是一樣的。但是,按照區間來查找數據這個操作,紅黑樹的效率沒有跳錶高。跳錶代碼相對更容易寫,更加靈活,它可以通過改變索引構建策略,有效平衡執行效率和內存消耗
	(3)散列表:是數組的一種擴展,由數組演化而來,插入刪除查找都是o(1)
	通過散列函數把元素的鍵值映射爲下標,然後將數據存儲在數組中對應下標的位置
	三個條件:
			散列函數計算得到的散列值是一個非負整數;
			如果 key1 = key2,那 hash(key1) == hash(key2);
			如果 key1 ≠ key2,那 hash(key1) ≠ hash(key2)。
	解決散列衝突:
		開放詢址法 :線性探測  、二次探測 、雙重散列 
							優點 :可有效利用緩存
										方便序列化
							缺點:刪除數據麻煩 需要特殊標記
									載因子的上限不能太大。這也導致這種方法比鏈表法更浪費內存空間
					總結:當數據量比較小、裝載因子小的時候,適合採用開放尋址法。這也是 Java 中的ThreadLocalMap使用開放尋址法解決散列衝突的原因
		鏈表法
						優點:內存利用率高 
									對大裝載因子的容忍度更高
						缺點:爲要存儲指針,所以對於比較小的對象的存儲,是比較消耗內存的,且對緩存不友好
			總結:基於鏈表的散列衝突處理方法比較適合存儲大對象、大數據量的散列表,而且,比起開放尋址法,它更加靈活,支持更多的優化策略,比如用紅黑樹代替鏈表
	怎麼設計合理的散列函數:
			1不能太複雜,不能消耗太多時間 時間複雜度要儘量低
			2散列函數生成的值要儘可能隨機並且均勻分
				--(1)數據分析法
				  (2)總數求模取餘法
	裝載因子過大怎麼辦?:
		擴容,需要通過散列函數重新計算每個數據的存儲位置
		如果內存空間不緊張,對執行效率要求很高,可以降低負載因子的閾值;相反,如果內存空間緊張,對執行效率要求又不高,可以增加負載因子的值,甚至可以大於 1
	如何解決擴容時時間複雜度過高?:
	    將擴容操作穿插在插入操作的過程中,分批完成,當裝載因子觸達閾值之後,我們只申請新空間,但並不將老的數據搬移到新散列表中,我們將新數據插入新散列表中,並且從老的散列表中拿出一個數據放入到新散列表。每次插入一個數據到散列表,我們都重複上面的過程;對於查詢操作:我們先從新散列表中查找,如果沒有找到,再去老的散列表中查找,這種實現方式,任何情況下,插入一個數據的時間複雜度都是 O(1) (雖然通過均攤)
哈希算法應用:
安全加密、唯一標識、數據校驗、散列函數、負載均衡、數據分片、分佈式存儲

基於哈希的字符串匹配:
(1)BF算法:
在主串中從0開始一直查找到n-m位置符不符合
	時間複雜度:O(n*m)
	實際中卻比較常用:一般主串 模式串不會特別長  而且不需要每次比較m個 比較到不匹配就結束了
									簡單  一般性能滿足後首先選擇算法簡單的 這樣出錯概率小 也好排查問題
(2)RK算法:
哈希n-m+1個值  跟模式串的哈希值進行比較  比較數字就能快很多
用特殊的哈希值算法來提高效率 因爲模式串中一定長度字符前後是有規律的 所以一次掃描o(n)即可以算出  時間複雜度也是o(n)   最壞會退化到o(m*n)

(3)BM算法:匹配到不合適的時候往後跳
1壞字符規則 
2好後綴規則

(4)KMP算法:對於已經比較過的好前綴 在遇到壞字符後能否跳過
可事先在模式串中計算好最長後綴和相匹配的最長前綴 ,   當最好前綴匹配一段後遇到一個不好的字符 就在模式串最好前綴中找到一段最長的能匹配主串前綴中的最長後綴的部分 然後移動(也是事先可以計算出每個最長前綴中可以移動多少位置)
	最難在於預求下標: 如果最長串的下一個字符和模式串下一個相等,那就是這次的最長的。如果不等需要找前一個的次長串, 還是不等就繼續找次長串(次長串的找法最難 上一個的次長串就是上一個的最長串的最長串) 也就是模式串中最長前綴慢慢縮小 ,通過上一次來計算下一次的來節省時間
	
	空間複雜度: o(模式串長度+原串長度)

-------------紅本書-----------------
遞歸
有幾個必須滿足的條件

1)方法第一條語句一定包含一個returnif 語句
(2*******遞歸一定是解決一個規模更小的子問題 : 最重要的概念 問題都是通過這個概念來列出數學等式
(3)遞歸的父問題和子問題之前不該有交集 因爲父問題討論過的範圍就沒必要讓子問題重複討論了
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章