排序:【详解】快排三种方法实现(优化版,三数取中),归并排序,递归和非递归版本

本文详细介绍了快速排序和归并排序的原理与实现,包括递归与非递归版本,并探讨了不同版本的特点及优化方法。

快速排序递归实现

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。将区间按照基准值划分为左右两半部分的常见方式有:

  1. hoare版本
    确定基准后,从左边开始找大于基准值的元素,右边开始找小于基准值的元素,找到后交换两个元素然后继续下一轮寻找,直到左右相遇
    在这里插入图片描述
    代码实现:
void Swap(int* a, int* b) {
	int temp = *a;
	*a = *b;
	*b = temp;
}


// 快速排序hoare版本
int PartSort1(int* a, int left, int right) {
	assert(a);
	int keyi = right;
	while (left < right) {
		while (left < right) {//左边开始找小于基准值的
			if (a[left] > a[keyi]) {
				break;
			}
			left++;
		}
		while (left < right) {//右边找大于基准值的
			if (a[right] < a[keyi]) {
				break;
			}
			right--;
		}
		Swap(&a[left], &a[right]);//交换小的与大的
	}
	Swap(&a[left], &a[keyi]);//左右相遇的位置放入基准值,左边都比他小,右边都比他大
	return left;
}


void QuickSort(int* a, int left, int right) {
	assert(a);

	if (left >= right) {
		return;
	}
	int div = PartSort1(a, left, right);//每一趟放入div位置的数都是最终有序时该数所在的位置
	QuickSort(a, left, div - 1);//递归排div位置的左边和右边
	QuickSort(a, div + 1, right);
}
  1. 挖坑法

确定基准后,定义临时变量保存基准值。基准值的位置就相等于一个“坑”然后从左边开始找大于基准值的元素,找到后将该值放入“坑”里,该元素原来的位置就是新的“坑”。然后从右边开始找小于基准值的,找到后放入新的“坑”。重复上述操作,直到左右边界相遇。
代码实现:

int PartSort2(int* a, int left, int right) {
	assert(a);
	int keyi = right;
	int temp = a[keyi];//保存基准值
	while (left < right) {
		while (left < right) {
			if (a[left] > temp) {//左边有比基准值大的,直接与基准值交换位置
				a[keyi] = a[left];
				keyi = left;
				break;
			}
			left++;
		}
		while (left < right) {//右边有比基准值小的,交换关键值位置
			if (a[right] < temp) {
				a[keyi] = a[right];
				keyi = right;
				break;
			}
			right--;
		}
	}
	a[keyi] = temp;//最终在基准值的位置放入最初保存的基准值
	return left;
}
  1. 前后指针版本
    确定基准后,定义前后两个指针(数组中即两个下标),开始两个指针指向同一个元素,前指针开始遍历元素,元素小于基准值的,前后指针交换元素且一同向下遍历一个元素,前指针遇见大于基准值的前指针向下遍历一个元素,后指针不动,前指针遍历结束时,与后指针的下一个元素交换。基准值放入最终位置。
    代码实现:

// 快速排序前后指针法
int PartSort3(int* a, int left, int right) {
	assert(a);
	int cur = left;
	int prev = left - 1;
	int keyi = right;
	while (cur <= right) {
		if (a[cur] < a[keyi] && ++prev != cur) {//当前位置的值小于基准值,perv++,若不等于cur,交换perv与cur的值
			Swap(&a[cur], &a[prev]);
		}
		cur++;//cur每次往后遍历
	}
	Swap(&a[prev + 1], &a[keyi]);//包括a[prev]之前的值均小于基准值,交换a[prev + 1]与基准值
	return prev + 1;
}

优化:
可以看到快排每次取基准值,把数组分为两个区间,然后递归再去分。理想情况下每次都是把一个数组均等分为两个区间,这样效率最高。但实际中往往不会这样,有时还会有最坏的情况,即每次取得基准值都是最大或最小的,这样每排一个基准值数组其实并没有分为两个区间,只是下次在排时少一个元素。所以在选取基准值时要注意不能选到最大或最小的,解决这一问题最简单的方法就是三数取中,一般取数组开始的元素,末尾的元素和中间的元素,取这三个元素中间的值为基准值,这样就避免了最坏的情况。
代码实现:


int GetMidNum(int *a, int left, int right) {//返回三个数中中间的数
	int mid = (left + right) / 2;
	if (a[left] > a[mid]) {
		if (a[mid] > a[right]) {
			return mid;
		}
		else {
			return a[left] > a[right] ? right : left;
		}
	}

	if (a[mid] < a[right]) {
		return mid;
	}
	else {
		return a[left] > a[right] ? left : right;
	}
}
// 快速排序挖坑法
int PartSort2(int* a, int left, int right) {
	assert(a);
	Swap(&a[GetMidNum(a, left, right)], &a[right]);
	int keyi = right;
	int temp = a[keyi];//保存关键值
	while (left < right) {
		while (left < right) {
			if (a[left] > temp) {//左边有比关键值大的,直接与关键值交换位置
				a[keyi] = a[left];
				keyi = left;
				break;
			}
			left++;
		}
		while (left < right) {//右边有比关键值小的,交换关键值位置
			if (a[right] < temp) {
				a[keyi] = a[right];
				keyi = right;
				break;
			}
			right--;
		}
	}
	a[keyi] = temp;//最终在关键值的位置放入最初保存的关键值
	return left;
}

特点:

  1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
  2. 时间复杂度:O(N*logN)
  3. 空间复杂度:O(logN)
  4. 稳定性:不稳定

快速排序非递归实现

其实递归就是多次调用同一个函数,创建多个栈帧。快排的递归就是函数主题部分执行完毕之后,又调用两个递归函数。我们可以借助栈来实现创建栈帧的过程。每调用一次函数,就把该函数放入栈中,执行函数时出栈即可。
代码实现:

// 初始化栈 
void StackInit(Stack* ps) {
	assert(ps);//对指针判空
	ps->_a = (STDataType*)malloc(sizeof(STDataType));//为栈开辟空间
	ps->_top = 0;
	ps->_capacity = 1;
}

// 入栈 
void StackPush(Stack* ps, STDataType data) {
	assert(ps);
	if (ps->_top == ps->_capacity) {//栈已满,需要增容
		//定义临时指针来接受重新开辟的空间,防止数据丢失,每次增容为原来的2倍
		STDataType* temp = (STDataType*)realloc(ps->_a, sizeof(STDataType) * ps->_capacity * 2);
		if (temp == NULL) {
			printf("申请空间失败!\n");//若空间申请失败,则报错且中止程序;
			assert(0);
		}
		else {//申请空间成功,将地址赋值到原指针,栈的空间翻倍
			ps->_a = temp;
			ps->_capacity *= 2;
		}
	}
	ps->_a[ps->_top] = data;//栈顶位置赋值为data
	ps->_top++;//栈顶后移一位
}

// 出栈 
void StackPop(Stack* ps) {
	if (StackEmpty(ps)) {//如果栈为空,则直接返回
		return;
	}
	ps->_top--;//栈不为空,栈顶前移一位
}

// 获取栈顶元素 
STDataType StackTop(Stack* ps) {
	if (StackEmpty(ps)) {//如果栈为空,则直接返回
		printf("栈为空!\n");
		return -1;
	}
	return ps->_a[ps->_top - 1];//栈不为空,返回栈顶元素
}
// 获取栈中有效元素个数 
int StackSize(Stack* ps) {
	assert(ps);//对指针判空
	return ps->_top;//栈内共有top个有效数据
}

// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0 
int StackEmpty(Stack* ps) {
	assert(ps);//对指针判空
	if (ps->_top) {
		return 0;
	}
	else {//如果top == 0表示栈为空
		return 1;
	}
}

// 销毁栈 
void StackDestroy(Stack* ps) {
	assert(ps);
	free(ps->_a);//释放申请的空间
}

// 快速排序 非递归实现
void QuickSortNonR(int* a, int left, int right) {
	assert(a);
	int div = PartSort2(a, left, right);
	Stack s;//创建栈,来实现非递归
	StackInit(&s);
	StackPush(&s, div + 1);
	StackPush(&s, right);
	StackPush(&s, left);
	StackPush(&s, div - 1);//将第一趟排好后,将左右两个区间都压入栈里,先入右栈,类似于先递归左边
	while (!StackEmpty(&s)) {//栈不为空时执行循环
		int right = StackTop(&s);
		StackPop(&s);
		int left = StackTop(&s);
		StackPop(&s);//取区间

		if (left < right) {//判断是否需要排序,如需要,则排序,然后将分割的两个区间继续入栈
			div = PartSort2(a, left, right);
			StackPush(&s, div + 1);
			StackPush(&s, right);
			StackPush(&s, left);
			StackPush(&s, div - 1);

		}
	}
}

归并排序递归版

归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
图解:
在这里插入图片描述
代码实现:

void MergeArr(int* a, int begin1, int end1, int begin2, int end2, int* tmp)
{
	int left = begin1, right = end2;//合并两个有序区间
	int index = begin1;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
			tmp[index++] = a[begin1++];
		else
			tmp[index++] = a[begin2++];
	}

	while (begin1 <= end1)
		tmp[index++] = a[begin1++];

	while (begin2 <= end2)
		tmp[index++] = a[begin2++];

	// 把归并好的再tmp的数据在拷贝回到原数组
	for (int i = left; i <= right; ++i)
		a[i] = tmp[i];
}

void _MergeSort(int* a, int left, int right, int* tem) {
	if (left >= right) {//如果不能再进行分割,返回
		return;
	}

	int mid = (left + right) / 2; //将区间分割为左右两部分,分别递归进行归并排序
	_MergeSort(a, left, mid, tem);
	_MergeSort(a, mid + 1, right, tem);

	MergeArr(a, left, mid, mid + 1, right, tem);
}

// 归并排序递归实现
void MergeSort(int* a, int n) {
	assert(a);
	int* tem = (int*)malloc(sizeof(int) * n);//创建临时空间保存归并的数组
	_MergeSort(a, 0, n - 1, tem);//调用子函数完成归并排序
	free(tem);
}

特点:

  1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
  2. 时间复杂度:O(N*logN)
  3. 空间复杂度:O(N)
  4. 稳定性:稳定

归并排序非递归版

我们可以直接把数组看为n个区间,省去了递归版的划分区间,然后只考虑合并的问题。合并的时候要考虑到区间总数可能为奇数个,导致有的区间只有第一个区间,没有与之合并的第二个区间,,然后就是选择合并的迭代条件,不断迭代。完成合并。
代码实现:

/归并排序非递归实现
void MergeSortNonR(int* a, int n) {
	assert(a);
	int* tem = (int*)malloc(sizeof(int) * n);//创建临时空间保存归并的数组
	int gap = 1;
	while (gap < n) {
		for (int i = 0; i < n; i += 2 * gap) {
			// [i,i+gap-1] [i+gap, i+2*gap-1]
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;
			// 1、合并时只有第一组,第二组不存在,就不需要合并
			if (begin2 >= n) {
				break;
			}
			// 2、合并时第二组只有部分数据,需要修正end2边界
			if (end2 >= n) {
				end2 = n - 1;
			}
			MergeArr(a, begin1, end1, begin2, end2, tem);
		}
		gap *= 2;
	}
	free(tem);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值