算法導論第二章結尾練習2.3-4提到將插入排序寫遞歸版本,然後嘗試寫了個,本來寫了就好了,但是調試的時候排序10w個數可以,排序100w個數就段錯誤,分析了一下,把結果放上來以後查看,先貼代碼:
#include <stdio.h>
#include <unistd.h>
#include <time.h>
#include <string.h>
#include <stdlib.h>
void insert_sort0( int *arr, int index )
{
int i, tmp = arr[index];
//printf("index:%d\n", index);
for ( i = index - 1; i >= 0; i-- ) {
if ( arr[i] > tmp ) {
arr[i + 1] = arr[i];
} else {
arr[i + 1] = tmp;
return;
}
}
arr[i + 1] = tmp;
}
void insert_sort( int *arr, int index )
{
//printf("index:%d\n", index);
if ( index > 0 ) {
insert_sort( arr, index - 1 );
//printf("index:%d\n", index);
insert_sort0( arr, index );
}
}
void sortLoop0( int *arr, int len )
{
//sleep( 20 );
insert_sort( arr, len - 1 );
}
//排序前後做時間對比,精度sec
void sortLoop( int *arr, int len )
{
time_t tt0 = time( NULL );
printf("\tbefore sort:%s", ctime(&tt0));
sortLoop0( arr, len );
time_t tt1 = time( NULL );
printf("\tafter sort:%s", ctime(&tt1));
printf("\tsort the array cost %d(sec),%d(min)\n",
(int)(tt1 - tt0), (int)((tt1 - tt0) / 60));
}
//初始化隨機數組
void initArr( int *arr, int lowV,
int upV, int len )
{
srand( (int)time(NULL) );
int i = 0;
int size = upV - lowV;
for ( ; i < len; i++ )
{
arr[i] = rand() % size + lowV;
}
}
//打印數組
void printArr( int *arr, int len )
{
int i = 0;
for ( ; i < len; i++ )
printf("%d ", arr[i]);
printf("\n");
}
//這裏寫了個簡單得數組檢查,檢查是否
//是正確的遞增序列(怕自己的代碼沒檢查
//邊界條件偶爾有錯誤的排序數組)
int check_arr_increase( int *arr, int len )
{
int i = 0;
for ( ; i < len - 1; i++ )
{
if ( arr[i] <= arr[i + 1] )
continue;
else
{
printf("array isn't increase!!!\n");
return -1;
}
}
printf("array is increase!!!!\n");
return 0;
}
int main( int argc, char **argv )
{
if ( argc != 4 )
{
printf("usage: ./execfile lowV upV len\n");
return 0;
}
int lowV = atoi( argv[1] );
int upV = atoi( argv[2] );
unsigned int len = atoi( argv[3] );
int *arr = NULL;
arr = ( int *) malloc( len * sizeof(int) );
initArr( arr, lowV, upV, len );
//printArr( arr, len );
sortLoop( arr, len );
check_arr_increase( arr, len );
//printArr( arr, len );
free( arr );
arr = NULL;
return 0;
}
開始一直以爲是代碼有數組越界或者邊界條件沒考慮到,後來看了代碼許久沒發現問題。然後gdb看core文件,bt看調用棧發現段在遞歸向下大概70w-80w的位置,而遞歸棧還沒返回,執行過程很正常沒出問題,想了想是棧溢出,每次自頂向下調用一次自身函數就要在棧裏保存一次函數地址和參數。
用ulimit -a|grep stack看了一下默認的棧空間大小爲8192kb=8m=8*1024*1024=8388608byte。網上查了下說一次函數調用每次佔用棧內存爲:4返回地址 + 4*參數個數 + 4寄存器保護 + 4*局部變量數(未驗證準確與否),我的排序佔用4+4*2+12=24字節,但根據我每次設置棧大小調試程序,設置到31256kb時程序沒有段錯誤,也就是31256*1024/1000000=32字節,這裏的一次函數調用爲32字節。
用ulimit -s 102400修改了棧空間大小100M(暫時修改),再次運行程序排序100w個數,發現打印正常,程序也沒段錯誤了,只是遞歸棧開始返回等了好久。
對歸併排序的思考:以前也有用歸併排序排1億個數(http://blog.csdn.net/u012785877/article/details/52475023),沒有出現棧溢出,裏面也是用遞歸的方法,因此仔細看了下歸併排序(算法導論裏有提到歸併排序的遞歸樹,可以算出數量,但是我看着頭大,準備略過這些數學問題,先照着把代碼寫出來,看完了再回頭深入看一遍),模擬了一下遞歸的棧過程如下:
假設有134217728個數待排序(2^27)。因此遞歸樹根節點爲2^27;遞歸第一次遞歸樹有二層,兩個節點分別爲2^26、2^26;遞歸第三次遞歸樹爲有三層,四個節點分別爲2^25、2^25、2^25、2^25……這樣一直推到遞歸樹葉子節點爲2^1,此遞歸樹有27層,2^(floor - 1) + 1 = 2^(26 -1) + 1 = 67108865個節點,也就是要維護67108865*30約爲20億的函數調用棧空間,爲什麼沒有棧溢出呢?。通過調試歸併排序,排序4個數時,是將0-3分爲0-1,在將0-1分爲0,對0再分發現不能分了,則此次遞歸返回到父節點,劃分右子樹1,對1再分發現不能分了,於是調用排序,將0、1排爲0-1,再回歸0-1的父節點,再對右子樹2-3遞歸進行此過程。其實排序4個數最大只需要5次遞歸函數調用(即遞歸樹走到最深度),如果排序n層遞歸樹,最大需要n+1次。回到以上排序2^27個數,則最大同時需要28次遞歸函數調用,其餘時候都是棧空間在不斷出棧入棧。
上面寫得有點昏,無奈不是學文科的,又不想畫圖,額。這個歸併排序的分析結果只花了2個小時分析,也不能確定準不準確,如果錯了歡迎批評指正。