《算法圖解》讀書筆記—像小說同樣有趣的算法入門書

前言

學習算法課程的時候,老師推薦了兩本算法和數據結構入門書,一本是《算法圖解》、一本是《大話數據結構》《算法圖解》這本書最近讀完了,讀完的最大感覺就是對算法再也不感到畏懼和陌生,對經常使用的算法和數據結構都會在內心有個基本的概念,這篇文章記錄下這本書的主要內容和知識點。node

總的來講,這本書是一本不錯的算法入門書,做者以從實際開發場景出發,介紹了軟件開發中最基本、最經常使用的一些數據結構算法思想,同時做者寫得很是深刻淺出,聯繫實際應用場景,並結合了大量的算法推演圖示,舉例生動形象,按部就班,使讀者易於理解,可以很大地培養讀者對算法的興趣,從而引導讀者進一步地進行學習研究。git

正如做者在書開頭關於本書中所說,閱讀這本書最佳姿式是從頭至尾讀,由於做者對內容順序專門作了精心地編排,從簡到難。前三章是最基礎的部分,第一章經過二分查找算法來引出衡量算法優劣的大O表示法概念,同時介紹了數組鏈表這兩種最基本的數據結構,經過這兩種最基本的數據結構,能夠來建立更高級更復雜的數據結構,第三章則介紹了遞歸,一種被衆多算法(如快速排序)採用的編程技巧。算法

從第四章開始,介紹地都是應用特別普遍的一些算法,第四章介紹了快速排序,一種效率最高的排序算法。第五章介紹了散列表,也叫哈希表或者字典,介紹了哈希表的實現,怎麼解決衝突,應用場景等內容。第六七章主要介紹的是這種數據結構,有向圖仍是無向圖,帶權值仍是不帶權值,以及和圖相關的幾種算法,廣度優先算法狄克斯特算法。第八章介紹了貪婪算法,在沒有高效的解決方案是,能夠考慮用貪婪算法來獲得近似答案。第九章介紹的是動態規劃,第十章介紹一種簡單的機器學習算法 K 最近鄰算法,能夠應用於建立推薦系統、OCR引擎、預測股價以及物體分類等領域,最後一章介紹了其餘一些解決特定問題的常見算法。編程

二分查找

二分查找解決的是如何最快地在一個有序的集合中找到一個目標數的問題。使用二分查找,每次都折半,經過和中間大的數比對,縮小範圍,最終只須要 O(logn) 的事件複雜度。swift

/*
    二分查找
    array:有序數組
    target: 目標數
    loopCount: 尋找次數
    return: 目標數下標
 */
- (NSInteger)binarySearchSortInArray:(NSArray<NSNumber *> *)array target:(NSNumber *)target loopCount:(NSInteger *)loopCount
{
    NSInteger low = 0;
    NSInteger high = array.count - 1;
    
    while (low <= high) {
        NSInteger mid = (low + high) / 2;
        NSInteger guess = array[mid].integerValue;
        *loopCount = *loopCount + 1;
        if (guess == target.integerValue) {
            // 猜中了
            return mid;
        }
        
        if (guess < target.integerValue) {
            // 猜小了
            low = mid + 1;
        } else {
            // 猜大了
            high = mid - 1;
        }
    }
    
    return -1;
}

// 測試數據 -----------------------------------------------------
NSArray *array = @[@1, @2, @5, @6, @9, @10, @13, @18, @22, @30];
NSInteger loopCount = 0;
NSInteger result = [self binarySearchInSortArray:array target:@2 loopCount:&loopCount];
if (result >= 0) {
    NSLog(@"找到目標數,目標數的的下標是:%ld,尋找:%ld 次", result, (long)loopCount);
} else {
    NSLog(@"沒有找到找到目標數,目標數的的下標是:%ld, 尋找:%ld 次", result, (long)loopCount);
}

// 打印日誌 ------------------------------------------------------
找到目標數,目標數的的下標是:1,尋找:2 次

