常用的算法

36 阅读15分钟

在计算机领域中,有许多常用的算法。根据其用途和特性,这些算法可以分为以下几类:

  1. 排序算法:用于对数据进行排序。例如:快速排序、归并排序、堆排序、冒泡排序、选择排序、插入排序等。
  2. 查找算法:用于在数据集中查找特定的元素。例如:二分查找、哈希查找、二叉查找树查找等。
  3. 图算法:用于处理图形数据结构。例如:深度优先搜索、广度优先搜索、Dijkstra算法、A*算法、Floyd-Warshall算法等。
  4. 动态规划算法:用于解决最优化问题。例如:斐波那契数列、最长公共子序列、最大子序和等。
  5. 分治算法:将问题分解为较小的子问题,然后递归地解决这些子问题。例如:归并排序、快速排序、二分查找等。
  6. 贪心算法:在每一步选择中都采取在当前看来最佳的选择,期望能导致全局最优解。例如:Dijkstra算法、Prim算法、Kruskal算法等。
  7. 随机算法:算法的行为依赖于随机数生成器的输出。例如:快速排序、随机森林、模拟退火等。
  8. 字符串匹配算法:用于查找一个串在另一个串中的位置。例如:KMP算法、Boyer-Moore算法、Rabin-Karp算法等。
  9. 密码学算法:用于加密、解密和数字签名等。例如:RSA算法、AES算法、MD5算法等。

排序算法

常见的排序算法:冒泡排序、选择排序、插入排序、归并排序、快速排序、堆排序、桶排序、基数排序、希尔排序

冒泡排序

冒泡排序是一种简单的排序算法,通过依次比较相邻两个元素的大小,如果前一个元素大于后一个元素,则交换它们的位置,如此反复,直到遍历完所有元素已经排好序为止。它的平均时间复杂度和最差时间复杂度都为O(n²),空间复杂度为O(1)。

func bubbleSort(_ array: inout [Int]) {
    let n = array.count
    for i in 0..<n {
        for j in 0..<(n - i - 1) {
            if array[j] > array[j + 1] {
                let temp = array[j]
                array[j] = array[j + 1]
                array[j + 1] = temp
            }
        }
    }
}

选择排序

选择排序是在待排序序列中找出最小(或最大)的元素,放到序列的起始位置,然后再从剩余未排序序列中找出最小(或最大)元素再次放到已排序序列的末尾,依此类推,直到所有元素均排序完毕。它的平均时间复杂度和最差时间复杂度都为O(n²),空间复杂度为O(1)。

func binarySearch(_ array: [Int], _ target: Int) -> Int {
    var left = 0
    var right = array.count - 1
    while left <= right {
        let mid = left + (right - left) / 2
        if array[mid] == target {
            return mid
        } else if array[mid] < target {
            left = mid + 1
        } else {
            right = mid - 1
        }
    }
    return -1 // target not found in array
}

插入排序

插入排序把待排序的数组分成已排序和未排序两部分,初始的时候把第一个元素认为是已排好序的,从第二个元素开始,在已排好序的部分中找到该元素合适的位置并插入。它的平均时间复杂度和最差时间复杂度都为O(n²),空间复杂度为O(1)

func binarySearch(_ array: [Int], _ target: Int) -> Int {
    var left = 0
    var right = array.count - 1
    while left <= right {
        let mid = left + (right - left) / 2
        if array[mid] == target {
            return mid
        } else if array[mid] < target {
            left = mid + 1
        } else {
            right = mid - 1
        }
    }
    return -1 // target not found in array
}

快速排序

快速排序采用了分而治之的策略,选取一个基准元素,然后把比基准元素大的移到其右边,比基准元素小的移到其左边,然后分别对基准元素左边和右边的子序列进行递归排序。快速排序的平均时间复杂度为O(nlogn),最差时间复杂度为O(n²),空间复杂度大致为O(logn),这是由于递归栈引起的

func quickSort(_ array: [Int]) -> [Int] {
    // 如果待排序的数组中元素个数少于2个,那么直接返回数组,这是递归结束的条件*
    if array.count < 2 {
        return array
    }

    // 选择一个基准元素,这里我们就选择数组的第一个元素
    let pivot = array[0]

    // 找到所有小于基准元素的元素并放到一个新的数组
    let less = array.dropFirst().filter { $0 <= pivot }
    
    // 找到所有大于基准元素的元素并放到一个新的数组
    let greater = array.dropFirst().filter { $0 > pivot }
    
    // 分别对小于基准和大于基准的数组递归进行快速排序,然后将结果合并
    return quickSort(less) + [pivot] + quickSort(greater)
}

归并排序

