一、复习数组的冒泡排序

http://blog.csdn.net/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.csdn.net/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行。

【完】

双向循环链表的冒泡排序相关推荐

  1. 数据结构与算法(2-2)线性表之链式存储(单链表、静态链表、循环链表、双向循环链表)

    目录 一.单链表 1.存储方式 2.插入 3.删除 总代码: 二.静态链表 1.存储方式 2.插入 3.删除 4.遍历 总代码: 三.循环链表 总代码: 四.双向循环链表 1.存储方式: 2.插入和删 ...

  2. 链表 -- 双向循环链表(线性表)

    1,双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱.所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点.一般我们都构造双向循环 ...

  3. C++实现有哨兵的双向循环链表

    C++实现有哨兵的双向循环链表: #include<iostream> #include<stack> using namespace std; template<cla ...

  4. 数据结构 -- 双向循环链表

    这篇文章写的是双向循环链表,这也是个非常经典的数据结构,我们在看 Linux 内核代码时就经常会遇到它.比如,在Linux的内核中实现进程描述符,使用的就是双向循环链表,因为它使用起来很方便,每个节点 ...

  5. 用动态数组模拟双向循环链表

    简单来说其实使用数组模拟LinkedList.同LinkedList的操作基本相似.  基本原理为:数组存放Entry对象,包含数据部分,指针部分(数组下标)  添加,删除基本操作改变指针.数组包含两 ...

  6. java与数据结构(4)---java实现双向循环链表

    线性表之链式存储结构双向循环链表 双向循环链表:每个结点包含了数据.直接前驱地址指针和直接后驱地址指针,头结点的直接前驱指向尾结点,尾结点的直接后驱指向头结点,头尾相连构成一个可正可反的圆环.可以形象 ...

  7. 数据结构-单向循环链表、双向循环链表、仿真链表

    一.单向循环链表: 1.概念: 单向循环链表是单链表的另一种形式,其结构特点是链表中最后一个结点的指针不再是结束标记,而是指向整个链表的第一个结点,从而使单链表形成一个环. 和单链表相比,循环单链表的 ...

  8. Python创建一个循环链表、双向循环链表

    循环链表与双向循环链表 循环链表 循环链表相较于单链表,将末尾结点的连接域指向了头结点,就类似于将播放器的顺序播放改成了列表循环 双向循环链表 双向链表相较于之前的链表多了上一节点连接域,在双向链表中 ...

  9. c++17(20)-双向循环链表(不依赖具体数据)

    双向循环链表(不依赖具体数据),借鉴LINUX内核的链表结构 #include <iostream> using namespace std; struct stdlist{stdlist ...

最新文章

  1. 数据结构之跳表Skiplist
  2. eclipse常用插件安装
  3. python3 numpy. ndarray 与 list 互转方法
  4. 六大设计原则之迪米特法则
  5. 西山居php面试,西山居面试经验
  6. 会计基础模拟练习一(3)
  7. 糖药病数据集分类_使用optuna和mlflow进行心脏病分类器调整
  8. 苦苦发愁学习Python?七天掌握Python就在此时
  9. 数据脱敏和加密_Apache ShardingSphere数据脱敏全解决方案详解
  10. java定向输出程序日志(输出到txt文件中)
  11. 12M电信宽带,为什么12台机器上网,总是掉线?有什么办法解决?请给位高手指教...
  12. HBase shell 示例
  13. 计算机辅助药物设计中的分子动力学模拟
  14. 金蝶K3采购价格管控杂谈
  15. python傅里叶谐波分析_利用傅里叶谐波分析法的时序数据周期迭代辨识算法
  16. 卡皇稳了,RTX3090获鲁大师Q1季度最强显卡!
  17. 【C++】C++基础语法
  18. 洪泰基金俞敏洪:创业24年,我的五点思考
  19. 放弃vlookup吧,这3种EXCEL多表关联方法“强得很”
  20. 电脑蓝牙打电话-总结(四、百瑞互联BRLink)

热门文章

  1. 如何在RCP程序中添加一个banner栏
  2. 响应信息不明确的接口做关联
  3. Class 17 - 1 动态渲染页面爬取 — Selenium使用
  4. 静态call 动态call LINK
  5. 大道至简第一章读后感(伪代码)
  6. Jmeter-获取响应结果中参数出现的次数
  7. (转)在Total Commander下使用SVN
  8. 64位/32位 C++/C# 数学计算性能对比测试
  9. 分数运算C++代码实现
  10. 救援模式下更改用户密码