複製代碼

遞歸

遞歸是一種本身調用本身的函數,每一個遞歸有兩個條件,分別是基線條件遞歸條件。如著名的斐波那契數列,在數學上,就是能夠用遞推公式來表示這個數列。數組

遞推公式

在編程領域,是常被不少算法所採用的一種編程技巧。如上面的二分查找也可使用遞歸來寫:緩存

int binary_search(int nums[], int target, int low, int high) {
    if(low > high) {return low;} // 基線條件
    int mid = low + (high - low) / 2;
    if(nums[mid] == target) {
        return mid;
    } else if (nums[mid] > target) {
        // 中間值大於目標值,遞歸條件
        return binary_search(nums, target, low, mid - 1);
    } else {
        // 中間值小於目標值,遞歸條件
        return binary_search(nums, target, mid + 1, high);
    } 
}

// 測試 -----------------------------------------------------
int array[10] = {1, 2, 5, 6, 9, 10, 13, 18, 22, 30};  
int result = binary_search(array, 9, 0, sizeof(array)/sizeof(int));
printf("result = %d", result); // result = 4
複製代碼

排序

#選擇排序

選擇排序的思想是,每次都從數組中選擇最小的數而後依次從起始的位置開始存放,有兩層循環,因此時間複雜度是 n^2。安全

// 選擇排序
void select_sort(int nums[], int length)
{
    int a = nums[0];
    // n -1 輪選擇
    for(int i = 0; i < length - 1; i++)
    {
        // 最小值所在索引
        int min_index = i;
        for(int j = i + 1; j < length; j++)
        {
            if(nums[min_index] > nums[j]) {
                // 有更小的
                min_index = j;
            }
        }

        // 若是正好,就不用交換
        if (i != min_index) {
            // 交換位置
            int temp = nums[i];
            nums[i] = nums[min_index];
            nums[min_index] = temp;
        }
    }
}

// 測試數據 ------------------------------------------
int a[10] = {12, 7, 67, 8, 5, 199, 78, 6, 2, 1};
select_sort(a, 10);
    
for(int i = 0; i < 10; i++)
{
    printf("%d ", a[i]);
}

// 打印日誌 -----------------------------------------
1 2 5 6 7 8 12 67 78 199 

複製代碼

#快速排序

快速排序是最快的一種排序算法,使用遞歸的思路,每次從數組中找出一個基準點,將數組分割成三分部分,小於全部基準點的元素組成一個數組less,大於基準點的元素組成一個數組greater,less + 基準點 + greater 就是新的數組,小數組也按照這種思路選取基準點進行分割,遞歸下去,遞歸的基線條件是數組的長度小於 2 時中止,意味着數組不能再分割下去了。這種思路下排序的時間複雜度是O(nlogn)。bash

// 快速排序
func quickSort(_ array:[Int]) -> [Int]
{
    if array.count < 2 {
        // 基線條件
        return array;
    } else {
        // 遞歸條件
        let pivot = array[0]
        let less = array.filter{$0 < pivot}
        let greater = array.filter{$0 > pivot}
        
        return quickSort(less) + [pivot] + quickSort(greater)
    }
}

// 測試
var array = [1, 78, 8, 76, 98, 90, 3, 100, 45, 78]
var newArray = quickSort(array)
// 打印
print(newArray) // [1, 3, 8, 45, 76, 78, 90, 98, 100]
複製代碼

散列表

散列表是一種很是常見的數據結構,經過數組結合散列函數實現,散列函數計算出值所對應的數組下標映射,在其餘一些平臺上也被稱爲散列映射、映射、字典和關聯數組,能作到 O(1) 平均複雜度的訪問、插入和刪除操做,是一種比較底層的數據結構。數據結構