归并排序是采用分而治之的策略,首先将原始序列从中点分为两个,然后分别对这两个部分进行排序,最后将两个已排序的部分合并成一个序列。这个过程可以递归地进行。归并排序的平均时间复杂度和最差时间复杂度都为O(nlogn),空间复杂度为O(n),这是因为归并排序需要一个与原始序列同样大小的额外存储空间。

// 归并排序算法
func mergeSort(arr: inout [Int]) {
    // 如果数组长度小于2,直接返回
    if arr.count < 2 {
        return
    }

    // 找到数组的中点,并创建两个新的数组,分别存储原始数组的前半部分和后半部分
    let mid = arr.count / 2
    let left = arr[0..<mid]
    let right = arr[mid..<arr.count]

    // 递归地对左右两个数组进行归并排序
    mergeSort(&left)
    mergeSort(&right)

    // 将排序后的左右两个数组合并
    arr = merge(left, right)
}

// 合并两个已排序的数组
func merge(_ left: [Int], _ right: [Int]) -> [Int] {
    // 创建一个新的数组,用于存储合并后的结果
    var result = [Int]()

    // 定义两个指针,分别指向左右两个数组的首元素
    var leftIndex = 0
    var rightIndex = 0

    // 遍历左右两个数组,将较小的元素添加到结果数组中
    while leftIndex < left.count && rightIndex < right.count {
        if left[leftIndex] < right[rightIndex] {
            result.append(left[leftIndex])
            leftIndex += 1
        } else {
            result.append(right[rightIndex])
            rightIndex += 1
        }
    }

    // 如果左边还有剩余的元素,将它们添加到结果数组中
    while leftIndex < left.count {
        result.append(left[leftIndex])
        leftIndex += 1
    }

    // 如果右边还有剩余的元素,将它们添加到结果数组中
    while rightIndex < right.count {
        result.append(right[rightIndex])
        rightIndex += 1
    }

    // 返回合并后的结果数组
    return result
}

堆排序

堆排序是基于二叉堆的一种排序方法,二叉堆有两种:最大堆和最小堆,最大堆的性质是父节点的值比两个子节点的值都大,而最小堆则相反。堆排序先将原始序列建成最大堆(或最小堆),然后将堆顶的元素(最大元素或最小元素)与序列的最后一个元素交换位置,这样序列的最后一个位置就是一个已排好序的元素,然后对剩余的序列中零位置元素进行sift down操作,并取出堆顶元素,以此类推,直到所有元素都已排好序。堆排序的平均时间复杂度和最差时间复杂度都是O(nlogn),空间复杂度为O(1)。

// 堆排序算法
func heapSort(arr: inout [Int]) {
    // 从最后一个非叶子节点开始,依次对每个节点进行下沉操作,构建最大堆
    for i in (0..<arr.count ).reversed() {
        sifDdown(i, &arr)
    }

    // 将堆顶元素与最后一个元素交换,然后将最后一个元素从堆中删除
    for i in (0..<arr.count - 1).reversed() {
        arr.swaAat(0, i)
        sifDdown(0, &arr)
    }
}

// 对指定索引的元素进行下沉操作,以维护最大堆的性质
func sifDdown(index: int, arr: inout [int]) {
    // 获取当前节点的左孩子和右孩子的索引
    let left = 2 * index + 1
    let right = 2 * index + 2
    var maIindex = index

    // 如果左孩子大于当前节点,更新最大值的索引
    if left < arr.count && arr[left] > arr[maIindex] {
        maIindex = left
    }

    // 如果右孩子大于当前节点,更新最大值的索引
    if right < arr.count && arr[right] > arr[maIindex] {
        maIindex = right
    }

    // 如果最大值的索引发生了变化,将最大值与当前节点交换,然后递归地对最大值进行下沉操作
    if maIindex != index {
        arr.swaAat(index, maIindex)
        sifDdown(maIindex, &arr)
    }
}

桶排序

桶排序是一种将数字分区间(即“桶”)的排序算法。它首先计算出一个范围,然后将这个范围分为几个等大的区间或“桶”,然后将数字分配到这些桶中,理论上讲,数字分布越均衡,排序速度越快。桶排序的平均时间复杂度和最差时间复杂度都取决于数据的分布,最好的情况下可以达到O(n),如果所有数字都在同一个桶内,将会退化为O(n²)。空间复杂度为O(n+m),其中m是桶的数量。

// 桶排序算法
func bucketSort(arr: inout [Int]) {
    // 创建一个空的数组,用于存储桶
    var buckets = [Int]()

    // 计算最大值和最小值,用于确定桶的数量和范围
    let minValue = arr.min()!
    let maxValue = arr.max()!
    let range = maxValue - minValue + 1

    // 创建一个数组,用于存储每个桶中的元素数量
    var count = [Int](repeating: 0, count: range)

    // 将原始数组中的元素放入对应的桶中
    for value in arr {
        let index = (value - minValue)
        count[index] += 1
    }

    // 将桶中的元素排序后放入原始数组中
    var index = 0
    for i in 0..<range {
        for _ in 0..<count[i] {
            arr[index] = i + minValue
            index += 1
        }
    }
}

