一、复习数组的冒泡排序
http://blog.youkuaiyun.com/longintchar/article/details/75710000
上面这篇博文我介绍了数组的冒泡排序。
冒泡排序属于蛮力法,它比较表中的相邻元素,如果它们是逆序的话就交换它们的位置。重复多次后,最终,最大的元素就“冒”到列表的最后一个位置。第二遍操作将第二大的元素“冒”出来。这样一直重复,直到n-1遍(假设列表共有n个元素)以后,该列表就排序好了。
示意图如下所示:
从上图中的start(最左边)开始,向右两两比较,比较一轮后,最大的数冒到最右边,占据end的位置;
end向左移动一个位置,再从start(最左边)开始,向右两两比较……
三轮过后,4个数就排序OK了。
数组的冒泡排序代码如下:
void bubble_sort(int *arr, int len)
{
int start;
int end;
for (end=len-1; end>0; end--)
{
for (start=0; start<end; ++start)
{
if(arr[start] > arr[start+1])
swap(arr+start, arr+start+1);
}
}
}
二、复习内核链表
既然是链表的排序,那肯定要有链表。用别人造好的轮子当然是最省时省力的办法,不如我们把Linux内核链表拿来用用吧。
下文要用到的函数如下:
1. 结点的插入
static inline void __list_add(struct list_head *new,
struct list_head *prev,
struct list_head *next)
{
next->prev = new;
new->next = next;
new->prev = prev;
prev->next = new;
}
__list_add
这个函数表示把新结点插入到prev和next之间。
2. 结点的删除
static inline void __list_del(struct list_head * prev, struct list_head * next)
{
next->prev = prev;
prev->next = next;
}
static inline void list_del(struct list_head *entry)
{
__list_del(entry->prev, entry->next);
}
list_del
用来删除某个结点。
3.遍历和逆向遍历
#define list_for_each(pos, head) \
for (pos = (head)->next; pos != (head); pos = pos->next)
#define list_for_each_reverse(cur, head) \
for (cur = (head)->prev; cur != head; cur = (cur)->prev) //内核源码好像没有这个宏,我们可以自己加上
另外,还用到了一些函数,由于经常用,这里就不贴了。源码可以参考我的博文 http://blog.youkuaiyun.com/longintchar/article/details/78034827
三、排序完整代码
#include <stdio.h>
#include "list.h" //list.h这个文件需要你自己打造,可以拷贝内核源码,也可以参考我的博文
struct data_info {
int data;
struct list_head list;
};
int cmp_data(struct list_head *a, struct list_head *b)
{
struct data_info *pa = list_entry(a, struct data_info, list);
struct data_info *pb = list_entry(b, struct data_info, list);
return pa->data - pb->data;
}
void swap(struct list_head *a, struct list_head *b)
{
struct list_head flag = {NULL, NULL};
__list_add(&flag, b->prev, b);
list_del(b);
__list_add(b, a->prev, a);
list_del(a);
__list_add(a, flag.prev, &flag);
list_del(&flag);
}
void bubble_sort(struct list_head *head,
int (*compar)(struct list_head *,
struct list_head *))
{
struct list_head *start = NULL;
struct list_head *end = NULL;
list_for_each_reverse(end, head)
{
list_for_each(start, head)
{
if (start == end)
break;
if (compar(start, start->next) > 0)
{
swap(start, start->next);
start = start->prev; //start归位
if (start == end)
end = end->next; //end归位
}
}
}
}
int main(void)
{
struct data_info s[] = {{6}, {4}, {7}, {9}, {2}, {8}, {5}, {1}, {3}};
LIST_HEAD(head);
int i;
for (i = 0; i < sizeof s/ sizeof *s; ++i)
{
list_add_tail(&s[i].list, &head);
} //尾插,构成链表
struct data_info *pdata = NULL;
list_for_each_entry(pdata, &head, list)
{
printf("%d ", pdata->data);
}
printf("\n"); //排序之前
bubble_sort(&head, cmp_data); //进行排序
list_for_each_entry(pdata, &head, list)
{
printf("%d ", pdata->data);
}
printf("\n"); //排序之后
return 0;
}
运行结果如下:
6 4 7 9 2 8 5 1 3
1 2 3 4 5 6 7 8 9
四、代码解析
1.比较大小是一个函数指针
排序函数的原型是:
void bubble_sort(struct list_head *head, int (*compar)(struct list_head *, struct list_head *))
第一个参数是链表的头结点(指针),第二个参数是指向函数的指针,这个函数由用户定义。因为数据类型是用户定义的,所以只有用户才清楚如何比较数据。
本代码中,我们定义的链表元素是整数,比较大小也很简单,直接相减就可以了。
struct data_info {
int data;
struct list_head list;
};
int cmp_data(struct list_head *a, struct list_head *b)
{
struct data_info *pa = list_entry(a, struct data_info, list);
struct data_info *pb = list_entry(b, struct data_info, list);
return pa->data - pb->data;
}
2.交换函数
冒泡排序就是靠一轮轮的比较和交换,比较前文说过了,不是难点,那么如何交换呢?
仔细想想,这个交换还是挺麻烦的。有人说,把a结点的数据域和b结点的数据域交换就可以了。这是一个办法,优点是不用移动结点,单纯拷贝数据域就行;缺点是不够通用,因为你无法预知用户定义的是什么数据。所以,为了通用一些,我们还是要移动结点。
试想,我们先把a结点从链表中删除,然后把a结点插入到b结点的后面,再把b结点删除,最后把b结点插入到a结点原来的位置。这里的问题是,一旦把a结点从链表中删除,a的原位置就丢失了,所以是无法把b结点插入到a结点原来的位置的。
所以,我们要想办法记录a结点的原位置。非常容易想到的办法是——用指针记录下a结点的前驱和后继。于是我写出了以下代码:
void swap_wrong(struct list_head *a, struct list_head *b)
{
struct list_head *prev = a->prev;
struct list_head *next = a->next;
list_del(a);
__list_add(a, b->prev, b);
list_del(b);
__list_add(b, prev, next);
}
乍一看,上面的代码还挺对的,可是仔细一想,考虑还不周全。经过测试,我发现上面的代码只适用于两个结点不相邻的情况,一旦a和b相邻,那么就出错了——无法正确交换,而且使b结点自己指向自己。
如果考虑相邻的情况,上面的代码可以修改为:
void swap(struct list_head *a, struct list_head *b)
{
struct list_head *prev = a->prev;
struct list_head *next = a->next;
if(a->next == b)
{
list_del(b);
__list_add(b, a->prev, a);
}
else if(b->next == a)
{
list_del(a);
__list_add(a, b->prev, b);
}
else
{
list_del(a);
__list_add(a, b->prev, b);
list_del(b);
__list_add(b, prev, next);
}
}
经过测试,以上代码没有问题。但是,这种写法和第三节的写法还是不一样的,显然三的写法更简洁。
void swap(struct list_head *a, struct list_head *b)
{
struct list_head flag = {NULL, NULL};
__list_add(&flag, b->prev, b);
list_del(b);
__list_add(b, a->prev, a);
list_del(a);
__list_add(a, flag.prev, &flag);
list_del(&flag);
}
这种写法的优点是不用分情况讨论,不管a和b是否相邻,都是适用的。
示意图如下:
3.排序函数
void bubble_sort(struct list_head *head,
int (*compar)(struct list_head *,
struct list_head *))
{
struct list_head *start = NULL;
struct list_head *end = NULL;
list_for_each_reverse(end, head)
{
list_for_each(start, head)
{
if (start == end)
break;
if (compar(start, start->next) > 0)
{
swap(start, start->next);
start = start->prev; //start归位
if (start == end)
end = end->next; //end归位
}
}
}
}
第7行:外层循环,使end结点依次从表尾向首结点取值;
第9行:内层循环,使start结点依次从首结点向表尾取值;
第11~12行:一旦start和end重合,跳出内层循环;
第14~16行:从表头到表尾按照升序排列;
第17~19:这几行非常重要,也非常容易被忽略。为了强调,我放到下节说。
4.指针的归位
在数组排序中,游标是不需要归位的,因为我们交换的不是内存地址,而是内存的内容。但是,在本文的链表排序中,我们交换的是结点的地址,也就是说结点的位置改变了。
举例来说,假设当前start指向第3个结点,之后发生了交换,第3个和第4个交换了,那么随着交换的发生,start指向了第4个结点(原来的3变成了现在的4),如果不修正start,继续迭代,那么start = start->next,即指向第5个结点,从第3到第5显然不对,4去哪里了?
所以,发生交换后,需要把start归位,之前指向第几个结点,现在还要指向第几个。所以有了第17行。
如果start和end交换了,那么还要归位end,道理同上。于是有了18~19行。
【完】