第九章-排序

第九章-排序

1.学习目标

排序主要关注各种算法的时间复杂度,空间复杂度和应用场景。根据实际的应用场景灵活地选用最优排序算法。

2.概述

1)排序方法的分类

在这里插入图片描述

(1)内部排序和外部排序

内部排序:数据量不大,数据在内存,无需与外存交换数据。

外部排序:数据量较大,数据在外存要将数据分批调入内存来排序中间结果要及时放入外存,相对复杂。

(2)串行排序和并行排序

串行排序:单处理机,同一时刻比较一对元素。

并行排序:多处理机,同一时刻比较多对元素。

(3)比较排序和基数排序

比较排序:用比较的方法进行排序。如:插入排序,交换排序,选择排序,归并排序。

基数排序:不比较元素大小,仅根据元素本身的取值确定其有序位置。如:桶排序,桶排序中的两种经典的排序,计数排序和基数排序。

(4)原地排序和非原地排序

原地排序:辅助空间用量为O(1)

非原地排序:辅助空间用量超过O(1)

(5)稳定排序和非稳定排序

稳定排序:任何数值相等的元素,排序后相对次序不变。(不改变其原始数组的情况

非稳定排序:改变了原始数组的情况。

注:基础类型的相对次序是没有意思的,自定义数据类型的相对次序才是有意义的。如下:
在这里插入图片描述

(6)自然排序和非自然排序

自然排序:输入数据越有序,排序速度越快的方法。

非自然排序:不是自然排序的方法。

2)排序依据

冒泡排序、简单选择排序和直接插入排序属于简单算法,希尔排序、堆排序、归并排序和快速排序属于改进算法。
在这里插入图片描述

3)排序所需工作量

简单的排序方法: O ( N 2 ) O(N^2) O(N2)

先进的排序方法: O ( N l o g 2 N ) O(Nlog{_2}N) O(Nlog2N)

非比较的排序方法: O ( N ) O(N) O(N)

3.插入排序

在插入a[i]前,a[0]-a[i-1]是有序的,a[i]-a[n-1]是无序的,即是将新来的无序的元素插入到原来有序的数组中关键点在于从0构建有序数组。

1)直接插入排序

逆序比较的方法插入数据。

在这里插入图片描述

(1)传统的直接插入排序

用num存储新来的a[i]的值,直接逆序比较,如果a[j] < a[j-1],将a[j-1]往后移动。当不满足要求时,num插入到a[j]处。

void insertionSort(vector<int> &a)
{
		int len = a.size(); //记录数组长度
		
		if(len < 2) return;

		for(int i = 1; i < len; i++)
		{
				int num = a[i]; //存储a[i],防止移动后a[i]元素被覆盖
				int j = i;

				for(; j > 0 && a[j] < a[j-1]; j--) //向后移动元素
				{
						a[j] = a[j-1];
				}

				a[j] = num;
		}
}

void swap(vector<int> &a, int i, int j) //此函数适用于本章节
{
		int tmp = a[i];
		a[i] = a[j];
		a[j] = tmp;
}

(2)改进的直接插入排序

0-0, 0-1, 0-2范围逐步扩大排好序,新来的j逐位与j-1,j-2…0比较(不用完全比较,只要大于某1个就可以跳出),小就换前面。类似于打扑克,新来了一个牌,看能滑到哪个位置插进去。

void insertionSort_improve(vector<int> &a)
{

		int len = a.size(); //记录数组长度
		
		if(len < 2) return;
	
		for(int i = 1; i < len; i++) //插入排序的位置
		{
				for(int j = i; j > 0 && a[j] < a[j - 1]; j--) //小于前面的元素就交换
				{
						swap(a,j,j-1);
				}
		}
}

(3)复杂度及稳定性

时间复杂度: O ( N 2 ) O(N^2) O(N2)

空间复杂度: O ( 1 ) O(1) O(1)

是否稳定:稳定

2)折半插入排序

采用高低指针结合二分法的方式,对已排好序的数组进行二分查找,定位新来的元素要插入的位置。假定新来的元素a[i],则低指针low指向首元素下标0,高指针high指向排好序的数组末元素a[i-1],中间指针mid=(low + high)/2。不断二分,直至low==high,找到数组中的最后一个元素。最后的一个元素与a[i]比较,[high + 1~i-1]元素后移一位,a[i]插入在a[high + 1]处。
在这里插入图片描述

