C++中qsort()函数源码改写

qsort库函数的源代码中用了recurese + goto的递归结构。

(1)由于某处看过学c不推荐使用goto;

(2)由于c中不能在一个函数中嵌套定义另一个函数;


所以我把递归的部分抽出来,放进另一个函数体。

写完发现可能自己过两天就看不懂了......于是几乎每一步都加了很多注释。

配合图解其实更方便。


①myqsort()改写实例:

//前置函数,按字节交换指针指向的内容
void myswap(char *p1, char *p2, size_t width) {
	//	char *p1 = (char*)a;
	//	char *p2 = (char*)b;
	char tmp;

	if (p1 != p2)
		while (width--) {
			tmp = *p1;
			*p1++ = *p2;
			*p2++ = tmp;
		}
	return;
}

void sort_recurs(char *lo, char *hi, size_t width, int(*comp)(const void*, const void*), char **lostkptr, char** histkptr, int &stkptr);

#define STKSIZ  64  //合适的上限 
void myqsort(void *base, size_t num, size_t width, int(*comp)(const void *, const void *)) {

	char *lo, *hi;
	int stkptr;
	char *lostk[STKSIZ], *histk[STKSIZ];

	//initial
	stkptr = 0;
	lo = (char*)base;
	hi = (char*)base + width * (num - 1);

	//validation check
	if (num <= 2) {
		if (comp(lo, hi)>0)
			myswap(lo, hi, width);
		return;
	}

	//递归处理较小区域 ,第一次是处理全域 
	sort_recurs(lo, hi, width, comp, lostk, histk, stkptr); //stkptr需要时时变更 

															//递归处理栈中留存的较大区域
	while (stkptr--) {
		lo = lostk[stkptr];
		hi = histk[stkptr];
		sort_recurs(lo, hi, width, comp, lostk, histk, stkptr); //stkptr需要时时变更 
	}

	//所有栈区数据递归完毕 
	return;

}

void sort_recurs(char *lo, char *hi, size_t width, int(*comp)(const void*, const void*), char **lostk, char** histk, int &stkptr) {

	//-----------start recursion-------三分排序-----
	//另写函数,需要 width,comp ,hi,lo, lostk,histk,stkptr



	//refresh data
	int size; //统计递归时,参与排序的子串长度 
	size = (hi - lo) / width + 1; //参与排序个数 ,可以有个cutoff分支 

	if (size == 2) { //递归时,size至少是2开始的。而size==2时无法3分,特殊处理,保证递归收敛。 
		if (comp(lo, hi)>0)
			myswap(lo, hi,width);
		return;
	}

	//divide into three partion
	char *mid; //定义中间点 
	mid = lo + (size / 2)*width;

	//对三个分割点排序
	if (comp(lo, mid)>0) {
		myswap(lo, mid, width);
	}
	if (comp(lo, hi)>0) {
		myswap(lo, hi, width);
	}
	if (comp(mid, hi)>0) {
		myswap(mid, hi, width);
	}

	//启动游动扫描
	char *loguy, *higuy; //定义游动指针 


	loguy = lo;
	higuy = hi;


	while (loguy<higuy) { //交叉时退出循环 

						  //扫描数组,找到A[loguy]>中位数 
		if (loguy<mid) {
			do {
				loguy += width;
			} while (loguy<mid && comp(loguy, mid) <= 0);
		}

		if (loguy >= mid) {
			do {
				loguy += width;
			} while (loguy <= hi && comp(loguy, mid) <= 0);
		}

		//扫描至此,保证loguy左侧所有数据<= A[mid] 
		//loguy 有2种情况, 
		//1. A[loguy]>A[mid],loguy<=hi,即找到一个比A[mid]大的数
		//2. loguy>hi,说明数组内所有数都满足<=A[mid],游动指针loguy超出定义范围


		//下面游动higuy,扫描至 A[higuy] <= A[mid],而hiugy位置保证在右半侧 
		do {
			higuy -= width;
		} while (higuy>mid && comp(higuy, mid)>0);
		//扫描结束时,higuy停留在mid,或者在mid右侧找到 A[higuy]<=A[mid] 


		if (loguy<higuy) { //说明尚未交叉
						   //交换数据
						   //loguy指向的比中位数大的数据,可以交换到右侧的higuy来
						   //higuy指向的<=中位数的数据,可以交换到loguy所在 
			myswap(loguy, higuy, width);

			//特殊情况下,higuy=mid,故交换后跟踪的原中位数A[mid]位置变去了loguy
			//需要调整mid指针,使其重新指向中位数 
			if (mid == higuy) {
				mid = loguy;
			}
		}
	}


	//退出循环了,说明loguy与higuy交叉
	//由于loguy左侧都是<=A[mid],loguy是第一个>A[mid]的点 
	//而higuy右侧都是>A[mid],higuy是第一个<=A[mid]的点 
	//有2种情况
	// 1. 两者相差1单位, higuy = loguy - width;
	// 2. 全部数据满足<=mid,所以loguy=hi+width,而higuy只运动1次,higuy= hi-width 

	//分类讨论
	higuy += width; //此时higuy与loguy重合,或者higuy抵达hi位置

					//loguy左侧都 <= mid 
					//找寻左侧第一个 < mid的位置,找不到时抵达lo 
	if (mid < higuy) {
		do {
			higuy -= width;
		} while (higuy > mid && comp(higuy, mid) == 0);  //设计上避免了comp(mid,mid) 
	}
	if (mid >= higuy) {
		do {
			higuy -= width;
		} while (higuy>lo && comp(higuy, mid) == 0); //同样不会出现comp(mid,mid) 
	}



	//此时若higuy未抵达lo,则出现3分天下
	// lo   <= x <= higuy, A[x]<=mid
	// higuy< x  <  loguy, A[x]==mid  //从loguy开始-=width
	// x>=loguy(假设loguy有定义), A[x]>mid

	//[lo~higuy],(higuy~loguy),[loguy~hi]


	//优先递归处理较小区域
	if (higuy - lo >= hi - loguy) { //巧妙之处在于,当loguy无定义时,也可以正常处理higuy-lo区间  
									// [loguy,hi]较小 

		if (lo<higuy) { //储存较大区域,相等时不做处理 
			lostk[stkptr] = lo;
			histk[stkptr] = higuy;
			++stkptr; //栈顶指针上移 
		}

		if (loguy<hi) { //相等或无定义时不做处理
			lo = loguy;
			//递归,三分整理 loguy~hi区间 
			sort_recurs(lo, hi, width, comp, lostk, histk, stkptr); //stkptr需要时时变更 
		}
	}
	else {
		// [lo,higuy]较小 
		if (loguy<hi) { //储存较大区域,相等或无定义时不处理 
			lostk[stkptr] = loguy;
			histk[stkptr] = hi;
			stkptr++;
		}

		if (lo<higuy) {
			hi = higuy;
			//递归,三分整理 lo~higuy区间 
			sort_recurs(lo, hi, width, comp, lostk, histk, stkptr); //stkptr需要时时变更 
		}
	}


	//--------end recursion---------

	return;
}