散列表經常使用於查找、去重、緩存等應用場景,幾乎每種編程語言都有本身的散列表實現,如 Objective-C 的 NSDictionary,要想散列表有較好的性能和避免衝突(兩個鍵映射到了同一個位置),意味着須要有較低的填裝因子(散列表包含的元素數/佔用存儲的位置總數)良好的散列函數

一個不錯的經驗是,一旦填裝因子大於0.7,就要調整散列表的長度。而一旦發生衝突的一種解決辦法是,在衝突的位置存儲一個鏈表,將多個值存儲到鏈表的不一樣的節點上,這樣訪問的時候就還須要從頭遍歷一遍該位置的鏈表才行,好的散列函數應該是將鍵均勻的映射到散列表的不一樣位置

leetCode上第一道兩數之和的題目就可使用散列表來下降算法的時間複雜度。

/*
 * @lc app=leetcode.cn id=1 lang=swift
 *
 * [1] 兩數之和
 *
 * https://leetcode-cn.com/problems/two-sum/description/
 *
 * algorithms
 * Easy (44.51%)
 * Total Accepted:    243.9K
 * Total Submissions: 548K
 * Testcase Example:  '[2,7,11,15]\n9'
 *
 * 給定一個整數數組 nums 和一個目標值 target,請你在該數組中找出和爲目標值的那 兩個 整數,並返回他們的數組下標。
 * 
 * 你能夠假設每種輸入只會對應一個答案。可是,你不能重複利用這個數組中一樣的元素。
 * 
 * 示例:
 * 
 * 給定 nums = [2, 7, 11, 15], target = 9
 * 
 * 由於 nums[0] + nums[1] = 2 + 7 = 9
 * 因此返回 [0, 1]
 * 
 * 
 */
 
// C 兩層循環實現
int* twoSum(int* nums, int numsSize, int target) {
    static int resultArray[2];
    for (int i = 0; i < numsSize; i ++) {
        for (int j = i + 1; j < numsSize; j++) {
            if (nums[i] + nums[j] == target) {
                resultArray[0] = i;
                resultArray[1] = j;
            }
        }
    }
    
    return resultArray;
}

// swift 使用散列表實現
class Solution {
    func twoSum(_ nums: [Int], _ target: Int) -> [Int] {
        
        var hash = [Int: Int]
        for (index, item) in nums {
        	  // 使用散列表來判斷元素是否在散列表中,有的話返回元素的下標,即散列表的值
            if let firstIndex = hash[target - item] {
                return [firstIndex, index]
            }
            
			  // 將數組元素的值當作散列表的鍵,下標當作散列表的值存儲在散列表中
            hash[item] = index
        }

        return [-1,-1]
    }
}
複製代碼

C語言

使用散列表實現後:

swift

廣度優先搜索算法(breadth-first-search,BFS)

廣度優先搜索要解決的問題是基於這種數據結構的最短路徑的問題。是一種用於圖的查找算法,可幫助回答兩類問題:

  1. 從頂點 a 出發,有前往頂點 b 的路徑嗎?
  2. 從頂點 a 出發,前往頂點 b 的哪條路徑最短?

再看一下的定義:

圖(Graph)是由頂點有窮非空集合頂點之間邊的集合組成,一般表示爲:G(V,E),其中,G 表示一個圖,V 是圖 G 中頂點的集合,E 是圖 G 中邊的集合。

圖有不少種類:

  • 按照有無方向能夠分爲有向圖無向圖,有向圖的邊又稱爲,有弧頭弧尾之分。
  • 按照邊或弧的多少又分爲稀疏圖稠密圖。若是任意兩個頂點之間都存在邊叫作徹底圖,邊有向的叫作有向徹底圖,若無重複的邊或者頂點到自身的邊叫作簡單圖
  • 圖上的邊或者弧帶上權則叫作

能夠得出結論,廣度優先搜索解決問題的數據模型是有向圖,且不加權。

書中列舉了兩個列子,先來看第一個,如何選擇換乘公交最少的路線?,假設你住在舊金山,如今要從雙子峯前往金門大橋,如圖所示:

