1. 常见算法以及实现
1.1 排序算法
1.1.1 快速排序
// 快速排序
// 0. 退出条件 arr.count>1 else return
// 1. 选择中间值pivot支点
// 2. > pivot的数组arr1, < pivot的数组arr2, = pivot的arr3
// 3. 拼接arr 返回
func quickSort<T: Comparable>(arr: [T]) -> [T] {
guard arr.count > 1 else {
reutrn arr
}
let pivot = arr[0]
let arr1 = arr.filter { $0 > pivot }
let arr2 = arr.filter { $0 == pivot}
let arr3 = arr.filter { $0 < pivot }
return quickSort(arr3) + arr2 + arr1
}
1.1.2 选择排序
// 选择排序:
// 选择最大放头
// 0. 退出条件 arr.count>1 else return arr
// 1. 找max 放数组sortArr
// 2. 返回 max + selectSort(arr)+ min
func selectionSort<T: Comparable>(_ arr: [T]) -> [T] {
guard arr.count > 1 else {
return arr
}
var sortArr = [T]()
var arr = arr
while(arr.count > 0) {
var minIndex = 0
var min = arr[minIndex]
for index in 0 ..< arr.count {
let value = arr[index]
if min > value {
min = value
minIndex = index
}
}
sortArr.append(min)
arr.remove(at: minIndex)
}
return sortArr
}
let selectionArray = selectionSort(unsortedArray) // [2, 3, 5, 6, 7, 10]
1.1.3 冒泡排序
// 冒泡排序:
// 比较大者前置
// 0. 循环遍历数组arr(count)次选取最大,i从0 ..< count
// 1. 循环遍历数组 arr j从i +1 到 count 排序置换
func bubbleSort<T: Comparable>(_ arr: [T]) -> [T] {
let count = arr.count
var arr = arr
for i in 0 ..< count {
for j in i+1 ..< count {
if arr[i] > arr[j] {
let temp = arr[i]
arr[i] = arr[j]
arr[j] = temp
}
}
}
return arr
}
1.1.4 插入排序
// 插入排序
// 将数组分为有序和无序两部分,每次从无序数组中取出第一个元素,插入到有序数组中的合适位置。
// 时间复杂度:O(n^2)
// 空间复杂度:O(1)
func insertionSort<T: Comparable>(_ arr: [T]) -> [T] {
let count = arr.count
var result = arr
for i in 1..<count {
var j = i
while j > 0 && result[j] < result[j-1] {
let value = result[j]
result.remove(at: j)
result.insert(value, at: j-1)
j -= 1
}
}
return result
}
let insertionArray = insertionSort(unsortedArray)
print("sort array = (insertionArray)")
1.1.5 归并排序
// 归并排序
// 分治思想,分解为有序表,合并为有序表
// 分解 mergeSort
// guard arr.count > 1 else { return arr }
// let sort1 = mergeSort(arr[0..<middle])
// let sort2 = mergeSort(arr[middle..<count)
// return merge(sort1, sort2)
// 合并 merge
// var leftIndex = 0
// var rightIndex = 0
// var result = [T]()
// while leftIndex < sort1.count && rightIndex < sort2.count {
// if sort1[leftIndex] < sort2[rightIndex] {
// result.append(sort1[leftIndex])
// leftIndex += 1
// } else {
// result.append(sort2[rightIndex])
// rightIndex += 1
// }
// }
// sort1\sort2剩余元素追加
// 时间复杂度: O(nlog2(n))
// 空间复杂度: O(n)
// 场景:空间换时间,排序时间复杂度优于O(n^2)
func mergeSort<T: Comparable>(_ arr: [T]) -> [T] {
guard arr.count > 1 else {
return arr
}
let middle = arr.count / 2
let left = mergeSort(Array(arr[0..<middle]))
let right = mergeSort(Array(arr[middle..<arr.count]))
return merge(leftArray: left, rightArray: right)
}
func merge<T: Comparable>(leftArray: [T], rightArray: [T]) -> [T] {
var result: [T] = []
var leftIndex = 0
var rightIndex = 0
while leftIndex < leftArray.count && rightIndex < rightArray.count {
if leftArray[leftIndex] > rightArray[rightIndex] {
result.append(rightArray[rightIndex])
rightIndex += 1
} else {
result.append(leftArray[leftIndex])
leftIndex += 1
}
}
while leftIndex < leftArray.count {
result.append(leftArray[leftIndex])
leftIndex += 1
}
while rightIndex < rightArray.count {
result.append(rightArray[rightIndex])
rightIndex += 1
}
return result
}
print("unsort array =(unsortedArray)")
let mergeArray = mergeSort(unsortedArray)
print("merged array =(mergeArray)")
1.1.6 堆排序
// 堆排序
// 1.将数组构建成一个大顶堆/小顶堆
// 2.每次取出堆顶元素,与最后一个元素交换位置,然后调整堆产生新的堆顶
// 3. 重复步骤2,直到堆大小为1
// 时间复杂度:最坏 O(nlog2(n)) 空间复杂度O(1)
// 场景:时间复杂度要求为O(nlog2(n))
func heapSort<T: Comparable>(_ arr: [T]) -> [T] {
var array = arr
// 构建大顶堆
for i in stride(from: (array.count - 2) / 2, through: 0, by: -1) {
siftDown(&array, i, array.count)
}
// 排序,将大顶堆转换成升序数组
for i in stride(from: array.count - 1, through: 1, by: -1) {
array.swapAt(0, i) // 交换0,i元素
siftDown(&array, 0, i)
}
return array
}
// 下滤操作,保持大顶堆性质
func siftDown<T: Comparable>(_ array: inout [T], _ index: Int, _ upTo: Int) {
var parent = index
while true {
let left = 2 * parent + 1
let right = 2 * parent + 2
var candidate = parent
if left < upTo && array[left] > array[candidate] {
candidate = left
}
if right < upTo && array[right] > array[candidate] {
candidate = right
}
if candidate == parent {
return
}
array.swapAt(parent, candidate)
parent = candidate
}
}
let heapArray = heapSort(unsortedArray)
print("heap arrary = (heapArray)")
引申
1. 堆数据结构:
堆是一种重要的数据结构,具有以下的主要特点:
堆是一个完全二叉树,适合使用数组来存储。
堆中每个节点的值都必须大于等于(或小于等于)其子节点的值,这就是堆的性质。
最大值(或最小值)元素位于根节点,便于快速访问。
主要操作有:
插入(Insert):新元素插入末尾,然后上移调整堆
查找最大/最小值(GetMax/GetMin):根节点即为最大/最小值
删除最大/最小值(DeleteMax/DeleteMin):移除根节点,并用末尾元素替换,下移调整堆
删除任意节点(Delete):将需要删除的节点和末尾元素交换,然后下移调整堆
实现了优先队列,支持按优先级快速访问最大或最小元素。
应用在堆排序,topk查询等算法中。
2. 大顶堆、小顶堆
大顶堆和小顶堆是堆这种数据结构的两种变体,它们之间的区别是:
大顶堆:每个节点的值都大于或等于其子节点的值,根节点的值最大。
小顶堆:每个节点的值都小于或等于其子节点的值,根节点的值最小。
也就是说,在大顶堆中,键值最大的元素在根节点,整棵树维持最大堆的性质。
而小顶堆中,键值最小的元素在根节点,维持最小堆的性质。
区别举例:
大顶堆:[95, 81, 62, 28, 65]
小顶堆:[28, 65, 62, 81, 95]
大顶堆适合用于优先级队列等需要快速访问最大值的场景。
小顶堆适合用于优先级队列等需要快速访问最小值的场景。
3. 什么是完全二叉树?
除了最后一层,其他层的节点都满足满二叉树的要求,即左右子树都存在,节点数达到最大。
最后一层可以不满,但必须按从左到右的顺序填充。
满足第2点后,可以证明完全二叉树的节点数达到最大。
完全二叉树的高度最小,在所有节点数相同的二叉树中,完全二叉树的高度最小。
完全二叉树很适合使用数组顺序存储。
1.1.7 GPT辅助
排序算法: www.shareclaude.top/c/jxqnqoa
2. 查询算法
1.2.1 二分查找
// 二分查找
// 升序表,从中间查找,> 则在左侧查找,< 在右侧查找
// while循环格式
// 0. var start = 0; var end = arr.count - 1; var targetIndex = -1
// 1. while(start <= end)
// 2. 找出中间index = (end - start) / 2 + start(取整) 中间值center = arr[index]
// 3. if center == target targetIndex = index break ;else if center > target; end = index - 1 else start = index + 1
// 4. 循环到1
func binarySearch<T: Comparable>(_ arr: [T], target: T) -> Int {
var start = 0
var end = arr.count - 1
var targetIndex = -1
while start <= end {
var index = (end - start) / 2 + start
var center = arr[index]
if center == target {
targetIndex = index
break
}else if center > target {
end = index - 1
} else {
start = index + 1
}
}
return targetIndex
}
let target = 10
let index = binarySearch(bubbleArray, target: target)
if index == -1 {
print("bubbleArray= (bubbleArray), 未找到(target) ")
}else {
print("bubbleArray= (bubbleArray), index= (index), value =(selectionArray[index])")
}
1.2.2 哈希查找
// Hash查找
//时间复杂度:
//
// 平均时间复杂度 O(1)
// 最坏时间复杂度 O(n)
// 空间复杂度: O(n)
// 注意hash函数冲突的问题
struct HashTable<Key: Hashable, Value> {
private var array = Array<Node?>(repeating: nil, count: 10)
struct Node {
let key: Key
let value: Value
}
mutating func insert(_ key: Key, _ value: Value) {
let index = key.hashValue % array.count
array[index] = Node(key: key, value: value)
}
mutating func get(_ key: Key) -> Value? {
let index = key.hashValue % array.count
return array[index]?.value
}
}
引申:
散列/哈希表,用于O(1)时间复杂度的查找。(Hash 散列的、切碎的)
目的:解决冲突,需要
- 装填因子——空位占比
- Hash函数——足够均匀分布
用于多种语言的字典实现、缓存技术(内存缓存、DNS)
3. 图算法
“在我所知道的算法中,图算法应该是最有用的” 《算法图解》-巴尔加瓦 (Aditya Bhargava)
图的构成:边、节点
图结构分类
图相关算法汇总
广度搜索:可回答两个问题
- 节点A,是否有往节点B的路径?
- 节点A,前往B的最短路径?
-
图结构实现
- 散列表+队列
-
// 散列表+队列 (有向边) // N:Node节点, W:Weight权重 class Graph<N: Hashable, W> { private var adjacencyList = [N: Queue<Edge<W>>]() struct Edge<W> { var node: N var weight: W? } func addNode(_ node: N) { if adjacencyList[node] == nil { adjacencyList[node] = Queue<Edge<W>>() } } func addEdge(from source: N, to destination: N, weight: W? = nil) { let edge = Edge(node: destination, weight: weight) adjacencyList[source]?.enqueue(edge) } func edgesQueue(from source: N) -> Queue<Edge<W>>? { return adjacencyList[source] } func showEdges(from source: N) { guard var queue = edgesQueue(from: source) else { return } while !queue.isEmpty { if let edge = queue.dequeue() { print("edge: (source) && (edge.node)") } } } } struct Queue<T> { private var array = [T]() var isEmpty: Bool { return array.isEmpty } mutating func enqueue(_ element: T) { array.append(element) } mutating func dequeue() -> T? { if isEmpty { return nil } else { return array.removeFirst() } } } class PersonGraph: Graph<String, Int> { } - 邻接矩阵
-
// 2. 邻接矩阵实现图 //使用二维数组存储任意两个节点之间的连接情况。 // //优点是判断两个节点之间是否存在边非常快捷,时间复杂度 O(1)。 // //缺点是占用空间大,需要为所有可能的边都分配空间。 struct MetrixGraph { private var adjMatrix = [[Int]]() mutating func addEdge(_ source: Int, _ dest: Int) { adjMatrix[source][dest] = 1 } func hasEdge(_ source: Int, _ dest: Int) -> Bool { return adjMatrix[source][dest] == 1 } } - 邻接表
-
// 1. 邻接表实现图 //使用散列表存储每个节点的邻接表。邻接表存储每个节点的出边。 // //优点是添加边和获取节点的邻居时间复杂度为 O(1)。 // //缺点是占用空间大,每个节点都需要存储对应的邻居节点。 struct TableGraph { private var adjList = [Int: [Int]]() mutating func addEdge(_ source: Int, _ dest: Int) { adjList[source, default: []].append(dest) } func neighbors(_ node: Int) -> [Int] { return adjList[node, default: []] } }
| 散列表+队列 | 邻接矩阵 | 邻接表 | |
|---|---|---|---|
| 优点 | 空间小、稀疏图适合、可考虑边顺序 | 查边快 | 空间小、稀疏图适合 |
| 缺点 | 查边较慢 | 占用空间大 | 查边较慢 |
- 广度优先遍历
// 广度优先
func bfs(start: Int) {
var visited = Set<Int>()
var queue = Queue<Int>()
visited.insert(start)
queue.enqueue(start)
while !queue.isEmpty {
let node = queue.dequeue()!
print(node)
if let neighbors = adjList[node] {
for neighbor in neighbors {
if !visited.contains(neighbor) {
visited.insert(neighbor)
queue.enqueue(neighbor)
}
}
}
}
}
4. 二叉树
-
深度遍历
-
广度遍历
5. B数与B+树
6. 动态规划
"""
动态规划算法的核心思想是将问题分解成若干个子问题,并保存子问题的解,避免重复计算。
时间复杂度分析:
动态规划算法的时间复杂度与状态数和状态转移关系相关。状态数为N,转移关系为M,则时间复杂度一般为O(NM)。
空间复杂度分析:
需要存储子问题解,因此空间复杂度与状态数N相关,一般为O(N)。
应用场景分析:
优化路径问题,如旅行商问题
资源配置问题,如背包问题
序列规划问题,如字符串编辑距离
"""
// 经典:斐波那契数列问题
func fib(_ n: Int) -> Int {
if n <= 1 {
return n
}
var dp = Array(repeating: 0, count: n+1)
dp[1] = 1
for i in 2...n {
dp[i] = dp[i-1] + dp[i-2]
}
return dp[n]
}
_ = (0..<10).map{ print(fib($0)) }
// 经典:矩阵链相乘顺序问题
// 给定多个矩阵,要求计算将它们链式相乘的最佳parentheses方式,使得总的计算量最小
// 最佳parentheses方式:指的是在矩阵链相乘问题中,用括号表示矩阵乘法的计算顺序,使得总的计算量最小的一种括号表示法
/*
动态规划解法:
定义子问题:OPT(i,j)表示计算$A_iA_{i+1}...A_j$的最优计算量
状态转移方程: OPT(i,j) = min{OPT(i,k) + OPT(k+1,j) + p_{i-1}p_kp_j}, ∀i <= k < j
初始条件和边界:
OPT(i,i) = 0
OPT(i,i+1) = p_{i-1}p_ip_{i+1}
计算顺序:
从小问题开始,逐渐计算更大的子问题
用自底向上的方法计算OPT(i,j)
时间复杂度分析:
状态数:O(n^2)
每个状态计算:O(n)
所以总时间复杂度为O(n^3)
空间复杂度: O(n^2)
存储OPT(i,j)表格需要O(n^2)空间。
*/
func matrixChainOrder(_ p: [Int]) -> Int {
let n = p.count - 1
var m = [[Int]](repeating: [Int](repeating: 0, count: n), count: n)
var s = [[Int]](repeating: [Int](repeating: 0, count: n), count: n)
for l in 2...n {
for i in 0..<n-l+1 {
let j = i + l - 1
m[i][j] = Int.max
for k in i...j-1 {
let q = m[i][k] + m[k+1][j] + p[i]*p[k+1]*p[j+1]
if q < m[i][j] {
m[i][j] = q
s[i][j] = k
}
}
}
}
return m[0][n-1]
}
// 输入矩阵链长度数组
//A1 是 10 x 100
//A2 是 100 x 5
//A3 是 5 x 50
let p = [10, 100, 5, 50]
// 调用函数计算最佳计算顺序
let minCost = matrixChainOrder(p)
// 输出结果
print("Minimum cost is: (minCost)")
7. 回溯算法
"""
核心思想:
回溯算法是一种搜索所有可能解的算法。它通过构建搜索树,系统地遍历所有解决方案,并在到达解决方案时回退,继续尝试不同的路径。
回溯算法(参数)
初始化解向量解={null}
函数回溯(步数k):
如果步数k==解向量长度,表示找到一个完整解,记录解
否则:
遍历当前所有可选的状态i
将状态i加入解向量解
回溯(k+1)
移出状态i
调用回溯(0)
时间复杂度: O(n!),遍历整棵解空间树
空间复杂度: O(n),递归栈空间
缺点:
时间和空间复杂度都很高
无法利用问题结构进行剪枝
使用场景:
棋盘问题、数独等全排列需要穷举的场景
深度优先遍历一个解空间
"""
func backtrack(n: Int) {
var solution = [Int]()
func dfs(_ k: Int) {
if k == n {
// 找到一个解
print(solution)
} else {
// 遍历每个分支
for i in 1...n {
solution.append(i)
dfs(k+1)
solution.removeLast()
}
}
}
dfs(0)
}
let n = 3
backtrack(n: n)
8. 分支限界
"""
分支限界
核心思想:
在解空间树上搜索最优解的方法。它通过递归地分割问题的可行解空间,并在分割的每一步计算一个界限,来判断这一子空间是否包含最优解,如果不包含可以直接剪枝。
伪代码:
分支限界算法(问题):
1. 初始化解空间S包含全部可能解
2. 初始化当前最优解best = 无穷大
3. 函数搜索(S):
如果S为空:
返回
选择S中的一个子集Si
计算Si的下界lb
如果lb ≥ best:
剪枝返回
否则:
搜索(Si)
如果Si包含更优解,更新best
剪枝移除不包含最优解的Si
4. 调用搜索(S)
时间复杂度: O(b^d) ,b为分支因子,d为树深度
空间复杂度: O(bd),存储树节点
缺点: 计算复杂,需要精确设计界限函数
使用场景: TSP,资源分配等组合优化问题
"""
// TSP问题分支限界
func branchAndBoundTSP(n: Int, costMatrix: [[Int]]) -> [Int] {
var bestPath: [Int] = []
var bestCost = Int.max
func backtrack(currPath: [Int], currCost: Int) {
if currPath.count == n {
// 找到一个完整路径
if currCost < bestCost {
// 更新最优解
bestPath = currPath
bestCost = currCost
}
} else {
// 尝试每个可能的下一城市
for nextCity in 1..<n {
if !currPath.contains(nextCity) {
// 计算新增成本
let newCost = currCost + costMatrix[currPath.last!][nextCity]
// 剪枝:如果成本已超过当前最优,则跳过
if newCost < bestCost {
backtrack(currPath: currPath + [nextCity], currCost: newCost)
}
}
}
}
}
backtrack(currPath: [0], currCost: 0)
return bestPath
}
// 测试数据
// 有5个城市,成本矩阵costTable给出了任意两城市间的距离成本。
// 调用branchAndBoundTSP来求解旅行商问题,获得最优路径optimalPath
let numOfCities = 5
let costTable = [ [0, 10, 8, 7, 5],
[10, 0, 6, 4, 3],
[8, 6, 0, 5, 9],
[7, 4, 5, 0, 6],
[5, 3, 9, 6, 0]
]
// 调用函数
let optimalPath = branchAndBoundTSP(n: numOfCities, costMatrix: costTable)
print("Optimal path is: (optimalPath)")
2. 特殊算法练习
2.1 大数运算
3. GPT提示词
请以“常见算法总结”为题,创作一个思维导图,要求:从算法应用场景角度分析,脉络清晰,尽可能全面、真实;使用```markdown文件代码的格式展示出来
展开讲一下堆排序算法,并从核心思想、伪代码、时间/空间复杂度、缺点、使用场景、swift代码示例等方面展示出来