(1)代码

void binaryInsertionSort(vector<int> &a)
{
		int len = a.size(); //记录数组长度
		
		if(len < 2) return;

		int low = 0; //低指针
		int high = 0; //高指针
		int mid = 0; //中指针
		int num = 0; //存储新来的元素值

		for(int i = 1; i < len; i++)
		{
				low = 0;
				high = i - 1; //i前面的区间是[0~i-1]
				num = a[i];

				while(low <= high) //==时到达最后一个元素,用a[i]与该元素比较,确定最后a[i]的插入位置
				{
						mid = (low + high)/2;
						if(num < a[mid]) high = mid - 1;
						else low = mid + 1;				
				}

				for(int j = i - 1; j >= (high + 1); j--) //元素后移
						a[j + 1] = a[j];

				a[high + 1] = num; //插入元素
		}

}

(2)复杂度及稳定性

时间复杂度: O ( N 2 ) O(N^2) O(N2)

空间复杂度: O ( 1 ) O(1) O(1)

是否稳定:稳定

3)希尔排序

直接插入排序的移动步幅只有1,希尔排序在直接插入排序的基础上做了改进,通过加大步幅,使得数组整体的更快地趋于有序,提高了排序的效率。

对数组的每一个元素进行遍历,遍历到的当前元素a[i]与之前的a[i - delta[k]]做比较。

注:步幅数组的最后一个步幅一定要是1,即最后进行一次直接插入排序。如步幅数组delta = [5,3,1]。

步幅的含义:假设步幅为N,当前元素为a[i],则下一个元素为a[i+N]。

在这里插入图片描述

希尔排序的过程如下:
在这里插入图片描述

(1)代码

void shellInsertionSort(vector<int> &a, vector<int> delta)
{
		int len = a.size();

		if(len < 2) return;

		for(int k = 0; k < delta.size(); k++) //遍历输入的步幅
		{
				//步幅为k的直接插入排序
				for(int i = delta[k]; i < len; i++)  //从下标1 + delat[k]开始构建有序数组,遍历的时候每个元素都遍历,只是与i-delta[k]作比较
				{
						int num = a[i]; //存储新来的元素值
						
						int j = i - delta[k]; //j指向新来元素的前一个位置

						for(; j >= 0 && num < a[j]; j = j - delta[k]) //后移元素
						{
								a[j + delta[k]] = a[j];
						}

						a[j + delta[k]] = num; //插入元素
				}
		}
}

(2)复杂度及稳定性

时间复杂度:与增量序列delta选择有关,最坏情况 O ( N 3 2 ) O(N^\frac{3}{2}) O(N23),平均情况 O ( N 5 4 ) O(N^\frac{5}{4}) O(N45)

空间复杂度: O ( 1 ) O(1) O(1)

是否稳定:稳定

4.交换排序

两两比较,如果发生逆序则交换,直至所有记录排好序为止。

常见的交换排序方法:冒泡排序 O ( N 2 ) O(N^2) O(N2)和快速排序 O ( N l o g 2 N ) O(Nlog_2N) O(Nlog2N)

1)冒泡排序

两两比较,若逆序则交换,一路交换,最终把最大的放在最后,末端边界-1。重复上述过程,直至整个数组排序结束。

void bubbleSort(vector<int> &a)
{
		int len = a.size();
		
		if(len < 2) return; //为空或者长度为1

		for(int i = len - 1; i > 0; i--) //末尾边界
		{
				for(int j = 0; j < i; j++)
				{
						if(a[j + 1] < a[j]) swap(a[j],a[j+1]); //后者小于前者
				}
		}

}

如果某个循环到达末尾边界时,数据两两并未完成交换,则证明从0到末尾边界的数字已经排序好了,此时可以结束循环。于是用标志位flag对上述冒泡排序进行了改进。

void bubbleSortImprove(vector<int> &a)
{
		int len = a.size();
		
		if(len < 2) return; //为空或者长度为1

		int flag = 1; //标志位,用于标识前面数据是否交换

		for(int i = len - 1; i > 0 && flag; i--)
		{
				for(int j = 0; j < i; j++)
				{
						flag = 0; //标志位清0

						if(a[j+1] < a[j]) 
						{
								swap(a,j+1,j);
								flag = 1; //发生了交换,flag置1,表示该次循环数据发生了交换,数组暂未有序
						}
				}
		}
}