②bug回顾

编译通过之后,测试IO时发现有bug。

检查了很久,发现是对三分端点的排序代码出错了。


提问:给定3个数,如何设计一个方案,保证3个数从小到大排序?

我一开始写的是

	if (comp(lo, mid)>0) {
		swap(lo, mid, width);
	}
	if (comp(mid, hi)>0) {
		swap(mid, hi, width);
	}
	if (comp(lo, hi)>0) {
		swap(lo, hi, width);

	}

后来发现这个顺序,(a,b) (b,c) (a,c)是不行的。


令数据(a,b,c)=(7,10,1)

1. 比较(a,b) => (7,10,1)
2. 比较(b,c) => (7,1,10)
3. 比较(a,c) => (7,1,10)

产生错误结果。

回头看了一下库源码,人家用的顺序是(a,b)(a,c) (b,c)。

这个低级错误没什么好说的只能背了......


-

第二个bug出现在swap函数里

一不留神写了swap(lo,hi);

原来c++中确实存在名为swap的这个函数,所以编译没有报错。

摘录源码,它是传入left和right的两个引用,然后交换他们的值。

template<class _Ty,
	class> inline
	void swap(_Ty& _Left, _Ty& _Right)
		_NOEXCEPT_COND(is_nothrow_move_constructible_v<_Ty>
			&& is_nothrow_move_assignable_v<_Ty>)
	{	// exchange values stored at _Left and _Right
	_Ty _Tmp = _STD move(_Left);
	_Left = _STD move(_Right);
	_Right = _STD move(_Tmp);
	}

但是传入指针时,交换的是指针地址,对地址上存的数值没有影响。


举例:
对数组a[0]=21,a[1]=3,存在两个指针。
int* 类型的p1= 0x0119c138 ,指向的数字为21
int* 类型的p2= 0x0119c13c ,指向的数字为3

传入swap(p1,p2)
两者交换之后
p1 = 0x0119c13c
p2 = 0x0119c138
只是改变了指针而已......

对于数组而言,
a[0]在0x0119c138地址上存的值还是21......
a[1]在0x0119c13c地址上存的值还是3......

这也太蠢了......

自定义函数命名还是选不常见的好,别跟库函数撞了。


③源码参考

两篇博文内容相近,对照着看收益颇多。
https://blog.csdn.net/u011822516/article/details/17059693
ttps://blog.csdn.net/gen_ye/article/details/52880461


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值