能夠從圖中得知,到達金門大橋最少須要三步,最短路徑是從雙子峯步行到搭乘 44 路公交,而後再換乘 28 路公交最短。

第二個例子是這樣的,假設你經營一個芒果農場,須要尋找芒果銷售商,如何在你本身的朋友關係網中找到芒果銷售商,這種關係包括你本身的朋友,這個是一度關係,也包括朋友的朋友,這個是二度關係,依次類推,那怎麼找到最短路徑呢?

解題思路:

使用散列表來存儲關係網,使用隊列來存儲查找順序,優先查找一度關係,再二度關係,依次類推。而後遍歷隊列判斷是不是芒果銷售商,是的話就是最近的芒果銷售商,若是不是,再查找二度關係,若是最後都沒有找到就是關係網裏面沒有芒果銷售商。

swift 實現代碼以下:

// 1. 使用數組來實現一個隊列,先進先出
struct Queue<Element> {
    private var elements: [Element] = []
    
    init() {
        
    }
    
    var count: Int {
        return elements.count
    }
    
    var isEmpty: Bool {
        return elements.isEmpty
    }
    
    // 隊列第一個元素
    var peek: Element? {
        return elements.first
    }
    
    // 入隊
    mutating func enqueue(_ element:Element) {
        elements.append(element)
    }
    
    // 出隊
    mutating func dequeue() -> Element?{
        return isEmpty ? nil : elements.removeFirst()
    }
}

extension Queue: CustomStringConvertible {
    var description: String {
        return elements.description
    }
}

// 2. 使用散列表來存儲關係網
var graph = [String:Any]()
// 一度關係
graph["you"] = ["claire", "bob", "alice"]
// 二度關係
graph["bob"] = ["anuj", "peggy"]
graph["claire"] = ["thom", "jonny"]
graph["alice"] = ["peggy"]
// 三度關係
graph["anuj"] = []
graph["peggy"] = []
graph["thom"] = []
graph["jonny"] = []

// 3. 廣度優先搜索算法
func search(_ name: String, inGraph g: [String:Any]) -> Bool {
    // 搜索隊列
    var searchQueue = Queue<String>()
    // 入隊列
    for item in g[name] as! Array<String> {
        searchQueue.enqueue(item)
    }
    
    // 記錄已經查找過的人
    var searched = [String]()
    // 循環結束條件 1. 找到一位芒果銷售商;2. 隊列爲空,意味着關係網裏面沒有芒果銷售商
    while !searchQueue.isEmpty {
    	  // 出隊
        let person = searchQueue.dequeue()
        // 判斷是否已經檢查過,檢查過就再也不檢查,防止出現死循環
        if !searched.contains(person!) {
            if personIsSeller(person!) {
                print("找到了芒果銷售商:\(person!)")
                return true
            } else {
                // 尋找下一度關係
                // 入隊列
                for item in g[person!] as! Array<String> {
                    searchQueue.enqueue(item)
                }
                
                // 將這我的標記檢查過
                searched.append(person!)
            }
        }
    }
    
    // 循環結束,沒有找到
    return false
}

// 是否是銷售商,規則能夠隨便設置
func personIsSeller(_ name: String) -> Bool {
    if let c = name.last {
        return c == "y"
    }
    
    return false
}

// 測試
search("you", inGraph: graph) // 找到了芒果銷售商:jonny
search("bob", inGraph: graph) // 找到了芒果銷售商:peggy
複製代碼

狄克斯特算法

深度優先算法BFS適用於無加權圖,找出的最短路徑是邊數最少的路徑,而狄克斯特算法解決的是加權圖中的最短路徑問題,找出的是最經濟最划算的路徑。

注意:狄克斯特算法也不適用於帶環的圖,只適用於有向無環圖,也不適用於負權邊的圖,負權邊的圖的最短路徑問題可使用貝爾曼-福德算法

