# 《算法图解》读书笔记—像小说一样有趣的算法入门书

## 二分查找

``````/*
二分查找
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);
}

// 打印日志 ------------------------------------------------------

## 递归

``````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

## 排序

#### #选择排序

``````// 选择排序
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

#### #快速排序

``````
// 快速排序
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) 平均复杂度的访问、插入和删除操作，是一种比较底层的数据结构。

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]
}
}

`广度优先搜索`要解决的问题是基于`图`这种数据结构的`最短路径`的问题。是一种`用于图的查找算法`，可帮助回答两类问题：

1. 从顶点 a 出发，有前往顶点 b 的路径吗？
2. 从顶点 a 出发，前往顶点 b 的哪条路径最短？

• 按照有无方向可以分为`有向图``无向图`，有向图的边又称为`弧`，有`弧头``弧尾`之分。
• 按照边或弧的多少又分为`稀疏图``稠密图`。如果任意两个顶点之间都存在边叫做`完全图`，边有向的叫做`有向完全图`，若无重复的边或者顶点到自身的边叫做`简单图`
• 图上的边或者弧带上权则叫做`网`

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

## 狄克斯特算法

`狄克斯特算法`包含四个步骤：

1. 找出最便宜的顶点，即可在最少权重内到达前往的顶点；
2. 对于该顶点的邻居，检查是否有前往他们的更短路径，如果有，就更新其开销；
3. 重复这个过程，直到图中的每个顶点都这样做了；
4. 计算最终路径；

``````// 狄克斯特搜索算法
// 参数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 parent:String? = targetNode
while parents[parent!] != nil {
parent = parents[parent!]!
}

}

// 测试 ————————————————————-------------------------------------------
// 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. 集合覆盖问题

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"]

#### 4. 如何判断 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 算法，是一种非常宽泛的算法，如所有的图算法都可以用线性规划来实现，图问题只是线性规划的一个子集。