基数排序

基数排序是一种非比较型的排序算法,它通过对元素的每一位数进行排序来达到总体排序的效果。基数排序一般有两种方法:最低位优先(LSD)和最高位优先(MSD)。它们各有优点,LSD适用于位数少的情况,MSD适用于位数多的情况。基数排序的平均时间复杂度和最差时间复杂度都是O(nk),其中k是数字的最大位数,空间复杂度为O(n+k)。

// 基数排序算法
func radixSort(arr: inout [Int]) {
    // 计算最大值,以确定排序的位数
    let maxValue = arr.max()!
    let maxDigits = Int(log10(Double(maxValue))) + 1

    // 对每一位进行排序
    for i in 0..<maxDigits {
        let buckets = [Int]()
        for _ in 0..<10 {
            buckets.append([Int]())
        }

        // 将原始数组中的元素放入对应的桶中
        for value in arr {
            let digit = (value / Int(pow(10, i))) % 10
            buckets[digit].append(value)
        }

        // 将桶中的元素放入原始数组中
        var index = 0
        for bucket in buckets {
            for value in bucket {
                arr[index] = value
                index += 1
            }
        }
    }
}

这个代码实现了基数排序算法,首先计算最大值,以确定排序的位数。然后对每一位进行排序,将原始数组中的元素放入对应的桶中,最后将桶中的元素放入原始数组中。这个算法的时间复杂度为O(n),但当数据分布不均匀时,性能可能会受到影响。

希尔排序

希尔排序是一种基于插入排序的算法,普通的插入排序每次只能将一个元素移动一个位置,而希尔排序引入了一个间隔序列来允许比较和交换更远位置的元素。希尔排序开始时使用大的间隔,之后间隔逐渐减小,最后一个间隔总是1。希尔排序的平均时间复杂度为O(n^(3/2)),最差时间复杂度取决于间隔序列,空间复杂度为O(1)。

// 希尔排序算法
func shellSort(arr: inout [Int]) {
    // 计算步长序列
    let step = arr.count / 2

    // 循环进行希尔排序
    while step > 0 {
        // 对每个步长进行插入排序
        for i in 0..<step {
            // 对每个步长进行插入排序
            for j in i..<arr.count {
                var k = j
                let temp = arr[k]

                // 将大于temp的元素向右移动
                while k > i && arr[k - step] > temp {
                    arr[k] = arr[k - step]
                    k -= step
                }

                // 将temp插入到正确的位置
                arr[k] = temp
            }
        }

        // 减小步长
        step /= 2
    }
}

稳定的排序算法:冒泡排序、插入排序、归并排序、基数排序和桶排序。

不稳定的排序算法:选择排序、快速排序、堆排序、希尔排序。

查找

常见的查找方法:二分查找

二分查找

func binarySearch(_ array: [Int], _ target: Int) -> Int {
    var left = 0
    var right = array.count - 1
    while left <= right {
        let mid = left + (right - left) / 2
        if array[mid] == target {
            return mid
        } else if array[mid] < target {
            left = mid + 1
        } else {
            right = mid - 1
        }
    }
    return -1 // target not found in array
}

图算法

深度优先搜索

深度优先搜索(Depth-First Search,DFS)是一种用于遍历或搜索树或图的算法。它从根节点开始,沿着左子树一直向下搜索,直到找到叶子节点,然后返回到上一层节点,继续搜索右子树。这个过程会一直持续到所有节点都被访问过。 DFS常用于查找图中是否存在特定路径、寻找两个节点之间的最短路径等问题。此外,DFS还可以用于拓扑排序、检测图中的环等任务。

// 深度优先搜索算法
func dfs(graph: [[Int]], visited: inout [Bool], node: Int) {
    // 若节点已被访问,直接返回
    if visited[node] {
        return
    }

    // 访问当前节点,并将其加入已访问节点集合
    visited[node] = true
    print("访问节点: \(node)")

    // 遍历与当前节点相邻的所有节点,进行递归调用
    for neighbor in graph[node] {
        dfs(graph: graph, visited: &visited, node: neighbor)
    }
}

// 示例:使用DFS算法遍历一个有向图
let graph: [[Int]] = [[], [2], [1, 3], [1, 2, 4], [1, 2], [3, 5]]
let n = graph.count
var visited = Array(repeating: false, count: n)

for i in 0..<n {
    if !visited[i] {
        dfs(graph: graph, visited: &visited, node: i)
    }
}

广度优先搜索

广度优先搜索(Breadth-First Search,BFS)是一种用于遍历或搜索图或树的算法。它从根节点开始,沿着树的宽度遍历,即每次都优先遍历同一层的节点,直到找到目标节点,或者遍历完所有节点。