时间复杂度: O ( N 2 ) O(N^2) O(N2)

空间复杂度: O ( 1 ) O(1) O(1)

是否稳定:稳定

2)快速排序

先取一个边界值,小于的放左子表,等于的放中间子表,大于的放右边子表。然后再对左、右子表进行同样的操作,即递归。

经典快排算法:取末元素为边界值。

随机快排算法:随机取L-R内的一个数值作为边界值。

在这里插入图片描述

(1)代码

void main_quickSort(vector<int> &a)
{
		int len = a.size();
		
		if(len < 2) return; //为空或者长度为1

		quickSort(a,0,len - 1);
}

void quickSort(vector<int> &a, int L, int R)
{
		if(L < R) return; //左边界 < 右边界
		{
				vector<int> equalBoundary = partition(a,L,R); //划分成左中右三个部分
				quickSort(a,L,equalBoundary[0] - 1); //继续排序左部分
				quickSort(a,equalBoundary[1] + 1,R); //继续排序右部分
		}
}

//把L-R范围的a数组按边界值划分成左中右三个部分,返回相等部分的边界值
vector<int> partition(vector<int> &a, int L, int R)
{
		int less = L - 1;
		int more = R + 1;
		int num = a[R]; //取末端为边界值,经典快排
		//int num = a[L + (rand()%(R - L + 1))]; //随机取边界,随机快排

		while(L < more)
		{
				if(a[L] < num) swap(a,++less,L++);
				else if(a[L] > num) swap(a,--more,L);  //大于的话换的是more前面的未知数,要继续判断,所以切记不能用for循环遍历
				else L++;
		}

		return vector<int>{less + 1, more - 1};		
}

(2)复杂度及稳定性

时间复杂度: O ( N l o g 2 N ) O(Nlog{_2}N) O(Nlog2N)

partition需要对所有元素进行遍历,时间复杂度是 O ( N ) O(N) O(N)

边界值刚好打在中间点时,左右两个子表均分,类似于二分,递归的次数取决于二叉树的高度,因此,递归最好时间复杂度是 O ( l o g 2 N ) O(log{_2}N) O(log2N)。若刚好边界值打在最值处,左右子表一个长度为0,一个为长度n-i-1,极其不平衡,故要执行n-1次调用。因此,递归最坏时间复杂度是 O ( N ) O(N) O(N)。数学证明平均时间复杂度是 O ( l o g 2 N ) O(log{_2}N) O(log2N)

因此,总的时间复杂度是 O ( N l o g 2 N ) O(Nlog{_2}N) O(Nlog2N)

空间复杂度: O ( l o g N ) O(logN) O(logN)

递归造成了栈空间的使用。最好情况,递归树深度 O ( l o g 2 N ) O(log{_2}N) O(log2N),最坏情况,递归次数 O ( N ) O(N) O(N)。数学证明平均时间复杂度是 O ( l o g 2 N ) O(log{_2}N) O(log2N)

是否稳定:不稳定

5.选择排序

1)简单选择排序

0到N-1找一个最小的交换到0位置,1到N-1找一个最小的交换到1位置,以此类推。

(1)代码

void selectionSort(vector<int> &a)
{
		int len = a.size();
		
		if(len < 2) return;
		
		int minIndex = 0;

		for(int i = 0; i < len; i++)
		{
				minIndex = i;

				for(int j = i + 1; j < len; j++) //加个1
				{
						if(a[j] < a[minIndex]) minIndex = j;
				}
				
				swap(a,i,minIndex);
		}
}

(2)复杂度及稳定性

时间复杂度: O ( N 2 ) O(N^2) O(N2)

空间复杂度: O ( 1 ) O(1) O(1)

是否稳定:不稳定

2)堆排序

(1)堆

<1>定义

堆是一个完全二叉树,即从左到右依次补齐非叶节点没有子代的节点称为叶节点。完全二叉树包括满二叉树。

在这里插入图片描述

<2>大根堆与小根堆

大根堆:每个节点的值都大于或等于其左右孩子节点的值。(堆顶具有最大值,子树堆顶亦具有在子树范围内的最大值)

