第九章-排序
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 2∗i+1, 2 ∗ i + 2 2*i+2 2∗i+2。若子节点编号为 i i i,则其父节点的编号为 ( i − 1 ) / 2 (i-1)/2 (i−1)/2。
若 n n n个元素的序列 a 0 , a 2 , . . . , a n − 1 a_0,a_2,...,a_{n-1} a0,a2,...,an−1,则大根堆和小根堆满足如下公式:
(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[(i−1)/2],若 a [ i ] > a [ ( i − 1 ) / 2 ] a[i]>a[{(i-1)/2}] a[i]>a[(i−1)/2],两者交换, i = ( i − 1 ) / 2 i=(i-1)/2 i=(i−1)/2。一路交换,直至 a [ i ] ≤ a [ ( i − 1 ) / 2 ] a[i]{\le}a[{(i-1)/2}] a[i]≤a[(i−1)/2]或者 a [ i ] a[i] a[i]到达堆顶,循环结束。
从 a [ 0 ] a[0] a[0]遍历到 a [ n − 1 ] a[n-1] a[n−1],大根堆构建完成。
<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(log2N−1)=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 n−1次堆顶记录,因此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=(max−min)/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=(50−2)/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),例如分治,二分,归并。