news 2026/9/30 3:29:22

排序算法综合分析:从实验设计到避坑全指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
排序算法综合分析:从实验设计到避坑全指南

简介:数据结构课程设计中的“排序算法综合分析”文档,围绕直接插入排序、希尔排序、快速排序、冒泡排序、堆排序和归并法排序六种经典算法展开,适合计算机专业学生在完成数据结构课程设计或复习排序章节时参考。文档基于自定义的SqList排序表结构,包含整型关键字数组与当前元素个数,实现了从初始化、排序前/后打印到各类排序函数及递归划分等完整代码,支持手动输入或随机生成待排序数据,并在各排序过程中统计比较次数和移动次数,便于直观对比算法效率。包体为单一doc文件,容量约15KB,内容紧凑,无需额外安装环境,可用Word直接打开阅读或复制代码调试。目前已有740人学习下载,适合需要快速获取可运行排序算法综合示例、课程设计报告思路及C++实现代码的读者。

1. 排序算法综合分析在分析什么:先绕过“贴代码”陷阱,建立可重复的比较框架

作为数据结构课程设计里最常见的主题之一,排序算法综合分析往往被当成“抄代码大赛”:把八大排序算法总结一遍,书上代码搬进文档,跑一组随机数据,贴一张时间表就算完事。可到了答辩环节,老师问一句“为什么你的快排在正序数据上慢成了冒泡排序”,或者“你的比较次数怎么和理论值差了十倍”,很多人就答不上来。真正的综合分析,要回答的是“在什么输入、什么规模、什么评价指标下,谁更快、为什么快、数据是否可信”。这篇笔记就从算法实现、测试数据生成、计数埋点、常见翻车点一路展开,帮你把一份平平无奇的数据结构实验报告,做成能经得起追问、能复现、能给他人提供参考的完整实验。内容不依赖某份现成文档,而是从零把路走一遍。

2. 把排序算法写成能对比的版本:统一的接口、计数与数据生成

排序综合对比的最大误区,是拿裸 int 数组直接测时间。时间只能告诉你“谁快”,但回答不了“为什么快”。而且 int 数组无法验证稳定性,也无法统计比较次数和移动次数。所以我一般会先建一个统一的数据结构,把所有算法接到同一个测试骨架上。

2.1 用带“原始序号”的元素结构打通所有排序接口

要同时统计时间、比较次数、移动次数、稳定性,就不能只对 int 排序。我定义一个Item结构体,key是排序关键字,seq是元素进入数组时的原始序号,专门用于稳定性校验。

typedef struct { int key; /* 排序关键字 */ int seq; /* 原始序号,用于稳定性校验 */ } Item;

所有排序函数操作的都是Item *a,这样稳定性测试就有了依据:排序前后,如果两个相同 key 的元素的seq保持递增,说明算法是稳定的。

计数埋点也是统一做的。用两个全局变量cmp_cnt、move_cnt累计,然后用宏包装比较和交换,避免在算法里到处插计数代码:

long long cmp_cnt, move_cnt; #define LESS(a, b) (++cmp_cnt, (a).key < (b).key) #define SWAP(a, b) do { Item _t = (a); (a) = (b); (b) = _t; move_cnt += 3; } while (0) #define ASSIGN(a, b) ((a) = (b), ++move_cnt)

这里SWAP记 3 次移动,因为交换相当于三次赋值;ASSIGN记 1 次移动。这样每个算法统计口径一致,横向对比才有意义。注意LESS里那个逗号表达式,先计数再用结果做判断,能保证每次比较都计入,不会漏。

2.2 生成测试数据的三种模式:随机、有序、重复

排序算法的最坏、最好、平均情况差异极大,只用随机数据会掩盖很多问题。我固定用四种模式生成数据:随机、正序、逆序、重复率高。随机反映平均情况,正序/逆序暴露最坏情况,重复数据用来配合稳定性测试。

void gen_data(Item *a, int n, int mode, int seed) { srand(seed); for (int i = 0; i < n; i++) { a[i].seq = i; switch (mode) { case 0: a[i].key = rand() % 1000000; break; /* 随机 */ case 1: a[i].key = i; break; /* 正序 */ case 2: a[i].key = n - i; break; /* 逆序 */ case 3: a[i].key = rand() % (n / 2); break; /* 约50%重复 */ } } }

