c++排序: 冒泡、选择、插入、归并、快速、堆排序、桶排序

排序

github源码地址


最优解:先满足时间复杂度最优,再满足最小空间

稳定性:相同元素在排序时的先后位置不变

递归:在调用子过程的时候,会把父过程放入中,当子过程结束会回到父的栈中找到中断行号,接着运行。

排序 时间复杂度 额外空间复杂度 稳定性 最好情况 最坏情况
冒泡排序 N^2 1 可以稳定
选择排序 N^2 1 不稳定
插入排序 N^2 1 可以稳定 N(已排序) N^2(逆序)
归并排序 NlogN N 可以稳定
快速排序 NlogN logN 不稳定 每次划分值刚好在中间区域 阈值左右两侧不平均
堆排序 NlogN 1 不稳定
桶排序 N N 稳定

一、冒泡排序

从小到大排序:从第一个数开始,两两比较,大的数往后放。

范围从0N-1…0N-2…0N-3…01,0

稳定性:可以稳定,相等的时候让后面的数做交换即可

void bubble_sort(vector<int> &a) {
	if (a.size() < 2) return;
	//每次找到的最小的元素被放到末尾
	for (int end = a.size() - 1; end > 0; --end)
	{
		for (int i = 0; i < end; i++)
		{
			if (a[i] < a[i + 1]) swap(a[i], a[i + 1]);
		}
	}
}

二、选择排序

找到最小值的下标,和最后的元素交换

稳定性:不稳定,因为需要最小值需要交换

void select_sort(vector<int> &a) {
	if (a.size() < 2)return;
	for (int end = a.size()-1; end > 0; --end)
	{
		int min = end;
		for (int j = 0; j < end; j++)
		{
			if (a[j] < a[min]) min = j;
		}
		swap(a[min], a[end]);
	}
}

三、插入排序

扑克牌插牌:新进来的牌和之前的元素依次比较确定插入的位置

稳定性:可以稳定,插入的时候只需放在相同的后面即可

void insert_sort(vector<int> &a) {
	if (a.size() < 2)return;
	for (int i = 1; i < a.size(); i++)
	{
		for (int j = i-1; j >=0; j--)
		{
			if (a[j] < a[j + 1])swap(a[j], a[j + 1]);
			else break;
		}
	}
}

更优质的写法:

void insert_sort(vector<int> &a) {
	if (a.size() < 2)return;
	for (int i = 1; i < a.size(); i++)
	{
		for (int j = i-1; j >=0 && a[j]<a[j+1]; j--)
		{
			swap(a[j], a[j + 1]);
		}
	}
}

四、归并排序:左排右排合并

递归,函数在调用的时候先分别排序自己的左右,然后再把左右合并起来

额外空间复杂度:O(N),需要额外的数组进行拷贝

稳定性:在merge的过程,只要小于等于就拷贝左边部分

void merge(vector<int> &a, int left,int mid, int right) {
	if (left >= right || a.size() < 2) return;
	vector<int> temp;
	int l = left;
	int r = mid + 1;
	while (l<=mid && r <= right){ temp.push_back((a[l] < a[r] ? a[l++] : a[r++]));}
	while (l<=mid){ temp.push_back(a[l++]);}
	while (r<=right){ temp.push_back(a[r++]);}

	for (size_t i =0;i<temp.size();){
		a[left++] = temp[i++];
	}
	cout <<endl<< "temp: ";
	for (auto ax:temp)
	{
		cout << ax << " ";
	}
	cout << endl;
}

void mergesort(vector<int> &a,int left, int right) {
	if (left >= right || a.size() < 2) return;
	int mid = left + (right - left)/2;
	mergesort(a, left, mid);
	mergesort(a, mid + 1, right);
	merge(a, left, mid, right);
	cout << endl;
}

复杂度分析:

​ T(n) = T(n/2)+T(n/2)+O(n) = 2*T(n/2)+O(n)

用T(n) = a*T(n/b)+O(n^d)公式计算复杂度,

  • logba>d ,则算法复杂度为 O(Nlogba)
  • logba<d,则算法复杂度为 O(Nd)
  • logba=d,则算法复杂度为 O(Nd*logN)

归并排序:a=2,b=2,d=1,取相等的情况。

五、快速排序:分区递归

用荷兰国旗问题的解决方法划分区间,再对子问题进行递归调用。

时间复杂度讨论:看递归调用的次数,如果数据按照1234567排放,需要不断进行递归调用,效率很低。也就是当最右的划分值左右两侧不平均的话代价就会很高。如果划分值刚好能将左右区域划分差不多大小,则每次大小减小为原来的一半。

所以要选取优良的划分值,最好用随机选取的方法。随机快速排序:将随机选中的数和最后一个位置交换,再使用最后一个位置快速排序的方法,它的长期期望为NlogN,并且常数项很低。工程的实际表现很好。