小根堆:每个节点的值都小于或等于其左右孩子节点的值。(堆顶具有最小值,子树堆顶亦具有在子树范围内的最小值)

在这里插入图片描述

假设遍历的节点方式从下标0开始编号。若父节点编号为 i i i,则其左右孩子节点的编号为 2 ∗ i + 1 2*i+1 2i+1 2 ∗ i + 2 2*i+2 2i+2。若子节点编号为 i i i,则其父节点的编号为 ( i − 1 ) / 2 (i-1)/2 (i1)/2

n n n个元素的序列 a 0 , a 2 , . . . , a n − 1 a_0,a_2,...,a_{n-1} a0,a2,...,an1,则大根堆和小根堆满足如下公式:

在这里插入图片描述

(2)堆排序

<1>堆排序的步骤

{1} 将待排序的序列构造成大根堆。(heapInsert函数)

{2} 交换堆顶元素和堆数组的末端元素,堆数组长度heapSize减1。(堆顶元素是最大的元素,交换到末端后,相当于末端元素最大,排好序了,堆数组长度-1,类似于冒泡排序)

{3} 将新的堆顶元素一路往下沉,父节点与子节点中较大的子节点交换,形成新的大根堆。(heapify函数)

{4} 当heapSize > 0,跳回步骤2,继续循环。

<2>heapInsert函数

heapInsert函数的功能是将待排序的序列构造成大根堆。假设原来的堆是有序的大根堆,现在新来了一个元素 a [ i ] a[i] a[i],则此时要使新的堆构成大根堆,则需要重新使得堆顶元素是最大值

于是我们可以一直回溯子节点 a [ i ] a[i] a[i]的父节点 a [ ( i − 1 ) / 2 ] a[{(i-1)/2}] a[(i1)/2],若 a [ i ] > a [ ( i − 1 ) / 2 ] a[i]>a[{(i-1)/2}] a[i]>a[(i1)/2],两者交换, i = ( i − 1 ) / 2 i=(i-1)/2 i=(i1)/2。一路交换,直至 a [ i ] ≤ a [ ( i − 1 ) / 2 ] a[i]{\le}a[{(i-1)/2}] a[i]a[(i1)/2]或者 a [ i ] a[i] a[i]到达堆顶,循环结束。

a [ 0 ] a[0] a[0]遍历到 a [ n − 1 ] a[n-1] a[n1],大根堆构建完成。

在这里插入图片描述

<3>heapify函数

heapify函数的功能是将首尾元素交换后的数组重新构建成大根堆。将新的堆顶元素与子节点中较大的子节点交换,一路往下沉,形成新的大根堆。

在这里插入图片描述

(3)代码

void heapSort(vector<int> &a)
{
		int len = a.size();
		
		if(len < 2) return;

		for(int i = 0; i < len; i++) //从0开始构建大根堆
		{
				heapInsertion(a,i);
		}
		
		int heapSize = len; //大根堆的长度

		swap(a,0,--heapSize); //首尾交换

		while(heapSize > 0)
		{
				heapify(a,0,heapSize); //此时边界还未收缩
				swap(a,0,--heapSize); //此时边界收缩
		}
}

//向上交换,构建大根堆,用于初始化大根堆
void heapInsert(vector<int> &a, int index)
{
		while(a[index] > a[(index - 1)/2]) //如果子节点一直比父节点大,一直向上交换
		{
				swap(a,index,(index - 1)/2);
				index = (index - 1)/2;
		}
}

//向下交换,构建大根堆,用于首尾交换后的大根堆构造
void heapify(vector<int> &a, int index, int heapSize)
{
		int left = 2*index + 1;

		while(left < heapSize) // 如果存在左节点
		{
				int largest = (left + 1 < heapSize && a[left + 1] > a[left])? left + 1 : left; //选出左右节点中最大的那个,右节点必须存在
				
				if(a[index] >= a[largest]) break; //父节点>=子节点,直接跳出
				
				swap(a, largest, index);
				index = largest;
				left = 2*index + 1;
		}

}

(4)复杂度及稳定性

时间复杂度: O ( N l o g 2 N ) O(Nlog{_2}N) O(Nlog2N)

