【深入探究系列(6)】算法设计:高效算法的实现与优化
算法设计:高效算法的实现与优化
- 前言:唠唠算法这事儿
- 一、排序算法:从笨办法到聪明招
-
- 1.1 笨办法有多慢?
- 1.2 分而治之的归并排序
- 1.3 归并排序的步骤图
- 1.4 各种排序算法大比拼
- 二、搜索算法:找东西的快慢门道
-
- 2.1 逐个找 vs 二分法
- 2.2 哈希表:一步到位的魔法
- 2.3 搜索速度对比
-
- 表格说明:
- 三、动态规划:用空间换时间的智慧
-
- 3.1 爬楼梯的学问
- 3.2 爬楼梯的思路图
- 四、语言自带的sort()方法:开箱即用的排序利器
-
- 4.1 Python的sort()方法:简单又强大
- 4.2 sort()的底层算法:Timsort
- 4.3 性能对比:手写算法vs内置方法
- 4.4 性能测试结果
-
- 4.4.1 排序算法性能对比总表
- 4.4.2 性能差异可视化(普通表格形式)
-
- 小数据量(1000条)
- 大数据量(100000条)
- 4.4.3 关键结论
- 4.4.4 应用场景建议
- 4.4.5 延伸思考
- 4.5 其他语言的排序方法
- 4.6 啥时候该用内置方法,啥时候该自己写?
- 五、算法优化的小窍门
- 总结
前言:唠唠算法这事儿
说白了,算法就是解决问题的步骤。但同样的问题,用不同的算法来处理,效率能差老远了。比如处理一万条数据,笨办法可能要算半天,好办法可能眨眼就完事。今天就用大白话聊聊排序、搜索这些常用算法,还有怎么把它们变得更给力。
一、排序算法:从笨办法到聪明招
排序这事儿太常见了,比如给成绩排个名、给订单按时间排顺序。但你知道吗?排序的门道可多了。
1.1 笨办法有多慢?
先说说最简单的选择排序。它的思路特直白:从一堆数里挑出最小的,放最前面;再从剩下的里挑最小的,放第二个……就这么挨个排。
def selection_sort(arr): n = len(arr) for i in range(n): min_idx = i # 先假设当前位置的数最小 # 从剩下的数里找真正的最小值 for j in range(i+1, n): if arr[j] < arr[min_idx]: min_idx = j # 把找到的最小值换过来 arr[i], arr[min_idx] = arr[min_idx], arr[i] return arr
但这招有个大问题:如果有1000个数,可能要做几十万次比较,数据一多就卡得不行。
1.2 分而治之的归并排序
归并排序就聪明多了,它先把数组拆成小块,各自排好序后再合并起来。比如要排[38,27,43,3,9,82,10],步骤大概是这样:
def merge_sort(arr): if len(arr) <= 1: return arr # 就一个数,不用排了 # 拆成两半 mid = len(arr) // 2 left = merge_sort(arr[:mid]) right = merge_sort(arr[mid:]) # 把两半合并成有序数组 return merge(left, right)def merge(left, right): result = [] i = j = 0 # 两个指针分别扫过两个数组,谁小就先放进结果 while i < len(left) and j < len(right): if left[i] <= right[j]: result.append(left[i]) i += 1 else: result.append(right[j]) j += 1 # 剩下的数直接补到后面 result.extend(left[i:]) result.extend(right[j:]) return result
1.3 归并排序的步骤图
#mermaid-svg-l1lfIFBbL2zfM5De {font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-l1lfIFBbL2zfM5De .error-icon{fill:#552222;}#mermaid-svg-l1lfIFBbL2zfM5De .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-l1lfIFBbL2zfM5De .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-l1lfIFBbL2zfM5De .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-l1lfIFBbL2zfM5De .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-l1lfIFBbL2zfM5De .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-l1lfIFBbL2zfM5De .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-l1lfIFBbL2zfM5De .marker{fill:#333333;stroke:#333333;}#mermaid-svg-l1lfIFBbL2zfM5De .marker.cross{stroke:#333333;}#mermaid-svg-l1lfIFBbL2zfM5De svg{font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-l1lfIFBbL2zfM5De .label{font-family:\"trebuchet ms\",verdana,arial,sans-serif;color:#333;}#mermaid-svg-l1lfIFBbL2zfM5De .cluster-label text{fill:#333;}#mermaid-svg-l1lfIFBbL2zfM5De .cluster-label span{color:#333;}#mermaid-svg-l1lfIFBbL2zfM5De .label text,#mermaid-svg-l1lfIFBbL2zfM5De span{fill:#333;color:#333;}#mermaid-svg-l1lfIFBbL2zfM5De .node rect,#mermaid-svg-l1lfIFBbL2zfM5De .node circle,#mermaid-svg-l1lfIFBbL2zfM5De .node ellipse,#mermaid-svg-l1lfIFBbL2zfM5De .node polygon,#mermaid-svg-l1lfIFBbL2zfM5De .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-l1lfIFBbL2zfM5De .node .label{text-align:center;}#mermaid-svg-l1lfIFBbL2zfM5De .node.clickable{cursor:pointer;}#mermaid-svg-l1lfIFBbL2zfM5De .arrowheadPath{fill:#333333;}#mermaid-svg-l1lfIFBbL2zfM5De .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-l1lfIFBbL2zfM5De .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-l1lfIFBbL2zfM5De .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-l1lfIFBbL2zfM5De .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-l1lfIFBbL2zfM5De .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-l1lfIFBbL2zfM5De .cluster text{fill:#333;}#mermaid-svg-l1lfIFBbL2zfM5De .cluster span{color:#333;}#mermaid-svg-l1lfIFBbL2zfM5De div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-l1lfIFBbL2zfM5De :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;} 原始数组: [ 38,27,43,3,9,82,10 ] 拆成两半: [ 38,27,43 ] 和 [ 3,9,82,10 ] 再拆: [ 38 ][ 27,43 ] 和 [ 3,9 ] [ 82,10 ] 拆到最小: [ 38 ] [ 27 ] [ 43 ] 和 [ 3 ] [ 9 ] [ 82] [ 10 ] 开始合并: [ 27,38 ] [ 43 ] 和 [ 3,9 ] [ 10,82 ] 继续合并: [ 27,38,43 ] 和 [ 3,9,10,82 ] 最终结果: [ 3,9,10,27,38,43,82 ]
1.4 各种排序算法大比拼
*注:稳定指的是相同大小的数排序后顺序不变,比如排成绩时,同分的人保持原来的先后。
二、搜索算法:找东西的快慢门道
搜索就是在一堆数据里找目标,比如在通讯录里找某人的电话。方法不同,速度天差地别。
2.1 逐个找 vs 二分法
线性搜索就是从头到尾挨个看,像在没索引的字典里找字,运气不好得翻完整本。而二分搜索就像查字典:先翻中间,看目标在左边还是右边,不断缩小范围。
def binary_search(arr, target): low, high = 0, len(arr) - 1 while low <= high: mid = low + (high - low) // 2 # 算中间位置 if arr[mid] == target: return mid # 找到了,返回位置 elif arr[mid] < target: low = mid + 1 # 目标在右边 else: high = mid - 1 # 目标在左边 return -1 # 没找到
比如在1000个数里找一个数,二分法最多只要10次,线性搜索平均要500次,差距太大了!
2.2 哈希表:一步到位的魔法
哈希表更狠,直接给每个数分配一个\"专属座位\",找的时候一步到位。就像用身份证号查人,直接定位,不用翻来翻去。
def hash_search(arr, target): # 先建个\"字典\",记录每个数的位置 hash_table = {num: index for index, num in enumerate(arr)} return hash_table.get(target, -1) # 有就返回位置,没有返回-1
2.3 搜索速度对比
以下是将柱状图转换为普通表格形式的图表,清晰展示不同搜索方法的性能对比:
表格说明:
- 表格通过「平均比较次数」直观反映三种搜索方法的效率差异,数值越小说明搜索速度越快。
- 「效率说明」补充了每种方法的核心特点,帮助理解性能差异的原因。
- 与柱状图相比,表格更适合精确查看具体数值,且在不支持图表渲染的场景下也能清晰展示信息。
三、动态规划:用空间换时间的智慧
有些问题拆开来看,里面有很多重复的小问题。动态规划就是把这些小问题的答案记下来,避免重复计算。
3.1 爬楼梯的学问
比如爬楼梯:每次能爬1级或2级,n级楼梯有几种走法?
如果用递归,算n=5就要算n=4和n=3,算n=4又要算n=3和n=2……好多重复计算,慢得很。
# 优化后的动态规划解法def climb_stairs_optimized(n): if n <= 2: return n # 1级1种,2级2种 a, b = 1, 2 # 记录n-2和n-1的结果 for _ in range(3, n + 1): a, b = b, a + b # 每次更新,不用存整个表 return b
就像算5级楼梯,先算3级(1+2=3种),再算4级(2+3=5种),再算5级(3+5=8种),一步步推,快多了。
3.2 爬楼梯的思路图
#mermaid-svg-09j1yq1v3Z2feSQX {font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-09j1yq1v3Z2feSQX .error-icon{fill:#552222;}#mermaid-svg-09j1yq1v3Z2feSQX .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-09j1yq1v3Z2feSQX .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-09j1yq1v3Z2feSQX .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-09j1yq1v3Z2feSQX .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-09j1yq1v3Z2feSQX .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-09j1yq1v3Z2feSQX .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-09j1yq1v3Z2feSQX .marker{fill:#333333;stroke:#333333;}#mermaid-svg-09j1yq1v3Z2feSQX .marker.cross{stroke:#333333;}#mermaid-svg-09j1yq1v3Z2feSQX svg{font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-09j1yq1v3Z2feSQX .label{font-family:\"trebuchet ms\",verdana,arial,sans-serif;color:#333;}#mermaid-svg-09j1yq1v3Z2feSQX .cluster-label text{fill:#333;}#mermaid-svg-09j1yq1v3Z2feSQX .cluster-label span{color:#333;}#mermaid-svg-09j1yq1v3Z2feSQX .label text,#mermaid-svg-09j1yq1v3Z2feSQX span{fill:#333;color:#333;}#mermaid-svg-09j1yq1v3Z2feSQX .node rect,#mermaid-svg-09j1yq1v3Z2feSQX .node circle,#mermaid-svg-09j1yq1v3Z2feSQX .node ellipse,#mermaid-svg-09j1yq1v3Z2feSQX .node polygon,#mermaid-svg-09j1yq1v3Z2feSQX .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-09j1yq1v3Z2feSQX .node .label{text-align:center;}#mermaid-svg-09j1yq1v3Z2feSQX .node.clickable{cursor:pointer;}#mermaid-svg-09j1yq1v3Z2feSQX .arrowheadPath{fill:#333333;}#mermaid-svg-09j1yq1v3Z2feSQX .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-09j1yq1v3Z2feSQX .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-09j1yq1v3Z2feSQX .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-09j1yq1v3Z2feSQX .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-09j1yq1v3Z2feSQX .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-09j1yq1v3Z2feSQX .cluster text{fill:#333;}#mermaid-svg-09j1yq1v3Z2feSQX .cluster span{color:#333;}#mermaid-svg-09j1yq1v3Z2feSQX div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-09j1yq1v3Z2feSQX :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;} 1级: 1种 2级: 2种 3级: 3种 1+2 4级: 5种 2+3 5级: 8种 3+5
四、语言自带的sort()方法:开箱即用的排序利器
4.1 Python的sort()方法:简单又强大
Python的列表自带sort()
方法,用法超简单:
# 直接修改原列表numbers = [3, 1, 4, 1, 5, 9, 2, 6, 5]numbers.sort()print(numbers) # 输出: [1, 1, 2, 3, 4, 5, 5, 6, 9]# 倒序排序numbers.sort(reverse=True)print(numbers) # 输出: [9, 6, 5, 5, 4, 3, 2, 1, 1]# 自定义排序规则words = [\"apple\", \"banana\", \"cherry\", \"date\"]words.sort(key=len) # 按字符串长度排序print(words) # 输出: [\'date\', \'apple\', \'banana\', \'cherry\']
4.2 sort()的底层算法:Timsort
Python的sort()
用的是Timsort算法,它是归并排序和插入排序的混合体。Timsort的厉害之处在于:
- 自适应:能根据数据的初始状态自动调整策略。比如数据已经部分有序,它会利用这一点,速度比普通归并排序还快。
- 稳定:相同元素的相对顺序不会变,这在排序对象列表时特别有用。
- 时间复杂度稳定:最坏情况下也是O(n log n),不会像快排那样偶尔\"翻车\"。
4.3 性能对比:手写算法vs内置方法
我们来测测不同规模数据下,选择排序和Python内置排序的速度差异:
import timeimport random# 生成随机数据small_data = [random.randint(1, 1000) for _ in range(1000)]large_data = [random.randint(1, 100000) for _ in range(100000)]# 测试选择排序def test_selection_sort(data): start = time.time() sorted_data = selection_sort(data.copy()) end = time.time() return end - start# 测试Python内置排序def test_python_sort(data): start = time.time() sorted_data = sorted(data.copy()) # sorted()返回新列表,不修改原列表 end = time.time() return end - start# 运行测试print(f\"小数据量(1000): 选择排序耗时 {test_selection_sort(small_data):.6f}秒\")print(f\"小数据量(1000): Python内置排序耗时 {test_python_sort(small_data):.6f}秒\")print(f\"大数据量(100000): 选择排序耗时 {test_selection_sort(large_data):.6f}秒\")print(f\"大数据量(100000): Python内置排序耗时 {test_python_sort(large_data):.6f}秒\")
4.4 性能测试结果
以下是整合所有排序算法数据的完整性能对比分析,采用表格与图表结合的方式呈现:
4.4.1 排序算法性能对比总表
4.4.2 性能差异可视化(普通表格形式)
小数据量(1000条)
大数据量(100000条)
4.4.3 关键结论
-
内置排序(Timsort)的统治力
- 在小数据量下比冒泡排序快 600倍,大数据量下快 35000倍。
- 处理10万条数据仅需 0.026秒,相当于冒泡排序处理100条数据的耗时。
-
O(n²) 算法的“死亡区”
- 当数据量从1000增至10万(×100):
- 冒泡排序耗时增长 10790倍(远超理论O(n²)的10000倍)。
- 选择排序耗时增长 10963倍。
- 而归并排序和内置排序仅增长 185倍(符合O(n log n)预期)。
- 当数据量从1000增至10万(×100):
-
归并排序的“性价比”
- 实现简单且时间复杂度稳定,在无内置排序可用时是最优选择(如面试手写)。
- 大数据量下比选择排序快 846倍,但仍比内置排序慢 17倍。
4.4.4 应用场景建议
4.4.5 延伸思考
如果将数据量扩大到 1000万条:
- 内置排序预计耗时约 0.3秒(线性增长)。
- 冒泡排序预计耗时约 251小时(按实测增速外推)。
这充分体现了算法选择对系统性能的决定性影响。
4.5 其他语言的排序方法
- Java:
Arrays.sort()
对基本类型用双轴快排,对对象用Timsort。 - JavaScript:
Array.prototype.sort()
默认按字符串排序,需要传入比较函数。 - C++:
std::sort()
结合了快排、堆排和插入排序,叫Introsort。
4.6 啥时候该用内置方法,啥时候该自己写?
- 优先用内置方法:简单场景,追求速度和稳定性。
- 自己写算法:
- 需要特定优化(比如对特定数据分布优化)。
- 内置方法不满足需求(比如需要稳定排序,而语言内置方法不稳定)。
- 学习算法原理(比如面试时需要手写)。
五、算法优化的小窍门
-
看数据下菜碟:小规模数据用简单算法就行(比如10个数排序,冒泡可能比归并还快);近乎有序的数据,插入排序比快速排序好用。
-
空间换时间:像动态规划,多记点中间结果,能省很多重复计算;哈希表也是用内存换速度的典型。
-
组合拳更厉害:比如Python里的Timsort,就是把归并和插入排序结合起来,取长补短,实际用起来特别快。
总结
算法这东西,不是越复杂越好,关键是找对适合场景的方法。理解了这些基本思路,遇到问题时就能判断:哪种方法快?能不能再优化一下?平时多琢磨琢磨,写出来的程序就能又快又省劲儿。