额外空间复杂度讨论:划分区间时需要记录断点值的信息,对于最好情况断点空间为logN,类似于二分。

稳定性:不稳定:因为会交换。可以做到论文级别的,很难

// 以最后一个数为划分阈值
vector<int> partition(vector<int> &a,int l,int r) {
	int left = l-1;
	int right = r;
	while (l < right)
	{
		if (a[l] < a[r]) { swap(a[l], a[left + 1]); left++; l++; }
		else if (a[l] == a[r]) l++;
		else if (a[l] > a[r]) { swap(a[l], a[right - 1]); right--; };
	}
	swap(a[right], a[r]);
	return { left,right+1}; //返回小区间和大区间的下标
}

void quicksort(vector<int>&a, int left, int right) {
	if (a.size() < 2 || left >= right) return;
	vector<int> ind = partition(a, left, right);
	quicksort(a, left, ind[0]);
	quicksort(a, ind[1], right);
}

六、堆排序:堆头交换下树循环

堆:-----完全二叉树结构:满二叉树或者通往满二叉树的路上,且都是从左到右排放的。要么是满的,要么是未满的层按左到右排放。

堆:----底层是数组。某节点下标: i;其子节点下标:左子 : 2 * i+1, 右子: 2 * i+2;其父节点下标:[(i-1)/2] 取整

大根堆:子树的头结点都是该子树的最大值小根堆:子树的头结点都是该子树的最小值

上树

数组调成大根堆的过程

改成大根堆的实例:5 7 0 6 8,按照二叉树的结构放入

  • 放入5
  • 放入7,7>父结点5,交换 7 5 0 6 8
  • 放入0,0<父结点7,不动
  • 放入6,6>父结点5,交换 7 6 0 5 8,6< 父结点7,不动
  • 放入8,8>父结点6,交换7 8 0 5 6,8>父结点7,交换 8 7 0 5 6
void bigRoot(vector<int> &a,int index) {//index用来确定数组尾端
	for (int i = 1; i <= index; i++)
	{
		if (a[i]>a[(i-1)/2])
		{
			int l = i;
			while(a[l]>a[(l-1)/2]){
				swap(a[l], a[(l - 1)/2]);
				l = (l-1)/2;
			}
		}
	}
}
下树

把大根堆的根节点和最后一个位置交换,也就是把最大值放在数组尾端,

把剩下的数组调整成大根堆结构,也就是让交换后的根节点下来

根节点和自己左右两个孩子比较,与大于自己的孩子交换,不断下树

void downHill(vector<int> &a,int index) {
	int i = 0;
	while (2*i+1<=index)
	{
		int larger = ((2 * i + 2) <= index && (a[2 * i + 1] < a[2 * i + 2])) ? (2 * i + 2) : (2 * i + 1);
		if (a[i] <= a[larger]) {
			swap(a[i], a[larger]);
			i = larger;
		}
		else break;
	}
}
堆排序

堆排序的过程:

  • 上树:建立大根堆,确定最大值,
  • 交换:将大根堆的根结点和末尾交换,把数组大小视作减小了1
  • 下树:将交换后的根节点和左右子树不断比较,下树
  • 交换…下树…交换…下树…
  • 排序结束
void heapSort(vector<int> &a) {
	if (a.size() < 2)return;
	int index = a.size() - 1;

	bigRoot(a, a.size() - 1);
	while (index>0)
	{
		swap(a[0], a[index--]);
		downHill(a, index);
	}
}

建立大根堆的过程复杂度为log1+log2+…+logN = O(N)
调整所有数的过程为O(NlogN)

缺点:不稳定,常数项大

七、桶排序

桶排序不是基于比较的排序。比如数字范围从0~200,共几亿个数。设置201个桶,放桶里,根据桶编号倒出到容器。如果容器不是栈,则桶排序稳定

计数排序
void buckersort(vector<int> &a) {
	if (a.size() < 2) return;
	int max = a[0];
	for (int i = 0; i < length; i++){
		max = a[i] > max ? a[i] : max;
	}
	vector<int> temp(max+1);
	for (int i = 0; i < length; i++){
		temp[a[i]]++;
	}
	for (int i = 0; i < max; i++){
		for (int j = 0; i < temp[i]; j++){
			temp.push_back(i);
		}
	}
	a = temp;
}
基数排序

补充:应用

归并排序:最小和&逆序对

  1. 求小和: 算一个数左边比它小的数的和,对数组中所有元素进行这个操作
int minsum(vector<int> &a) {
	int sum = 0;
	for (size_t i = 0; i < a.size(); i++){
		for (size_t j = 0; j < i; j++){
			if (a[i] > a[j]) sum += a[j];
		}
	}
	return sum;
}