每执行一次heapInsert,每加入一个节点,时间复杂度显然取决于二叉树的高度,于是加入一个节点的时间复杂度是 O ( l o g 2 i ) O(log{_2}i) O(log2i)。一共有N个节点,调整代价是 O ( l o g 2 1 ) + O ( l o g 2 2 ) + . . . + O ( l o g 2 N − 1 ) = O ( N ) O(log{_2}1)+O(log{_2}2)+...+O(log{_2}N-1)=O(N) O(log21)+O(log22)+...+O(log2N1)=O(N)。因此,建立一个大根堆的时间复杂度是 O ( N ) O(N) O(N)

每执行一次heapify,第 i i i次取堆顶记录重建堆的时间复杂度是 O ( l o g 2 i ) O(log{_2}i) O(log2i),需要取 n − 1 n-1 n1次堆顶记录,因此heapify的时间复杂度是 O ( N l o g 2 N ) O(Nlog{_2}N) O(Nlog2N)

因此,总的时间复杂度是 O ( N l o g 2 N ) O(Nlog{_2}N) O(Nlog2N)

空间复杂度: O ( 1 ) O(1) O(1)

是否稳定:不稳定

6.归并排序

归并,将两个有序表合成一个新的有序表。归并采用分治的思想,不断二分成n个子序列,直至每个子序列的长度为1,然后再两两有序归并,如此重复,直至得到一个长度为n的有序序列为止。

简单点的理解是:对于一个有序序列,左右等分成两个有序子序列,采用外排将两个有序子序列进行合并。

1)归并

归并的次数取决于树的高度,即 O ( l o g 2 N ) O(log{_2}N) O(log2N)

假设现在有序列 a = [ 48 , 34 , 60 , 80 , 75 , 12 , 26 , 4 8 ∗ ] a=[48,34,60,80,75,12,26,48^*] a=[48,34,60,80,75,12,26,48],则其归并过程如下。

在这里插入图片描述

2)外排

定义一个辅助数组help,长度是两个有序序列长度的和,用于暂存排序后的结果。分别用p1,p2指针指向两个有序序列的头部,若 a [ p 1 ] < a [ p 2 ] a[p1]<a[p2] a[p1]<a[p2],则把 a [ p 1 ] a[p1] a[p1]添加进help数组中,p1++。否则把 a [ p 2 ] a[p2] a[p2]添加进help数组中,p2++。最后把help的值重新赋予原始数组a。

外排的次数取决于两个子序列的长度,即 O ( M + N ) O(M+N) O(M+N)
在这里插入图片描述

3)代码

void main_mergeSort(vector<int> &a)
{
		int len = a.size();

		if(len < 2) return;

		mergeSort(a,0,len - 1);
}

//把一整个数组划分成两个子数组,对两个有序的子数组进行外排
void mergeSort(vector<int> &a, int L, int R)
{
		if(L == R) return;

		int mid = (L + R)/2;
		
		mergeSort(a,L,mid);
		mergeSort(a,mid + 1,R);
		merge(a,L,mid,R);
}

//外排,使得L~R范围内的两个数组归并,有序
void merge(vector<int> &a, int L, int mid, int R)
{
		int p1 = L;
		int p2 = mid + 1;
		
		vector<int> help(R - L + 1,0); //定义指定L~R范围内的辅助数组,用于存储外排的排序结果
		int i = 0; //辅助数组的计数

		while(p1 <= mid && p2 <= R)
				help[i++] = a[p1] < a[p2]? a[p1++]:a[p2++];

		//下面2个while只会执行一个
		while(p1 <= mid)
				help[i++] = a[p1++];

		while(p2 <=R)
				help[i++] = a[p2++];

		for(int i = 0; i < help.size(); i++)  //将辅助数组的值赋值给原数组
				a[L + i] = help[i];
}

4)复杂度及稳定性

时间复杂度: O ( N l o g 2 N ) O(Nlog{_2}N) O(Nlog2N)。归并的次数为 O ( l o g 2 N ) O(log{_2}N) O(log2N),外排的次数位 O ( N ) O(N) O(N),则总的时间复杂度是 O ( N l o g 2 N ) O(Nlog{_2}N) O(Nlog2N)

空间复杂度: O ( N ) O(N) O(N)。归并排序需要一个辅助数组help来临时存放外排后的结果。

稳定性:稳定。

7.桶排序

