数据结构与算法基础:从线性表到排序算法详解
📚 目录
线性表的两种存储方式
顺序存储结构
特点:逻辑上相邻的元素在物理位置上也相邻
c
// 顺序表的定义
#define MAXSIZE 100
typedef struct {
int data[MAXSIZE]; // 存储数组
int length; // 当前长度
} SeqList;
优点:
- 随机访问速度快:O(1)
- 存储密度高
缺点: - 插入删除操作需要移动大量元素:O(n)
- 必须预先分配连续空间
链式存储结构
特点:用指针来表示元素之间的逻辑关系
c
// 单链表的定义
typedef struct LNode {
int data; // 数据域
struct LNode *next; // 指针域
} LNode, *LinkList;
优点:
- 插入删除操作方便:O(1)
- 动态分配内存,空间利用率高
缺点: - 访问第i个元素需要遍历:O(n)
- 额外空间存储指针
三大基础排序算法详解
1. 直接插入排序
核心思想:将待排序的元素插入到已排序序列的合适位置
c
void InsertSort(int A[], int n) {
for (int i = 2; i <= n; i++) { // 从第二个元素开始
if (A[i] < A[i-1]) { // 需要插入
A[0] = A[i]; // 哨兵暂存
int j;
for (j = i-1; A[0] < A[j]; j--) {
A[j+1] = A[j]; // 元素后移
}
A[j+1] = A[0]; // 插入到正确位置
}
}
}
特点:
- ✅ 稳定排序
- 📊 时间复杂度:O(n²)
- 🎯 适用于小规模数据或基本有序的数据
2. 简单选择排序
核心思想:每次从未排序部分选择最小元素放到已排序部分末尾
c
void SelectSort(int A[], int n) {
for (int i = 0; i < n-1; i++) { // n-1趟选择
int min = i; // 记录最小元素下标
for (int j = i+1; j < n; j++) {
if (A[j] < A[min]) min = j; // 找到更小元素
}
if (min != i) { // 交换到正确位置
int temp = A[i];
A[i] = A[min];
A[min] = temp;
}
}
}
特点:
- ❌ 不稳定排序
- 📊 时间复杂度:O(n²)
- 🔄 交换次数少,适合交换成本高的场景
3. 冒泡排序
核心思想:相邻元素两两比较,逆序则交换,每轮将最大元素"冒泡"到末尾
c
void BubbleSort(int A[], int n) {
for (int i = 0; i < n-1; i++) { // n-1趟
bool flag = false; // 优化标记
for (int j = n-1; j > i; j--) { // 从后往前
if (A[j] < A[j-1]) { // 逆序则交换
int temp = A[j];
A[j] = A[j-1];
A[j-1] = temp;
flag = true; // 标记有交换
}
}
if (!flag) break; // 无交换则提前结束
}
}
特点:
- ✅ 稳定排序
- 📊 时间复杂度:O(n²)
- ⚡ 有优化空间,最好情况可达O(n)
归并排序:分治思想的经典应用
核心思想
归并排序采用分治策略:
- 分解:将序列分成两半
- 解决:递归排序两个子序列
- 合并:将两个有序子序列合并
递归实现
c
// 归并排序主函数
void mergeSort(ListNode* p, int n) {
if (n < 2) return; // 递归终止条件
int m = n >> 1; // 中间位置
ListNode* q = p; // 后半段起点
// 寻找中间节点
for (int i = 0; i < m; i++) {
q = q->next;
}
// 递归排序前后两段
mergeSort(p, m);
mergeSort(q, n - m);
// 合并两个有序子序列
merge(p, m, q, n - m);
}
合并操作
c
// 合并两个有序链表
ListNode* merge(ListNode* p, int m, ListNode* q, int n) {
ListNode dummy; // 哨兵节点
ListNode* tail = &dummy;
while (m > 0 && n > 0) {
if (p->data <= q->data) { // 选择较小元素
tail->next = p;
p = p->next;
m--;
} else {
tail->next = q;
q = q->next;
n--;
}
tail = tail->next;
}
// 处理剩余元素
if (m > 0) tail->next = p;
if (n > 0) tail->next = q;
return dummy.next;
}
特点:
- ✅ 稳定排序
- 📊 时间复杂度:O(n log n)
- 💾 空间复杂度:O(n)
- 🎯 适用于大规模数据排序
算法对比与实际应用
性能对比表
| 算法 | 时间复杂度 | 空间复杂度 | 稳定性 | 适用场景 |
|---|---|---|---|---|
| 插入排序 | O(n²) | O(1) | ✅ 稳定 | 小规模、基本有序数据 |
| 选择排序 | O(n²) | O(1) | ❌ 不稳定 | 交换成本高的场景 |
| 冒泡排序 | O(n²) | O(1) | ✅ 稳定 | 教学演示、简单场景 |
| 归并排序 | O(n log n) | O(n) | ✅ 稳定 | 大规模数据、外部排序 |
实际应用建议
- 小规模数据(n < 50):使用插入排序
- 中等规模数据:考虑快速排序或堆排序
- 大规模数据且要求稳定:使用归并排序
- 内存受限环境:选择堆排序
优化技巧
c
// 混合排序:小数组用插入排序,大数组用归并排序
void hybridSort(int A[], int n) {
if (n < 16) {
InsertSort(A, n); // 小数组用插入排序
} else {
// 大数组用归并排序
int mid = n / 2;
hybridSort(A, mid);
hybridSort(A + mid, n - mid);
merge(A, mid, A + mid, n - mid);
}
}
总结
通过今天的学习,我们掌握了:
- 线性表的两种存储方式及其优缺点
- 三大基础排序算法的实现和特点
- 归并排序的分治思想和实现
- 算法选择的实际应用指导