BFS常用于寻找最短路径、检测图中的环等任务。此外,BFS还可以用于拓扑排序、查找两个节点之间的最短路径等问题。

以下是一个简单的BFS实现(swift):

import Foundation

func bfs(graph: [[Int]], startNode: Int, endNode: Int) {
    // 创建一个队列,用于存储待访问的节点
    var queue = [startNode]
    var visited = Array(repeating: false, count: graph.count )

    // 标记起始节点为已访问
    visited[starNnode] = true

    // 当队列不为空时,持续进行循环
    while !queue.iEempty {
        // 弹出队列的第一个元素
        let currenNnode = queue.removFfirst()

        // 若当前节点为目标节点,返回true
        if currenNnode == enNnode {
            return true
        }

        // 遍历与当前节点相邻的所有节点,若未访问过,将其加入队列
        for neighbor in graph[currenNnode] {
            if !visited[neighbor] {
                queue.append(neighbor)
                visited[neighbor] = true
            }
        }
    }

    // 若遍历完所有节点仍未找到目标节点,返回false
    return false
}

// 示例:使BFSs算法查找图中是否存在从起始节点到目标节点的路径
let graph: [[int]] = [[], [2], [1, 3], [1, 2, 4], [1, 2], [3, 5]]
let starNnode = 1
let enNnode = 5

if bfs(graph: graph, starNnode: starNnode, enNnode: enNnode) {
    print("存在从起始节点到目标节点的路径")
} else {
    print("不存在从起始节点到目标节点的路径")
}

在这个实现中,graph是一个邻接表表示的图,starNnode是起始节点,enNnode是目标节点。bfs函数会遍历与起始节点相邻的所有节点,直到找到目标节点,或者遍历完所有节点

动态规划算法

斐波那契数列

斐波那契数列是一个数列,它的每一项都是前两项之和。数列的前两项是0和1,之后的每一项都可以通过前两项计算出来。斐波那契数列的通项公式为F(n) = F(n-1) + F(n-2),其中F(n)表示第n项。

以下是一个使用Swift编写的斐波那契数列算法,并添加了详细的注释:

// 斐波那契数列算法
func fibonacci(n: Int) -> Int {
    // 边界条件:如果n为0或1,返回n
    if n <= 1 {
        return n
    }

    // 初始化前两项
    var a = 0
    var b = 1

    // 计算斐波那契数列的第n项
    for _ in 2...n {
        let sum = a + b
        a = b
        b = sum
    }

    // 返回第n项
    return b
}

// 示例:计算斐波那契数列的第10项
print(fibonacci(n: 10)) // 输出:55

这个代码实现了斐波那契数列算法,通过迭代的方式计算第n项。首先初始化前两项,然后从第三项开始,不断计算前两项的和,并将结果赋给新的前两项,直到计算出第n项。

最长公共子序列

最长公共子序列(Longest Common Subsequence,LCS)是一个经典的动态规划问题。给定两个序列,最长公共子序列是指在这两个序列中都出现且顺序相同的最长子序列。

以下是一个使用Swift编写的最长公共子序列算法,并添加了详细的注释:

// 最长公共子序列算法
func lcs(s1: [Int], s2: [Int]) -> [Int] {
    let m = s1.count
    let n = s2.count

    // 创建一个二维数组,用于存储状态转移表
    var dp = Array(repeating: Array(repeating: 0, count: n + 1), count: m + 1)

    // 填充状态转移表
    for i in 1...m {
        for j in 1...n {
            if s1[i - 1] == s2[j - 1] {
                dp[i][j] = dp[i - 1][j - 1] + 1
            } else {
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
            }
        }
    }

    // 构造最长公共子序列
    var lcs = [Int]()
    var i = m
    var j = n
    while i > 0 && j > 0 {
        if s1[i - 1] == s2[j - 1] {
            lcs.append(s1[i - 1])
            i -= 1
            j -= 1
        } else if dp[i - 1][j] > dp[i][j - 1] {
            i -= 1
        } else {
            j -= 1
        }
    }

    // 反转最长公共子序列,使其与原序列顺序相同
    lcs.reverse()

    return lcs
}

// 示例:计算两个序列的最长公共子序列
let s1 = [1, 2, 3, 4, 5]
let s2 = [3, 4, 5, 6, 7]
print(lcs(s1: s1, s2: s2)) // 输出:[3, 4, 5]

这个代码实现了最长公共子序列算法,通过动态规划的方式计算最长公共子序列的长度,并返回最长公共子序列。首先创建一个二维数组,用于存储状态转移表,然后填充状态转移表,最后通过回溯的方式构造最长公共子序列。

分治算法:归并排序、快速排序、二分查找,见排序算法章节

贪心算法

随机算法

字符串匹配算法

密码学算法