【C++】八大排序

前言

此篇博客使用C++语言来实现排序讲解,以下是一个Sort类,我将把所有的排序算法封装成静态成员函数函数放到这个类中。我会将所有实现代码和测试代码放在文章最后

template<class T>
struct my_less {   // 升序
	bool operator()(const T& a, const T& b) { return a > b; }
};

template<class T>
struct my_greater { // 降序
	bool operator()(const T& a, const T& b) { return a < b; }
};
template<class T, class C = my_less<T>>  //默认升序
class Sort{
public:
	static C comp;	//仿函数类对象,用于控制升序或者降序
	static void mySwap(T& a, T& b) { T tmp = a; a = b; b = tmp; } //交换两个元素
	static void InsertSort(vector<T>& v);		// 直接插入排序
	static void ShellSort(vector<T>& v);		// 希尔排序
	static void SelectSort(vector<T>& v);		// 选择排序
	static void BubbleSort(vector<T>& v);		// 冒泡排序
	static void HeapSort(vector<T>& v);			// 堆排序
	static void MergeSort(vector<T>& v);		// 归并排序(递归)
	static void nonreMergeSort(vector<T>& v);   // 归并排序(非递归)

	//快速排序
	static void hoareQuickSort(vector<T>& v);   // hoare版快速排序(递归)
	static void digpitQuickSort(vector<T>& v);  // 挖坑版快速排序(递归)
	static void froaftQuickSort(vector<T>& v);  // 前后指针版快速排序(递归)
	static void nonreQuickSort(vector<T>& v);   // 快速排序(非递归)
	static int midinTree(vector<T>& v, int left, int right); // 三数取中(优化)

	//递归子函数
	static void _MergeSort(vector<T>& v, vector<T>& tmp, int left, int right);
	static void _hoareQuickSort(vector<T>& v, int begin, int end);
	static void _digpitQuickSort(vector<T>& v, int begin, int end);
	static void _froaftQuickSort(vector<T>& v, int left, int right);
};

template<class T, class C>
C Sort<T, C>::comp;

1. 插入排序

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

实现代码

template<class T, class C>
void  Sort<T, C>::InsertSort(vector<T>& v) {
	for (int i = 0; i < v.size(); i++) { // i 表示准备插入数据的下标
		if (i == 0) continue;
		int temp = v[i], cur = i - 1;    // temp表示准备插入的数据 cur为对比的数据下标
		while (cur >= 0) {               // 当cur < 0 时说明没有数据比temp大,则退出
			if (comp(temp, v[cur])) {    // 默认是降序的情况下,若temp > v[cur]则进入
				v[cur + 1] = v[cur];     // 将cur 位置的数据移动至cur + 1
				cur--;                   // 向前寻找插入位置
			}
			else {                       // 说明前面[0, cur]数据均大于temp,[cur + 1, i]位置均小于temp
				break;
			}
		}
		v[cur + 1] = temp;               // 在cur + 1位置插入temp      
	}
}

测试代码

#include "Sort.h"

void printVector(vector<int>& v) {
	for (const auto& el : v) {
		cout << el << " ";
	}
	cout << endl;
}

int main() {
	vector<int> v1;
	vector<int> v2{ 3, 2, 5, 1 ,4 };
	vector<int> v3;
	for (int i = 0; i < 10; i++) {
		v3.push_back(rand() % 100);
	}
	
	Sort<int>::InsertSort(v1);
	Sort<int>::InsertSort(v2);
	Sort<int>::InsertSort(v3);
	printVector(v1);
	printVector(v2);
	printVector(v3);
	cout << endl;
	return 0;
}

直接插入排序的特性总结:

  • 元素集合越接近有序,直接插入排序算法的时间效率越高
  • 时间复杂度:O(N^2)
  • 空间复杂度:O(1),它是一种稳定的排序算法
  • 稳定性:稳定

2. 希尔排序

基本思想:
希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当组距=1时,所有记录在统一组内排好序。
实现代码

template<class T, class C> 
void Sort<T, C>::ShellSort(vector<T>& v) {
	int gap = v.size();                 // 步长
	while (gap > 1) {                   // 当 gap == 1时结束,并且数组为空时不会进入
		gap = gap / 3 + 1;              // gap不断减小
		for (int i = 0; i < v.size() - gap; i++) {
			int temp = v[i + gap];      // temp表示准备插入的数据 
			int cur = i;				// cur为对比的数据下标
			while (cur >= 0) {
				if (comp(temp, v[cur])) { // 默认是降序的情况下,若temp > v[cur]则进入
					v[cur + gap] = v[cur];
					cur -= gap;
				}
				else {
					break;				// 说明前面[0, cur]数据均大于temp,[cur + 1, i]位置均小于temp
				}
			}
			v[cur + gap] = temp;		// 在cur + 1位置插入temp    
		}
	}
}

测试代码

void shellSortTest() {
	vector<int> v1;
	vector<int> v2{ 3, 2, 5, 1 ,4 };
	vector<int> v3;
	for (int i = 0; i < 10; i++) {
		v3.push_back(rand() % 100);
	}

	Sort<int>::ShellSort(v1);
	Sort<int>::ShellSort(v2);
	Sort<int>::ShellSort(v3);
	printVector(v1);
	printVector(v2);
	printVector(v3);
	cout << endl;
}

希尔排序的特性总结:

  • 希尔排序是对直接插入排序的优化。
  • 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
  • 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些树中给出的希尔排序的时间复杂度都不固定
    在这里插入图片描述

3. 选择排序

基本思想:
每一次从待排序的数据元素中选出最小和最大的一个元素,存放在序列的起始位置和结束位置,直到全部待排序的数据元素排完。
实现代码:

template<class T, class C>
void Sort<T, C>::SelectSort(vector<T>& v) {
	if (v.size() == 0) return;
	int begin = 0, end = v.size() - 1;
	while (begin < end) {
		int max = begin, min = begin;
		for (int i = begin; i <= end; i++) {
			if (comp(v[i], v[max])) max = i;
			if (comp(v[min], v[i])) min = i;
		}
		mySwap(v[begin], v[max]);
		mySwap(v[end], v[min]);
		begin++;
		end--;
	}
}      

测试代码

void selectSortTest() {
	vector<int> v1;
	vector<int> v2{ 3, 2, 5, 1 ,4 };
	vector<int> v3;
	for (int i = 0; i < 10; i++) {
		v3.push_back(rand() % 100);
	}

	Sort<int>::SelectSort(v1);
	Sort<int>::SelectSort(v2);
	Sort<int>::SelectSort(v3);
	printVector(v1);
	printVector(v2);
	printVector(v3);
	cout << endl;
}

直接选择排序的特性总结:

  • 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
  • 时间复杂度:O(N^2)
  • 空间复杂度:O(1)
  • 稳定性:不稳定

4. 堆排序

基本思想:
堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。
实现代码

template<class T, class C>
void Sort<T, C>::HeapSort(vector<T>& v) {
	if (comp(1, 0)) {
		priority_queue<T, vector<int>, less<int>> heap(v.begin(), v.end()); // 降序使用大堆
		for (int i = 0; i < v.size(); i++) {
			v[i] = heap.top();                         // 堆顶元素最大
			heap.pop();								   // 删除最大元素
		}
	}
	else {
		priority_queue<T, vector<int>, greater<int>> heap(v.begin(), v.end()); // 升序使用小堆
		for (int i = 0; i < v.size(); i++) {
			v[i] = heap.top();
			heap.pop();
		}
	}
}     // 堆排序

测试代码

void heapSortTest() {
	vector<int> v1;
	vector<int> v2{ 3, 2, 5, 1 ,4 };
	vector<int> v3;
	for (int i = 0; i < 10; i++) {
		v3.push_back(rand() % 100);
	}

	Sort<int, my_greater<int>>::HeapSort(v1);
	Sort<int, my_greater<int>>::HeapSort(v2);
	Sort<int, my_greater<int>>::HeapSort(v3);
	printVector(v1);
	printVector(v2);
	printVector(v3);
	cout << endl;
}

堆排序特性

  • 堆排序使用堆来选数,效率就高了很多。
  • 时间复杂度:O(N*logN)
  • 空间复杂度:O(1)
  • 稳定性:不稳定

5. 冒泡排序

template<class T, class C>
void Sort<T, C>::BubbleSort(vector<T>& v) {
	if (v.size() == 0) return;
	for (int i = 0; i < v.size() - 1; i++) {          // 每次选一个数字,需要选v.size() - 1次
		for (int j = 0; j < v.size() - i - 1; j++) {  // 第i次有v.size() - i个数字进行冒泡,需要进行v.size() - 1次比较
			if (comp(v[j + 1], v[j])) {               // 若v[j + 1] > v[j] 则将较小数往后移动
				swap(v[j + 1], v[j]);
			}
		}
	}
}   

6. 快速排序(重点)

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

将区间按照基准值划分为左右两半部分的常见方式有:
1.hoare版本
2.挖坑法
3.前后指针版本
上面三种方法我们都会讲解实现一下

快速排序的特性总结:

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

6.1 快速排序(hoare版本)

1.选定一个基准值,最好选定最左边或者最右边
2.确定两个指针left 和right 分别从左边和右边向中间遍历数组。
3.如果选最右边为基准值,那么left指针先走,如果遇到大于基准值的数就停下来。
4.然后右边的指针再走,遇到小于基准值的数就停下来。
5.交换left和right指针对应位置的值。
6.重复以上步骤,直到left = right ,最后将基准值与left(right)位置的值交换

为何选右为基准值就要让左指针先走呢,这是因为保证右指针经过的每个值在交换之后都会小于基准值,因为右指针左移的前提是,左指针已经找到比基准值小的值,右指针经过的每个值都会比基准值小,若比基准值大就会停止进行交换。然后又到左指针右移了,这样相交处一定比基准值小。相交左遍一定大于等于基准值,右边一定小于等于基准值

实现代码

template<class T, class C>
void Sort<T, C>::hoareQuickSort(vector<T>& v) {
	if (v.size() == 0) return;
	_hoareQuickSort(v, 0, v.size() - 1);
}   