不基于比较,而是基于元素对应的位置进行排序。**将元素按照其位置分别装入对应的桶中,再从桶中按序取出,最后成为一个有序的数组。基本思想是:分配+收集。**适用于数量大但是范围小的排序场景。

1)计数排序

按照元素位置装入对应的桶中,而后分别对应的取出桶中元素,形成有序的数组。

桶在此处用计数数组count表示,count中的每个元素count[i]表示该桶内的元素个数。计数排序的计数数组count长度是max-min+1。

(1)算法步骤

<1>遍历数组,求得数组的最大值max和最小值min

<2>定义计数数组count,长度为max-min+1(这样做的目的是限定桶在一定的区间内,以免造成资源的浪费)

<3>遍历数组,元素对应位置的count[i]元素++,统计

<4>count数组累加,记录该桶最后一个元素应该插入的位置,保证元素的稳定性

<5>逆序遍历数组,将元素插入到暂存数组tmp的对应位置

<6>将暂存数组tmp赋值给原数组

(2)代码

void countSort(vector<int> &a)
{
		int len = a.size();
		
		if(len < 2) return;

		int max_data = INT_MIN;
		int min_data = INT_MAX;

		for(int i = 0; i < len; i++)  //遍历一遍求得数组中的最大值和最小值
		{
				max_data = max(a[i],max_data);
				min_data = min(a[i], min_data);
		}

		vector<int> count(max_data - min_data + 1,0); //定义一个计数数组
		vector<int> tmp(len,0); //定义一个暂存数组,收集从计数数组中取出来的数,再赋值给原数组a
		
		int i = 0;

		for(i = 0; i < len; i++)
				count[a[i]-min_data]++;

		for(i = 1; i < count.size(); i++)  //做计数数组的累加,使得逆序插入的元素能到达其对应的末端位置,解决了不稳定的问题
				count[i] = count[i] + count[i - 1];

		for(i = len - 1; i >= 0; i--)
				tmp[--count[a[i] - min_data]] = a[i];	//count存储的是数量,下标要--

		for(i = 0; i < len; i++)
				a[i] = tmp[i];
}

(3)复杂度及稳定性

时间复杂度: O ( N + K ) O(N+K) O(N+K)

K为桶的个数。遍历原数组取最大值N次,遍历数组进桶N次,遍历桶取出数K次,共N+K次。也有人直接忽略K次,认为太小了,直接O(N)次。

空间复杂度: O ( N + K ) O(N+K) O(N+K)

暂存数组tmp的长度N和计数数组的长度K。

是否稳定:不稳定

2)基数排序

基数排序就是按照关键字进行排序。基数排序就是外层关键字遍历,内层计数排序。一般,基数排序的计数数组count长度是10。

例如按个十百位进行排序。先对个位数进行排序,再对十位数进行排序,最后对百位数进行排序。

在这里插入图片描述

(1)代码

void radixSort(vector<int> &a)
{
		int len = a.size();

		if(len < 2) rerturn;

		int d = maxbit(a);
		
		vector<int> count(10,0); //初始化计数数组
		vector<int> tmp(len,0); //暂时存储收集到数据的数组
		
		int j = 0;
		int radix = 1; //比率 个:1,十:10,百:100

		for(int i = 0; i < d; i++) //遍历多少位数,个,十,百,千...
		{
				for(j = 0; j < count.size(); j++) //清空计时器
						count[j] = 0;

				for(j = 0; j < len; j++) //直接计数排序
						count[(a[j]/radix)%10]++;

				for(j = 1; j < count.size(); j++)
						count[j] = count[j] + count[j - 1];

				for(j = len - 1; j >=0; j--)
						tmp[--count[(a[j]/radix)%10]] = a[j];

				for(j = 0; j < len; j++)
						a[j] = tmp[j];

				radix = radix * 10;
		}
}

//求得遍历多少位数
int maxbit(vector<int> &a)
{
		int max = a[0];

		for(int i = 0; i < a.size(); i++)
				if(a[i] > max) max = a[i];

		int p = 10; //每次除以10
		int d = 1; //1位数,最小是从个位数开始的

		while(max >= p)
		{
				max = max / p;
				d++;
		}

		return d;
}

(2)复杂度及稳定性

