排序算法总结
排序方法 | 时间复杂度(平均) | 时间复杂度(最坏) | 时间复杂度(最好) | 空间复杂度 | 稳定性 |
冒泡排序 | O(N^2) | O(N^2) | O(N) | O(1) | 稳定 |
选择排序 | O(N^2) | O(N^2) | O(N^2) | O(1) | 不稳定 |
插入排序 | O(N^2) | O(N^2) | O(N) | O(1) | 稳定 |
希尔排序 | O(N^1.5) | O(N^2) | O(N^1.3) | O(1) | 不稳定 |
堆排序 | O(NlogN) | O(NlogN) | O(NlogN) | O(1) | 不稳定 |
快速排序 | O(NlogN) | O(N^2) | O(NlogN) | O(logN) | 不稳定 |
归并排序 | O(NlogN) | O(NlogN) | O(NlogN) | O(N) | 稳定 |
计数排序 | O(N+K) | O(N+K) | O(N) | O(K) | 稳定 |
基数排序 | O(d(N+K)) | O(d(N+K)) | O(K) | 稳定 | |
桶排序 | O(N) | O(N^2) | O(N) | 稳定 |
注意
1 希尔排序的复杂度与增量系列有关。
2.计数排序中的K表示数据的范围
3.基数排序中每个整数有d位数,每个数字可能取k个值
一、冒泡排序
算法描述
冒泡排序是最简单的一种排序算法,它重复的走过要要排序的序列,一次比较两个元素,如果它们的顺序错误就把它们交换位置,最小的元素会由交换慢慢“浮”到顶端。
具体算法描述如下
比较相邻的元素。如果第一个比第二个大,就交换它们两个;
对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
针对所有的元素重复以上的步骤,除了最后一个;
重复步骤1~3,直到排序完成。
typedef int ElementType;
void BubbleSort(ElementType A[], int N)
{
ElementType temp;
for (int i = 0; i< N -1; ++i)
for (int j = 0; j <N-i-1; ++j)
{
if (A[j] > A[j + 1])
{
temp = A[j];
A[j] = A[j +1];
A[j +1] = temp;
}
}
}
复杂度分析
当输入的序列本身就是有序序列,那么将不需要排序,此时时间复杂度为O(n);当输入序列是反序时,就是最坏情况,此时的时间复杂度为O(n^2)。它是稳定的排序算法,平均时间复杂度为O(n^2),其空间复杂度为O(1)。
二、选择排序
算法描述
选择排序无论什么样的数据输进去都是O(n^2)的时间复杂度。它每次在未排序序列中选出最小的元素,再存放到排序序列的末尾,如此重复直到所有元素全部排完。
void SelectionSort(ElementType A[], int N) {
for (int i = 0; i < N; ++i)
{
int minIndex = i;
for (int j = i+1; j < N; ++j)
{
if (A[j] < A[minIndex])
{
minIndex = j;
}
}
int temp = A[minIndex];
A[minIndex] = A[i];
A[i] = temp;
}
}
复杂度分析
因为每次都是从未排序序列中选出最小的元素放入已排序序列的末尾,每选择一个元素的时间复杂度为O(n),总共需要选择n次,所以时间复杂度为O(n^2);最坏、最好、平均时间复杂度都为O(n^2),空间复杂度为O(1),因为每次找到最小的元素,将该元素与已排序序列后的第一个(未排序首元素)进行交换,所以它是不稳定的排序算法
三、插入排序
算法描述
插入排序的原理就和打扑克牌一样,从一堆没有排好序的牌中拿起一张,然后放入到手中正确的位置。即每次从未排序序列中取出第一个元素,然后插入到已排序序列中正确的位置,插入过程需要反复把已排序序列往后挪。
![](https://i-blog.csdnimg.cn/blog_migrate/5dff532ce5668e3742e37ece5e3101e0.gif)
void
InsertionSort(ElementType A[], int N)
{
ElementType Tmp;
int j;
for (int i= 1; i < N; ++i)
{
Tmp = A[i];
for ( j = i; j > 0 && A[j - 1] > Tmp; --j)
A[j] = A[j - 1];
A[j] = Tmp;
}
}
复杂度分析
如果输入序列是反序的,那么每次都需要挪动已排序序列中的所有元素,此时的时间复杂度为O(n^2);当输入元素已经排好序时,每次插入时不需要挪动元素,此时的时间复杂度为O(n),插入排序的平均情形O(N^2)。空间复杂度为O(1)。因为相同元素在插入时不会改变原有的位置,所以它是 稳定 的排序算法。插入排序最坏情况的运行时间和冒泡排序一样,但一般来说BubbleSort会慢点,因为它有许多的交换操作。
四、计数排序
算法描述
计数排序的思想就是引入一个额外的数组C,用这个数组C统计待排序数组中每个元素的个数,再根据数组C来将待排序数组中的元素放到正确的位置。
找出待排序的数组中最大和最小的元素;
统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。
![](https://i-blog.csdnimg.cn/blog_migrate/80df3f49ea76b58c180ff91277f67a54.gif)
//计数排序
void CountSort(int A[], int N)
{
int max = A[0];
int min = A[0];
//选出最大数与最小数,确定哈希表的大小
for (int i = 0; i < N; ++i)
{
if (A[i] > max)
{
max = A[i];
}
if (A[i] < min)
{
min = A[i];
}
}
int range = max - min + 1;
int *count = new int[range](); //初始化为0
for (int i = 0; i < N; ++i)
{
++count[A[i]-min];
}
//将数据重新写回数组
int j = 0;
for (int i = 0; i < N; ++i)
{
while (count[i]--)
{
A[j] = i + min;
++j;
}
}
}
复杂度分析
计数排序不是基于比较的排序算法,基于比较的排序算法时间复杂度不可能低于O(nlogn)。当输入的n个元素在0到k之间时,它的运行时间为O(n+k)。最好、最差、平均时间复杂度都为O(n+k),其空间复杂度为O(k),是一个 稳定 的排序算法
五、希尔排序
算法描述
希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序,同时该算法是冲破O(n2)的第一批算法之一。希尔排序实质上是一种分组插入方法。它的基本思想是:对于n个待排序的数列,取一个小于n的整数Increment(增量)将待排序元素分成若干个组子序列,所有距离为Increment的倍数的记录放在同一个组中;然后,对各组内的元素进行直接插入排序。 这一趟排序完成之后,每一个组的元素都是有序的。然后减小Increment的值,并重复执行上述的分组和排序。重复这样的操作,当Increment=1时,整个数列就是有序的。
void Shellsort(ElementType A[], int N)
{
int i, j, Increment;
ElementType Tmp;
for (Increment=N/2;Increment>0;Increment/=2)
for (i = Increment; i < N; i++)
{
Tmp = A[i];
for (j = i; j >= Increment; j -= Increment)
{
if (Tmp < A[j - Increment])
A[j] = A[j - Increment];
else
break;
}
A[j] = Tmp;
}
}
复杂度分析
我们将希尔排序使用的序列h(1),h(2),、、、h(t)叫做增量序列,自要h(1)=1,任何增量序列都是可行的,但是增量序列的选取会影响运行时间。使用希尔增量时最坏的运行时间为O(N^2)。Hibbard增量的希尔排序的最坏运行时间为O(N^(3/2)),它的增量行如1、3、7、,,,、2^K-1。Sedgewick提出的增量序列的最坏的情形的运行时间为O(N^(4/3)),在实践中这些序列的运行要比Hibbard的好得多,其中最好的序列为{1,5,19,41,109,...},该序列中的项或者为9*4^i-9*2^i+1,或者是4^i-3*2^i+1。一次插入排序是稳定的,不会改变相同元素的相对顺序,但由于多次插入排序,在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以希尔排序是不稳定的。
六、堆排序
算法描述
堆排序是利用堆这种数据结构来进行排序的,若要实现从小到大的排序,我们在实现中将使用一个最大堆。第一步是以线性时间建立一个堆。然后通过将堆中的最后一个元素与第一个元素交换,缩减堆的大小并进行下滤,来执行N-1次DeleteMax操作,当算法终止时,数组则以所排的顺序包含这些元素。
//交换
void
Swap(ElementType *Lhs, ElementType *Rhs)
{
ElementType Tmp = *Lhs;
*Lhs = *Rhs;
*Rhs = Tmp;
}
//上滤
#define LeftChild(i) (2*(i)+1)
void PercDown(ElementType A[], int i, int N)
{
int child;
ElementType Tmp;
for (Tmp = A[i]; LeftChild(i) < N; i = child)
{
child = LeftChild(i);
if (child != N - 1 && A[child + 1] > A[child]) //比较左右儿子确定最大者
++child;
if (Tmp < A[child])
A[i] = A[child];
else break;
}
A[i] = Tmp;
}
void Heapsort(ElementType A[], int N)
{
int i;
for ( i = N / 2; i >= 0; --i)
{
PercDown(A, i, N);
}
for (i = N - 1; i > 0; --i)
{
Swap(&A[0], &A[i]);
PercDown(A, 0, i);
}
}
复杂度分析
堆排序的时间复杂度是O(nlogn)[在实践中它慢与Sedgewick增量序列的希尔排序。堆排序是一种选择排序,整体主要由构建初始堆+交换堆顶元素和末尾元素并重建堆两部分组成。其中构建初始堆经推导复杂度为O(n),在交换并重建堆的过程中,需交换n-1次,而重建堆的过程中,根据完全二叉树的性质,[log2(n-1),log2(n-2)…1]逐步递减,近似为nlogn。所以堆排序时间复杂度一般认为就是O(nlogn)级。空间复杂度为O(1)。是一个不稳定的排序算法。
七、归并排序
算法描述
归并排序采用算法是经典的分治策略,其核心思想是将两个已经排序的序列合并成一个序列,而如果一个序列本身只有一个元素,那么就是有序序列。所以我们可以将未排序的序列分割成更小的有序序列,然后再将序列合并起来就完成了排序。我们可以递归的将数据的前半部分和后半部分各自归并排序,最后剩下N=1,那么只有一个元素需要排序。该算法的基本操作就是合并两个已经排序的表,若将输出放到第三个表中则该算法可以通过对输入数据进行一趟排序来完成。
![](https://i-blog.csdnimg.cn/blog_migrate/e098a4024646e134946653866a4b284d.gif)
//合并操作
void Merge(ElementType A[], ElementType TmpArray[], int Lpos,int Rpos, int RightEnd)
{
int LeftEnd,NumElements, TmpPos;
NumElements = RightEnd - Lpos + 1;
LeftEnd = Rpos - 1;
TmpPos = Lpos;
while (Lpos <= LeftEnd && Rpos <= RightEnd)
if (A[Lpos] <= A[Rpos])
TmpArray[TmpPos++] = A[Lpos++];
else
TmpArray[TmpPos++] = A[Rpos++];
while(Lpos <= LeftEnd)
TmpArray[TmpPos++] = A[Lpos++];
while (Rpos <= RightEnd)
TmpArray[TmpPos++] = A[Rpos++];
for (int i = 0; i < NumElements; i++, RightEnd--)
A[RightEnd] = TmpArray[RightEnd];
}
//分治处理
void MSort(ElementType A[], ElementType TmpArray[],
int Left, int Right)
{
int Center;
if (Left < Right)
{
Center = (Left + Right) / 2;
MSort(A, TmpArray, Left, Center);
MSort(A, TmpArray, Center + 1, Right);
Merge(A, TmpArray, Left, Center + 1, Right);
}
}
//归并排序
void Mergesort(ElementType A[], int N)
{
ElementType *TmpArray;
TmpArray = (ElementType*)malloc(N * sizeof(ElementType));
if (TmpArray != NULL)
{
MSort(A, TmpArray, 0, N - 1);
free(TmpArray);
}
else
printf("No space for tmp array!!!");
}
复杂度分析
虽然归并排序的运行时间是O(NlogN),代价是需要O(n)的空间复杂度,是一个稳定的排序算法。它很难用于主存排序,主要问题在于合并两个排序的表需要线性附加内存,在整个算法中还要花费将数据拷贝到临时数组再拷贝回来这样附加的工作,严重放慢了排序的速度。这种拷贝可以通过在递归交替层次中审慎的转换A和TmpArray的角色避免。归并排序的非递归实现方法是通过合并相邻的元素,如先对相邻的两个排序,再对4个排序...,以此类推。八、快速排序
算法描述
像归并排序一样,快速排序也是一种分治的递归算法。将数组A其基本算法步骤为:
1)如果A中的元素个数为0或者1,则返回
2)取A任一元素V,称为枢纽元。
3)将A-{V}分为两个不相交的集合:A1和A2,其中A1中的元素值小于等于V,A2中的元素值大于等于V
4)返回{quicksort(A1)}后,继随V,继而quicksort(A2)}。
对于选取枢纽元V,可以采用三数中值分割的方法:对A[Left]、A[Center]和A[Right]适当的排序,三个数中最小的被分在A[Left],三个数中最大的数被分在A[Right],最后的一个元素作为枢纽元并被放在A[Right-1],因此可以在分割阶段将i和j初始化为Left+1和Right-2.
图为选取第一个元素作为枢纽元
对于i和j遇到等于枢纽元的情况,做法是让i和j都停止,并进行交换,这样保证分割不偏向一方的倾向,也能防止出现越界行为。
ElementType
Median3( ElementType A[ ], int Left, int Right )
{
int Center = ( Left + Right ) / 2;
if( A[ Left ] > A[ Center ] )
Swap( &A[ Left ], &A[ Center ] );
if( A[ Left ] > A[ Right ] )
Swap( &A[ Left ], &A[ Right ] );
if( A[ Center ] > A[ Right ] )
Swap( &A[ Center ], &A[ Right ] );
/* Invariant: A[ Left ] <= A[ Center ] <= A[ Right ] */
Swap( &A[ Center ], &A[ Right - 1 ] ); /* Hide pivot */
return A[ Right - 1 ]; /* Return pivot */
}
#define Cutoff ( 10 )
void
Qsort( ElementType A[ ], int Left, int Right )
{
int i, j;
ElementType Pivot;
/* 1*/ if( Left + Cutoff <= Right )
{
/* 2*/ Pivot = Median3( A, Left, Right );
/* 3*/ i = Left; j = Right - 1;
/* 4*/ for( ; ; )
{
/* 5*/ while( A[ ++i ] < Pivot ){ }
/* 6*/ while( A[ --j ] > Pivot ){ }
/* 7*/ if( i < j )
/* 8*/ Swap( &A[ i ], &A[ j ] );
else
/* 9*/ break;
}
/*10*/ Swap( &A[ i ], &A[ Right - 1 ] ); /* Restore pivot */
/*11*/ Qsort( A, Left, i - 1 );
/*12*/ Qsort( A, i + 1, Right );
}
else /* Do an insertion sort on the subarray */
/*13*/ InsertionSort( A + Left, Right - Left + 1 );
}
对于Left+10<Right的截止情况我们选择了直接用插入排序,因为在小数组时快速排序不如插入排序好。
void
Quicksort( ElementType A[ ], int N )
{
Qsort( A, 0, N - 1 );
}
注意代码中的第3行到第9行不能用下面的代码代替,因为若A[i]=A[j]=Pivot则会出现无限循环。
/* 3*/ i = Left + 1; j = Right - 2;
/* 4*/ for( ; ; )
{
/* 5*/ while( A[ i ] < Pivot ) i++;
/* 6*/ while( A[ j ] > Pivot ) j--;
/* 7*/ if( i < j )
/* 8*/ Swap( &A[ i ], &A[ j ] );
else
/* 9*/ break;
}
复杂度分析
快速排序因为基于递归分治的思想,每次划分都很平均时性能表现为最优,需要O(nlogn)的时间复杂度;它的最坏的情形为O(N^2),由于关键字的比较和交换是跳跃进行的,所以它是一个 不稳定 的排序算法。
快速选择算法
我们可以将快速排序算法稍作修改将其应用在选择问题上,该算法称为快速选择算法,复杂度O(NlogN),最坏情况为O(N^2)。令|A|为A中的元素个数,查找A中第k个最小元,算法步骤如下:
1)如果|A|=1,那么k=1,则将A中的元素作为答案返回
2)取A中一元素V,称为枢纽元。
3)将A-{V}分为两个不相交的集合:A1和A2,其中A1中的元素值小于等于V,A2中的元素值大于等于V
4)若k<=|A1|,那么第k个最小元必然在A1中,在这种情况下返回quciksort(A1,k).如果k=1+|A1|,那么枢纽元就是第K个元素,我们将其返回。否则第k个最小元就在|A2|中,它是A2中的第(k-|A1|-1)个最小元,则递归调用返回quicksort(A2,k-|A1|-1).
#define Cutoff ( 10 )
void
Qselect( ElementType A[ ], int k, int Left, int Right )
{
int i, j;
ElementType Pivot;
/* 1*/ if( Left + Cutoff <= Right )
{
/* 2*/ Pivot = Median3( A, Left, Right );
/* 3*/ i = Left; j = Right - 1;
/* 4*/ for( ; ; )
{
/* 5*/ while( A[ ++i ] < Pivot ){ }
/* 6*/ while( A[ --j ] > Pivot ){ }
/* 7*/ if( i < j )
/* 8*/ Swap( &A[ i ], &A[ j ] );
else
/* 9*/ break;
}
/*10*/ Swap( &A[ i ], &A[ Right - 1 ] ); /* Restore pivot */
/*11*/ if( k <= i )
/*12*/ Qselect( A, k, Left, i - 1 );
/*13*/ else if( k > i + 1 )
/*14*/ Qselect( A, k, i + 1, Right );
}
else /* Do an insertion sort on the subarray */
/*15*/ InsertionSort( A + Left, Right - Left + 1 );
}
该算法结束时,第k小的元素就在数组中第k个位置。
九、基数排序
算法描述
基数排序不是基于比较的排序算法,它是根据关键字中各位的值,通过对排序的N个元素进行若干趟“分配”与“收集”来实现排序的,和计数排序有一些地方相通。它有两种排序方案,MSD 从高位开始进行排序;LSD 从低位开始进行排序。
数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以是稳定的。
![](https://i-blog.csdnimg.cn/blog_migrate/a36b5b18d39f111ad41164a14bfe1278.gif)
//基数排序
//LSD 先以低位排,再以高位排
void RadixSort(int A[], int n)
{
//求出其最大位数 个 十 百 千 万....
int digit = 0;
int bash = A[0];
for (int i = 0; i < n; ++i)
{
while (A[i] >(pow(10, digit)))
{
digit++;
}
}
int flag = 1;
for (int j = 1; j <= digit; ++j)
{
//建立数组统计每个位出现数据次数
int Digit[10] = { 0 };
for (int i = 0; i < n; ++i)
{
Digit[(A[i] / flag) % 10]++;
}
//建立数组统计起始下标
int BeginIndex[10] = { 0 };
for (int i = 1; i < 10; ++i)
{
BeginIndex[i] = BeginIndex[i - 1] + Digit[i - 1];
}
//建立辅助空间
int *tmp = new int[n]();
for (int i = 0; i < n; ++i)
{
int index = (A[i] / flag) % 10;
tmp[BeginIndex[index]++] = A[i];
}
//将数据重新写回原空间
for (int i = 0; i < n; ++i)
{
A[i] = tmp[i];
}
flag = flag * 10;
delete[] tmp;
}
}
复杂度分析
基数排序是稳定的排序算法,假设在n个d位数,其中每个数位有k个可能的取值。如果每一轮采用稳定的排序算法耗时O(n+k),则基数排序的时间复杂度为O(d(n+k)),基数排序的效率和初始序列是否有序没有关联。
十 桶排序
算法描述
桶排序是对计数排序的改进,它假设输入数据服从均匀分布,和计数排序类似,都是对输入数据做了某种假设。计数排序假设输入的数据属于一个小区间内的整数,而桶排序假设输入是由一个随机过程产生。该过程均匀、独立的将元素分布在[0,1)区间上。桶排序利用了函数映射关系,其实就和java里面的hashMap很像,使用拉链法解决hash冲突。桶是有序的,但是每个桶里面的元素不是有序的,因此再使用一个排序算法,对每一个链表进行排序。最后得到的就是一个有序序列。
#include<iterator>
#include<iostream>
#include<vector>
using namespace std;
const int BUCKET_NUM = 10;
struct ListNode {
explicit ListNode(int i = 0) :mData(i), mNext(NULL) {}
ListNode* mNext;
int mData;
};
ListNode* insert(ListNode* head, int val) {
ListNode dummyNode;
ListNode *newNode = new ListNode(val);
ListNode *pre, *curr;
dummyNode.mNext = head;
pre = &dummyNode;
curr = head;
while (NULL != curr && curr->mData <= val) {
pre = curr;
curr = curr->mNext;
}
newNode->mNext = curr;
pre->mNext = newNode;
return dummyNode.mNext;
}
ListNode* Merge(ListNode *head1, ListNode *head2) {
ListNode dummyNode;
ListNode *dummy = &dummyNode;
while (NULL != head1 && NULL != head2) {
if (head1->mData <= head2->mData) {
dummy->mNext = head1;
head1 = head1->mNext;
}
else {
dummy->mNext = head2;
head2 = head2->mNext;
}
dummy = dummy->mNext;
}
if (NULL != head1) dummy->mNext = head1;
if (NULL != head2) dummy->mNext = head2;
return dummyNode.mNext;
}
void BucketSort( int arr[], int n ) {
vector<ListNode*> buckets(BUCKET_NUM, (ListNode*)(0));
for (int i = 0; i<n; ++i) {
int index = arr[i] / BUCKET_NUM;
ListNode *head = buckets.at(index);
buckets.at(index) = insert(head, arr[i]);
}
ListNode *head = buckets.at(0);
for (int i = 1; i<BUCKET_NUM; ++i) {
head = Merge(head, buckets.at(i));
}
for (int i = 0; i<n; ++i) {
arr[i] = head->mData;
head = head->mNext;
}
}
复杂度分析
桶排序平钧时间代价O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越多,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大,是一个稳定的排序算法。
O(N^2) |