本篇博文将详细总结一些排序算法。
将 A ( 1 : n ) A(1:n) A(1:n) 中的元素按非降次序分类, n ≥ 1 n≥1 n≥1
插入排序:插入即表示将一个新的数据插入到一个***有序*** 数组中,并继续保持有序。例如有一个长度为 N N N 的无序数组,进行 N − 1 N-1 N−1 次的插入即能完成排序;第一次,数组第 1 1 1 个数认为是有序的数组,将数组第二个元素插入仅有 1 1 1 个有序的数组中;第二次,数组前两个元素组成有序的数组,将数组第三个元素插入由两个元素构成的有序数组中…第 N − 1 N-1 N−1 次,数组前 N − 1 N-1 N−1 个元素组成有序的数组,将数组的第 N N N 个元素插入由 N − 1 N-1 N−1 个元素构成的有序数组中,则完成了整个插入排序。
插入排序就是每一步都将一个待排数据按其大小插入到已经排序的数据中的适当位置,直到全部插入完毕。
下图演示了对 4 4 4 个元素进行直接插入排序的过程,共需要 ( a ) , ( b ) , ( c ) (a),(b),(c) (a),(b),(c) 三次插入。
void InsertSort(int* pDataArray, int iDataNum)
{
for (int i = 1; i < iDataNum; i++)
{
int j = i - 1;
int temp = pDataArray[i];//暂存当前要插入的数
while (j>=0 && pDataArray[j]>temp)//从当前temp位置向前寻找小于等于temp数的位置
{
pDataArray[j + 1] = pDataArray[j];//大于temp的数依次向后滑动
j--;
}
if (j != i - 1)//如果j!=i-1表示j向前滑动了
pDataArray[j + 1] = temp;//把要插入的数放在小于等于它前的位置
}
}
平均时间复杂度: O ( n 2 ) O({n}^{2}) O(n2)
空间复杂度: O ( 1 ) O(1) O(1) (用于记录需要插入的数据)
稳定性:稳定
采用***分治法***(个人认为分治必然用到递归,否则分治无意义),将数组 A [ 0 … n − 1 ] A[0…n-1] A[0…n−1] 中的元素分成两个子数组: A 1 [ 0 … n / 2 ] {A}_{1} [0…n/2] A1[0…n/2] 和 A 2 [ n / 2 + 1 … n − 1 ] {A}_{2} [n/2+1…n-1] A2[n/2+1…n−1] 。分别对这两个子数组单独排序,然后将 已排序的两个子数组归并 成一个含有 n n n 个元素的有序数组。
这是一个递归的过程,递归的将数组分为两子数组,然后在各个子数组再分为两子数组,以此递归下去,当分出来的子数组只有一个数据时,可以认为这个子数组内已经达到了有序,然后再合并相邻的两个子数组就可以了。这样通过先递归的分解数组,再合并数组就完成了归并排序。
int temp[100];//用来暂存排序过程中数组
void Merge(int* a, int low,int mid,int high)//对有序的两个子数组进行归并
{
int i = low;
int j = mid + 1;
int size = 0;
for (; i <= mid &&j <= high; size++)
{
if (a[i] < a[j])//两子数组两两比较
temp[size] = a[i++];
else
temp[size] = a[j++];
}
while (i<=mid)
temp[size++] = a[i++];
while (j <= high)
temp[size++] = a[j++];
for (i = 0; i < size; i++)
a[low++] = temp[i];
}
void MergeSort(int* a, int low, int high)
{
if (low >= high)
return;
int mid = (low + high) / 2;
MergeSort(a, low, mid);
MergeSort(a, mid + 1, high);
Merge(a, low, mid, high);
}
算法的递推关系:
由上面的代码可知,在 M e r g e S o r t MergeSort MergeSort 方法中不断的将数组递归的分为两个子数组,一个子数组的时间复杂度为 T ( n 2 ) T\left(\frac{n}{2} \right) T(2n),而在 M e r g e Merge Merge 方法中,代码虽然看起来长,但是其循环长度都是 s i z e size size ,并且没有嵌套循环,故时间复杂度为 o ( n ) o(n) o(n),因为是线性的,故前面有系数 c c c 。
由此我们可以得出: T ( n ) = 2 ⋅ T ( n 2 ) + c ⋅ n T(n)=2\cdot T\left(\frac{n}{2} \right)+c\cdot n T(n)=2⋅T(2n)+c⋅n
若 n = 2 k n={2}^{k} n=2k,不断的递归拆分成两子数字,则有:
若 2 k < n < 2 k + 1 {2}_{k}
所以得: T ( n ) = O ( n l o g n ) T(n) = Ο(nlogn) T(n)=O(nlogn) 。
注:基于关键字***比较*** 的排序算法的***平均时间复杂度的下界*** 为 O ( n l o g n ) O(nlogn) O(nlogn),也就是没有比它更小的时间复杂度了。
给定一个数组 A [ 0 … N − 1 ] A[0…N-1] A[0…N−1],若对于某两个元素 a [ i ] 、 a [ j ] a[i]、a[j] a[i]、a[j],若 i < j i<j i<j 且 a [ i ] > a [ j ] a[i]>a[j] a[i]>a[j] ,则称 ( a [ i ] , a [ j ] ) (a[i],a[j]) (a[i],a[j]) 为逆序对。一个数组中包含的逆序对的数目称为该数组的逆序数。试设计算法,求一个数组的逆序数。
如: 3 , 56 , 2 , 7 3,56,2,7 3,56,2,7 的逆序数为 3 3 3 。
我们可以这样分析这个问题,把数组 A A A 分为两个有序的子数组,如上图所示,子数组 A [ l o w , m i d ] A[low,mid] A[low,mid] 和子数组 A [ m i d + 1 , h i g h ] A[mid+1,high] A[mid+1,high] 分别有序,并且为升序排序。现在假设 A [ i ] > A [ j ] A[i]>A[j] A[i]>A[j],那么我们可以得出 A [ i ] A[i] A[i] 到 A [ m i d ] A[mid] A[mid] 中任何一个数都大于 A [ m i d + 1 ] A[mid+1] A[mid+1] 到 A [ j ] A[j] A[j] 中任何一个数。由此可以计算出数组 A A A 的逆序数。每遇到一对这样的 i , j i,j i,j,其逆序数都加上 ( m i d − i + 1 ) (mid-i+1) (mid−i+1) 或 ( j − m i d ) (j-mid) (j−mid)。
int temp[100];//用来暂存排序过程中数组
void Merge(int* a, int low,int mid,int high,int& count)
{
int i = low;
int j = mid + 1;
int size = 0;
for (; i <= mid &&j <= high; size++)
{
if (a[i] < a[j])
temp[size] = a[i++];
else if (a[i]>a[j])//说明存在逆序数
{
count += mid - i + 1;//和上面的归并排序代码几乎一样,只是这里多了一句
//每遇到这样的(i,j) 都加上 (mid-i+1)或(j-mid)
//count += j - mid;//本句和上一句代码效果完全一样。
temp[size] = a[j++];
}
}
while (i<=mid)
temp[size++] = a[i++];
while (j <= high)
temp[size++] = a[j++];
for (i = 0; i < size; i++)
a[low++] = temp[i];
}
void MergeSort(int* a, int low, int high,int& count)
{
if (low >= high)
return;
int mid = (low + high) / 2;
MergeSort(a, low, mid,count);
MergeSort(a, mid + 1, high,count);
Merge(a, low, mid, high,count);
}
int main()
{
int a[] = { 3, 56, 2, 7 };
int size = sizeof(a) / sizeof(int);
int count = 0;
MergeSort(a, 0, size - 1, count);
cout << count << endl;
return 0;
}
定义:对于一棵完全二叉树,若树中***任一非叶子结点的关键字均不大于(或不小于)其左右孩子*** (若存在)结点的关键字,则这棵二叉树,叫做小顶堆(大顶堆)。
完全二叉树可以用***数组完美存储***,对于长度为 n n n 的数组 a [ 0 … n − 1 ] a[0…n-1] a[0…n−1] ,若 ∀ 0 ≤ i ≤ n − 1 , a [ i ] ≤ a [ 2 i + 1 ] ∀0≤i≤n-1,a[i]≤a[2i+1] ∀0≤i≤n−1,a[i]≤a[2i+1] 且 a [ i ] ≤ a [ 2 i + 2 ] a[i]≤a[2i+2] a[i]≤a[2i+2],那么 a a a 是一个小顶堆。
重要结论:大顶堆的堆顶元素是最大的。
***注:每第 i i i 次调整前都是先将 a [ i ] a[i] a[i] 与 a [ n − i ] a[n-i] a[n−i] 交换,然后再调整成大顶堆。***上面示意图中只有调整后的图,没有显示详细的交换和调整过程。
16 , 14 , 10 , 8 , 7 , 9 , 3 , 2 , 4 , 1 16,14,10,8,7,9,3,2,4,1 16,14,10,8,7,9,3,2,4,1
9 , 8 , 3 , 4 , 7 , 1 , 2 9,8,3,4,7,1,2 9,8,3,4,7,1,2
初始化堆(建堆)的过程: O ( N ) O(N) O(N)
调整堆的过程:每次弹出堆顶元素,并且调整堆的时间复杂度为 l o g N logN logN,对N个做堆排序故 O ( N l o g N ) O(NlogN) O(NlogN)
堆可以用数组完美表示,数组元素的***先后顺序以及其元素大小关系表示了这个堆是大顶堆还是小顶堆***。我们在***编写程序时经常以数组来存储堆***。
k k k 的孩子结点是 2 k + 1 , 2 k + 2 2k+1,2k+2 2k+1,2k+2 (如果存在)
k k k 的父结点:
对于大小为 s i z e size size 的 A A A 数组里面的元素进行建堆,只需要从第 A [ s i z e / 2 ] A[size/2] A[size/2] 元素到 A [ s i z e − 1 ] A[size-1] A[size−1] 元素进行插入建堆。
//调用该函数前,n的左右孩子都是大顶堆,调整以n为顶的堆为大顶堆
void HeadAjust(int* a, int n, int size)
{
int Lchild = 2 * n + 1;//左孩子
int nChild = Lchild;
int t;
while (nChilda[nChild]))//找出左右孩子大的那个
nChild += 1;
if (a[nChild] < a[n])//如果孩子比父亲小,说明调整完毕
break;
//反正,孩子比父亲大,需要交换调整
t = a[nChild];
a[nChild] = a[n];
a[n] = t;
n = nChild;
nChild = 2 * n + 1;//循环的继续向下调整
}
}
void HeadSort(int* a, int size, int k)//前k大
{
int i;
for (i = size / 2 - 1; i >= 0; i--)//初始建大顶堆
{
HeadAjust(a, i, size);
}
int t;
int s = size - k;
while (size>s)//依次找到最大的并放在数组末尾,然后重新调整建堆
{
t = a[size - 1];
a[size - 1] = a[0];//将当前堆的最大值放在数组末尾
a[0] = t;
size--;
HeadAjust(a, 0, size);//调整堆,也即是将新的a[0]插入到堆的适当位置
}
}
这个问题肯定要对这 N N N 个数进行排序,那么选择哪一种排序方式使得时间复杂度最小呢?
这里我们仅以堆排序为例。
i f if if 这个数比小顶堆的堆顶元素大
弹出小顶堆的最小元素
把这个数插入到小顶堆,并且进行堆调整。
小顶堆的作用:
时间复杂度:从大小为 k k k 的堆中弹出 1 1 1 个(最小)元素,并且调整堆的时间复杂度为 O ( l o g K ) O(logK) O(logK)。遍历数组中 N N N 个元素,每次都要选择堆中最小的那个元素与数组中元素作替换,然后再调整堆,共 N N N 次。 故 O ( N ∗ l o g k ) O(N*logk) O(N∗logk)
代码实现:
#include
#include
#include
#include
#include
#include
using namespace std;
//调用该函数前,n的左右孩子都是小顶堆,调整以n为顶的堆为小顶堆
void HeadAjust(int* a, int n, int size)
{
int Lchild = 2 * n + 1;//左孩子
int nChild = Lchild;
int t;
while (nChild a[n])//如果父亲比孩子小,说明调整完毕
break;
//反正,孩子比父亲小,需要交换调整
t = a[nChild];
a[nChild] = a[n];
a[n] = t;
n = nChild;
nChild = 2 * n + 1;//循环的继续向下调整
}
}
void Print(int* b, int k)
{
for (int i = 0; i < k; i++)
cout << b[i] << " ";
cout << endl;
}
void HeadSort(int* a,int* b, int size, int k)//前k大
{
int i;
for (i = 0; i < k; i++)
b[i] = a[i];
for (i = k/2; i >= 0; i--)//根据数组b建大小为k的小顶堆
{
HeadAjust(b, i, k);
}
for (i = k; i < size; i++)
{
if (a[i]>b[0])//不断去除堆里面最小的元素,插入比它大的元素
{
b[0] = a[i];
HeadAjust(b, 0, k);//调整堆,也即是将新的a[0]插入堆中适合的位置。
}
}
}
int main()
{
int a[] = { 7, 54,12, 8,53, 13, 32, 45, 19,101,100};
int size = sizeof(a) / sizeof(int);
int k = 4;
int* b = new int[k];
HeadSort(a, b, size, k);
Print(b, k);
}
算法描述:
时间复杂度分析:
该方法的实现代码和上面差不多就不写了。
快速排序是一种基于***划分*** 的排序方法; 划分 P a r t i t i o n i n g Partitioning Partitioning :选取待排序集合 A A A 中的某个元素 t t t,按照与 t t t 的大小关系重新整理 A A A 中元素,使得整理后的序列中所有在t以前出现的元素均小于 t t t,而所有出现在 t t t 以后的元素均大于等于 t t t;元素 t t t 称为划分元素。
快速排序:通过反复地对 A A A 进行划分达到排序的目的。
以一个数组作为示例:
取区间第一个数 p r i v o t e K e y = A [ 0 ] = 72 privoteKey=A[0]=72 privoteKey=A[0]=72 为基准数。
[ 0 1 2 3 4 5 6 7 8 9 72 6 57 88 60 42 83 73 48 85 ] \begin{bmatrix} 0 & 1 & 2 & 3 & 4 & 5 & 6 &7 &8 &9 \\ 72 & 6 &57 &88 &60 &42 &83 &73 &48 &85 \end{bmatrix} [07216257388460542683773848985]
此时 i = 0 , j = 9 i=0,j=9 i=0,j=9, j j j 从右往左的遍历找到第一个小于 p r i v o t e K e y privoteKey privoteKey 的数,当 j = 8 j=8 j=8 时对应的数值 48 < 72 48<72 48<72,此时将 48 48 48 与 72 72 72 互换位置并且执行 i + + i++ i++ 得:
[ 0 1 2 3 4 5 6 7 8 9 48 6 57 88 60 42 83 73 72 85 ] \begin{bmatrix} 0 & 1 & 2 & 3 & 4 & 5 & 6 &7 &8 &9 \\ 48 & 6 &57 &88 &60 &42 &83 &73 &72 &85 \end{bmatrix} [04816257388460542683773872985]
注意此时 i = 1 , j = 8 i=1,j=8 i=1,j=8
此时 i i i 再从左向右遍历,找到第一个大于 p r i v o t e K e y privoteKey privoteKey 的数,找为 i = 3 i=3 i=3 时对应的数值 88 > 72 88>72 88>72 ,互换两个数的位置并且执行 j − − j-- j−− 得:
[ 0 1 2 3 4 5 6 7 8 9 48 6 57 72 60 42 83 73 88 85 ] \begin{bmatrix} 0 & 1 & 2 & 3 & 4 & 5 & 6 &7 &8 &9 \\ 48 & 6 &57 &72 &60 &42 &83 &73 &88 &85 \end{bmatrix} [04816257372460542683773888985]
注意此时 i = 3 , j = 7 i=3,j=7 i=3,j=7
此时 j j j 再从右向左遍历,找到第一个小于 p r i v o t e K e y privoteKey privoteKey 的数,找为 j = 5 j=5 j=5 时对应的数值 42 < 72 42<72 42<72 ,互换两个数的位置并且执行 i + + i++ i++ 得:
[ 0 1 2 3 4 5 6 7 8 9 48 6 57 42 60 72 83 73 88 85 ] \begin{bmatrix} 0 & 1 & 2 & 3 & 4 & 5 & 6 &7 &8 &9 \\ 48 & 6 &57 &42 &60 &72 &83 &73 &88 &85 \end{bmatrix} [04816257342460572683773888985]
注意此时 i = 3 , j = 5 i=3,j=5 i=3,j=5
此时 i i i 再从左向右遍历,当 i = = j i==j i==j 时退出,此时, i = j = 5 i =j=5 i=j=5 时,将 A [ 5 ] = p r i v o t e K e y A[5]=privoteKey A[5]=privoteKey。
这样第一轮的排序完成。
void quick_sort(int s[], int l, int r)//c++中 数组*a表示可修改原始数组,int& a表示可修改原始a值
{
if (l < r)
{
int i = l, j = r, x = s[l];//x就是privoteKey,也就是当前基准数
while (ix)//j从右向左找第一个小于x的值
j--;
if (i < j)
//下面是先赋值,然后i再加1
s[i++] = s[j];//之前s[i]为x,可以这样理解把s[j]的值赋值给s[i],s[j]里面可以理解为x
//(或者把s[j]理解为下一个需要填的坑)
while (i < j&&s[i] < x)//i从左向右找第一个大于x的值
i++;
if (i < j)
s[j--] = s[i];//填坑
}
s[i] = x;//此时i==j
//分治递归
quick_sort(s, l, i - 1);
quick_sort(s, i + 1, r);
}
}
int main()
{
int a[] = { 23, 54, 12, 4, 7, 1, 3, 54, 13 };
int size = sizeof(a) / sizeof(int);
quick_sort(a, 0, size - 1);
Print(a, size);
}
最好的情况:
最坏的情况:
###快速排序与归并排序比较
快速排序的最直接竞争者是堆排序。堆排序通常比快速排序稍微慢,但是最坏情况的运行时间总是 O ( n l o g n ) O(nlogn) O(nlogn)。快速排序是经常比较快,但仍然有最坏情况性能的机会。
堆排序拥有重要的特点:仅使用固定额外的空间,即***堆排序是原地排序***,而快速排序需要 O ( l o g n ) O(logn) O(logn) (递归栈)的空间。
将元素分到若干个桶中,每个桶分别排序,然后归并
由于***桶之间往往是有序*** 的(如:洗牌中的 1 − 13 1-13 1−13 个点数,整数按照数位0-9基数排序等),所以,它们不是(完全)基于比较的,时间复杂度下限不是 O ( N l o g N ) O(NlogN) O(NlogN)
请查看最大间隔问题
###问题描述
输入一个数组 A [ 0 … N − 1 ] A[0…N-1] A[0…N−1] 和一个数字 S u m Sum Sum,在数组中查找两个数 A i , A j {A}_{i} ,{A}_ {j} Ai,Aj ,使得 A i + A j = S u m {A}_{i} +{A}_{j} =Sum Ai+Aj=Sum。
之前我们分析过 N − s u m N-sum N−sum 问题,采用的办法是深度优先搜索,也即是 递归深入+回溯 来搜索解空间里所有的可能解。这是一种暴力解法,在本问题中依然可以采用这种暴力解法, 从数组中任意选取两个数 x , y x,y x,y,判定它们的和是否为输入的数字 S u m Sum Sum。时间复杂度为 O ( N 2 ) O({N}^{2}) O(N2),空间复杂度 O ( 1 ) O(1) O(1) 。那么可有更好的解决方法呢?
两头扫:
数组***无序*** 的时候,时间复杂度最终为 O ( N l o g N + N ) = O ( N l o g N ) O(NlogN+N)=O(NlogN) O(NlogN+N)=O(NlogN)。
#include
#include
#include
#include
#include
#include
using namespace std;
void quick_sort(int *s, int l, int r)
{
if (l < r)
{
int i = l, j = r, x = s[l];//x就是privoteKey,也就是当前基准数
while (ix)//j从右向左找第一个小于x的值
j--;
if (i < j)
//下面是先赋值,然后i再加1
s[i++] = s[j];//之前s[i]为x,可以这样理解把s[j]的值赋值给s[i],s[j]里面为x(或者把它理解为下一个需要填的坑)
while (i < j&&s[i] < x)//i从左向右找第一个大于x的值
i++;
if (i < j)
s[j--] = s[i];
}
s[i] = x;//此时i==j
//分治递归
quick_sort(s, l, i-1);
quick_sort(s, i + 1, r);
}
}
void Print(int *a, int size)
{
for (int i = 0; i < size; i++)
cout << a[i] << " ";
cout << endl;
}
void twoSum(int a[], int size,int sum)
{
int i = 0, j = size - 1;
while (i sum)
j--;
else if (a[i] + a[j] < sum)
i++;
else
{
cout << a[i] << " " << a[j] << endl;
i++;
j--;
}
}
}
int main()
{
int a[] = { 8, 14, 12, 4, 7, 1, 3, 11,13 };
int size = sizeof(a) / sizeof(int);
quick_sort(a, 0, size - 1);
Print(a, size);
twoSum(a, size, 20);
return 0;
}
外排序( E x t e r n a l s o r t i n g External sorting Externalsorting )是指处理***超过内存*** 限度的数据的排序算法。通常将中间结果放在读写较慢的外存储器(通常是硬盘)上。
外排序常采用“排序-归并”策略。
排序阶段,读入能放在内存中的数据量,将其排序输出到临时文件,依次进行,将待排序数据组织为多个有序的临时文件。
归并阶段,将这些临时文件组合为大的有序文件。
例如使用 100 M 100M 100M 内存对 900 M B 900MB 900MB 的数据进行排序:
读入 100 M 100M 100M 数据至内存,用常规方式(如堆排序)排序。
将排序后的数据写入磁盘。
重复前两个步骤,得到 9 9 9 个 100 M B 100MB 100MB 的块(临时文件)中。
将 100 M 100M 100M 内存划分为 10 10 10 份,前 9 9 9 份中为***输入缓冲区***,第 10 10 10 份为***输出缓冲区***。如前 9 9 9 份各 8 8 8 M,第 10 10 10 份 18 M 18M 18M ;或 10 10 10 份大小同时为 10 M 10M 10M 。
在上面得出的 10 10 10 个临时文件块中,取其前 9 9 9 块各 8 8 8 M,第 10 10 10 块 18 18 18 M;或 10 10 10 份大小同时为 10 10 10 M。
执行九路归并算法(例如在上面获得的 9 9 9 个有序的块中,每块选取前 8 M 8M 8M 有序内容到输入缓冲区,进行归并),将结果输出到输出缓冲区。
若输出缓冲区满,将数据写至目标文件,清空缓冲区。
若输入缓冲区空,读入相应文件的下一份数据。
***排序通常不是目的,而是手段,是为了方便求解其他问题的一个手段。***比如上面说的 2 − s u m 2-sum 2−sum 问题,通过对数组的排序使得问题很方便的得到解决。
各种排序算法的时间复杂度:
稳定性分析:
一般的说,如果排序过程中,只有***相邻元素进行比较,是稳定的***,如冒泡排序、归并排序;如果***间隔元素进行了比较,往往是非稳定的***,如堆排序、快速排序。
一般的说,如果能够方便整理数据,对于不稳定的排序,可以使用(A[i],i)键对来进行算法,可以使得不稳定排序变成稳定排序。