【八大排序之交换与归并排序】

目录

1 交换排序

1.1 冒泡排序

1.2 快速排序(重点)

 1.2.1 挖坑法

1.2.2 前后指针法:

1.2.3 左右指针法

1.2.4 非递归实现快排

1.2.5 快排的3路并归

2 归并排序

2.1 归并排序的递归实现

 2.2 归并排序的非递归实现

 3.排序算法复杂度及稳定性分析



1 交换排序

1.1 冒泡排序

基本思想:

  • 所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动(升序)。
  • 冒泡排序的特性总结:
  • 1. 冒泡排序是一种非常容易理解的排序
  • 2. 时间复杂度:O(N^2)
  • 3. 空间复杂度:O(1)
  • 4. 稳定性:稳定 (可以控制相同数据的相对位置不发生改变)

 具体代码:

void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

void BubbleSort(int* a, int sz)
{
	int i = 0;
	int j = 0;
    int flag=0;
	for (i = 0; i < sz - 1; i++)
	{
		for (j = 0; j < sz - 1 - i; j++)
		{
			if (a[j] > a[j + 1])
			{
				Swap(&a[j], &a[j + 1]);
                flag=1;
			}
		}
        if(0==flag)
        {
            break;
        }
	}
}

冒泡排序比较简单,写起来也很容易,这里就不再多说了。

1.2 快速排序(重点)

基本思想:

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

 1.2.1 挖坑法

基本思想:

首先寻找一个坑位(一般选最左边或者中间或者最右边)假设我这里选最左边为坑位,然后用一个关键值key保存坑位值;再从最右边找到小于key的值,找到后就将坑位的值改为找到后的值,然后将找到位置的下标作为新的坑位;再从左边去找到大于坑位的值,找到后同样将坑位的值改为找到后的值,然后将找到位置的下标作为新的坑位;直至相遇。最后将相遇点的值改为key.  这样就完成了将key排到了正确位置。然后分治,用递归将左边与右边分别排,直至所有元素都排到了正确位置。

 具体代码实现:

void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

void QuickSort(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}

	int begin = left;
	int end = right;
	int pivot = begin;
	int key = a[pivot];
	while (begin < end)
	{
		//右边找小 放在左边
		while (begin < end && a[end] >= key)
		{
			end--;
		}
		a[pivot] = a[end];
		pivot = end;
		//左边找大 放在右边
		while (begin < end && a[begin] <= key)
		{
			begin++;
		}
		a[pivot] = a[begin];
		pivot = begin;
	}
	a[pivot] = key;

	QuickSort(a, left, pivot - 1);
	QuickSort(a, pivot+1, right);

}

注意事项:

  1. 进行key值单趟排序也可以单独分装一个PartSort函数来实现(通过返回值来确定已经排序好的key值),这里我就不分装了,后面前后指针与左右指针采用的是分装过的。
  2. 右边找小和左边找大时要加上=,否则可能会造成死循环。

 优化版本:

优化版本主要从两点优化:

1 实现快排的3中方法比较高效的原因就是用了二叉树的思想先排出较靠近中间值的元素,但是上面的方法并不能保证我们选择的key是较为靠近中间值,所以我们用一个3数取中的方法来处理。