seed必须固定,否则每次运行数据不同,结论无法复现。正序或逆序数据规模大时,key = i可能溢出 int,建议保留两位数乘法,例如把规模控制在 10^6 以内。重复模式下key取模n/2,相同 key 大量出现,能顺带观察排序算法对相同值的处理方式。

2.3 O(n²) 算法的实现边界:冒泡的提前结束、选择排序的交换次数

冒泡排序给内层循环加一个flag,如果某轮没有发生交换,说明数组已经有序,直接结束。这是最常见的优化,能让正序数据的冒泡从 O(n²) 降到 O(n)。

void bubble_sort(Item *a, int n) { for (int i = 0; i < n - 1; i++) { int flag = 0; for (int j = 0; j < n - i - 1; j++) { if (LESS(a[j + 1], a[j])) { SWAP(a[j], a[j + 1]); flag = 1; } } if (!flag) break; } }

插入排序的特性是“近邻移动快”,它在正序数据下也有 O(n) 的表现。实现时注意把待插入元素先存到临时变量,腾出空位:

void insert_sort(Item *a, int n) { for (int i = 1; i < n; i++) { Item tmp = a[i]; move_cnt++; /* 取出元素算一次移动 */ int j = i - 1; while (j >= 0 && LESS(tmp, a[j])) { ASSIGN(a[j + 1], a[j]); /* 后移元素 */ j--; } ASSIGN(a[j + 1], tmp); /* 放入空位 */ } }

选择排序的交换次数是 O(n),每次找最小值的扫描比较次数才是 O(n²)。它最大的问题是不能提前终止,即使数组原本有序也要跑完全部轮次。实现时把找最小值和交换分开,计数会更清晰:

void select_sort(Item *a, int n) { for (int i = 0; i < n - 1; i++) { int min_i = i; for (int j = i + 1; j < n; j++) { if (LESS(a[j], a[min_i])) min_i = j; } if (min_i != i) SWAP(a[i], a[min_i]); } }

为什么把这三兄弟放在一起讲?因为它们的理论复杂度相同,但实际移动次数差异很大。插入排序移动次数与逆序度直接相关,冒泡排序的最佳情况优化后可以“白嫖”,选择排序稳定但交换次数不受输入影响。这些细节就是综合分析里“为什么实测曲线与理论不完全一致”的解释来源。

2.4 快排、归并、堆排的写法与三数取中参数

快速排序是综合分析里的明星,也是最容易写砸的。经典实现把第一个元素当基准,遇到正序或逆序数据会退化成 O(n²),递归深度达到 n,稍微大一点的数据就能让栈溢出。我一般采用三数取中选基准:取左端、中间、右端三个元素的中位数,与末尾交换作为基准。

int pivot(Item *a, int l, int r) { int mid = l + (r - l) / 2; if (LESS(a[mid], a[l])) SWAP(a[l], a[mid]); if (LESS(a[r], a[l])) SWAP(a[l], a[r]); if (LESS(a[r], a[mid])) SWAP(a[mid], a[r]); SWAP(a[mid], a[r]); /* 基准放到末尾,便于单趟划分 */ return a[r].key; } void quick_sort(Item *a, int l, int r) { if (l >= r) return; int k = pivot(a, l, r); int i = l, j = r; while (i < j) { while (i < j && a[i].key <= k) i++; while (i < j && a[j].key >= k) j--; if (i < j) SWAP(a[i], a[j]); } SWAP(a[i], a[r]); quick_sort(a, l, i - 1); quick_sort(a, i + 1, r); }

有一件事要特别提醒:上面代码里的a[i].key <= k和a[j].key >= k也要计入比较次数吗?如果严格按“元素与基准比较”的口径,应该计数。但很多教材不计循环边界判断。所以我在统计时会把这两处也用LESS或==的计数宏包起来,否则实测比较数会少一截,和理论对不上。这个坑在避坑章里还会展开。

归并排序的常见毛病是在递归函数里malloc临时数组,每层递归都分配释放,耗时和内存碎片都让人头疼。正确做法是只分配一次全局临时缓冲区:

Item *g_tmp; /* 全局或外部变量,测试前一次分配 */ void merge_sort(Item *a, int l, int r) { if (l >= r) return; int m = l + (r - l) / 2; merge_sort(a, l, m); merge_sort(a, m + 1, r); int i = l, j = m + 1, k = 0; while (i <= m && j <= r) { if (LESS(a[i], a[j])) ASSIGN(g_tmp[k++], a[i++]); else ASSIGN(g_tmp[k++], a[j++]); } while (i <= m) ASSIGN(g_tmp[k++], a[i++]); while (j <= r) ASSIGN(g_tmp[k++], a[j++]); memcpy(a + l, g_tmp, (r - l + 1) * sizeof(Item)); /* 拷贝不算移动次数 */ }

这里的memcpy不计入移动次数,因为它是内存块整体拷贝,不是算法语义里的“赋值”。如果你要严格统计,也可以把memcpy展开成逐个元素赋值,但那样移动次数的口径会很大,需要统一说明。

堆排序的核心是下滤操作。下滤实际是将当前节点与较大子节点比较、交换,直到堆序恢复:

void sift_down(Item *a, int n, int i) { while (1) { int child = 2 * i + 1; if (child >= n) break; if (child + 1 < n && LESS(a[child], a[child + 1])) child++; if (!LESS(a[i], a[child])) break; SWAP(a[i], a[child]); i = child; } } void heap_sort(Item *a, int n) { for (int i = n / 2 - 1; i >= 0; i--) sift_down(a, n, i); for (int i = n - 1; i > 0; i--) { SWAP(a[0], a[i]); sift_down(a, i, 0); } }

堆排序的比较次数与移动次数都集中在建堆和下滤上,实际表现常比归并略差,但它是“原地排序”的代表,空间复杂度 O(1)。这也是综合分析里必须讲清楚的点:不只看时间,还要讲资源占用。

2.5 基数排序的桶参数与边界选择

基数排序不是基于比较的排序,所以严格说它不适用“比较次数”这个指标。我在综合分析里会把基数排序单独列一栏,比较次数只能填“不适用”,移动次数就是元素进出桶的次数。LSD 基数排序的典型实现:

void radix_sort(Item *a, int n, int d) { int *cnt = calloc(10, sizeof(int)); Item *buf = malloc(n * sizeof(Item)); for (int p = 1; p <= d; p *= 10) { memset(cnt, 0, 10 * sizeof(int)); for (int i = 0; i < n; i++) cnt[a[i].key / p % 10]++; for (int i = 1; i < 10; i++) cnt[i] += cnt[i - 1]; for (int i = n - 1; i >= 0; i--) { int digit = a[i].key / p % 10; buf[--cnt[digit]] = a[i]; } memcpy(a, buf, n * sizeof(Item)); } free(cnt); free(buf); }

参数d是最大值的位数上限。比如 key 最大为 999999,d取 1000000,循环就会依次按个位、十位……处理。基数排序对负数不支持,除非先整体偏移为非负。它最大的价值是在 key 值域有限时,时间复杂度能到 O(n),并且是稳定排序。这是综合分析里用来对照“基于比较的排序下界 O(n log n)”的反例,算是一个很好的讨论点。

到这里,八个算法的 C 语言版本都有了统一接口。下一步就是设计实验流程,让这些代码跑出可信的数据。

3. 排序算法实验设计:从控制变量到对比表格

有了算法实现,接下来的问题是:怎么跑才能说明问题?如果直接用完整数组一次性测试,计时波动、缓存命中、后台进程都可能污染结果。我一般会把实验设计成三层:先控制变量,再确定指标,最后生成可视化图表。

3.1 同一份输入、同一台机器、同一套计数规则

控制变量是综合分析的地基。第一,所有算法必须排序同一份输入。做法是生成一份原始数组,存到内存,每个算法测试前用memcpy恢复到这份原始数组。第二,固定随机种子,保证数据可复现。第三,每个配置重复跑多次,取中位数而不是平均值——中位数能排除偶发性波动。

主循环的框架大致是这样:

int sizes[] = {1000, 5000, 10000, 50000, 100000}; int modes[] = {0, 1, 2, 3}; for (int mode = 0; mode < 4; mode++) { for (int s = 0; s < 5; s++) { int n = sizes[s]; Item *src = malloc(n * sizeof(Item)); Item *work = malloc(n * sizeof(Item)); gen_data(src, n, mode, 2024); for (int alg = 0; alg < 8; alg++) { for (int rep = 0; rep < 5; rep++) { memcpy(work, src, n * sizeof(Item)); reset_cnt(); clock_t st = clock(); run_alg(alg, work, n); clock_t ed = clock(); record_result(alg, mode, n, rep, (double)(ed - st) / CLOCKS_PER_SEC * 1000, cmp_cnt, move_cnt); } } free(src); free(work); } }

这里run_alg是一个函数指针数组,把八个排序函数统一为void (*)(Item*, int)签名(归并排序需要额外传全局g_tmp,所以单独包装一下)。每个 rep 记录下来耗时、比较次数、移动次数,最后用排序取中位数。

有人会问“每次memcpy恢复原始数组会不会太慢?”实测中小规模数据拷贝是纳秒级,相对排序时间可以忽略。而且这样保证了每个算法面对的是同一份数据,比起每次重新gen_data更公平。

3.2 三个评价指标:耗时、比较次数、移动次数,缺一不可

只看耗时,你会被机器环境骗;只看比较次数,你会忽略赋值开销。我个人习惯把三个指标并列输出到同一张表里:

指标统计方式主要用途
耗时clock()或clock_gettime真实机器表现,最直接,但受CPU频率、缓存影响
比较次数LESS宏计数衡量算法决策逻辑,与输入规模强相关
移动次数SWAP/ASSIGN宏计数评价赋值开销,结构体越大越有参考价值

比如在快排实现里,如果基准选择和单趟划分都计入比较,几个基于比较的算法之间就能公平对比。移动次数方面,基数排序的“移动”语义和其他算法不同,单独说明即可。

除了三个指标,稳定性也是排序算法的重要属性。稳定性要通过seq字段校验:

int check_stable(Item *a, int n) { for (int i = 1; i < n; i++) if (a[i].key == a[i - 1].key && a[i].seq < a[i - 1].seq) return 0; return 1; }

如果相同 key 的seq不再递增,就是不稳定的。这个函数放到每个算法排序之后执行,结果输出到表格里,答辩时直接展示。

3.3 用 Python 把结果画成对数坐标折线图

纯表格不直观,排序算法对比曲线是课程设计报告里的加分项。我通常把 C 程序输出为 CSV 文件,再用 Python 画图。CSV 一行一条记录:算法名, 数据模式, 规模, 耗时ms, 比较次数, 移动次数, 是否稳定。

import matplotlib.pyplot as plt import pandas as pd df = pd.read_csv("sort_result.csv") random_df = df[df["mode"] == 0] for alg in random_df["algorithm"].unique(): sub = random_df[random_df["algorithm"] == alg].sort_values("size") plt.loglog(sub["size"], sub["time_ms"], marker="o", label=alg) plt.xlabel("n (log scale)") plt.ylabel("time ms (log scale)") plt.legend(fontsize=8) plt.grid(True, which="both", ls="--", alpha=0.3) plt.savefig("sort_compare.png", dpi=300)

为什么用loglog?因为 O(n²) 和 O(n log n) 在同一条线性坐标轴上,小规模差距不明显,对数坐标能让复杂度“斜率”直接显现:O(n²) 的曲线斜率约等于 2,O(n log n) 的斜率约等于 1.1~1.2。这是答辩时最直观的理论对照。

第三章的重点是把实验做成闭环:固定数据、运行多次、记录全部指标、输出图表。这里最大的教训是“先跑通,再跑快”。不要一上来就跑到 10^6 规模,先用 1000、5000 验证各算法正确性和计数逻辑,再逐步加规模。否则数据错误时,排错成本极高。

4. 排序综合分析里最容易翻车的5个地方:从计时为0到稳定性错判

这一章是我在批改课程设计里最常见的坑。每条按“现象 → 原因 → 解决”给出,几乎每个都有人踩。

4.1 计出来的时间总是0,或者忽大忽小

现象:对几千个随机数排序,clock()前后差值永远是 0;或者偶尔跑出 0.01ms,偶尔又跳成 50ms,结果毫无规律。

原因:clock()的精度依赖系统时钟粒度,Windows 下通常约 1ms 量级。小规模排序在优化编译下可能连 0.1ms 都不到,自然测不到。忽大忽小则可能是后台进程、CPU 频率调度干扰。

