算法练习

103 阅读2分钟

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)

图的构成:边、节点

图结构分类

图相关算法汇总

广度搜索:可回答两个问题

  1. 节点A,是否有往节点B的路径?
  1. 节点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
       如果lbbest:
          剪枝返回
       否则:
          搜索(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代码示例等方面展示出来