// 5 3 4 1 2
template<class T, class C>
void Sort<T, C>::_hoareQuickSort(vector<T>& v, int begin, int end) {
	if (begin >= end) return;							
	int mark = v[begin];									// 设最左边的元素为基准值
	int left = begin, right = end;							// 设置边界,注意这里left不能越过基准值,因为若已经有序,右边指针会不断左移至基准值
	while (left < right) {
		while (left < right && !comp(v[right], mark)) {     // 从右开始找到比基准值大的元素, !加仿函数可以表示 >=, <= 等
			right--;
		}
		while (left < right && !comp(mark, v[left])) {		// 从左开始找比基准值小的元素
			left++;
		}
		mySwap(v[left], v[right]);							
	}
	mySwap(v[begin], v[left]);								
	_hoareQuickSort(v, begin, left - 1);					// 此时left左边均大于基准值,对左边进行排序
	_hoareQuickSort(v, left + 1, end);						// 此时right右边均小于基准值,对右边进行排序

测试代码

void hoareQuickSortTest() {
	vector<int> v1;
	vector<int> v2{ 3, 2, 5, 1 ,4 };
	vector<int> v3;
	for (int i = 0; i < 10; i++) {
		v3.push_back(rand() % 100);
	}
	Sort<int>::hoareQuickSort(v1);
	Sort<int>::hoareQuickSort(v2);
	Sort<int>::hoareQuickSort(v3);
	printVector(v1);
	printVector(v2);
	printVector(v3);
	cout << endl;
}

6.2 快速排序(挖坑法)

挖坑法的实现思想与hoare的左右指针法类似。若设置右边为基准值时,让左边先走。因为第一个坑在最右边,是用来寻找最小的元素的

挖坑法和左右指针法不同点在于,左右指针法退出循环时交换基准值数据,而挖坑法是将基准值赋值重复指针处。这是因为
情况1: 已经有序,左右指针移动过程中没有发生数据转移和坑位转移的情况,所以坑位至始至终没有变
情况2:若坑位有变化挖坑法左右指针碰撞处的数据一定是坑,并且坑内数据已经被转移

实现代码

template<class T, class C>
void Sort<T, C>::digpitQuickSort(vector<T>& v) {
	if (v.size() == 0) return;
	_digpitQuickSort(v, 0, v.size() - 1);
}

template<class T, class C>
void Sort<T, C>::_digpitQuickSort(vector<T>& v, int begin, int end) {
	if (begin >= end) return;								// 若没有元素或只有一个元素则已经有序
	int mark = v[end], pit_index = end;						// 设基准值是最右边的元素, 坑的下标为pit_index
	int left = begin; int right = end;	
	while (left < right) {
		while (left < right && !comp(mark, v[left])) {		// 从左边开始找比基准值小的数字,若大于等于基准值则跳过
			left++;
		}
		if (left < right) {									
			v[pit_index] = v[left];							// 找到比基准值小的数字,则将小的数字填充到坑位中	
			pit_index = left;								// 将现位置更新为新的坑
		}
		while (left < right && !comp(v[right], mark)) {		// 从右边开始找比基准值大的数字,若小于等于基准值则跳过
			right--;
		}
		if (left < right) {
			v[pit_index] = v[right];						// 找到比基准值小的数字,则将大的数字填充到坑位中		
			pit_index = right;								// 更新坑位
		}
	}
	v[left] = mark;											// 左右指针相遇,将此处更新为mark基准值
	_digpitQuickSort(v, begin, left - 1);
	_digpitQuickSort(v, left + 1, end);
}

测试代码

void digpitQuickSortTest() {
	vector<int> v1;
	vector<int> v2{ 3, 2, 5, 1 ,4 };
	vector<int> v3;
	for (int i = 0; i < 10; i++) {
		v3.push_back(rand() % 100);
	}
	Sort<int>::digpitQuickSort(v1);
	Sort<int>::digpitQuickSort(v2);
	Sort<int>::digpitQuickSort(v3);
	printVector(v1);
	printVector(v2);
	printVector(v3);
	cout << endl;
}

6.3 快速排序(前后指针法)

template<class T, class C>
void Sort<T, C>::_froaftQuickSort(vector<T>& v, int begin, int end) {
	if (begin >= end) return;
	int prev = begin, cur = begin + 1;        // 定义快指针慢指针
	int mark = v[begin];					  // 定义基准值
	while (cur <= end) {					  // 快指针若超过范围则退出
		if (comp(v[cur], mark) && ++prev != cur) { // 如果快指针指向元素大于基准值,并且prev不指向基准值才可交换
			mySwap(v[cur], v[prev]);		  // 替换快慢指针中的元素					        	
		}
		cur++;								  // 快指针一直往后走	
	}	
	mySwap(v[begin], v[prev]);				  // 将基准值和v[prev]交换 v[prev]一定小于等于基准值	
	_froaftQuickSort(v, begin, prev - 1);     // [begin, prev - 1]元素一定都大于等于基准值
	_froaftQuickSort(v, prev + 1, end);		  // [prev + 1, end] 元素一定小于等于基准值
}

6.4 快速排序(非递归)

快速排序非递归实现,需要借助栈,栈存放的是需要排序的左右边界
而且非递归可以彻底解决栈溢出的问题,递归和非递归的思想是十分类似的。
这里单趟排序使用的是挖坑法(基准值:最左边元素)

template<class T, class C>
void Sort<T, C>::nonreQuickSort(vector<T>& v) {
	if (v.empty()) return;
	stack<int> st;												// 使用栈来记录单趟排序的左右边界
	st.push(v.size() - 1);										// 试着初始边界
	st.push(0);
	while (!st.empty()) {
		int begin = st.top(); st.pop();							// 拿出边界,注意边界插入和拿去顺序是相反的
		int end = st.top();   st.pop();							
		int mark = v[end], pit_index = end;						// 设基准值是最右边的元素, 坑的下标为pit_index
		int left = begin; int right = end;
		while (left < right) {
			while (left < right && !comp(mark, v[left])) {		// 从左边开始找比基准值小的数字,若大于等于基准值则跳过
				left++;
			}
			if (left < right) {
				v[pit_index] = v[left];							// 找到比基准值小的数字,则将小的数字填充到坑位中	
				pit_index = left;								// 将现位置更新为新的坑
			}
			while (left < right && !comp(v[right], mark)) {		// 从右边开始找比基准值大的数字,若小于等于基准值则跳过
				right--;
			}
			if (left < right) {
				v[pit_index] = v[right];						// 找到比基准值小的数字,则将大的数字填充到坑位中		
				pit_index = right;								// 更新坑位
			}
		}
		v[left] = mark;											// 此时[begin, left - 1] 均大于等于基准值[left + 1, end]均小于等于基准值
		if (begin < left - 1) {									// 将左右部分的左右边界放入栈,若左右部分只有一个元素则直接跳过
			st.push(left - 1);					
			st.push(begin);
		}
		if (left + 1 < end) {
			st.push(end);
			st.push(left + 1);
		}
	}
}

6.5 快速排序(优化)

上面就是快速排序递归的三种方法。
但是上面的程序还有一些缺陷:

  • 若基准值最小或最大则递归效率低,至多只能排一个数 (三数取中优化)
  • 在排序大量有序数据或者接近有序数据时,效率会比较低,甚至可能会出现程序崩溃的情况。
    这是因为在排序有序数据时,快速排序的递归调用次数过多,会导致栈溢出的情况。
  • 若快排数组中含有大量的重复元素,效率也会比较低(类似有序),我们可以通过单排过后遍历数组,进行遍历左右部分的元素,将相同元素聚集到中间,重新定义左右边界

为了解决这些问题,这里有两种优化方法:
1.即在在起始位置,中间位置,末尾位置中选出中间值,作为基准值
2.递归到小的子区间时,可以考虑使用插入排序

三数取中

template<class T, class C>
int Sort<T, C>::midinTree(vector<T>& v, int left, int right) {
	int mid = (left + right) / 2;
	int mid_val = min(max(v[left], v[mid]), max(v[mid], v[right]));
	if (mid_val == v[mid]) 
		mySwap(v[mid], v[left]);
	else if (mid_val == v[right]) 
		mySwap(v[left], v[right]);
	return v[left];
}

我们可以在定义基准值的时候使用三数取中函数,样例的三数取中函数标准值默认最左

int mark = midinTree(v, begin, end);							

递归小区间
因为我们上面写的插入排序是针对一整个vector的,若想要在递归小区间使用则需要构建一个带left,和right的重载函数,思路很简单这里就不演示了

7. 归并排序

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

7.1 归并排序(递归实现)

在这里插入图片描述

递归实现

template<class T, class C>
void Sort<T, C>::MergeSort(vector<T>& v) {
	vector<T> tmp(v.size());
	_MergeSort(v, tmp, 0, v.size() - 1);               // 调用子函数
}

template<class T, class C>
void Sort<T, C>::_MergeSort(vector<T>& v, vector<T>& tmp, int left, int right) {
	if (left >= right) return;                         // 若最多只有一个元素则退出
	int mid = (left + right) / 2;                      
	_MergeSort(v, tmp, left, mid);                     // 将左半部分进行归并排序
	_MergeSort(v, tmp, mid + 1, right);                // 将右半部分进行归并排序
	// 走到这左半部分和右半部分都已经有序,并且左右至少都有一个数字
	int left_cur = left, right_cur = mid + 1;          // 记录左,右半部分现元素下标,
	for (int i = left; i <= right; i++) {              // 需要将tmp [left, right] 数据进行填充
		if (left_cur <= mid && right_cur <= right) {   // 若左半部分和右半部分都还有剩余元素
			if (comp(v[left_cur], v[right_cur])) {     // 若左比较大, 则选左元素,并且左部分下标++
				tmp[i] = v[left_cur++];
			}
			else {                                     // 右比较大,选右元素,并且右部分下标++
				tmp[i] = v[right_cur++];
			}
		}
		else if (left_cur > mid) tmp[i] = v[right_cur++];  // 左部分走完了,一直将右部分走完
		else if (right_cur > right) tmp[i] = v[left_cur++];// 右部分走完了,一直将左部分走完
		else break;
	}
	copy(tmp.begin() + left, tmp.begin() + right + 1, v.begin() + left); //将tmp中排序好的[left, right]里的数据拷贝到v中

测试代码

void MergeSortTest() {
	vector<int> v1;
	vector<int> v2{ 3, 2, 5, 1 ,4 };
	vector<int> v3;
	for (int i = 0; i < 10; i++) {
		v3.push_back(rand() % 100);
	}

	Sort<int, my_greater<int>>::MergeSort(v1);
	Sort<int, my_greater<int>>::MergeSort(v2);
	Sort<int, my_greater<int>>::MergeSort(v3);
	printVector(v1);
	printVector(v2);
	printVector(v3);
	cout << endl;
}

7.2 归并排序非递归实现

非递归实现和递归实现实际上是非常类似的,不过非递归使用的并非一分为二,而是一个元素一组,两个元素一组,四个元素一组进行分组,直到所有元素被分完
在这里插入图片描述

template<class T, class C>
void Sort<T, C>::nonreMergeSort(vector<T>& v) {
	if (v.size() == 0) return;
	int gap = 1, end = v.size() - 1;                                     // gap 表示每一组的个数, end表示v最后一个元素的下标
	vector<T> tmp(v.size());											 // 开辟一块和v相同大小的空间
	while (gap < v.size()) {	
		for (int i = 0; i < v.size(); i += 2 * gap) {					 // 两组合在一起进行依次循环,将两组合并					
			int left_begin = i, left_end = i + gap - 1;					 
			int right_begin = i + gap, right_end = i + 2 * gap - 1;
			if (left_end >= end) break;									 // 若左组没有gap 或者刚好只有 gap个元素, 则不需要进行排序
// 因为这组前面一定是偶数组,若前面偶数组再次合并,次数为偶,则这一组还会被单独分出来,若次数为奇数,则这组会和前面一组合并
			else if (right_end >= end) right_end = end;                  // 若右组没有gap个元素,则需要调整right_end
			for (int j = left_begin; j <= right_end; j++) {				 // 这里就是和递归一样的,将两组数据合并到tmp上
				if (left_begin <= left_end && right_begin <= right_end) {
					if (comp(v[left_begin], v[right_begin])) {
						tmp[j] = v[left_begin++];
					}
					else {
						tmp[j] = v[right_begin++];
					}
				}
				else if (left_begin > left_end) tmp[j] = v[right_begin++];
				else if (right_begin > right_end) tmp[j] = v[left_begin++];
				else break;
			}
		}
		v.swap(tmp);	// 将tmp处理好的数据转移到v中											
		gap *= 2;		// 组距加倍
	}
}

测试代码

void nonreMergeSortTest() {
	vector<int> v1;
	vector<int> v2{ 3, 2, 5, 1 ,4 };
	vector<int> v3;
	for (int i = 0; i < 10; i++) {
		v3.push_back(rand() % 100);
	}
	Sort<int>::nonreMergeSort(v1);
	Sort<int>::nonreMergeSort(v2);
	Sort<int>::nonreMergeSort(v3);
	printVector(v1);
	printVector(v2);
	printVector(v3);
	cout << endl;
}

归并排序特性

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

8. 计数排序

基本思想:
计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。操作步骤:
1.统计相同元素出现次数
2.根据统计的结果将序列回收到原来的序列中
在这里插入图片描述
计数排序的特性总结:

  • 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
  • 时间复杂度:O(MAX(N,范围))
  • 空间复杂度:O(范围)
  • 稳定性:稳定

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

在这里插入图片描述
在这里插入图片描述

代码

Sort类实现代码

#include <vector>
#include <iostream>
#include <cstdlib>
#include <queue>
#include <stack>
using namespace std;
//template<class T>
//void mySwap(T& a, T& b) {
//		T tmp = a;
//		a = b;
//		b = tmp;
//	}

template<class T>
struct my_less {
	bool operator()(const T& a, const T& b) { return a > b; }
};

template<class T>
struct my_greater {
	bool operator()(const T& a, const T& b) { return a < b; }
};
template<class T, class C = my_less<T>>
class Sort{
public:
	static C comp;
	static void mySwap(T& a, T& b) { T tmp = a; a = b; b = tmp; } //交换两个元素
	static void InsertSort(vector<T>& v);		// 直接插入排序
	static void ShellSort(vector<T>& v);		// 希尔排序
	static void SelectSort(vector<T>& v);		// 选择排序
	static void BubbleSort(vector<T>& v);		// 冒泡排序
	static void HeapSort(vector<T>& v);			// 堆排序
	static void MergeSort(vector<T>& v);		// 归并排序(递归)
	static void nonreMergeSort(vector<T>& v);   // 归并排序(非递归)

	//快速排序
	static void hoareQuickSort(vector<T>& v);   // hoare版快速排序(递归)
	static void digpitQuickSort(vector<T>& v);  // 挖坑版快速排序(递归)
	static void froaftQuickSort(vector<T>& v);  // 前后指针版快速排序(递归)
	static void nonreQuickSort(vector<T>& v);   // 快速排序(非递归)
	static int midinTree(vector<T>& v, int left, int right); // 三数取中(优化)

	//递归子函数
	static void _MergeSort(vector<T>& v, vector<T>& tmp, int left, int right);
	static void _hoareQuickSort(vector<T>& v, int begin, int end);
	static void _digpitQuickSort(vector<T>& v, int begin, int end);
	static void _froaftQuickSort(vector<T>& v, int left, int right);
};

template<class T, class C>
C Sort<T, C>::comp;

template<class T, class C>
void  Sort<T, C>::InsertSort(vector<T>& v) {
	for (int i = 0; i < v.size(); i++) { // i 表示准备插入数据的下标
		if (i == 0) continue;
		int temp = v[i], cur = i - 1;    // temp表示准备插入的数据 cur为对比的数据下标
		while (cur >= 0) {               // 当cur < 0 时说明没有数据比temp大,则退出
			if (comp(temp, v[cur])) {    // 默认是降序的情况下,若temp > v[cur]则进入
				v[cur + 1] = v[cur];     // 将cur 位置的数据移动至cur + 1
				cur--;                   // 向前寻找插入位置
			}
			else {                       // 说明前面[0, cur]数据均大于temp,[cur + 1, i]位置均小于temp
				break;
			}
		}
		v[cur + 1] = temp;               // 在cur + 1位置插入temp      
	}
}

template<class T, class C> 
void Sort<T, C>::ShellSort(vector<T>& v) {
	int gap = v.size();                 // 步长
	while (gap > 1) {                   // 当 gap == 1时结束,并且数组为空时不会进入
		gap = gap / 3 + 1;              // gap不断减小
		for (int i = 0; i < v.size() - gap; i++) {
			int temp = v[i + gap];      // temp表示准备插入的数据 
			int cur = i;				// cur为对比的数据下标
			while (cur >= 0) {
				if (comp(temp, v[cur])) { // 默认是降序的情况下,若temp > v[cur]则进入
					v[cur + gap] = v[cur];
					cur -= gap;
				}
				else {
					break;				// 说明前面[0, cur]数据均大于temp,[cur + 1, i]位置均小于temp
				}
			}
			v[cur + gap] = temp;		// 在cur + 1位置插入temp    
		}
	}
}

template<class T, class C>
void Sort<T, C>::SelectSort(vector<T>& v) {
	if (v.size() == 0) return;
	int begin = 0, end = v.size() - 1;
	while (begin < end) {
		int max = begin, min = begin;
		for (int i = begin; i <= end; i++) {
			if (comp(v[i], v[max])) max = i;
			if (comp(v[min], v[i])) min = i;
		}
		mySwap(v[begin], v[max]);
		mySwap(v[end], v[min]);
		begin++;
		end--;
	}
}   

template<class T, class C>
void Sort<T, C>::BubbleSort(vector<T>& v) {
	if (v.size() == 0) return;
	for (int i = 0; i < v.size() - 1; i++) {          // 每次选一个数字,需要选v.size() - 1次
		for (int j = 0; j < v.size() - i - 1; j++) {  // 第i次有v.size() - i个数字进行冒泡,需要进行v.size() - 1次比较
			if (comp(v[j + 1], v[j])) {               // 若v[j + 1] > v[j] 则将较小数往后移动
				swap(v[j + 1], v[j]);
			}
		}
	}
}   // 冒泡排序

template<class T, class C>
void Sort<T, C>::HeapSort(vector<T>& v) {
	if (comp(1, 0)) {
		priority_queue<T, vector<int>, less<int>> heap(v.begin(), v.end()); // 降序使用大堆
		for (int i = 0; i < v.size(); i++) {
			v[i] = heap.top();                         // 堆顶元素最大
			heap.pop();								   // 删除最大元素
		}
	}
	else {
		priority_queue<T, vector<int>, greater<int>> heap(v.begin(), v.end()); // 升序使用小堆
		for (int i = 0; i < v.size(); i++) {
			v[i] = heap.top();
			heap.pop();
		}
	}
}     // 堆排序

template<class T, class C>
void Sort<T, C>::MergeSort(vector<T>& v) {
	vector<T> tmp(v.size());
	_MergeSort(v, tmp, 0, v.size() - 1);               // 调用子函数
}

template<class T, class C>
void Sort<T, C>::_MergeSort(vector<T>& v, vector<T>& tmp, int left, int right) {
	if (left >= right) return;                         // 若最多只有一个元素则退出
	int mid = (left + right) / 2;                      
	_MergeSort(v, tmp, left, mid);                     // 将左半部分进行归并排序
	_MergeSort(v, tmp, mid + 1, right);                // 将右半部分进行归并排序
	// 走到这左半部分和右半部分都已经有序,并且左右至少都有一个数字
	int left_cur = left, right_cur = mid + 1;          // 记录左,右半部分现元素下标,
	for (int i = left; i <= right; i++) {              // 需要将tmp [left, right] 数据进行填充
		if (left_cur <= mid && right_cur <= right) {   // 若左半部分和右半部分都还有剩余元素
			if (comp(v[left_cur], v[right_cur])) {     // 若左比较大, 则选左元素,并且左部分下标++
				tmp[i] = v[left_cur++];
			}
			else {                                     // 右比较大,选右元素,并且右部分下标++
				tmp[i] = v[right_cur++];
			}
		}
		else if (left_cur > mid) tmp[i] = v[right_cur++];  // 左部分走完了,一直将右部分走完
		else if (right_cur > right) tmp[i] = v[left_cur++];// 右部分走完了,一直将左部分走完
		else break;
	}
	copy(tmp.begin() + left, tmp.begin() + right + 1, v.begin() + left); //将tmp中排序好的[left, right]里的数据拷贝到v中
}

template<class T, class C>
void Sort<T, C>::nonreMergeSort(vector<T>& v) {
	if (v.size() == 0) return;
	int gap = 1, end = v.size() - 1;                                     // gap 表示每一组的个数, end表示v最后一个元素的下标
	vector<T> tmp(v.size());											 // 开辟一块和v相同大小的空间
	while (gap < v.size()) {	
		for (int i = 0; i < v.size(); i += 2 * gap) {					 // 两组合在一起进行依次循环,将两组合并					
			int left_begin = i, left_end = i + gap - 1;					 
			int right_begin = i + gap, right_end = i + 2 * gap - 1;
			if (left_end >= end) break;									 // 若左组没有gap 或者刚好只有 gap个元素, 则不需要进行排序
// 因为这组前面一定是偶数组,若前面偶数组再次合并,次数为偶,则这一组还会被单独分出来,若次数为奇数,则这组会和前面一组合并
			else if (right_end >= end) right_end = end;                  // 若右组没有gap个元素,则需要调整right_end
			for (int j = left_begin; j <= right_end; j++) {				 // 这里就是和递归一样的,将两组数据合并到tmp上
				if (left_begin <= left_end && right_begin <= right_end) {
					if (comp(v[left_begin], v[right_begin])) {
						tmp[j] = v[left_begin++];
					}
					else {
						tmp[j] = v[right_begin++];
					}
				}
				else if (left_begin > left_end) tmp[j] = v[right_begin++];
				else if (right_begin > right_end) tmp[j] = v[left_begin++];
				else break;
			}
		}
		v.swap(tmp);	// 将tmp处理好的数据转移到v中											
		gap *= 2;		// 组距加倍
	}
}

template<class T, class C>
int Sort<T, C>::midinTree(vector<T>& v, int left, int right) {
	int mid = (left + right) / 2;
	int mid_val = min(max(v[left], v[mid]), max(v[mid], v[right]));
	if (mid_val == v[mid]) 
		mySwap(v[mid], v[left]);
	else if (mid_val == v[right]) 
		mySwap(v[left], v[right]);
	return v[left];
}

template<class T, class C>
void Sort<T, C>::hoareQuickSort(vector<T>& v) {
	if (v.size() == 0) return;
	_hoareQuickSort(v, 0, v.size() - 1);
}   

// 5 3 4 1 2
template<class T, class C>
void Sort<T, C>::_hoareQuickSort(vector<T>& v, int begin, int end) {
	if (begin >= end) return;							
	int mark = midinTree(v, begin, end);					// 设最左边的元素为基准值
	int left = begin, right = end;							// 设置边界,注意这里left不能越过基准值,因为若已经有序,右边指针会不断左移至基准值
	while (left < right) {
		while (left < right && !comp(v[right], mark)) {     // 从右开始找到比基准值大的元素, !加仿函数可以表示 >=, <= 等
			right--;
		}
		while (left < right && !comp(mark, v[left])) {		// 从左开始找比基准值小的元素
			left++;
		}
		mySwap(v[left], v[right]);							
	}
	mySwap(v[begin], v[left]);
	_hoareQuickSort(v, begin, left - 1);					// 此时left左边均大于基准值,对左边进行排序
	_hoareQuickSort(v, left + 1, end);						// 此时right右边均小于基准值,对右边进行排序
}

template<class T, class C>
void Sort<T, C>::digpitQuickSort(vector<T>& v) {
	if (v.size() == 0) return;
	_digpitQuickSort(v, 0, v.size() - 1);
}

template<class T, class C>
void Sort<T, C>::_digpitQuickSort(vector<T>& v, int begin, int end) {
	if (begin >= end) return;								// 若没有元素或只有一个元素则已经有序
	int mark = v[end], pit_index = end;						// 设基准值是最右边的元素, 坑的下标为pit_index
	int left = begin; int right = end;	
	while (left < right) {
		while (left < right && !comp(mark, v[left])) {		// 从左边开始找比基准值小的数字,若大于等于基准值则跳过
			left++;
		}
		if (left < right) {									
			v[pit_index] = v[left];							// 找到比基准值小的数字,则将小的数字填充到坑位中	
			pit_index = left;								// 将现位置更新为新的坑
		}
		while (left < right && !comp(v[right], mark)) {		// 从右边开始找比基准值大的数字,若小于等于基准值则跳过
			right--;
		}
		if (left < right) {
			v[pit_index] = v[right];						// 找到比基准值小的数字,则将大的数字填充到坑位中		
			pit_index = right;								// 更新坑位
		}
	}
	v[left] = mark;											// 左右指针相遇,将此处更新为mark基准值
	_digpitQuickSort(v, begin, left - 1);
	_digpitQuickSort(v, left + 1, end);
}

template<class T, class C>
void Sort<T, C>::froaftQuickSort(vector<T>& v) {
	if (v.empty()) return;
	_froaftQuickSort(v, 0, v.size() - 1);
}

template<class T, class C>
void Sort<T, C>::_froaftQuickSort(vector<T>& v, int begin, int end) {
	if (begin >= end) return;
	int prev = begin, cur = begin + 1;        // 定义快指针慢指针
	int mark = v[begin];					  // 定义基准值
	while (cur <= end) {					  // 快指针若超过范围则退出
		if (comp(v[cur], mark) && ++prev != cur) { // 如果快指针指向元素大于基准值,并且prev不指向基准值才可交换
			mySwap(v[cur], v[prev]);		  // 替换快慢指针中的元素					        	
		}
		cur++;								  // 快指针一直往后走	
	}	
	mySwap(v[begin], v[prev]);				  // 将基准值和v[prev]交换 v[prev]一定小于等于基准值	
	_froaftQuickSort(v, begin, prev - 1);     // [begin, prev - 1]元素一定都大于等于基准值
	_froaftQuickSort(v, prev + 1, end);		      // [prev, end] 元素一定小于基准值
}

template<class T, class C>
void Sort<T, C>::nonreQuickSort(vector<T>& v) {
	if (v.empty()) return;
	stack<int> st;												// 使用栈来记录单趟排序的左右边界
	st.push(v.size() - 1);										// 试着初始边界
	st.push(0);
	while (!st.empty()) {
		int begin = st.top(); st.pop();							// 拿出边界,注意边界插入和拿去顺序是相反的
		int end = st.top();   st.pop();							
		int mark = v[end], pit_index = end;						// 设基准值是最右边的元素, 坑的下标为pit_index
		int left = begin; int right = end;
		while (left < right) {
			while (left < right && !comp(mark, v[left])) {		// 从左边开始找比基准值小的数字,若大于等于基准值则跳过
				left++;
			}
			if (left < right) {
				v[pit_index] = v[left];							// 找到比基准值小的数字,则将小的数字填充到坑位中	
				pit_index = left;								// 将现位置更新为新的坑
			}
			while (left < right && !comp(v[right], mark)) {		// 从右边开始找比基准值大的数字,若小于等于基准值则跳过
				right--;
			}
			if (left < right) {
				v[pit_index] = v[right];						// 找到比基准值小的数字,则将大的数字填充到坑位中		
				pit_index = right;								// 更新坑位
			}
		}
		v[left] = mark;											// 此时[begin, left - 1] 均大于等于基准值[left + 1, end]均小于等于基准值
		if (begin < left - 1) {									// 将左右部分的左右边界放入栈,若左右部分只有一个元素则直接跳过
			st.push(left - 1);					
			st.push(begin);
		}
		if (left + 1 < end) {
			st.push(end);
			st.push(left + 1);
		}
	}
}

测试代码

#include "Sort.h"

void printVector(vector<int>& v) {
	for (const auto& el : v) {
		cout << el << " ";
	}
	cout << endl;
}

void insertSortTest() {
	vector<int> v1;
	vector<int> v2{ 3, 2, 5, 1 ,4 };
	vector<int> v3;
	for (int i = 0; i < 10; i++) {
		v3.push_back(rand() % 100);
	}

	Sort<int>::InsertSort(v1);
	Sort<int>::InsertSort(v2);
	Sort<int>::InsertSort(v3);
	printVector(v1);
	printVector(v2);
	printVector(v3);
	cout << endl;
}
void shellSortTest() {
	vector<int> v1;
	vector<int> v2{ 3, 2, 5, 1 ,4 };
	vector<int> v3;
	for (int i = 0; i < 10; i++) {
		v3.push_back(rand() % 100);
	}

	Sort<int>::ShellSort(v1);
	Sort<int>::ShellSort(v2);
	Sort<int>::ShellSort(v3);
	printVector(v1);
	printVector(v2);
	printVector(v3);
	cout << endl;
}

void selectSortTest() {
	vector<int> v1;
	vector<int> v2{ 3, 2, 5, 1 ,4 };
	vector<int> v3;
	for (int i = 0; i < 10; i++) {
		v3.push_back(rand() % 100);
	}

	Sort<int>::SelectSort(v1);
	Sort<int>::SelectSort(v2);
	Sort<int>::SelectSort(v3);
	printVector(v1);
	printVector(v2);
	printVector(v3);
	cout << endl;
}

void bubbleSortTest() {
	vector<int> v1;
	vector<int> v2{ 3, 2, 5, 1 ,4 };
	vector<int> v3;
	for (int i = 0; i < 10; i++) {
		v3.push_back(rand() % 100);
	}

	Sort<int>::BubbleSort(v1);
	Sort<int>::BubbleSort(v2);
	Sort<int>::BubbleSort(v3);
	printVector(v1);
	printVector(v2);
	printVector(v3);
	cout << endl;
}

void heapSortTest() {
	vector<int> v1;
	vector<int> v2{ 3, 2, 5, 1 ,4 };
	vector<int> v3;
	for (int i = 0; i < 10; i++) {
		v3.push_back(rand() % 100);
	}

	Sort<int, my_greater<int>>::HeapSort(v1);
	Sort<int, my_greater<int>>::HeapSort(v2);
	Sort<int, my_greater<int>>::HeapSort(v3);
	printVector(v1);
	printVector(v2);
	printVector(v3);
	cout << endl;
}

void MergeSortTest() {
	vector<int> v1;
	vector<int> v2{ 3, 2, 5, 1 ,4 };
	vector<int> v3;
	for (int i = 0; i < 10; i++) {
		v3.push_back(rand() % 100);
	}

	Sort<int, my_greater<int>>::MergeSort(v1);
	Sort<int, my_greater<int>>::MergeSort(v2);
	Sort<int, my_greater<int>>::MergeSort(v3);
	printVector(v1);
	printVector(v2);
	printVector(v3);
	cout << endl;
}

void nonreMergeSortTest() {
	vector<int> v1;
	vector<int> v2{ 3, 2, 5, 1 ,4 };
	vector<int> v3;
	for (int i = 0; i < 10; i++) {
		v3.push_back(rand() % 100);
	}
	Sort<int>::nonreMergeSort(v1);
	Sort<int>::nonreMergeSort(v2);
	Sort<int>::nonreMergeSort(v3);
	printVector(v1);
	printVector(v2);
	printVector(v3);
	cout << endl;
}

void hoareQuickSortTest() {
	vector<int> v1;
	vector<int> v2{ 3, 2, 5, 1 ,4 };
	vector<int> v3;
	for (int i = 0; i < 10; i++) {
		v3.push_back(rand() % 100);
	}
	Sort<int>::hoareQuickSort(v1);
	Sort<int>::hoareQuickSort(v2);
	Sort<int>::hoareQuickSort(v3);
	printVector(v1);
	printVector(v2);
	printVector(v3);
	cout << endl;
}

void digpitQuickSortTest() {
	vector<int> v1;
	vector<int> v2{ 3, 2, 5, 1 ,4 };
	vector<int> v3;
	for (int i = 0; i < 10; i++) {
		v3.push_back(rand() % 100);
	}
	Sort<int>::digpitQuickSort(v1);
	Sort<int>::digpitQuickSort(v2);
	Sort<int>::digpitQuickSort(v3);
	printVector(v1);
	printVector(v2);
	printVector(v3);
	cout << endl;
}

void froaftQuickSortTest() {
	vector<int> v1;
	vector<int> v2{ 3, 3, 2, 2, 4, 5, 1 ,4 };
	vector<int> v3;
	for (int i = 0; i < 10; i++) {
		v3.push_back(rand() % 100);
	}
	Sort<int>::froaftQuickSort(v1);
	Sort<int>::froaftQuickSort(v2);
	Sort<int>::froaftQuickSort(v3);
	printVector(v1);
	printVector(v2);
	printVector(v3);
	cout << endl;
}

void nonreQuickSortTest() {
	vector<int> v1;
	vector<int> v2{ 3, 3, 2, 2, 4, 5, 1 ,4 };
	vector<int> v3;
	for (int i = 0; i < 10; i++) {
		v3.push_back(rand() % 100);
	}
	Sort<int>::nonreQuickSort(v1);
	Sort<int>::nonreQuickSort(v2);
	Sort<int>::nonreQuickSort(v3);
	printVector(v1);
	printVector(v2);
	printVector(v3);
	cout << endl;
}

int main() {
	//insertSortTest();
	//shellSortTest();
	//selectSortTest();
	//bubbleSortTest();
	//heapSortTest();
	//MergeSortTest();
	//nonreMergeSortTest();
	//hoareQuickSortTest();
	//digpitQuickSortTest();
	//froaftQuickSortTest();
	//nonreQuickSortTest();
	return 0;
}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小白在进击

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值