狄克斯特算法包含四個步驟:

  1. 找出最便宜的頂點,便可在最少權重內到達前往的頂點;
  2. 對於該頂點的鄰居,檢查是否有前往他們的更短路徑,若是有,就更新其開銷;
  3. 重複這個過程,直到圖中的每一個頂點都這樣作了;
  4. 計算最終路徑;

書中舉了一個換鋼琴的例子,如何用曲譜再加最少的錢換到鋼琴的問題,使用狄克斯特算法的解決思路是:

解題思路:

1、找出最便宜的節點,樂普的鄰居是唱片和海報,換唱片要支付 5 美圓,換海報不用錢。先建立一個表格,記錄每一個節點的開銷以及父節點。因此下一個最便宜的節點是海報。

2、計算前往該節點的各個鄰居節點的開銷。第一步中海報最便宜,因此先從海報出發,海報能夠換吉他須要加 30 美圓,還能夠換架子鼓須要加 35 美圓,再計算黑膠唱片,黑膠唱片能夠換吉他須要加 15 美圓,還能夠換架子鼓須要加 20 美圓,和第一步的相加能夠算出,經過黑膠唱片這條路徑前往,開銷最短,更新上一步中最便宜的節點爲黑膠唱片。從樂普換吉他最少須要花 20 美圓,換架子鼓最少須要花 25 美圓。因此下一個最便宜的節點是海報。

3、優先從吉他出發,計算其鄰居節點開銷。吉他的鄰居就是鋼琴,須要加 20 美圓,總開銷加上以前的 20 美圓是 40 美圓,再從架子鼓出發,計算架子鼓到其鄰居節點的開銷,架子鼓的鄰居節點也是鋼琴,開銷 10 美圓,加上以前的 25 美圓總開銷是 35 美圓,因此更新最優節點是架子鼓。

最終路徑是從曲譜->黑膠唱片->架子鼓->鋼琴。

算法實現

須要三個散列表,一個用來存儲整個圖結構,一個用來存儲節點的開銷,最後一個用來存儲節點的父節點。算法執行流程圖:

// 狄克斯特搜索算法
// 參數g:圖形結構散列表
// costs:節點開銷散列表 inout 關鍵字能夠是得函數內部能夠修改外部參數的值
// parents: 父節點散列表
// targetNode:目標節點
// 返回值:(總的最少花銷,最短路徑步驟,排序數組)
func search(costs:inout [String :Int], parents:inout [String: String?], targetNode: String, inGraph g: [String : Any]) ->(Int, String)
{
    // 存儲處理過的節點
    var processed = [String]()
    
    // 找出最便宜的節點
    func findLowerCostNode(_ costs: [String:Int]) -> String? {
        var lowestCost = Int.max
        var lowerCostNode: String?
        for (key, value) in costs {
            // 遍歷開銷散列表,找出沒有處理過且到起點花費最小開銷的節點
            if value < lowestCost && !processed.contains(key){
                lowestCost = value
                lowerCostNode = key
            }
        }
        
        return lowerCostNode
    }
    
    var node = findLowerCostNode(costs)
    // 遍歷全部的節點
    while (node != nil) {
        // 節點開銷
        let cost = costs[node!]
        // 遍歷當前節點的全部的鄰居節點
        var neighbors = graph[node!]
        for n in (neighbors?.keys)! {
            // 判斷該鄰居節點若是從當前節點通過的總開銷是否小於該鄰居節點原來的開銷,若是小於,就更新該鄰居節點的開銷
            let new_cost = cost! + (neighbors?[n])!
            if costs[n]! > new_cost {
                // 更新該節點的鄰居節點的最新最短開銷
                costs[n] = new_cost
                // 更新該鄰居節點的父節點爲當前節點
                parents[n] = node!
            }
        }
        
        // 將當前節點標記爲已經處理過的節點
        processed.append(node!)
        // 繼續尋找下一個最少開銷且未處理過的節點
        node = findLowerCostNode(costs)
    }
    
    // 到達目標節點的最少開銷
    let minCost = costs[targetNode]
    // 最短路徑
    var minRoadArray = [targetNode]
    var parent:String? = targetNode
    while parents[parent!] != nil {
        parent = parents[parent!]!
        minRoadArray.append(parent!)
    }
    
    return (minCost!, minRoadArray.reversed().joined(separator: "->"))
}