解决:把数据规模提到 10^5 以上,保证单次排序至少几十毫秒;或者用高精度计时接口clock_gettime(CLOCK_MONOTONIC)(Linux)或QueryPerformanceCounter(Windows)。更稳健的做法是每个配置重复 5~7 次,排序后去掉最大值最小值再取平均或中位数。

4.2 快排在正序数据上超时,甚至栈溢出

现象:随机数据下快排很猛,一到正序或逆序,耗时比冒泡还离谱,数据规模稍大程序直接崩溃。

原因:经典快排取第一个元素当基准,正序时每次划分只有一个元素被分出去,递归深度变成 n,时间复杂度退化为 O(n²),调用栈也随之爆掉。

解决:采用三数取中选基准,或者随机选一个元素做基准,至少保证在大概率下划分均衡。如果递归深度仍然不可控,可以把递归改为显式栈,但课程设计里三数取中就够用了。排查时可以先打印出递归深度,如果超过n/10就说明基准选取有问题。

4.3 稳定性的测试方法完全错误

现象:拿{3, 1, 3, 2}这种普通数组排序后,有人说“所有排序都稳定”,因为输出里两个 3 看起来没变化。

原因:整数数组里相同键值没有区分标识,排序后是否维持相对顺序根本无法观察。稳定性讨论的是相等元素的相对次序,必须给每个元素一个原始序号,用键值+序号的组合去验证。

解决:用前面说的Item结构体。排序前把相同 key 的seq顺序打乱一些(例如让第一个 3 的 seq 大、第二个 3 的 seq 小),排序后检查相同 key 的seq是否仍然按原序递增。例如check_stable函数返回 0 就是不稳定。

4.4 归并排序的临时数组导致内存分配爆炸

现象:数据规模到 10^6 时,归并排序比其他 O(n log n) 算法慢一大截,程序运行时间曲线异常陡峭。

原因:在merge_sort递归函数内部malloc临时数组,每层递归都做一次堆分配,时间开销和内存碎片会爆炸。

解决:只分配一次全局g_tmp,在测试主循环里一次性malloc,排序结束后free。这样归并排序最终只持有一次额外空间 O(n)。如果数据量太大到内存放不下,那就要讨论外部排序了,但这已经超出课程设计范围。

4.5 统计的比较次数和理论对不上

现象:插入排序在随机数据下的比较次数统计出来是 n²/2,但有人成 n²,有人成 n²/4;快速排序的理论值约 1.44 n log n,但统计结果有时只有一半。

原因:统计口径不统一。有人把循环条件里的i < n也算进比较,有人漏掉了while (j >= 0 && LESS(...))中j >= 0的判断,还有人在快排基准选择时用了宏计数,但单趟划分里的a[i].key <= k没有计数。

解决:明确统计口径——只统计元素关键字之间的比较,循环边界判断不算。为统一实现,把数组中所有需要比较关键字的地方都用LESS或等价的计数宏包裹,最后在报告里写明“本实验的比较次数仅统计元素关键字比较,不包括循环控制判断”。这样所有算法对齐,才能和理论值做对照。

第 4 章的这些坑,本质上是“实验设计不严谨”的具体体现。如果能把这几条全部避开,你的综合分析的代码和数据可信度就已经超过大多数交了作业就跑的同学了。

5. 让排序分析结论经受住答辩:对数坐标走势图、理论对照与选型建议

实验数据跑出来之后,真正的综合分析才刚刚开始。只贴一张时间表是没有说服力的,你需要用数据证明“我理解这些排序算法”。

第一个进阶技巧是验证时间复杂度的斜率。选取n = 2000, 4000, 8000, 16000, 32000这几个翻倍规模,在双对数坐标下分别跑 O(n²) 和 O(n log n) 算法。用一段短 Python 计算相邻两点的斜率:

import numpy as np n = np.array([2000, 4000, 8000, 16000, 32000]) for alg in ["bubble_sort", "quick_sort"]: t = df[(df.algorithm == alg) & (df.mode == 0)].sort_values("size")["time_ms"].values slope = np.polyfit(np.log(n), np.log(t), 1)[0] print(f"{alg} 复杂度斜率: {slope:.2f}")

冒泡排序斜率会接近 2.0,快排接近 1.1~1.2,正好对应 O(n²) 和 O(n log n)。这个结果写进报告,比任何口头分析都有力。