int merge(vector<int> &a, int left,int mid, int right) {
	if (left >= right || a.size() < 2) return 0;
    vector<int> temp;
	int l = left;
	int r = mid + 1;
	int sum = 0;
	while (l<=mid && r <= right){ 
		sum += (a[l] < a[r]) ? a[l] * (right - r+1) : 0;
		temp.push_back((a[l] < a[r] ? a[l++] : a[r++]));
		
	}
	while (l<=mid){ temp.push_back(a[l++]);}
	while (r<=right){ temp.push_back(a[r++]);}

	for (size_t i =0;i<temp.size();){
		a[left++] = temp[i++];
	}
	cout <<endl<< "temp: ";
	for (auto ax:temp)
	{
		cout << ax << " ";
	}
	cout << endl;
	return sum;
}

int sort(vector<int> &a,int left, int right) {
	if (left >= right || a.size() < 2) return 0;
	int mid = left + (right - left)/2;
	return sort(a, left, mid) + sort(a, mid + 1, right)+merge(a, left, mid, right);
}
  1. 逆序对: 算数组中逆序对的数量

快速排序:荷兰国旗问题

  1. 根据最后一个元素将vector分成两个部分
// 以最后一个数为划分阈值
//小于等于(最后一个数)的区域,从左边界开始
//返回元素被交换的位置
//当一个数小于等于p,就直接扩展小于等于区域的范围
//当一个数大于p,将这个数交换到小于等于区的下一个位置,再扩充
int partition(vector<int> &a, int left, int right) {
	int temp = a[right];
	int l = left-1;
	for (int i = left; i < right;i++) {
		if(a[i] <= temp) 
			swap(a[++l],a[i]);
	}
	return l;
}
  1. 荷兰国旗问题

选择最后一个数为划分值,小于这个数的放左边,等于放中间,大于放右边。

做法:分两块区域:大于区和小于区,大于区以最后一个数为起始点,小于区以-1位置为起始点。

  • 当数等于划分值,箭头往下跳
  • 当数小于划分值,小于区扩张1位
  • 当数大于划分值,该数和大于区外的后一个数交换,大于区扩张1大小
  • 全部完成后,将划分值和最后一位交换位置
void holland(vector<int> &a) {
	if (a.size() < 2) return;
	int left = -1;
	int cur = 0;
	int right = a.size() - 1;
	int last = a.size() - 1;
	int temp = a[last];
	while (cur<right)
	{
		if (a[cur] < temp) { swap(a[cur], a[left + 1]); left++; cur++; }
		else if (a[cur] == temp) cur++;
		else if (a[cur] > temp) { swap(a[cur], a[right - 1]); right--; };
	}
	swap(a[right], a[last]);
}

桶排序:计算数组排序后的相邻两数的最大差值

计算数组排序后的相邻两数的最大差值,要求时间复杂度O(N),也就是不允许用排序做。

思路:根据桶个数建立桶,范围上划分成n+1份。

比如:假设9个[0-99]的数,min=0, max = 99,准备9+1=10个桶,范围分别为[0-9],[10-19],[20-29]…[90-99],将9个数放入桶中。

边界的桶由于min,max存在必不为空,9数10桶,中间必有空桶。于是只需考虑空桶和其相邻桶之间的关系,无需考虑桶内相邻数,只需考虑桶内出现的最大值和最小值

A , B , _ , _ , C , D

first = min(B)-max(A)

second = min©-max(B)

third = min(D) - max©

return max(first,second,third)

int neighbormin(vector<int> a) {
	if (a.size() < 2)return 0;
	int min = a[0];
	int max = a[0];
	for (int i = 1; i < a.size(); i++)
	{
		if (a[i] > max) max = a[i];
		if (a[i] < min) min = a[i];
	}
	int bucketlen = (max - min) / (a.size() + 1) + 1;
	vector<int> bucketmin(a.size() + 1);
	vector<int> bucketmax(a.size() + 1);
	vector<bool> hasnum(a.size() + 1);

	for (int i = 0; i < a.size(); i++)
	{
		int bid = (a[i] - min) / bucketlen;
		bucketmax[bid] = hasnum[bid]? ((bucketmax[bid] <= a[i]) ? a[i] : bucketmax[bid]):a[i];
		bucketmin[bid] = hasnum[bid] ? ((bucketmin[bid] >= a[i]) ? a[i] : bucketmin[bid]) : a[i];
		hasnum[bid] = true;
	}

	int maxval = bucketmax[0];
	int res = 0;
	for (size_t i = 1; i < a.size() + 1; i++)
	{
		if (hasnum[i])
		{
			res = (bucketmin[i] - maxval) > res ? (bucketmin[i] - maxval):res;
			maxval = bucketmax[i];
		}
	}

	return res;
}

综合排序

综合排序分范围

  • n < thred : insert
  • n > thred :merge(自定义类型)/quick(基础类型)

理由:在数据量小的时候,常数量的优势会体现,insert的常数量很低。

基础类型的排序不关心稳定性,quick快

自定义类型的排序可能需要稳定性,比如数据库排序中id和age,先按id排,再按age排就需要id稳定,merge稳定。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章