【C++版】排序算法详解

目录

直接插入排序

希尔排序

选择排序

冒泡排序

堆排序 

快速排序

hoare法

挖坑法

 前后指针法

 非递归版本

 快速排序中的优化

归并排序

递归版本

非递归版本

计数排序

 总结


直接插入排序

直接插入排序的思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列。

其实我们在打牌的时候就是运用了这种插入的思想

【C++版】排序算法详解_第1张图片
 

动图演示

【C++版】排序算法详解_第2张图片

步骤

  1.  从第一个元素开始,可以认为这个元素是有序的序列
  2. 遍历下一个元素,往这个有序的序列进行插入
  3. 从后往前扫描这个有序序列,如果待插入的元素小于这个有序序列中的元素,就将这个有序序列的元素移到下一位,直到遇到比这个待插入元素小的数据就停下来,将这个元素插入到这个数据的下一个位置。

【C++版】排序算法详解_第3张图片

代码实现: 

//直接插入排序—时间复杂度O(N^2)
void InsertSort(vector<int> &v, int n)
{
	//end只用到n-2位置即可
	for (int i = 0; i < n - 1; i++)
	{
		int end = i;
		int tmp = v[end + 1];
		while (end >= 0)
		{
			if (tmp < v[end])
			{
				v[end + 1] = v[end];
				end--;
			}
			else
			{
				break;
			}
		}
		v[end + 1] = tmp;
	}
}

希尔排序

希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数(gap),把待排序文件中所有记录分成多个组,所有距离为gap的记录分在同一组内,并对每一组内的记录进行排序。然后,取重复上述分组和排序的工作。当gap == 1时,所有记录在同一组内的数已经排好序,最后再来个直接插入排序,那么排序就完成了。

说白了希尔排序其实就是在直接插入排序的基础之上先给这组数分组做预排序,让这组数据接近有序,最好再用直接插入排序的思想,将排序完成。因为直接插入排序在一组数接近有序的情况下效率是非常高的。

在进行预排序的时候gap的选择很关键,太大了使数据接近有序的效果不行,太小了又浪费效率。所以可以参考下面的方法。

《数据结构-用面相对象方法与C++描述》--- 殷人昆

但是我这里还是用了gap = gap / 3 + 1这种方法,预排序完成的更快。

代码实现: 