// 測試 ————————————————————-------------------------------------------
// 1. 存儲圖結構
var graph = [String : Dictionary<String, Int>]()

// 曲譜的鄰居節點
graph["曲譜"] = [String : Int]()
// 到唱片的開銷
graph["曲譜"]?["黑膠唱片"] = 5
// 到海報的開銷
graph["曲譜"]?["海報"] = 0

// 唱片節點的鄰居節點
graph["黑膠唱片"] = [String : Int]()
// 唱片節點到吉他的開銷
graph["黑膠唱片"]?["吉他"] = 15
// 唱片節點到架子鼓的開銷
graph["黑膠唱片"]?["架子鼓"] = 20

// 海報節點的鄰居節點
graph["海報"] = [String : Int]()
// 海報節點到吉他的開銷
graph["海報"]?["吉他"] = 30
// 海報節點到架子鼓的開銷
graph["海報"]?["架子鼓"] = 35

// 吉他節點的鄰居節點
graph["吉他"] = [String : Int]()
// 吉他節點到鋼琴的開銷
graph["吉他"]?["鋼琴"] = 20


// 架子鼓節點的鄰居節點
graph["架子鼓"] = [String : Int]()
// 架子鼓節點到鋼琴的開銷
graph["架子鼓"]?["鋼琴"] = 10

// 鋼琴節點
graph["鋼琴"] = [String : Int]()

print(graph)

// 2. 建立開銷表,表示從曲譜到各個節點的開銷
var costs = [String : Int]()
// 到海報節點開銷
costs["海報"] = 0
// 到黑膠唱片節點開銷
costs["黑膠唱片"] = 5
// 到鋼琴節點開銷,不知道,初始化爲最大
costs["吉他"] = Int.max
// 到鋼琴節點開銷,不知道,初始化爲最大
costs["架子鼓"] = Int.max
// 到鋼琴節點開銷,不知道,初始化爲最大
costs["鋼琴"] = Int.max

print(costs)

// 3. 建立父節點表
var parents = [String : String?]()
// 海報節點的父節點是曲譜
parents["海報"] = "曲譜"
// 黑膠唱片的父節點是曲譜
parents["黑膠唱片"] = "曲譜"
// 吉他的父節點,不知道
parents["吉他"] = nil
// 架子鼓的父節點,不知道
parents["架子鼓"] = nil
// 鋼琴的父節點, 不知道
parents["鋼琴"] = nil

print(parents)

// 測試
var (minCost, minRoadString) = search(costs: &costs, parents: &parents, targetNode: "鋼琴", inGraph: graph)

複製代碼

打印日誌:

貪婪算法

貪婪算法是一種經過尋找局部最優解來試圖獲取全局最優解的一種易於實現、運行速度快的近似算法。是解決 NP 徹底問題(沒有快速算法問題)的一種簡單策略。書上總共舉了四個例子來講明貪婪算法的一些場景。

1. 教師調度問題

有以下課程表,要怎麼排課調度,使得有儘量多的課程安排在某間教室上。

這個問題就可使用貪婪算法來解決。

  1. 選出結束最先的課,他就是要在這件教室上的第一節課。
  2. 接着,選擇第一節課結束後纔開始的課,一樣選擇結束最先的課,這就是要在這件教室上的第二節課。重複這樣作,就能找出答案。

2. 揹包問題

