【数据结构】快速排序与归并排序的实现

1. 快速排序

1.1 算法思想

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

1.2 算法实现

快速排序的实现方式有很多,我们主要介绍三种方法。

1.2.1 Hoare 法

  1. 利用两个变量left,right分别指向数组的起始位置与末尾位置。并且以数组第一个元素作为key值。

  2. right先从右往左依次遍历找到比key小的数,left从左往右依次遍历找到比key大的数。然后交换left与right下标对应的值。重复步骤2直至right>=left。

  3. 之后交换key与left或者right对应的值,并且把该位置记为mid。

  4. 最后划分区间[left,mid-1]与[mid+1,right]继续重复1,2步骤。直至不能划分。

思考:为什么最后相遇位置一定小于或等于 key 值?

我们知道right与left相遇无非两种情况:

情况一:right停住,left移动与right相遇·。因为right一直再找比key小的值,所以right停下位置一定比key小,相遇位置也一定比key小。

情况二:left停住,right移动与left相遇·。此时又分为两种情况:

left从未移动,右侧数据都比key大,相遇位置就是key,交换不变。

left移动过至少一次,也就是至少交换过一次,此时left停留位置的值是上一轮right所对应的值,又因为right一直在找比key小的值,所以相遇位置也一定比key小。

代码实现:

复制代码
void swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

int PartSort1(int* arr, int begin, int end)
{
	int left = begin, right = end;
	int keyi = begin;
	while (left < right)
	{
		//left<right防止越界
		//使用>=而不是>防止数据出现死循环
		while (left<right && arr[right]>=arr[keyi])
		    //寻找比key小的值
		    {
			    right--;
		    }
		while (left < right && arr[left] <= arr[keyi])
			//寻找比key大的值
		    {
			    left++;
		    }
		swap(&arr[left], &arr[right]);
	}
	int mid = left;
	swap(&arr[keyi], &arr[mid]);
	return mid;
}

void QuickSort(int* arr, int left, int right)
{
	if (left >= right)//不能划分
	{
		return;
	}
	int mid = PartSort1(arr, left, right);
	QuickSort(arr, left, mid - 1);//左区间
	QuickSort(arr, mid+1, right);//右区间
}

1.2.2 挖坑法

  1. 先将起始位置key值设为坑,之后right从右往左找比key值小的值,找到之后放入坑位,此时right就形成新的坑。然后left从左往右找比key大的值, 找到之后放入坑位,此时left就又形成新的坑。

  2. 最后left与right相遇,将key放入最后一个坑,并将该位置记为mid,。·

  3. 最后划分区间[left,mid-1]与[mid+1,right]继续重复1,2步骤。直至不能划分。

代码实现:

复制代码
int PartSort2(int* arr, int begin, int end)
{
	int left = begin, right = end;
	int hole = begin;//记录坑位
	int key = arr[left];
	while (left < right)
	{
		//left<right防止越界
		//使用>=而不是>防止数据出现死循环
		while (left < right && arr[right] >= key)
			//寻找比key小的值
		    {
			    right--;
		    }
		arr[hole] = arr[right];
		hole = right;
		while (left < right && arr[left] <= key)
			//寻找比key大的值
		    {
		    	left++;
		    }
		arr[hole] = arr[left];
		hole = left;
	}
	arr[hole] = key;
	return hole;
}

void QuickSort(int* arr, int left, int right)
{
	if (left >= right)//不能划分
	{
		return;
	}
	int mid = PartSort2(arr, left, right);
	QuickSort(arr, left, mid - 1);//左区间
	QuickSort(arr, mid+1, right);//右区间
}

1.2.3 双指针法

  1. 先定义一个prev指向数组首元素,然后定义一个cur指向第二个位置。

  2. cur从左往右依次遍历找key小的值,找到之后++prev,然后交换prev与cur指向的值。之后cur++继续遍历。(key为起始位置的值)

  3. 当cur遍历完之后,此时交换prev指向的值与key。将此时位置记为mid。

  4. 最后划分区间[left,mid-1]与[mid+1,right]继续重复1,2,3步骤。直至不能划分。

代码实现:

复制代码
int PartSort3(int* arr, int begin, int end)
{
	int prev = begin;
	int cur = begin + 1;
	int keyi = begin;
	while (cur <= end)
	{
		if (arr[cur] < arr[keyi])//小于则交换
		{
			swap(&arr[++prev], &arr[cur]);
		}
		cur++;	
	}
	swap(&arr[prev], &arr[keyi]);
	return prev;
}

void QuickSort(int* arr, int left, int right)
{
	if (left >= right)//不能划分
	{
		return;
	}
	int mid = PartSort3(arr, left, right);
	QuickSort(arr, left, mid - 1);//左区间
	QuickSort(arr, mid+1, right);//右区间
}

1.3 算法优化

1.3.1 改变基准元素

当数组有序时,我们再对其进行快速排序,其时间复杂度讲话劣化为O(N2)。

这时候我们为了防止这种现象,可以选择提前改变基准元素 key。

三数取中:即取出数组首尾以及中间元素,选取数值位于中间的元素作为准元素 key。

复制代码
int GetMidNum(int*arr, int left, int right)
{
	int mid = (left + right) >> 1;
	if (arr[mid] > arr[left])
	{
		if (arr[mid] < arr[right])
		{	//left  mid  right
			return mid;
		}
		else if (arr[left] > arr[right])
		{	//right  left  mid
			return left;
		}
		else
		{	//left  right  mid
			return right;
		}
	}
}

int PartSort3(int* arr, int begin, int end)
{
	int prev = begin;
	int cur = begin + 1;
	int keyi = begin;
	int mid=GetMidNum(arr, begin, end);
	swap(&arr[begin], &arr[mid]);
	while (cur <= end)
	{
		if (arr[cur] < arr[keyi])//小于则交换
		{
			swap(&arr[++prev], &arr[cur]);
		}
		cur++;	
	}
	swap(&arr[prev], &arr[keyi]);
	return prev;
}

随机数取中:三数取中有时候也并不能保证基准元素的准确性,这时候我们最好使用随机数获取基准值。

复制代码
int GetRanNum(int*arr, int left, int right)
{
	srand(time(0));//生成随机种子
	int mid = rand() % (right - left) + left;//随机数
	return mid;
}

1.3.2 区间优化

我们进行递归调用时,递归越深递归调用的次数就会越多,为了优化这个问题,我们可以当区间较小时采用其他排序,如插入排序。

复制代码
void QuickSort(int* arr, int left, int right)
{
	if (left >= right)//不能划分
		return;

	if ((right - left + 1) < 10)//小区间优化
	{
		InsertSort(arr+left, right - left + 1);
		return ;
	}
	int mid = PartSort3(arr, left, right);
	QuickSort(arr, left, mid - 1);//左区间
	QuickSort(arr, mid+1, right);//右区间
}

2. 归并排序

2.1 算法思想

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

2.2 算法步骤

  1. 创建一个与待排序数组同等大小的tmp数组。

  2. 然后将待排序数组分为两个子数组,让两个子数组有序。为了让这两个子数组有序,我们又要将每个子数组分为两个子数组,让其有序。

  3. 当子数组没有元素或者只有一个元素时,我们可以认为其有序,然后将两个子数组开始归并。

  4. 归并时因为两个子数组有序,我们可以定义两个指针begin1,begin2分别指向两个数组起始位置。然后遍历比较arr[begin1]与arr[begin2],取较小的元素尾插进tmp数组。

  5. 最后tmp数组数据拷贝回原数组。

2.3 动画演示

2.4 代码实现

复制代码
void _MergeSort(int* arr, int begin, int end, int* tmp)
{
	if (begin >= end)
		return;

	int mid = (begin + end) >> 1;
	_MergeSort(arr, begin, mid, tmp);//归并左区间
	_MergeSort(arr, mid+1, end, tmp);//归并右区间

	int i = begin;		
	int begin1 = begin, end1 = mid;
	int begin2 = mid + 1, end2 = end;
	while (begin1 <= end1 && begin2 <= end2)
	{
	    if (arr[begin1] < arr[begin2])
	    {
		    tmp[i++] = arr[begin1++];
	    }
	    else
	    {
		    tmp[i++] = arr[begin2++];
	    }
	}
	//若是还有区间存在数据
	while (begin1 <= end1)
	{
		tmp[i++] = arr[begin1++];
	}

	while (begin2 <= end2)
	{
		tmp[i++] = arr[begin2++];
	}

	//最后将归并完后后的数据拷贝回原数组
	memcpy(arr + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}

void MergeSort(int* arr, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail:");
		return;
	}
	_MergeSort(arr, 0, n - 1, tmp);
	free(tmp);
	tmp = NULL;
}

