排序算法总结:直接插入排序、冒泡排序、快速排序、简单选择排序、堆排序。
以下均是对数组a[n](a[0…n-1])按升序排序,如果需要降序,类似.
原创文章,转载请注明出处:http://blog.csdn.net/fastsort/article/details/10050565
1、直接插入排序
思路:初始时认为a[0]是有序的,然后依次将a[1]…a[n-1]插入到序列中。
/**@brief 对数组a[n]进行插入排序(升序)
** @note a[n] : a[0...n-1]
*/
void Sort_Insert(int a[], int n)
{
for(int i=1; i<n; i++)
{
for(int j=i;j>0 && a[j-1]>a[j];j--)
{
swap(a[j-1],a[j]);
}
}
}
复杂度:时间O(n2),空间O(1),
最坏情况:逆序,移动次数和比较次数均达到最大。
最好情况:正序,移动0次,比较n-1次。
稳定性:稳定
可优化:前m个已经排序好,第m+1个插入时可以使用二分查找插入位置。这时仅仅是减少了关键字比较次数,移动次数不变。
2、冒泡排序
思路:从第0个元素开始,如果某个元素大于它后面的元素,则交换。第一次循环完成后,最大的元素移动到最后一个位置。下一轮排序过程,仍然从第0个元素开始,直到倒数第二个元素,本轮排序完成后第二大的元素再倒数第二个位置上。
如果某一轮没有任何元素交换,则结束排序过程。(所谓的优化)
/**@brief 对数组a[n]进行冒泡排序
*/
void Sort_Bubble(int a[], int n)
{
for(int i=0; i<n-1; i++)
{
int flag=false;
for(int j=0; j<n-i-1; j++)
{
if(a[j]>a[j+1])
{
swap(a[j+1],a[j]);
flag = true;
}
}
if(flag==false) break;
}
}
复杂度:时间O(n2),空间O(1),
最坏情况:逆序,移动次数和比较次数均达到最大。
最好情况:正序,移动0次,比较n-1次,只需一轮。
稳定性:稳定
3、快速排序
快速排序的思路:通过一轮排序将记录分成两个独立的部分,其中一部分的关键字均比另一部分小,然后再分别对这两部分记录继续排序,直到整个记录有序。
其使用了“分治”的思想,将问题的规模分解为两个小规模的问题。显然,当一轮排序只有一个元素时为出口,这时直接返回。
代码:
/**@brief 对数组a[l...u]进行划分
** @return 返回划分的支点的下标
** @note a[l...q-1]<=a[q]<=a[q+1...u]
*/
int Partition(int a[], int l, int u)
{
int t=a[l];
while(l<u)
{
while(l<u && a[u]>=t) u--;
a[l] = a[u];
while(l<u && a[l]<=t) l++;
a[u] = a[l];
}
a[l] = t;
return l;
}
/**@brief 辅助函数,实际的快排 */
void qSortHelper(int a[], int l, int u)
{
if(l>=u) return;///递归出口
int q = Partition(a,l,u);
qSortHelper(a,l,q-1);
qSortHelper(a,q+1,u);
}
/**@brief 对外调用的快速排序
** @note 对数组a[n]进行快排
*/
void Sort_Quick(int a[], int n)
{
qSortHelper(a,0,n-1);
}
其中Partition函数将数组a[l…u]划分为两个部分,返回的即为这两个部分的分界点q,划分后,a[l…q-1]≤a[q] ≤a[q+1…u]。
复杂度:时间O(nlogn),空间O(logn).
最坏情况:正序或基本有序,退化为冒泡排序,时间复杂度O(n2),栈空间变为O(n)。
最好情况:数组随机乱序
稳定性:不稳定
优化:选取支点时随机选取,或者选取三个元素中的中间值,可改善最坏情况下的性能。也可以在基本有序时(递归出口不再是l>=u,而是u-l<C,C为某个比较小的常数),调用插入排序。
4、简单选择排序
思路:第一次从第0个元素开始选取最小的元素,与第0个元素交换,第二次从第1个元素开始选取最小的元素与第1个元素交换,直到最后一个元素。
/**@brief 对数组a[n]进行选择排序
*/
void Sort_Select(int a[], int n)
{
for(int i=0; i<n; i++)
{
int min = i;///第i个最小的数的下标
for(int j=i+1; j<n; j++)
{
if(a[min]>a[j])
min = j;
}
if(min!=i)
swap(a[min],a[i]);
}
}
复杂度:时间O(n2),空间O(1),
最坏情况:逆序,移动n次,比较O(n2)次。
最好情况:正序,移动0次,比较O(n2)次。
稳定性:这里的代码是稳定的,但是一般认为是不稳定的,要看具体的实现方式,比如这里将if(a[min]>a[j])改成if(a[min]>=a[j])则变成不稳定的了。
关于不稳定的排序:可以即为“希(希尔)望快(快排)点选(选择)对(堆排序)象”。
5、堆排序
堆排序是在选择排序的基础上优化而来。
堆的定义:略,
其实就是一颗特殊的完全二叉树:对于大根堆,任何一个节点都大于其左右孩子节点,这样堆顶元素就是最大的。对于小根堆,则堆顶元素是最小的。
下面讨论大根堆,小根堆的情况于此类似。
我们使用数组存储堆,数组a[n]的元素范围为a[0…n-1],则对于元素a[i],其左孩子为a[2i+1],右孩子为a[2i+2],父节点为a[(i-1)/2] ,其中i=0…n-1。
堆排序的过程可以归纳为:
1、建立大根堆
2、将堆顶元素与倒数第一个元素交换,这时最大元素在倒数第一个位置
3、将数组长度缩小1个,重新调整堆为大根堆。重复2-3,直到数组只有一个元素为止。
实质上就是堆的不断调整和删除的过程。所谓删除就是将堆顶元素(最大)和换到未排序的数组的末尾,从而实现排序。
而建堆过程也可以是看做不断插入和调整的过程:对叶子节点,已经是堆,所以从第一个非叶子节点开始调整,直到根节点。
假设数组a[n]中,a[s-1…m]已经满足堆的性质,那么将a[s]插入堆,使a[s…m]仍然是堆的过程就是:
将t=a[s]和其左右孩子节点比较,如果t大于左右孩子节点,那么这时a[s…m]已经满足堆的性质,直接返回;
否则沿着a[s]较大的孩子节点j(2s+1或者2s+2),将a[j]向上移动;然后再以j为根节点,继续向下调整,直到最后一个节点。
代码如下:
/**@brief 数组a[s+1...m]满足大根堆要求
** 现从s节点开始调整堆,使a[s...m]成为大根堆
** 数组从0开始,i节点左右孩子节点分别为2i+1,2i+2
*/
void HeapAdjust(int a[],int s,int m)
{
int temp = a[s] ;
for(int j=2*s+1; j<=m; j=2*j+1)///沿较大孩子节点向下筛选
{
if(j<m &&a[j]<a[j+1]) j++;///j取左右孩子较大的那个
if(temp>=a[j]) break; ///已经满足堆,跳出循环
a[s] = a[j];///较大的元素向上移动
s = j;///更新s
}
a[s] = temp;///a[s]的最终位置
}
有了这个函数,堆排序就简单了:
第一步,从第一个非叶子节点开始,逐步插入到堆中,构建一个大根堆
然后就是不断的删除堆顶元素和调整堆的过程:
void Sort_Heap(int a[],int n)
{ ///第一个非叶节点为n/2-1
for(int i=n/2-1; i>=0; i--)///创建大根堆
HeapAdjust(a,i,n-1);
for(int i=n-1;i>0;i--)///堆排序
{
swap(a[0],a[i]);///将大根堆堆顶元素交换到最后
HeapAdjust(a,0,i-1);///调整使其再次成为大根堆
}
}
复杂度:最好和最坏情况下都是时间O(nlogn),空间O(1),
但是对于n较小时性能不明显,因为其常数较大。
稳定性:不稳定。
6、求第k个数的问题,以及top-k问题。
不需要排序。参考快速排序的划分。如果当前划分返回的就是k,则这个元素就是待求的;否则看k和返回的kn大小关系,以确定在前部分继续查找还是在后部分继续查找。
int topHelper(int a[],int l,int u,int k)
{
int kn=Partition(a,l,u);
if(kn==k) return a[kn];
if(kn>k ) return topHelper(a,l,kn-1,k);
else return topHelper(a,kn+1,u,k);
}
/**@brief 返回数组a[n]中第k小的数
** @param 0<=k<n.
*/
int KMin(int a[], int n, int k)
{
return topHelper(a,0,n-1,k);
}
求得第k个数后,再便利一遍数组,即可得到top-k。
其他相关问题:参考《编程珠玑》第11章。