一次快速排序错误引发的思考
来源:互联网 发布:fanuc编程软件 编辑:程序博客网 时间:2024/05/27 14:13
http://blog.jobbole.com/93806/
快速排序是目前基于关键字的内部排序算法中平均性能最好的,它采用了分治策略,这既是快速排序的优点也是它的缺点。从快速排序的算法描述上我们可以发现它具有递归的结构:
- (1)确定一个分界,将待排序的数组分为左、右两个部分;
- (2)使所有小(大)于临界值的数据移到左部分,大(小)于临界值的数据移到右部分;
- (3)这时左、右两个部分成为了两个独立的数组,分别对它们执行(1)(2)(3)的操作,直到所有数据都是有序的状态为止。
照这样的描述我们不难写出快排的代码,我平时遇到排序的问题,只要数据量上了100,想都不想就用快排来解决,但是当我用下面这个程序测试时却出现了问题:
#include <stdio.h>#include <time.h>#include <stdlib.h> #define NUM 10000000 /*待排序的数据量*/ voidquick_sort(doublea[], longleft, longright); intmain(void){ clock_tt_s, t_e; longi; doublea[NUM]; srand(time(NULL)); for(i = 0; i < NUM; ++i) { a[i] = rand(); } t_s = clock(); quick_sort(a, 0, NUM-1); t_e = clock(); doublet = (t_e - t_s) / (double)CLOCKS_PER_SEC; /*计算排序用时*/ printf("Quick sort %d items used time:%f s\n", NUM, t); return0;} voidquick_sort(doublea[], longleft, longright){ longi = left; longj = right; doublemid = a[(i + j) / 2]; /*以中间元素作为比较的基准*/ while(i <= j) { while(a[i] < mid) ++i; while(mid < a[j]) --j; if(i <= j) { doublet = a[i]; a[i] = a[j]; a[j] =t; ++i; --j; } } if(i < right) quick_sort(a, i, right); if(left < j) quick_sort(a, left, j);
我在Linux上运行这个程序出现了”Segmentation fault “错误,而当NUM==1000000时却没有这个错误。查阅相关资料得知这是由于程序递归次数太多,大量的压栈使程序占用的栈空间超过了操作系统所规定的大小,从而出现的内存错误。
我用ulimit -s指令的得到的结果是8192,也就是说我的系统默认给每个程序分配的大概是8M的栈空间。用指令ulimit -s unlimited使栈空间变成实际内存大小后,上面的程序就可以顺利运行而不出错误了(因为Linux上不像Windows可以把栈的大小写入可执行文件中,所以只能用ulimit -s更改的方法了)。
难道因为栈的限制,快速排序能够处理的数据量就有上限了吗?那还不如用选择排序——虽然慢,但至少不会出错,于是我找到了这篇文章:快速排序的非递归实现。其实说是“非递归”,只不过是用自己管理的栈来消除递归,算法本质上没有区别,而且从这篇文章作者的测试来看,用栈的方法比用递归的方法反而更慢(作者将其解释为:“用栈的效率比递归高,但是在这个程序中局部变量也就是要每次压栈的数据很少,栈的优势体现不出来,反而更慢……”,我认为这种观点是不对的,由于递归可以理解为有了一个“系统帮你自动管理的栈”,它的效率肯定是要比你自己管理的栈要高的,况且你在进行弹栈和压栈操作时又调用了新函数,算上调用的开支,用栈的方法肯定比递归慢),不过栈在这里的优势是可以不用考虑操作系统的问题,而且能够处理的数据量只和内存大小有关,不必受到操作系统对栈空间大小的限制(即使用栈,快排也比很多排序算法要快得多)。
以前在学排序算法的时候,专门有讲怎样根据实际问题来选择合适的排序算法,但是我图“省事”,就只用快排和简单选择排序。遇到了这个问题也让我对算法的选择和实现上有了更多认识,同时也了解到用栈消除递归在有些场合(比如系统栈空间受限)的重要意义。
前面我说到所谓的“非递归”快速排序算法,不过是用栈来消除了递归,它的运行时间肯定比递归算法长,我们不妨来实际实现一下。代码如下:
#include <stdio.h>#include <stdlib.h>#include <time.h> #define MAX_TOP 10000 /*一个很大的栈*/#define NUM 500L /*有关栈的数据结构*/structRegion { longleft; longright;}; structStack { structRegion reg[MAX_TOP+1]; longtop;}; /*对栈进行操作的函数*/voidinit_stack(structStack *s);voidpush_stack(structStack *s, structRegion r);structRegion pop_stack(structStack *s);intis_stack_empty(structStack *s); /*与排序有关的函数*/ longpartition(doublea[], longleft, longright); /*划分区间*/voidnr_qsort(doublea[], longleft, longright); intmain(void){ doublea[NUM]; /*待排序数据*/ clock_tt_s, t_e; longi; srand(time(NULL)); for(i = 0; i < NUM; ++i) a[i] = rand() % 1000000; /*统计运行时间*/ t_s = clock(); nr_qsort(a, 0, NUM-1); t_e = clock(); doublet = (t_e - t_s) / (double) CLOCKS_PER_SEC; printf("Non Recursive quick sort %ld items used time: %f s\n", NUM, t); return0;} /*implementation*/ voidinit_stack(structStack *s){ s->top = -1;} voidpush_stack(structStack *s, structRegion r){ if(s->top == MAX_TOP) { fprintf(stderr,"Stack overflow!\n"); exit(0); } s->reg[++s->top] = r;} structRegion pop_stack(structStack *s){ if(s->top == -1) { fprintf(stderr,"Stack underflow!\n"); exit(0); } return(s->reg[s->top--]);} intis_stack_empty(structStack *s){ return(s->top == -1);} /*返回划分的区间*/longpartition(doublea[], longleft, longright){ doublebase = a[left]; /*以最左边的元素作为比较基准*/ while(left < right) { while(left < right && a[right] > base) --right; a[left] = a[right]; while(left <right && a[left] < base) ++left; a[right] = a[left]; } a[left] = base; return left; } voidnr_qsort(doublea[], longleft, longright){ structStack s; structRegion region, regionlow, regionhi; longp; /*记录划分出的分界点*/ init_stack(&s); region.left = left; region.right = right; push_stack(&s, region); while(!is_stack_empty(&s)) { region = pop_stack(&s); p = partition(a, region.left, region.right); if(p-1 > region.left) { regionlow.left = region.left; regionlow.right = p - 1; push_stack(&s, regionlow); } if(region.right > p + 1) { regionhi.left = p + 1; regionhi.right = region.right; push_stack(&s, regionhi); } } }
在代码的第110行至第122行的while循环中,做的正是用栈消除递归的工作。想想递归的算法中,把划分好的左右区间界限(即left,right)保存到了系统管理的栈中,这里手动把每次划分出来的区间分界保存至栈中,当第113和118行的两个条件不满足时,所在区间的元素都是有序的状态,此时不进行压栈操作而向前返回(即递归的回调)。关于用栈消除递归的算法可以参考关于数据结构的书籍,比如陈锐的《零基础学数据结构》有关栈的那一章就有介绍。实际运行两个程序的结果如下:
$ ./nr_qsort #非递归算法的快排Non Recursive quick sort500 items used time: 0.000261 s$ ./qsort#递归算法的快排 Quicksort500 items used time:0.000104 s
之所以只用了500个数据,是因为超过1000个数据后,非递归快排的速度就慢的令人难以忍受。下面是另外两次关于递归算法快排的测试:
$time./qsortQuick sort 1000000 items used time:0.289171 s real 0m0.372suser 0m0.332ssys 0m0.012s #下面更改NUM即数据的个数为10000000 $ ./qsortSegmentation fault #超出栈的大小 $ ulimit -s unlimited #更改栈的大小为不受限$time./qsortQuick sort 10000000 items used time:3.259025 s #成功进行了排序 real 0m4.044suser 0m3.740ssys 0m0.172s
这也印证了之前谈到的系统默认限制带来的问题。
- 一次快速排序错误引发的思考
- 由一次merge错误引发git 分支模型的思考
- 一次跳槽引发的人力资源思考
- 一次产品讨论会引发的思考
- 一次out of memory引发的思考
- 一次由于IsPostBack引发的错误
- 由contenttype引发的一次小错误
- 一个页面错误引发的思考
- strcmp段错误引发的思考
- 一次乘的士引发的有关项目管理的思考
- 一次“淘宝购物”引发出来对产品的思考
- 一次curl超时引发的项目问题思考
- 快速排序一次排序的应用
- 由今天的错误引发对编程的思考总结
- 由使用Scanf()函数导致程序逻辑错误引发的思考
- Oracle之ORA-25150错误引发的思考
- 由APUE中的错误处理函数err_xxx引发的思考
- MySQL 错误 “could not be resolved: ..” 和引发的思考
- #pragma分析
- C++学习——copy构造函数所涉及匿名对象的问题
- TCP/IP实现以及常见问题
- spring-boot通用配置文件整理
- POJ 2155 Matrix
- 一次快速排序错误引发的思考
- cocos2d-x 进度条实现(被砍)掉血效果
- Linux命令大观
- C/C++高阶语法:函数指针及其应用
- 避免用char类型作为数组小标
- hdu 5532 Almost Sorted Array
- sapi_module_struct 研究(一)
- LeetCode:Rectangle Area
- Lesson 6 Transposition and conjugation