這個問題是這樣的,如何在容量有限的揹包中裝入總價值最高的商品。這個問題不適用貪婪算法,按照貪婪算法,每次都須要往揹包裏先裝最大價值的商品,而後根據揹包剩餘容量,再選擇最大價值的商品,這樣有可能揹包的空間利用率不是最高的,卻不必定會是最優解。

2. 集合覆蓋問題

背景是這樣的,有個廣播節目,要讓全美 50 各州的聽衆都能聽到,須要在選出覆蓋全美 50 各州的最小廣播臺集合。若是要需求最優解,可能的子集有 2^n 次方個,若是個廣播臺不少,那麼這個算法的複雜度將很是之高,幾乎沒有任何算法能夠足夠快速的解決這個問題,而使用貪婪算法能夠快速的找到近似解,算法的複雜度是 O(n^2)。步驟以下:

  1. 選擇一個覆蓋最多未覆州的廣播臺,即時已經覆蓋了一些已經覆蓋過的州也不要緊。
  2. 重複第一步,知道覆蓋了全部的州。

代碼模擬實現:

// 要覆蓋的州列表
var statesNeeded = Set(["mt", "wa", "or", "id", "nv", "ut", "ca", "az"])

// 廣播臺清單
var stations  = [String: Set<String>]()
stations["kone"] = Set(["id", "nv", "ut"])
stations["ktwo"] = Set(["wa", "id", "mt"])
stations["kthree"] = Set(["or", "nv", "ca"])
stations["kfour"] = Set(["nv", "ut"])
stations["kfive"] = Set(["ca", "az"])

// 最終選擇的廣播臺集合
var finalStations = Set<String>()

// 若是要覆蓋的州列表不爲空
while !statesNeeded.isEmpty {
    // 覆蓋了最多沒覆蓋州的廣播臺
    var bestStation:String?
    // 已經覆蓋了的州的集合
    var statesCovered = Set<String>()
    // 遍歷全部的廣播臺
    for (station, states) in stations {
        // 取交集操做
        let covered = statesNeeded.intersection(states)
        if covered.count > statesCovered.count {
            bestStation = station
            statesCovered = covered
        }
    }
    
    // 取差集操做
    statesNeeded = statesNeeded.subtracting(statesCovered)
    finalStations.insert(bestStation!)
}

// 打印
print(finalStations) // ["ktwo", "kfive", "kone", "kthree"]
複製代碼

對比一下兩種算法的運行效率差別:

3. 旅行商問題

旅行商問題的背景是一個旅行商商想要找出前往若干個城市的最短路徑,若是城市數量少,好比兩個,可能的路徑有 2 條,三個城市有 有 6 條,4 個城市有 24 條,5 個城市有 120 條,n 個城市有 n! 個可能路線,n! 複雜度是一種很是難以快速找出最優解的問題。就能夠考慮貪婪算法快速地去求近似解

4. 如何判斷 NP 徹底問題

上面的集合覆蓋問題和旅行商問題都是 NP 徹底問題,若是可以判斷出問題屬於 NP 徹底問題,就能夠考慮貪婪算法求近似解的策略了,可是判斷 NP 徹底問題並不容易,做者概括了一些常見場景:

  1. 隨着元素增長,速度會變得很是慢。
  2. 涉及全部組合的問題一般是 NP 徹底問題。
  3. 不能將問題分紅小問題,必須考慮各類可能的狀況。也多是 NP 徹底問題。
  4. 若是問題涉及序列(如旅行商問題中的城市序列)且難以解決,可能就是 NP 徹底問題。
  5. 若是問題涉及集合(如廣播臺集合的例子)且難以解決,可能就是 NP 徹底問題。
  6. 若是問題可轉換成集合覆蓋問題或者旅行商問題,那確定是 NP 徹底問題。

動態規劃