2.5 算法优化

2.5.1 区间优化

当递归调用层数越多时,最后三层的递归调用会浪费大量时间。为了避免这种情况,这时我们就可以采用小区间使用插入排序的方法。

复制代码
void _MergeSort(int* arr, int begin, int end, int* tmp)
{
	if (begin >= end)
		return;

	if (end - begin + 1 < 10)//小区间优化
	{
		InsertSort(arr + begin, end - begin + 1);
		return;
	}

	int mid = (begin + end) >> 1;
	_MergeSort(arr, begin, mid, tmp);//归并左区间
	_MergeSort(arr, mid+1, end, tmp);//归并右区间

	int i = begin;		
	int begin1 = begin, end1 = mid;
	int begin2 = mid + 1, end2 = end;
	while (begin1 <= end1 && begin2 <= end2)
	{
	    if (arr[begin1] < arr[begin2])
	    {    
		    tmp[i++] = arr[begin1++];
	    }
	    else
	    {
		    tmp[i++] = arr[begin2++];
	    }
	}
	//若是还有区间存在数据
	while (begin1 <= end1)
	{
		tmp[i++] = arr[begin1++];
	}

	while (begin2 <= end2)
	{
		tmp[i++] = arr[begin2++];
	}

	//最后将归并完后后的数据拷贝回原数组
	memcpy(arr + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}

2.5.2 判断区间有序

在归并排序合并时,如果两个区间是有序,即 arr[end1] <= arr[begin2] 时就不需要对其进行归并。

复制代码
void _MergeSort(int* arr, int begin, int end, int* tmp)
{
	if (begin >= end)
		return;

	int mid = (begin + end) >> 1;
	_MergeSort(arr, begin, mid, tmp);//归并左区间
	_MergeSort(arr, mid+1, end, tmp);//归并右区间

	int i = begin;		
	int begin1 = begin, end1 = mid;
	int begin2 = mid + 1, end2 = end;
	if (arr[begin2] < arr[end1])//区间有序则不合并
	{
		while (begin1 <= end1 && begin2 <= end2)
		{
			if (arr[begin1] < arr[begin2])
			{
				tmp[i++] = arr[begin1++];
			}
			else
			{
				tmp[i++] = arr[begin2++];
			}
		}
		//若是还有区间存在数据
		while (begin1 <= end1)
		{
			tmp[i++] = arr[begin1++];
		}

		while (begin2 <= end2)
		{
			tmp[i++] = arr[begin2++];
		}
		//最后将归并完后后的数据拷贝回原数组
		memcpy(arr + begin, tmp + begin, sizeof(int) * (end - begin + 1));

	}
}
相关推荐
Algo-hx2 小时前
数据结构入门 (三):链表的时空博弈 —— 循环链表与哑节点详解
数据结构·链表
南莺莺2 小时前
树的存储结构
数据结构·算法·
Sapphire~2 小时前
重学JS-009 --- JavaScript算法与数据结构(九)Javascript 方法
javascript·数据结构·算法
没有口袋啦2 小时前
《决策树、随机森林与模型调优》
人工智能·算法·决策树·随机森林·机器学习
小邓儿◑.◑3 小时前
贪心算法 | 每周8题(一)
算法·贪心算法
stolentime3 小时前
二维凸包——Andrew 算法学习笔记
c++·笔记·学习·算法·计算几何·凸包
Q741_1473 小时前
C++ 位运算 高频面试考点 力扣 371. 两整数之和 题解 每日一题
c++·算法·leetcode·面试·位运算
aramae3 小时前
链表理论基础
数据结构·c++·算法·leetcode·链表
2401_845417453 小时前
哈希表原理详解
数据结构·哈希算法·散列表