单链表排序程序
单链表排序程序详细解释
程序结构概述
这个C程序实现了单链表的创建、冒泡排序和输出功能。程序包含四个主要函数:
creatlist()
: 创建链表fun()
: 对链表进行冒泡排序outlist()
: 输出链表内容main()
: 主函数
数据结构定义
#define N 6
typedef struct node {
int data; // 存储数据
struct node *next; // 指向下一个节点的指针
} NODE;
N
定义为6,表示链表将有6个数据节点NODE
结构体定义了链表节点,包含整型数据和指向下一个节点的指针
函数详细分析
1. creatlist函数 - 创建链表
NODE *creatlist(int a[])
{
NODE *h, *p, *q;
int i;
// 创建头节点
h = (NODE *)malloc(sizeof(NODE));
h->next = NULL;
// 逐个添加数据节点
for(i=0; i<N; i++) {
q = (NODE *)malloc(sizeof(NODE));
q->data = a[i];
q->next = NULL;
if (h->next == NULL)
h->next = p = q; // 第一个数据节点
else {
p->next = q; // 链接到链表末尾
p = q; // 更新尾指针
}
}
return h;
}
工作原理:
- 创建一个头节点
h
(不存储实际数据,只作为链表入口) - 遍历数组
a[]
,为每个元素创建新节点 - 使用尾插法将新节点添加到链表末尾
- 返回头节点指针
链表结构: 头节点 -> 0 -> 10 -> 4 -> 2 -> 8 -> 6 -> NULL
2. fun函数 - 冒泡排序
void fun(NODE *h)
{
NODE *p, *q;
int t;
p = h->next; // p指向第一个数据节点
while (p) {
q = p->next; // q指向p的下一个节点
while (q) {
if (p->data > q->data) {
// 交换数据
t = p->data;
p->data = q->data;
q->data = t;
}
q = q->next;
}
p = p->next;
}
}
排序过程分析:
这是冒泡排序的链表版本:
- 外层循环:
p
指针遍历每个节点 - 内层循环:
q
指针从p
的下一个节点开始,与p
比较 - 如果
p->data > q->data
,交换两个节点的数据值 - 每轮比较后,较小的值会"冒泡"到前面
排序步骤示例:
初始: 0 -> 10 -> 4 -> 2 -> 8 -> 6
第1轮p=0: 0与后面所有元素比较,0已是最小,无变化
第2轮p=10: 10与4,2,8,6比较,最终变为: 0 -> 4 -> 2 -> 8 -> 6 -> 10
第3轮p=4: 4与2,8,6比较,最终变为: 0 -> 2 -> 4 -> 6 -> 8 -> 10
...
最终: 0 -> 2 -> 4 -> 6 -> 8 -> 10
3. outlist函数 - 输出链表
void outlist(NODE *h)
{
NODE *p;
p = h->next; // 跳过头节点,指向第一个数据节点
if (p == NULL)
printf("The list is NULL!\n");
else {
printf("\nHead ");
do {
printf("->%d", p->data);
p = p->next;
} while(p != NULL);
printf("->End\n");
}
}
输出格式: Head ->0->10->4->2->8->6->End
4. main函数 - 程序入口
void main()
{
NODE *head;
int a[N] = {0, 10, 4, 2, 8, 6};
head = creatlist(a); // 创建链表
printf("\nThe original list:\n");
outlist(head); // 输出原始链表
fun(head); // 排序
printf("\nThe list after sorting :\n");
outlist(head); // 输出排序后链表
}
程序执行结果
The original list:
Head ->0->10->4->2->8->6->End
The list after sorting :
Head ->0->2->4->6->8->10->End
关键技术点
1. 头节点的作用
- 简化链表操作,不需要特殊处理第一个数据节点
- 头节点不存储实际数据,只是作为链表的入口点
2. 指针操作
p = h->next
:跳过头节点q = p->next
:获取下一个节点p->next = q
:建立链接关系
3. 内存分配
- 使用
malloc()
动态分配内存 - 每个节点都需要单独分配内存空间
4. 排序算法
- 时间复杂度:O(n²)
- 空间复杂度:O(1)
- 只交换数据值,不改变节点链接关系
程序特点
优点:
- 代码结构清晰,功能模块化
- 使用头节点简化操作
- 冒泡排序实现简单易懂
可能的改进:
- 可以添加内存释放函数
- 可以实现更高效的排序算法
- 可以添加错误处理机制
这个程序是学习链表操作和排序算法的很好例子,展示了基本的数据结构操作和算法实现。
让我详细解释为什么这个链表排序算法的时间复杂度是O(n²)。
时间复杂度分析
1. 算法结构分析
void fun(NODE *h)
{
NODE *p, *q;
int t;
p = h->next; // 外层循环起点
while (p) { // 外层循环:遍历每个节点
q = p->next; // 内层循环起点
while (q) { // 内层循环:与后续节点比较
if (p->data > q->data) {
// 交换操作 O(1)
t = p->data;
p->data = q->data;
q->data = t;
}
q = q->next;
}
p = p->next;
}
}
2. 循环次数计算
假设链表有n个数据节点:
- 第1轮(p指向第1个节点):内层循环执行
n-1
次 - 第2轮(p指向第2个节点):内层循环执行
n-2
次 - 第3轮(p指向第3个节点):内层循环执行
n-3
次 - ...
- 第n-1轮(p指向第n-1个节点):内层循环执行
1
次 - 第n轮(p指向第n个节点):内层循环执行
0
次(直接结束)
3. 总比较次数
总的比较次数 = (n-1) + (n-2) + (n-3) + ... + 2 + 1
这是一个等差数列求和:
总次数 = (n-1) × n / 2 = (n² - n) / 2
4. 具体例子
以程序中的6个元素为例:
第1轮:p=0,与10,4,2,8,6比较 → 5次比较
第2轮:p=10,与4,2,8,6比较 → 4次比较
第3轮:p=4,与2,8,6比较 → 3次比较
第4轮:p=2,与8,6比较 → 2次比较
第5轮:p=8,与6比较 → 1次比较
第6轮:p=6,无后续节点 → 0次比较
总计:5+4+3+2+1 = 15次比较
验证:(6²-6)/2 = 30/2 = 15 ✓
5. 时间复杂度推导
T(n) = (n² - n) / 2
= n²/2 - n/2
= O(n²)
当n足够大时:
- n²项占主导地位
- 常数系数1/2被忽略
- 低阶项-n/2被忽略
因此时间复杂度为O(n²)
6. 为什么是平方级复杂度?
这是因为算法使用了嵌套循环:
- 外层循环执行约n次
- 内层循环平均执行约n/2次
- 总执行次数:n × (n/2) = n²/2 = O(n²)
7. 与数组冒泡排序的对比
数组版本的冒泡排序时间复杂度也是O(n²):
for(i=0; i<n-1; i++) // 外层循环n-1次
for(j=0; j<n-1-i; j++) // 内层循环递减
if(a[j] > a[j+1]) // 比较相邻元素
swap(a[j], a[j+1]);
链表版本本质上是相同的算法,只是用指针代替了数组索引,所以时间复杂度相同。
总结: O(n²)的复杂度来源于双重嵌套循环,每个元素都要与其后面的所有元素进行比较,导致比较次数随输入规模的平方增长。