排序算法总结
1. 概述
定义
排序是将一个数据元素记录的任意序列,重新排成一个按关键字有序的序列。
稳定性
标准:对于两个大小相同的元素,排序前位置领先的元素在排序后依然领先,则这种排序算法
是稳定的 ;反之,若可能使排序后两者的位置交换,则这种算法就是不稳定的。判断:说某种排序算法是稳定的,意思是说存在一种代码实现,使之满足以上标准;说某种排
序算法不是稳定的,意思是说,该算法的任何一种实现形式,都存在某组特殊的关键字,使之
不稳定。分类
按排序过程中的不同原则来分,可以分为 交换排序、选择排序、插入排序、归并排序和计数排序等五类。
按时间复杂度来分,可以分为简单排序(时间复杂度为O (n^2))、先进排序(时间复杂度为O (nlogn))和
基数排序(时间复杂度为O (dn))三类。
2. 交换排序
2.1 冒泡排序(Bubble Sort)
- 思想
首先将记录的第一个数据和第二个数据比较,若为逆序则将两个数据的位置交换,然后比较第二个和第三个数据。
以此类推,直到第n-1和第n个数据比较完,完成一趟冒泡排序,此时最大值位于最后一个位置处。进行完n-1趟排序
或者在一趟排序过程中没有发生数据交换,则排序完成。 - 伪代码
do
swapped = false
for i = 1 to indexOfLastUnsortedElement
if leftElement > rightElement
swap(leftElement, rightElement)
swapped = true; swapCounter++
while swapped
- 代码
void bubble_sort(int *a, int n)
{
swapped = 1;
while(swapped)
{
int i,j,temp;
swapped = 0;
for(i=0;i<n-1-j;i++)
{
if(a[i]>a[i+1])
{
temp = a[i];
a[i]=a[i+1];
a[i+1] = temp;
swapped = 1;
}
}
j++;
}
}
void bubble_sort(int *a, int n)
{
int i, j, temp,swapped;
for(i = 0; i < n-1; i++)
{
swapped =0;
for(j = 0; j < n-1-i; j++)
{
if(a[j] > a[j+1])
{
temp = a[j];
a[j] = a[j+1];
a[j+1] = temp;
swapped =1;
}
}
if(swapped==0) break;
}
}
- 分析
交换次数较少,最少为0,比较次数多,为n(n-1)/2,时间复杂度为 O (n^2)。
2.2 快速排序(Quick Sort)
思想
快速排序是对冒泡排序的一种改进,也是交换排序的一种。快速排序首先选择一个基数(通常选择序列的第一个数据),
然后以这个基数为标准将序列分成两个部分,其中一部分中的数据全部大于该基数,另一部分全部小于该基数。然后
将分成的两个部分再分别进行以上的排序处理,从而使整个序列有序。一般用递归来实现。代码
///////////////////////快速排序
//查找位置
int find_pos(int *a, int low, int high)
{
int val = a[low];
while(low < high)
{
while(low < high && a[high] >= val)
{//大于移动,小于则赋值,降序则相反
high--;
}
a[low] = a[high];
while(low < high && a[low] <= val)
{//小于移动,大于则赋值,降序则相反
low++;
}
a[high] = a[low];
}//终止while循环之后low和high一定是相等的
//high可以改为low
a[low] = val;
return low;
}
//low:第一个元素下标
//high:最后一个元素下标
void quick_sort(int *a, int low, int high)
{
if(low < high) //长度大于1
{
int pos = find_pos(a, low, high); //将序列一分为二
quick_sort(a, low, pos-1); //对低位序列快速排序
quick_sort(a, pos+1, high); //对高位序列快速排序
}
}
- 分析
快速排序平均时间复杂度为 O (nlogn),最坏情况(逆序)的时间复杂度同冒泡法一样为 O (n^2)。
快速排序是对冒泡排序的改进,冒泡排序是对每个相邻的元素进行比较,快速排序是对所有的数据同基准元素进行比较,空间跨度更大,比较次数和交换次数减少。同时,用到了二分的思想,降低的时间复杂度。
快速排序在同等数量级knlogn的排序算法中常数因子k最小,是平均时间最少的一种排序算法。
平均空间复杂度为 O (log2n +1),最坏情况为 O(n),此时栈的深度为n。
3. 选择排序
3.1 简单选择排序(Simple Selection Sort)
思想
令i从1到n-1,进行n-1趟选择排序,每一趟都从未进行排序的n-i+1个数据中找出最小的数据,然后和第i个数据进行交换,完成排序。
伪代码
repeat (numOfElements - 1) times
{
set the first unsorted element as the minimum
for each of the unsorted elements
{
if element < currentMinimum
set element as new minimum
}
swap minimum with first unsorted position
}
- 代码
void select_sort(int *a, int n)
{
int i, j, k, temp;
for(i = 0; i < n-1; i++)
{
k = i;
for(j = i+1; j < n; j++)
{
if(a[k] > a[j])
{
k = j;
}
}
if(i != k)
{
temp = a[i];
a[i] = a[k];
a[k] = temp;
}
}
}
- 分析
交换次数较少,最少为0,最大为3(n-1),比较次数多,为n(n-1)/2,时间复杂度为 O (n^2)。
3.2 堆排序(Heap Sort)
回顾补充
先定义一下二叉堆,二叉堆是完全二叉树或者是近似完全二叉树,且满足:
1.父结点的键值总是大于或等于(小于或等于)任何一个子节点的键值。
2.每个结点的左子树和右子树都是一个二叉堆(都是最大堆或最小堆)。
当父结点的键值总是大于或等于任何一个子节点的键值时为最大堆。当父结点的键值总是小于或等于任何一个子节点的键值时为最小堆。下图展示一个最小堆:
堆的存储: 一般用数组来表示堆,i节点的父节点下标为(i-1)/2,它的左右子节点的下标为2*i+1和2*i+2。
思想
堆顶是序列的最小值(或最大值),输出堆顶后,将其余的元素重新排列成堆,可以得到序列的次小值,
然后将次小值输出,如此反复执行,便能得到一个有序序列。
所以,堆排序要做的就是输出堆顶和重新排列一个堆。- 代码
// 从i节点开始调整,n为节点总数 从0开始计算 i节点的子节点为 2*i+1, 2*i+2
void RebuildMinHeap(int *a,int i, int n)
{
int j,temp;
temp =a[i];
j=2*i+1;
while(j<n)
{
if(j+1<n && a[j+1]<a[j]) //在左右孩子中找最小的
j++;
if(a[j]<temp) //如果孩子节点比父节点小,则将孩子节点的值赋值给父节点,并继续往下走,
{ //如果上述赋值破坏了原有的堆结构,则重建。
a[i]=a[j];
i=j;
j=2*i+1;
}
else break;
}
a[i]=temp; //最后,将开始的调整的顶节点的值放到合适的位置上
}
// 堆排序
//1.建立堆化数组;
//2.取出堆的顶点,把最后一位放到顶点位置;
//3.重新建立堆化数组,重复步骤2,直到所有数据都被取出;
void heap_sort (int *a,int n)
{
int i,j,temp;
for(i=n/2 -1;i>=0;i--) //对于叶子节点来说,可以认为它已经是一个合法的堆了,
{ //所以从第一个非叶子节点n/2 -1开始重建堆。
RebuildMinHeap(a,i,n); //建立堆化数组
}
for(i=n-1;i>=1;i--) //数组中第一个数据已经是最小值,先将该值取出,最后一个数据放到堆顶,
{ //然后,重新建立堆化数组。
temp= a[0];
a[0]=a[i];
a[i]=temp;
RebuildMinHeap(a,0,i);
}
}
//* 注意 使用最小堆排序后是递减数组,要得到递增数组,可以使用最大堆。
- 分析
由于每次重新恢复堆的时间复杂度为O(logn),共n- 1次重新恢复堆操作,再加上前面建立堆时n / 2次向>下调整,每次调整时间复杂度也为O(logn)。二次操作时间相加还是O(nlogn)。故堆排序的时间复杂度>为时间复杂度为 O (nlogn)。
4. 插入排序
4.1 直接插入排序 (Straight Insertion Sort)
思想
最简单的排序方法,将一个元素插入到已经排好序的有序表中,得到一个新的、元素加1的有序表。
将表中的第一个元素看做是已经有序的表,然后将后面的元素依次插入到该表中。
具体做法是,将第一个元素看做是已经有序的,从第二个到第n个元素依次进行排序,排序的元素
先和已经排好序的表中的最后一个元素开始比较,若大于最后一个元素则插入到最后一个元素的后
一位,若小于最后一个元素,则最后一个元素后移一位,然后和倒数第二个元素比较,依次类推,
最终将排序的元素插入到有序列表中。然后再进行下一个元素的排序。伪代码
mark first element as sorted
for each unsorted element
'extract' the element
for i = lastSortedIndex to 0
if currentSortedElement > extractedElement
move sorted element to the right by 1
else: insert extracted element
- 代码
void insert_sort(int *a, int n)
{
int i, j, temp;
for(i = 1; i < n; i++)
{
temp = a[i];
for(j = i-1; j >= 0 && a[j] > temp; j--)
{
a[j+1] = a[j];//将前面的值往后移一位
}
a[j+1] = temp; //待排序元素放在a[j]的后面
}
}
- 分析
需要的辅助空间为1,比较和交换次数约为(n^2)/4,时间复杂度为 O (n^2)。
4.2 希尔排序 (Shells Sort)
思想
先将整个待排序列分割成若干子序列,分别进行直接插入排序,待整个序列中的记录“基本有序”时,
再对全体记录进行一次直接插入排序。子序列的分割是将相隔某个“增量”的记录组成一个序列。增量
的选取较复杂。若增量选取为:n,n/2,n/4,n/8···2,1 则称之为折半插入排序。所以说折半插入排序是希
尔排序的一种,下面以折半插入排序为例进行介绍。代码
void shell_sort(int *a, int n) //希尔排序(折半插入排序)
{
int i, j, flag, temp;
int gap = n;
while(gap > 1)
{
gap = gap/2; //增量缩小,每次减半(折半)
do
{
flag = 0;
//n-gap是控制上限不让越界
for(i = 0; i < n-gap; i++)
{
j = i + gap; //相邻间隔的前后值进行比较
if(a[i] > a[j])
{
temp = a[i];
a[i] = a[j];
a[j] = temp;
flag = 1;
}
}
}while(flag != 0);
}
}
- 分析
需要的辅助空间和直接插入排序相同,比较次数较直接插入排序减少了,但交换次数不变,故约时间复杂度仍为 O (n^2)。若增量选取的好,希尔排序的时间复杂度可以降为 O (n^3/2)。
5. 归并排序 (Merge Sort)
思想
归并排序建立在归并操作上的一种有效的排序算法。该算法是采用分治法。
首先考虑下如何将将二个有序数列合并。这个非常简单,只要从比较二个数列的第一个数,谁小就先取谁,取了后就在对应数列中删除这个数。然后再进行比较,如果有数列为空,那直接将另一个数列的数据依次取出即可。
解决了上面的合并有序数列问题,再来看归并排序,其的基本思路就是将数组分成二组A,B,如果这二组组内的数据都是有序的,那么就可以很方便的将这二组数据进行排序。如何让这二组组内数据有序了?
可以将A,B组各自再分成二组。依次类推,当分出来的小组只有一个数据时,可以认为这个小组组内已经达到了有序,然后再合并相邻的二个小组就可以了。这样通过先递归的分解数列,再合并数列就完成了归并排序。代码
//合并两个有序数列
//从first到mid为第一个序列,mid+1到last为第二个序列
void mergearray(int *a,int first,int mid,int last,int *temp)
{
int i=first,j= mid +1;
int n=mid, m= last;
int k=0;
while(i<=n && j<=m)
{
if(a[i]<=a[j])
temp[k++]=a[i++];
else
temp[k++]=a[j++];
}
while(i<=n)
{
temp[k++]=a[i++];
}
while(j<=m)
{
temp[k++]=a[j++];
}
for(i=0;i<k;i++)
{
a[first+i]=temp[i]; //将缓存到temp中的有序序列转移到a数组中
}
}
//归并排序
void merge_sort(int *a,int first,int last,int *temp)
{
if(first < last)
{
int mid =(first + last)/2;
merge_sort(a,first,mid,temp); //左侧序列有序
merge_sort(a,mid+1,last,temp); //右侧序列有序
mergearray(a,first,mid,last,temp); //两个有序序列合并
}
}
- 分析
将数列分开成小数列一共要logn步,每步都是一个合并有序数列的过程,时间复杂度可以记为O(n),故时间复杂度一共为 O (nlogn)。
辅助空间为n。
是一种稳定的排序算法(快速排序和堆排序都是不稳定的)。
6. 计数排序 (Count sort)
思想
计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数。然后根据数组C来将A中的元素排到正确的位置。
算法的步骤如下:- 找出待排序的数组中最大和最小的元素
- 统计数组中每个值为i的元素出现的次数,存入数组C的第i项
- 对所有的计数累加(从C中的位置为1的元素开始,每一项和前一项相加)
- 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
代码
void count_sort(int *a,int n) { int i,j,num,max=-INF,min=INF; for(i=0;i<n;i++) //找最大值最小值 { if(a[i]>max) { max=a[i]; } if(a[i]<min) { min=a[i]; } } num=max-min+1; //计算C数组的大小 int c[num]; for(i=0;i<num;i++) //初始化C数组 { c[i]=0; } for(i=0;i<n;i++) //填充C数组 { c[a[i]-min]++; } j=0; i=0; while(i<n && j<num) //从C数组中取值出来 { while(i<n && c[j]!=0) { a[i]=j+min; c[j]--; i++; } j++; } }- 分析
由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。时间复杂度为O (n)。 是一种稳定排序算法。
7. 总结
比较以上几种排序算法,有以下结果:
| 排序方法 | 平均情况 | 最好情况 | 最坏情况 | 辅助空间 | 稳定性 |
|---|---|---|---|---|---|
| 冒泡排序 | O (n^2) | O (n) | O (n^2) | O (1) | 稳定 |
| 简单选择排序 | O (n^2) | O (n^2) | O (n^2) | O (1) | 稳定 |
| 直接插入排序 | O (n^2) | O (n) | O (n^2) | O (1) | 稳定 |
| 希尔排序 | O (nlogn)~ O (n^2) | O (n^1.3) | O (n^2) | O (1) | 不稳定 |
| 堆排序 | O (nlogn) | O (nlogn) | O (nlogn) | O (1) | 不稳定 |
| 归并排序 | O (nlogn) | O (nlogn) | O (nlogn) | O (n) | 稳定 |
| 快速排序 | O (nlogn) | O (nlogn) | O (n^2) | O (logn)~ O (n) | 不稳定 |
以上就是对排序算法的一个小的总结,还有一些算法没有涉及到,如果以后涉及到了,再做补充。
参考:
1. 《数据结构-C语言版》(严蔚敏,吴伟民版);
2. http://blog.youkuaiyun.com/jnu_simba/article/details/9705111?utm_source=tuicool&utm_medium=referral
3. http://blog.youkuaiyun.com/morewindows/article/details/6709644
4. http://blog.youkuaiyun.com/morewindows/article/details/6678165/
2748

被折叠的 条评论
为什么被折叠?



