快速排序(quick sort)

9 篇文章 0 订阅
5 篇文章 0 订阅

基本概念

快速排序的最差时间性能为O(n^2),期望时间性能为O(n*logn)

快速排序和归并排序一样,都属于分治法,其三个步骤如下

  • 分解(关键步骤):将序列(A[p], ..., A[r])划分成两个(可能空)的子序列(A[p], ..., A[q-1])和(A[q+1], ..., A[r]),使得(A[p], ..., A[q-1])中的任意元素均小于等于A[q],(A[q+1], ..., A[r])中的任意元素均大于等于A[q]。Aq称为“轴”(pivot)
  • 解决:递归地对分解得到的两个子序列进行快速排序
  • 合并:经过分解步骤后,两个子序列之间已有大小关系,无需再合并

pivot的选取

有三种方法:

  • 将第一个元素作为pivot——放弃,遇到预排序或反序的输入序列,快排的效率会变得很差
  • 随机选取pivot——放弃,虽然克服了方法1的缺点,但随机数生成本身的开销较大,得不偿失
  • 三数中值分割法——选用

三数中值分割法的一般做法是:使用左端(left)、右端(right)和中心位置mid=floor((low+high)/2)三个数中的中间大的那个数作为pivot


分解(partition)策略
策略一(详见严蔚敏《数据结构》):维护三个指针pl, ph, pm,其中pl从序列头部向尾部搜索,ph从尾部向头部搜索,pm始终指向“轴”,步骤如下
  • 若*pl > *pm,则swap(pl, pm), pm = pl, pl++
  • 若*ph < *pm,则swap(ph, pm), pm = ph, ph--
  • 上面两个步骤交替进行
策略二(详见见《数据结构及算法分析——C语言描述》):将pivot放至最右端,维护两个下标i和j(此策略容易理解,但在处理临界情况(如相等元素、2/3小数组)时比较麻烦一点
  • i从左向右搜索找到大于等于a[pivot]的元素停止
  • j从右向左搜索找到小于等于a[pivot]的元素停止
  • 若i和j尚未交错,则swap(&a[i], &a[j]),然后继续步骤1和2;否则swap(&a[i], &a[pivot]),分解结束
策略三(详见《算法导论》) 以A[r]为“轴",记为x,从序列头部向尾部遍历(下标记为k)。若A[k] ≤ x,则放入左侧浅阴影区域;若A[k] ≥ x,则放入中间深阴影区域。遍历完成则分解步骤结束。此策略理论上需要同时维护两个区间边界,即图中的i和j,而实际代码中只需维护i即可



代码(采用分解策略二):
void swap(int * a, int * b){  //利用位异或进行swap
	if(a != b){  //位异或需要加条件判断,注意
		*a ^= *b;
		*b ^= *a;
		*a ^= *b;
	}
}

void med3(int a[], int left, int right){  //三数中值分割法获得pivot
	int mid = (left+right)/2;
	if(a[left] > a[mid])
		swap(&a[left], &a[mid]);
	if(a[left] > a[right])
		swap(&a[left], &a[right]);
	if(a[mid] > a[right])
		swap(&a[mid], &a[right]);  
	if(mid != right-1)
		swap(&a[mid], &a[right-1]);  //min放在a[left],max放在a[right],pivot放在a[right-1]
}

void q_sort(int a[], int left, int right){
	if(left < right){
		med3(a, left, right);
		if(right-left <= 2)  //长度小于3的小数组已被med3排序好,直接跳过
			return;
		int i = left, j = right-1, pivot = right-1;  //注意初始条件
		while(1){
			while(a[++i] < a[pivot]);  //遇到等于a[pivot]的元素也要停止
			while(a[--j] > a[pivot]);  //注意自增减的位置
			if(i < j){
				swap(&a[i], &a[j]);
			}
			else  //i和j交错时退出循环
				break;
		}
		swap(&a[i], &a[pivot]);
		q_sort(a, left, i-1);
		q_sort(a, i+1, right);
	}
}



代码(采用分解策略三):
void swap(int * a, int * b){  //swap的另一个版本
	int tmp = *a;
	*a = *b;
	*b = tmp;
}

int partition(int * a, int low, int high){
	int x = low+rand()%(high-low+1);  //随机选择a[low]...a[high]之间的一个作为pivot
	swap(&a[x], &a[high]);
	int div = low;  //实际只需要维护小区间和大区间的分界点div即可
	for(int k = low; k < high; k++){  //大区间和未遍历区间的分界点实际由游标k维护
		if(a[k] < a[high]){
			swap(&a[k], &a[div]);
			div++;
		}
	}
  	swap(&a[high], &a[div]);  //这步别漏
	return div;
}

void q_sort(int * a, int low, int high){  //快速排序没有“合并”过程
	if(low < high){  //注意这里判断条件只能是low < high,不能是low != high,因为对于只含两个元素的数组,partition后会出现low > high的情况
		int div = partition(a, low, high);
		q_sort(a, low, div-1);
		q_sort(a, div+1, high);
	}
}


综上,最终代码(位异或swap+三数中值分割法+策略3)为:

void swap(int * a, int * b){
	if(a != b){
		*a ^= *b;
		*b ^= *a;
		*a ^= *b;
	}
}

void q_sort(int a[], int left, int right){
	if(left < right){
		int mid = (left+right)/2;
		if(a[left] > a[mid]) swap(&a[left], &a[mid]);
		if(a[left] > a[right]) swap(&a[left], &a[right]);
		if(a[mid] < a[right]) swap(&a[mid], &a[right]);  //把pivot放在最右端right处方能用策略三

		int i, div = left;
		for(i = left; i < right; i++){
			if(a[i] < a[right]){
				swap(&a[i], &a[div]);
				div++;
			}
		}
		swap(&a[div], &a[right]);
		q_sort(a, left, div-1);
		q_sort(a, div+1, right);
	}
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值