動態規劃經常使用來解決一些在給定約束條件下的最優解問題,如揹包問題的約束條件就是揹包的容量,通常能夠將大問題分解爲彼此獨立且離散的小問題時,能夠經過使用網格,每一個小網格就是分解的小問題的手段的方式來解決。使用動態規劃來解決的實際問題場景有:

  1. 最長公共子串、最長公共子序列來判斷兩個字符串的類似程度。生物學家經過最長公共序列來肯定 DNA 鏈的類似性。
  2. git diff 文件對比命令,也是經過動態規劃實現的。
  3. word 中的斷字功能。

K 最近鄰算法

KNN 用來分類和迴歸,分類是編組,迴歸是預測一個結果。經過特徵抽取,大規模訓練,而後比較數據之間的類似性,從而分類或是預測一個結果。有以下常見應用場景:

  1. 推薦系統。
  2. 機器學習。
  3. OCR(光學字符識別),計算機經過瀏覽大量的數字圖像,並將這個圖像的特徵提取出來,遇到新圖像時,從數據中尋找他最近的鄰居。
  4. 垃圾郵件過濾器,經過使用樸素貝葉斯分類器計算出郵件爲垃圾郵件的機率。
  5. 預測股票市場。

其餘一些算法介紹

  1. 樹,二叉查找樹,左子節點的值總比本身小,右子節點的值總比本身大,能夠用來優化數組插入和刪除的複雜度底的問題,二叉查找樹的查找、插入、刪除的複雜度都是O(logn)。
  2. 反向索引。一種將單詞映射到包含他的頁面的數據結構,可用於建立搜索引擎
  3. 傅里葉變換。傅里葉變換能夠將數字信號中的各類頻率分離出來,可計算歌曲中每一個音符對整個音樂的貢獻,用來作音樂壓縮,音樂識別等。
  4. 並行算法。利用設備的多核優點,提升算法的執行性能。也會有一些額外開銷,如並行性管理和負載均衡。
  5. MapReduce。一種分佈式並行算法,可以讓算法在多臺計算機上運行,很是適合用於在短期內完成海量工做,MapReduce的兩個概念,映射(map)和歸併(reduce)函數。映射(map)是將序列中的元素都執行一種處理而後返回另外一個序列,歸併(reduce)是序列歸併爲一個元素。MapReduce 使用這個兩個概念在多臺計算機上執行數據查詢,能極大的提升速度。
  6. 布隆過濾器和 HypelogLog。布隆過濾器是一種機率性數據結構,有點在於佔用空間小,很是適用於不要求答案絕對準確的狀況。HypelogLog 近似地計算集合中不一樣的元素數,也是相似於布隆過濾器的一種機率性算法。
  7. SHA 算法。SHA 算法是一種安全散列算法,給定一個字符串,SHA 算法返回其散列值,可用來比較文件,檢查密碼。
  8. 局部敏感散列算法。Simhash 是一種局部敏感散列算法。若是字符串只有細微的差異,那麼散列值也只存在細微的差異,這就可以用來經過比對散列值來判斷兩個字符串的類似程度,也頗有用。
  9. Diffie-Hellman 祕鑰交換算法。可用來解決如何對消息加密後只有收件人能看懂的問題。這個種算法使用兩個祕鑰,公鑰和私鑰,公鑰是公開的,發送消息使用公鑰加密,加密後的消息只有使用私鑰才能解密。通訊雙方無需知道加密算法,要破解比登天還難。
  10. 線性規劃。線性規劃算法用於在給定約束條件下最大限度地改善指定的指標。線性規劃使用 Simplex 算法,是一種很是寬泛的算法,如全部的圖算法均可以用線性規劃來實現,圖問題只是線性規劃的一個子集。

擴展閱讀

數據結構與算法學習-鏈表下

數據結構與算法學習-鏈表上

數據結構與算法學習-數組

數據結構與算法學習-複雜度分析

數據結構與算法學習-開篇


分享我的技術學習記錄和跑步馬拉松訓練比賽、讀書筆記等內容,感興趣的朋友能夠關注個人公衆號「青爭哥哥」。

相關文章
相關標籤/搜索