2 由于排最后的几个元素用快排的效率较低,所以我们用直接插入来排(小区间优化

具体代码:

//3数取中
int GetMidIndex(int* a, int left, int right)
{
	int mid = (left + right) / 2;
	if (a[left] < a[mid])
	{
		if (a[right] > a[mid])
			return mid;
		else if (a[right] < a[left])
			return left;
		else
			return right;
	}
	else
	{
		if (a[right] > a[left])
			return left;
		else if (a[right] < a[mid])
			return mid;
		else
			return right;
	}
}

void QuickSort(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int index = GetMidIndex(a, left, right);
	Swap(&a[left], &a[index]);
	int start = left;
	int end = right;
	int pivot = start;
	int key = a[pivot];
	while (start < end)
	{
		while (start < end && a[end] <= key)
		{
			end--;
		}
		a[pivot] = a[end];
		pivot = end;

		while (start < end && a[start] >= key)
		{
			start++;
		}
		a[pivot] = a[start];
		pivot = start;
	}

	a[pivot] = key;

	//小区间优化
	if (pivot - 1 - left < 10)
	{
		InsertSort(a+left, pivot - 1 - left + 1);
	}
	else
	{
		QuickSort(a, left, pivot - 1);
	}

	if (right - (pivot + 1) < 10)
	{
		InsertSort(a+pivot+1, right - (pivot + 1) + 1);
	}
	else
	{
		QuickSort(a, pivot + 1, right);
	}
}

1.2.2 前后指针法:

基本思想:

目的都是与挖坑法一样,就是将选出的数排到正确的位置,先保存最左边元素;然后定义left为prev,下一位为cur,从cur开始向后找小,找到后就将++prev的值与cur交换,直至找到right,最后再将prev与保存的关键值交换直到cur访问到最后一个元素。最后别忘了将保存的下标值与下标为prev的元素交换。

具体代码实现:

int PartSort2(int* a, int left, int right)
{
	int keyi = left;
	int prev = left;
	int cur = left + 1;;
	while (cur <= right)
	{
		while (a[cur] < a[keyi] && ++prev != cur)
		{
			Swap(&a[prev], &a[cur]);
		}

		cur++;

	}
	Swap(&a[keyi], &a[prev]);
	return prev;
}

void QuickSort2(int* a, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	int keyi = PartSort2(a, begin, end);
	
	QuickSort2(a, begin, keyi - 1);
	QuickSort2(a, keyi + 1, end);
}

1.2.3 左右指针法

基本思想:

先保存最左边的值,从右边找比保存值小的元素,从左边找到比保存值大的元素,然后交换,直至相遇,再交换相遇点与保存值,将相遇点的下标返回。

具体代码实现:

int PartSort3(int* a, int left, int right)
{
	int keyi = left;
	while (left < right)
	{
		while (left < right && a[right] >= a[keyi])
		{
			right--;
		}
		while (left < right && a[left] <= a[keyi])
		{
			left++;
		}
		if (left < right)
		{
			Swap(&a[left], &a[right]);
		}
	}

	int meeti = left;
	Swap(&a[keyi], &a[right]);
	return meeti;
}

void QuickSort3(int* a, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	int keyi = PartSort2(a, begin, end);
	
	QuickSort3(a, begin, keyi-1);
	QuickSort3(a, keyi + 1, end);
}


1.2.4 非递归实现快排

基本思想:

借助栈来实现(用队列也行,只是栈更能生动的表示分治时不断递归的过程),先让最右边的下标先入栈,再让最左边的下标入栈,当栈不为空时然后出栈,先出的就是最左边的下标,再出最右边的下标,单趟排序后再不断重复入栈出栈过程,直至栈为空就排序好了。

具体代码实现:

void QuickSort3NonR(int* a, int sz)
{
	ST st;
	StackInit(&st);

	StackPush(&st, sz - 1);
	StackPush(&st, 0);

	while (!StackEmpty(&st))
	{
		int left = StackTop(&st);
		StackPop(&st);
		int right= StackTop(&st);
		StackPop(&st);

		int keyIndex = PartSort3(a, left, right);
		//[left,keyIndex-1] keyIndex [keyIndex+1,right]
		if (keyIndex + 1 < right)
		{
			StackPush(&st, right);
			StackPush(&st, keyIndex + 1);
		}
		if (left < keyIndex)
		{
			StackPush(&st, keyIndex);
			StackPush(&st, left);
		}
	}
	StackDestroy(&st);
}

注意事项:

  1. 入栈的时候其实先入右和先入左都行,只是取的时候要控制好取得是啥。
  2. 单趟排序用快排的3中方法都行,但是为了调试效果明显最好就不要加3数取中了。

1.2.5 快排的3路并归

通过上面我们知道快排当重复数据过多时效率就比较低下了,解决得办法是用3路并归来实现。具体思路是单趟排序将其分为3个区间[left,begin-1] [begin,end] [end+1,right] 

其中[begin,end]区间维护得是每次选出来的等于key的数据。

具体代码:

void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;
	int mid = GetMidNum(a, left, right);
	Swap(&a[left], &a[mid]);

	int key = a[left];
	int begin = left, cur = left + 1, end = right;
	//分成3个区间[left,begin-1] [begin,end] [end+1,right]
	while (cur <= end)
	{
		if (a[cur] < key)
		{
			Swap(&a[cur], &a[begin]);
			cur++;
			begin++;
		}
		else if (a[cur] == key)
			cur++;
		else
		{
			Swap(&a[cur], &a[end]);
			end--;
		}
	}

	QuickSort(a, left, begin - 1);
	QuickSort(a, end + 1, right);
}

快速排序的特性总结:
1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫 快速 排序
2. 时间复杂度: O(N*logN)
3. 空间复杂度: O(logN)
4. 稳定性: 不稳定 (单趟排序时有可能将相同数据的相对位置打乱)


2 归并排序

2.1 归并排序的递归实现

基本思想:

归并排序( MERGE-SORT )是建立在归并操作上的一种有效的排序算法 , 该算法是采用 分治 法( Divide and Conquer )的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序 列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有表,称为二 路归并。

具体代码:

void _MergeSort(int* a, int left, int right, int* tmp)
{
	if (left >= right)
	{
		return;
	}
	int mid = (left + right) >> 1;

	//假设[left,mid]  [mid+1,right] 有序
	_MergeSort(a, left, mid, tmp);
	_MergeSort(a, mid + 1, right, tmp);

	int begin1 = left, end1 = mid;
	int begin2 = mid + 1, end2 = right;
	int index = left;
	//合并
	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++];
	}
	//拷贝过去
	for (int i = left; i <= right; i++)
	{
		a[i] = tmp[i];
	}
}
void MergeSort(int* a, int sz)
{
	int* tmp = (int*)malloc(sizeof(int) * sz);
	_MergeSort(a, 0, sz - 1, tmp);
	free(tmp);
}

注意事项:

当数据归并到最后一部分时别忘了将数据拷回去。

 2.2 归并排序的非递归实现

基本思想:

归并排序的非递归用栈和队列实现起来比较困难,比较优的方法是借助循环来整。

具体代码:

第一种方式:

void MergeSortNonR(int* a, int sz)
{
	int* tmp = (int*)malloc(sizeof(int) * sz);
	if (!tmp)
	{
		perror("malloc:");
		exit(-1);
	}

	int rangeN = 1;//代表每组归并的数据个数
	while (rangeN < sz)
	{
		for (int i = 0; i < sz; i += 2 * rangeN)
		{
			int begin1 = i, end1 = i + rangeN - 1,
				begin2 = i + rangeN, end2 = i + 2 * rangeN - 1,
				index = i;
			//printf("[%d %d] [%d %d]\n", begin1, end1, begin2, end2);
			//通过打印不难发现越界有三种情况:
			//1 end1 begin2 end2 越界
			//2 begin2 end2 越界
			//3 只有end2越界//

			if (end1 >= sz)
			{
				//有两种处理方式:
				break;//直接break跳出去,这种方式拷贝只能放在里面一次一次的拷贝,不能一次性全部拷贝,否则有可能将随机数(越界)给拷了回去
				//第二种方式:修正

				/*end1 = sz - 1;
				begin2 = sz;
				end2 = sz - 1;*/    //这里注意要将begin2和end2修正成begin2>end2形式(不能够加+),不让其进入下面的循环
			}
			else if (begin2 >= sz)
			{
				break;//1 直接跳出

				/*begin2 = sz;
				end2 = sz - 1;*/   //修正
			}
			else if (end2 >= sz)
			{
				end2 = sz - 1;
			}
			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++];
			memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));//不能写成2*rangeN,有可能会越界
		}
		rangeN *= 2;
		//memcpy(a, tmp, sizeof(int) * sz);//这种方式是错误的,由于没有修正让随机数也被拷贝了过去,让原数据被覆盖
	}
}

第二种方式:

void MergeSortNonR(int* a, int sz)
{
	int* tmp = (int*)malloc(sizeof(int) * sz);
	if (!tmp)
	{
		perror("malloc:");
		exit(-1);
	}

	int rangeN = 1;//代表每组归并的数据个数
	while (rangeN < sz)
	{
		for (int i = 0; i < sz; i += 2 * rangeN)
		{
			int begin1 = i, end1 = i + rangeN - 1,
				begin2 = i + rangeN, end2 = i + 2 * rangeN - 1,
				index = i;
			//printf("[%d %d] [%d %d]\n", begin1, end1, begin2, end2);
			//通过打印不难发现越界有三种情况:
			//1 end1 begin2 end2 越界
			//2 begin2 end2 越界
			//3 只有end2越界//

			if (end1 >= sz)
			{
				//第二种方式:修正
				end1 = sz - 1;
				begin2 = sz;
				end2 = sz - 1;    //这里注意要将begin2和end2修正成begin2>end2形式(不能够加+),不让其进入下面的循环
				//如果是整体拷贝end2可以修改成任意小于begin2的数,一个一个拷贝的话只能修改成sz-1
			}
			else if (begin2 >= sz)
			{
				begin2 = sz;
				end2 = sz - 1;   //修正成不合理的形式
			}
			else if (end2 >= sz)
			{
				end2 = sz - 1;
			}
			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++];
			//memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));//不能写成2*rangeN,有可能会越界
		}
		rangeN *= 2;
		memcpy(a, tmp, sizeof(int) * sz);

	}
}

注意事项:

注意事项代码中都已经给出了说明。


归并排序的特性总结:
1. 归并的缺点在于需要 O(N) 的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
2. 时间复杂度: O(N*logN)
3. 空间复杂度: O(N)
4. 稳定性: 稳定 (可以控制相同数据的相对位置不发生改变)

 3.排序算法复杂度及稳定性分析


 排序这方面就到处结束啦,如果有哪儿不对的地方希望各位佬能够指正。

  • 56
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 39
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 39
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值