//希尔排序
void ShellSort(vector<int>& v, int n)
{
	//gap > 1时是预排序
	//gap == 1时直接插入排序
	int gap = n;
	while (gap > 1)
	{
		gap = gap / 3 + 1;
		for (int i = 0; i < n - gap; i++)
		{
			int end = i;
			int tmp = v[end + gap];
			while (end >= 0)
			{
				if (tmp < v[end])
				{
					v[end + gap] = v[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			v[end + gap] = tmp;
		}
	}
}

选择排序

选择排序的思想是:每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
选择排序在众多排序算法中,效率并不算好,时间复杂度为O(N^2)无论在最好还是最坏的情况下都是一样的,所以在实际情况下,很少人会使用它。

动图演示

代码实现: 

代码上小优化,可以同时找最大和最小的数,使得排序更快

//选择排序—时间复杂度O(N^2)
void SelectSort(vector<int>& v, int n)
{
	int begin = 0, end = n - 1;
	int mini = begin, maxi = begin;
	while (begin < end)
	{
		for (int i = begin + 1; i < end; i++)
		{
			//找小的数
			if (v[i] < v[mini])
				mini = i;

			//找大的数
			if (v[i] > v[maxi])
				maxi = i;
		}
		Swap(v[begin], v[mini]);

		//如果begin和maxi重叠,那么需要修正maxi
		if (begin == maxi)
		{
			maxi = mini;
		}

		Swap(v[end], v[maxi]);
		begin++;
		end--;
	}
}

 冒泡排序

冒泡排序的基本思想是:每次比较两个相邻的元素,如果他们的顺序错误就把它们交换过来,就好像水底下的气泡一样逐渐向上冒。 

 动图演示

【C++版】排序算法详解_第4张图片

代码实现: 

//冒泡排序—时间复杂度O(N^2)
void BubbleSort(vector<int>& v, int n)
{
	for (int i = 0; i < n; i++)
	{
		int exchange = 0;
		for (int j = 1; j < n - i; j++)
		{
			if (v[j - 1] > v[j])
			{
				Swap(v[j - 1], v[j]);
				exchange = 1;
			}
		}
		if (exchange == 0)
		{
			break;
		}
	}
}

堆排序 

堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据,是一个二叉树,在排序之前需要建堆,建堆的方法有两种,①建大堆,即根节点的元素要比孩子结点的元素要大。②建小堆,即根节点的元素比孩子结点的元素要小需要注意的是排升序要建大堆,排降序建小堆。

堆的性质

  1. 堆中某个节点的值总是不大于或不小于其父节点的值;
  2. 堆总是一棵完全二叉树。

 

 代码实现: 

//堆排序—建大根堆
void AdjustDown(vector<int> &v, int n, int root)
{
	int child = root * 2 + 1;
	while (child < n)
	{
		//选出左右孩子中大的那个
		if (child + 1 < n && v[child + 1] > v[child])//防止数组越界
		{
			child++;
		}
		//孩子和父亲比较
		if (v[child] > v[root])
		{
			Swap(v[child], v[root]);
			root = child;
			child = root * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

void HeapSort(vector<int>& v, int n)
{
	for (int i = (n - 1 - 1 ) / 2; i >= 0; i--)
	{
		AdjustDown(v, n, i);//向下调整前提:左右子树必须是大/小堆
	}
	// o(N*logN)
	int end = n - 1;
	while (end > 0)
	{
		Swap(v[0], v[end]);
		AdjustDown(v, end, 0);
		end--;
	}
}

 快速排序

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

要实现快速排序可以有以下几种方法:

hoare法

【C++版】排序算法详解_第5张图片

需要注意的是选择左边的数作为key值,那必须右边先开始走,右边作为key值,那必须左边先开始走。因为这样做才能保证左边和右边相遇的时候是比key值要小的。

下面的挖坑法也是这样。 

代码实现: 

void QuickSort(vector<int>& v, int begin, int end)
{
	//如果只剩一个值或者区间不存在直接返回
	if (begin >= end)
	{
		return;
	}

	int left = begin, right = end;
	int keyi = left;
	while (left < right)
	{
		//右边先走,找到比key小的就停下来
		while (left < right && v[right] >= v[keyi])
		{
			right--;
		}

		//左边走,找到比key大的就停下来
		while (left < right && v[left] <= v[keyi])
		{
			left++;
		}

		Swap(v[left], v[right]);
	}
	Swap(v[keyi], v[left]);
	keyi = left;
	//左区间
	QuickSort(v, begin, keyi - 1);
	//右区间
	QuickSort(v, keyi + 1, end);
}

挖坑法

代码实现: 

void QuickSort(vector<int>& v, int begin, int end)
{
	//如果只剩一个值或者区间不存在直接返回
	if (begin >= end)
	{
		return;
	}

	int left = begin, right = end;
	int key = v[begin];
	int piti = begin;
	while (left < right)
	{
		//右边先走,找到比key小的就停下来
		while (left < right && v[right] >= key)
		{
			right--;
		}

		v[piti] = v[right];
		piti = right;

		//左边走,找到比key大的就停下来
		while (left < right && v[left] <= key)
		{
			left++;
		}

		v[piti] = v[left];
		piti = left;
	}
	
	v[piti] = key;

	//左区间
	QuickSort(v, begin, piti - 1);
	//右区间
	QuickSort(v, piti + 1, end);
}

 前后指针法

代码实现: 

void QuickSort(vector<int>& v, int begin, int end)
{
	//如果只剩一个值或者区间不存在直接返回
	if (begin >= end)
	{
		return;
	}

	int prev = begin, cur = prev + 1;
	int keyi = begin;

	while (cur <= end)
	{
		if (v[cur] < v[keyi] && ++prev != cur)
			Swap(v[prev], v[cur]);

		cur++;
	}
	Swap(v[prev], v[keyi]);
	keyi = prev;

	//左区间
	QuickSort(v, begin, keyi - 1);
	//右区间
	QuickSort(v, keyi + 1, end);
}

 非递归版本

递归的问题,在极端场景下如果深度太深,会出现栈溢出,所以我们要改成非递归版本

我这里就用个栈来模拟递归过程,当然你用个队列来实现也是可以的。

步骤:

  1. 先将大区间进行入栈,先入区间右边的数,再入区间左边的数
  2. 取栈顶元素,划分为小区间
  3. 将这些小区间全部入栈

一直重复这三步就模拟出了递归的过程

int PartQuickSort(vector<int> &v, int begin, int end)
{
	int key = v[begin];
	int piti = begin;
	while (begin < end)
	{
		//右边先走,找到比key小的就停下来
		while (begin < end && v[end] >= key)
		{
			end--;
		}

		v[piti] = v[end];
		piti = end;

		//左边走,找到比key大的就停下来
		while (begin < end && v[begin] <= key)
		{
			begin++;
		}

		v[piti] = v[begin];
		piti = begin;
	}
	v[piti] = key;
	return piti;

}

//利用栈来模拟递归从场景—栈是先进后出
void QuickSortNonR(vector<int> &v, int begin, int end)
{
	stack<int> st;
	//先入右在入左
	st.push(end);
	st.push(begin);

	while (!st.empty())
	{
		int left = st.top();
		st.pop();

		int right = st.top();
		st.pop();

		int keyi = PartQuickSort(v, left, right);

		//左区间入栈
		if (left < keyi - 1)
		{
			st.push(keyi - 1);
			st.push(left);
		}
		
		//右区间入栈
		if (keyi + 1 < right)
		{
			st.push(right);
			st.push(keyi + 1);
		}
	}
}

快速排序中的优化

①对于选择key值的优化,key的选择对于快速排序来说至关重要,因为如果你选择的key值为最大值或者最小值时快速排序的效率就会有所影响,key值最好的是取这组数据的中间值来达到完全二分的情况。所以折中一下在一组数据的首、尾、中间位置去中间值。

//三个数取中间值做为key
int GetMidKeyi(vector<int>& v, int begin, int end)
{
	int mid = (begin + end) / 2;
	if (v[begin] < v[mid])
	{
		if (v[mid] < v[end])
		{
			return mid;
		}
		else if (v[begin] < v[end])
		{
			return end;
		}
		else
		{
			return begin;
		}
	}
	else//v[begin] > v[mid]
	{
		if (v[mid] > v[end])
		{
			return mid;
		}
		else if (v[begin] < v[end])
		{
			return begin;
		}
		else
		{
			return end;
		}
	}
}

②对于递归的小区间优化,在递归到小区间时,我们可以选择用插入排序来完成排序工作,因为此时这段小区间的数据肯定是接近有序的而插入排序在接近排序的情况下效率最高为O(N)。

归并排序

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

【C++版】排序算法详解_第6张图片

动图演示

【C++版】排序算法详解_第7张图片

代码实现:

递归版本

//归并排序—时间复杂度O(N*logN)
//空间复杂度O(N)
void _MergeSort(vector<int> &v, int begin, int end, vector<int> &tmp)
{
	//递归返回的条件
	if (begin >= end)
		return;

	int mid = (begin + end) / 2;

	//进行递归
	_MergeSort(v, begin, mid, tmp);
	_MergeSort(v, mid + 1, end, tmp);

	int begin1 = begin, end1 = mid;
	int begin2 = mid + 1, end2 = end;
	int i = begin1;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (v[begin1] < v[begin2])
		{
			tmp[i++] = v[begin1++];
		}
		else
		{
			tmp[i++] = v[begin2++];
		}
	}

	//如果后面还有数,也要拷贝到tmp中去
	while (begin1 <= end1)
		tmp[i++] = v[begin1++];

	while (begin2 <= end2)
		tmp[i++] = v[begin2++];

	//深拷贝
	int j = begin, k = end - begin + 1;
	while(k-- && j <= end)
	{
		v[j] = tmp[j];
		j++;
	}
}


void MergeSort(vector<int> &v, int n)
{
	vector<int> tmp(n, 0);
	_MergeSort(v, 0, n - 1, tmp);
}

非递归版本

注意边界问题防止越界,对于越界的区间直接进行修正即可

//非递归版本
void MergeSortNonR(vector<int>& v, int n)
{
	vector<int> tmp(n, 0);

	int gap = 1;
	while (gap < n)
	{
		for (int i = 0; i < n; i += 2 * gap)
		{
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;

			//修正边界,防止越界
			if (end1 >= n)
			{
				end1 = n - 1;
				begin2 = n;
				end2 = n - 1;
			}
			else if (begin2 >= n)
			{
				begin2 = n;
				end2 = n - 1;
			}
			else if (end2 >= n)
			{
				end2 = n - 1;
			}


			int j = begin1;
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (v[begin1] < v[begin2])
				{
					tmp[j++] = v[begin1++];
				}
				else
				{
					tmp[j++] = v[begin2++];
				}
			}

			//如果后面还有数,也要拷贝到tmp中去
			while (begin1 <= end1)
				tmp[j++] = v[begin1++];

			while (begin2 <= end2)
				tmp[j++] = v[begin2++];
		}

		//深拷贝
		for (int k = 0; k < n; k++)
		{
			v[k] = tmp[k];
		}

		gap *= 2;
	}
}

这里是归并完一次,就将tmp数据一次性拷回原数组中。

你也可以归并完一部分就将这一部分的数据拷回原数组中去,也就是下面的这种写法。

或者

void MergeSortNonR(vector<int>& v, int n)
{
	vector<int> tmp(n, 0);

	int gap = 1;
	while (gap < n)
	{
		for (int i = 0; i < n; i += 2 * gap)
		{
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;

			//修正边界,防止越界
			if (end1 >= n || begin2 >= n)
			{
				break;
			}
			else if (end2 >= n)
			{
				end2 = n - 1;
			}

			int k = end2 - begin1 + 1;
			int j = begin1;
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (v[begin1] < v[begin2])
				{
					tmp[j++] = v[begin1++];
				}
				else
				{
					tmp[j++] = v[begin2++];
				}
			}

			//如果后面还有数,也要拷贝到tmp中去
			while (begin1 <= end1)
				tmp[j++] = v[begin1++];

			while (begin2 <= end2)
				tmp[j++] = v[begin2++];

			int m = i;
			while (k-- && m < n)
			{
				v[m] = tmp[m];
				m++;
			}
		}

		gap *= 2;
	}
}

由于我这里用的是C++来编写的,所以在将tmp数组中的数据拷贝回原数组中时, 不能使用memcpy函数进行拷贝,因为这里涉及到了深浅拷贝的问题,memcpy是浅拷贝。如果你用的是C语言来写的话就可以使用memcpy函数。

计数排序

计数排序的思想:统计相同元素出现的次数,然后将统计的结果写回到原数组中去。

【C++版】排序算法详解_第8张图片 

【C++版】排序算法详解_第9张图片

对于B应该开多少空间来存放数据合适,使用A中的最大值 - 最小值 + 1,即为B应该开的空间数。时间复杂度:O(max(range,N)) ,空间复杂度:O(range),range为所开空间数。

计数排序的局限性:

  1. 如果是浮点数,字符串之类的就排不了了。
  2. 如果数据范围很大,空间复杂度就会很高,相对不合适。

代码实现:

//计数排序
//时间复杂度:O(max(range,N))
//空间复杂度:O(range)
void CountSort(vector<int>& v, int n)
{
	int max = v[0], min = v[0];
	for (auto vv : v)
	{
		if (vv < min)
			min = vv;

		if (vv > max)
			max = vv;
	}

	int range = max - min + 1;
	vector<int> count(range, 0);

	//统计次数
	for (int i = 0; i < n; i++)
	{
		count[v[i] - min]++;
	}

	//回写排序
	int j = 0;
	for (int i = 0; i < range; i++)
	{
		while (count[i]--)
		{
			v[j++] = i + min;
		}
	}
}

 总结

稳定性的概念:假定在待排序的记录序列中,存在多个具有相同的关键字,若经过排序,这些记录的相对次序保持不变,即在原序列中,v[i]=v[j],且v[i]在v[j]之前,而在排序后的序列中,v[i]仍然在v[j]之前,则称这种排序算法使稳定的,否则称为不稳定。 

【C++版】排序算法详解_第10张图片

【C++版】排序算法详解_第11张图片

【C++版】排序算法详解_第12张图片

计数排序在这里不做比较。 

今天的分享就到这里,如果觉得有所收获的话,就给博主三连吧,创作不易,你的支持将是我创作的动力。

谢谢!!!

你可能感兴趣的:(数据结构,c++,排序算法,算法,数据结构)