第二个技巧是把实测比较次数和理论公式对照。归并排序的平均比较次数约n log n - 1.1 n,插入排序约n²/4。你不需要精确吻合,但误差应该在 10% 以内。如果差出一个数量级,回头检查统计口径,多半是某个循环漏掉了计数宏。

第三个技巧是给出算法选型建议。结合数据模式来写:随机大数组,快排平均最优;基本有序的小数组,插入排序反而最快,因为它的比较次数接近 n;需要稳定且数据规模很大,归并排序最稳,但空间开销 O(n);内存紧张又想原地排序,堆排序更合适。基数排序只在 key 值域受限时使用,比如百万范围的非负整数能体现出线性优势。

我自己的习惯是,每次完成排序综合分析后,把固定随机种子的生成器、完整实验脚本、CSV 原始数据和作图代码全部内嵌到报告的附录里。不是因为报告越长越好,而是排序实验最大的敌人是“复现不了”——换一台机器,数据会变;换一个随机种子,结论可能就反转。只有把所有变量固定住,你的分析结论才是真正可验证的。希望这篇笔记能帮你做出第一份敢拍胸脯说“这结论是我自己跑出来的”课程设计。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 3:28:10

Spring Boot 整合 MyBatis 与 PostgreSQL:从配置到性能优化全解析

做 Java 后端这几年&#xff0c;Spring Boot、MyBatis、PostgreSQL 这三样东西几乎成了我项目里的固定搭配。不管是刚入行的新手&#xff0c;还是已经被线上事故磨过几轮的老兵&#xff0c;最终都会发现&#xff1a;一套用得住、讲得清、改得动的数据访问方案&#xff0c;比追着…

作者头像 李华
网站建设 2026/9/30 3:28:08

宠物店管理系统全栈实战:SpringBoot+Vue+uniapp设计与部署

又到毕业设计选题季和各路程序员找项目练手的节点&#xff0c;宠物店管理系统在Java方向的热度一直居高不下。我自己做毕设辅导和全栈项目交付这些年&#xff0c;基于Java SpringBoot/SSM Vue uniapp这套组合的宠物店系统&#xff0c;前前后后落地了不少。这篇文章把这类项目…

作者头像 李华
网站建设 2026/9/30 3:27:32

Cisco 3560三层交换机配置实战:SVI、路由、PBR与安全加固

简介&#xff1a;本资源是一份面向网络工程师、高校通信/计算机专业学生及思科认证备考者的三层交换机实操指南&#xff0c;聚焦Cisco Catalyst 3560-E系列设备的全面配置与应用。内容系统覆盖设备硬件特性&#xff08;如万兆上行、PoE供电、冗余电源&#xff09;、IOS软件操作…

作者头像 李华
网站建设 2026/9/30 3:27:04

东方云权通全开源商城源码:中小企业高并发架构设计与部署实战

1. 项目整体认识与选型拆解1.1 项目定位&#xff1a;中小企业商城系统的“开源答案”先说说我为什么会盯上东方云权通这套东西。做电商系统这行久了&#xff0c;很多朋友问我要一套“能跑起来、能撑住流量、又不至于把预算烧穿”的商城源码&#xff0c;坦白说市面上选择很多——…

作者头像 李华
网站建设 2026/9/30 3:26:45

告别容器数据丢失:Docker数据卷挂载原理与实战

作为一个成天跟容器打交道的开发者&#xff0c;我想先聊一个特别普遍的痛点——很多人第一次用 Docker 跑 MySQL、Redis 或者 Nginx 的时候&#xff0c;容器跑得好好的&#xff0c;数据往里写了一大堆&#xff0c;结果某天一个docker rm或者docker compose down之后&#xff0c…

作者头像 李华
网站建设 2026/9/30 3:26:45

CentOS 7安装Docker CE报错container-selinux依赖:三种解决路径

1. 报错现场&#xff1a;又卡在 container-selinux 上了看到这个报错&#xff0c;我一点都不意外。凡是这几年在 CentOS 7 上手动装过 Docker CE 的运维&#xff0c;大概率都被这条依赖卡过至少一次。当时的情况一般是这样的&#xff1a;你按网上教程配好了 docker-ce 的 yum 源…

作者头像 李华