时间复杂度: O ( K ∗ ( N + M ) ) O(K*(N+M)) O(K(N+M))

K为关键字的个数,M为桶的个数。遍历原数组取最大值N次,遍历数组进桶N次,遍历桶取出数M次,共N+M次。

空间复杂度: O ( N + M ) O(N+M) O(N+M)

暂存数组tmp的长度N和计数数组的长度M。

是否稳定:不稳定

3)桶排序

(1)算法步骤

遍历数组,求max和min。设置桶的个数N,用(max-min)/N来等分区间。min放在最小的桶,max放在最大的桶。遍历数组,每个数根据范围放进去相应的桶里。最后对每个桶里的数据进行排序。最后收集出来有序的数组。

不太常用的原因:每个桶存放的元素个数多少个不确定。虽然可以用链表,但是对链表进行排序很麻烦。

(2)算法改进

对于每个桶,只存储最大元素maxs、最小元素mins和有没有元素haveNum进来过桶。

<1>练习题目

给定一个数组,求如果排序之后,相邻两数的最大差值,要求时间复杂度 O ( N ) O(N) O(N)

,且要求不能用非基于比较的排序。

<2>算法思路

记住是排好序的数组!若有N个数,则设置N+1个桶,搞一个空桶,使得最大差值不会存在一个桶内部。最小值存在第一个桶,最大值存在最后一个桶,中间的桶进行等分。区间长度M的等分公式为 M = ( m a x − m i n ) / N M={(max-min)}/{N} M=(maxmin)/N。数组i号元素a[i]属于的桶号码是 B = ( a [ i ] − m i n ) / M B={(a[i]-min)}/{M} B=(a[i]min)/M减去min因为区间的分布是从min开始的,归零。

例:数组 [ 2 , 50 , 11 , 92 , 33 , 44 ] [2,50,11,92,33,44] [2,50,11,92,33,44]

N:6, Min:2, max:92, ,等分后的区间分布如下:

a[1]=50所属于的区间是 B = ( 50 − 2 ) / 15 = 3 B={(50-2)}/{15}=3 B=(502)/15=3

在这里插入图片描述

空桶存在的意义:

空桶只能使得最大差值不会存在一个桶内部。空桶最大差值是右非空桶的最小值与左非空桶的最大值的最大差值。注意空桶左右并不一定是差值最大的,例子如下:

在这里插入图片描述

<3>代码

int maxGap(vector<int> &a)
{
		int len = a.size();

		if(len < 2) return;

		int max_data = INT_MIN;
		int min_data = INT_MAX;

		for(int i = 0; i < len; i++) //求得数组的最大值和最小值
		{
				max_data = max(max_data,a[i]);
				min_data = min(min_data,a[i]);
		}

		if(max_data == min_data) return 0; //一系列相同的数,差值为0

		vector<int> maxs(len + 1,0);
		vector<int> mins(len + 1,0);
		vector<bool> haveNum(len + 1,false);
	
		int index = 0; //用于存储数字对应的位置

		for(int i = 0; i < len; i++)
		{
				index = bucket(a[i],len,max_data,min_data);
				mins[index] = haveNum[index]? min(mins[index],a[i]): a[i];
				maxs[index] = haveNum[index]? max(maxs[index],a[i]): a[i];
				haveNum[index] = true;
		}

		int lastMax = maxs[0];
		int res = 0; //用于存储差值

		for(int i = 1; i <= len; i++) //遍历桶,挨个求最大值 <=len!
		{
				if(haveNum[i])
				{
						res = max(res, mins[i] - lastMax);	
						lastMax = maxs[i];
				}
		}
		
		return res;							
}

void bucket(long num, long len, long max, long min)
{
		return (int)((num - min)*len / (max - min));
}

8.总结

1)时间复杂度,空间复杂度和稳定性表

在这里插入图片描述

2)应用场景

数组长度短,不管什么类型都用插入排序。虽然时间复杂度O(N^2),但是小样本并不处于劣势,反而常数项很低,很快。

数组长度很长,基础类型用快排,自定义类型用归并。基础类型的相同值无差异,故无须保证稳定性。

如果数据很长,也可以分治成小数据,数据量小于60直接插排。

3)Tips

均分的时间复杂度是 O ( l o g 2 N ) O(log{_2}N) O(log2N),例如分治,二分,归并。

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值