GitHub: KolinHuang
个人博客:KolHuang Blog
欢迎交流~
写在前头:
刚刚考完研究生初试,正在紧张的准备复试当中…
怎奈实在是难以静下心好好的看书,于是想到了写博客这个办法,希望奏效……=-=
话不多说,开始吧~
提醒:本文排序均为升序,即是第一个元素为最小值。
冒泡排序
就从最为大家熟知的冒泡排序开始,
原理:
假设有一个无序数组s[n],一趟排序的过程:把第一个元素与第二个元素比较,如果第一个比第二个大,那么交换他们的位置。接着继续比较第二个元素和第三个元素的大小,如果第二个比第三个大,那么交换他们的位置,以此类推…
代码分析:
一趟排序只能将数组中的最大值挪到数组的末尾(即一次确定一个元素的最终位置),所以在每一趟排序结束后要减少待排元素个数,因此2个for循环即可搞定,废话不多说,上代码~
void Bubble_Sort(ElementType s[],int N)
{
int i,j;
for(i = N - 1; i >= 0; --i)
{
for ( j = 0; j < i; ++j)
{
if(s[j] > s[j+1])
swap(s[j],s[j+1]);
}
}
}
但是这样的冒泡并不高效,如果有这么一种情况:当进行了k趟排序后(k<n),待排序列已经完全有序,但是程序依旧会继续进行余下比较,但都是无意义的,所以只需要设置一个flag,当某趟排序发生了交换时,则继续下一次比较,否则break。代码如下:
void Bubble_Sort(ElementType s[],int N)
{
int i,j;
for(i = N-1; i >= 0; --i)
{
int flag = 0;
for ( j = 0; j < i; ++j)
{
if(s[j] > s[j+1])
{
swap(s[j],s[j+1]);
flag = 1;
}
}
if(flag == 0) break;
}
}
性能分析:
仅用了常数个辅助单元,因而空间复杂度为O(1)。
平均的时间复杂度为O(n^2)。
是一个稳定的算法。(补充:稳定不稳定是看对于相等的两个元素在进行排序后,相对位置是否发生了改变,没有改变则是稳定的,否则不稳定)
快速排序
原理:
快速排序是对冒泡排序的一种改进。其基本思想是基于分治的:在待排序列s[0…n-1]中,选取一个元素pivot作为基准,通过一趟排序将原表划分为两个子表,左边的值均小于pivot,右边的值均大于pivot,而pivot也处于最终位置上。而后对两个子表进行递归排序,以此类推…
代码分析:
假设我们已经有了一个划分的辅助函数Partition(),返回值为pivot在表中的最终位置,递归地调用快速排序即可,代码如下:
void Quick_Sort(ElementType s[],int low,int high)
{
if(low < high)//递归跳出条件
{
int pivotPos = Partition(s,low,high);
Quick_Sort(s,low,pivotPos-1);
Quick_Sort(s,pivotPos+1,high);//对两个子表递归
}
}
然后就是重点的Partition()函数,如果我们能够选择出一个pivot值,使得其最终位置处于待排序列的中间,那么两个子表的长度相近,则递归的次数就会少很多,代码也相对高效(在这里,我选择第一个元素作为pivot,在性能分析中会提出更优的选择方案。),接下来的操作很简单,只需要将比pivot大的向右边移动,将比pivot小的向左边移动即可。代码如下:
int Partition(ElementType s[],int low,int high)
{
ElementType pivot = s[low];
while(low < high)
{
while(low < high && s[high] >= pivot) --high;
s[low] = s[high];//将比pivot小的移到左边
while(low < high && s[low] <= pivot) ++low;
s[high] = s[low];//将比pivot大的移到右边
}
a[low] = pivot;
return low;//此时low或者high就是pivot的最终位置
}
性能分析:
由于采用递归算法,因此需要借助一个递归工作栈来保存每一层递归调用的必要信息(如果要改成非递归算法,可以从这里出发,借用一个栈来保存信息),所以空间复杂度为O(log2n)。
时间复杂度为O(nlog2n)。
上文提到的适当pivot ,可以提高算法效率,这里给出一个方案:从序列的头尾以及中间选取三个元素,再取这三个元素的中间值作为最终的pivot即可,当然还有其他更好方案。
快速排序不是一个稳定的排序算法,值得一提的是,快速排序算法是所有内部排序算法中平均性能最优的。(当然我们要明确一个概念,这世界上没有最好的算法,每个算法都有最适合自己的应用场景,这里讨论的是平均性能~)
插入排序
插入排序有多种:直接插入排序,折半插入排序,希尔排序等。
首先来看直接插入排序:
原理:将一个已知元素s[ i ]插入到已有序的子序列s[1…i-1]当中,过程分三步:1.查找出s[ i ]在子序列s[1…i-1]中的位置k。2.将k开始往后的元素后移。3插入元素s[ i ]。
代码分析:
其实我们可以将这个过程看做打牌时抽牌的过程,一般情况下,先抽牌,再落位。代码如下:
void Insertion_Sort(ElementType s[],int N)
{
int i,j;
for(i = 0;i < N;++i)
{
tmp = s[i];//抽牌
for (j = i; j > 0&&s[j-1] > tmp ; --j)
s[j] = s[j-1];//挪出空位
s[j] = tmp;//落位
}
}
折半插入排序
在直接插入排序中,我们可以将过程简化为查找到元素位置,并插入。在此基础上可以对查找过程进行优化,折半插入,顾名思义就是先通过折半查找,找到元素的位置,再插入即可。
代码如下:
void HalfInsertion_Sort(ElementType s[],int n)
{
int i,j,low,mid,high;
for(i = 2;i <= n; i++)
{
s[0] = s[i];
low = 1;
high = i - 1;
while(low <= high)
{
mid = (low + high) / 2;
if(a[mid] > s[0]) high = mid - 1;
else low = mid + 1;
}
for(j = i - 1;j > high + 1; --j)
s[j+1] = s[j];
s[high + 1] = s[0];
}
}
希尔排序
希尔排序又成缩小增量排序,基本思想是:先将待排序列分割为形如s[i],s[i+d],s[i+2d],s[i+3d]…的特殊间隔子表,再对每个子表进行直接插入排序。最关键的环节就是如何确定增量序列,一个简单的选择就是d[1] = n/2,d[i+1] = d[i] / 2,并且最后一个增量为1。代码如下:
void Shell_Sort(ElementType s[],int N)
{
int D,i;
for(D = N / 2;D > 0; D /= 2)//步长变化
{
ElementType tmp = s[D];
for (i = D; i >= D && s[i-D] > tmp; i -= D)
{
s[i] = s[i-D];
}
s[i] = tmp;
}
}
下面我们对这三个常见的直接插入排序进行总结:
算法种类 | 最好时间复杂度 | 平均时间复杂度 | 是否稳定 |
---|---|---|---|
直接插入排序 | O(n) | O(n^2) | 是 |
折半插入排序 | O(n) | O(n^2) | 是 |
希尔排序 | O(n^1.3) | O(n^d) | 否 |
简单选择排序
原理:假设待排序列为s[1…n],第 i 趟排序就是从s[i…n]中选择出最小的那个与s[ i ]交换。一次确定一个最小元素的最终位置。
代码很简单,如下:
void selectSort(ElementType s[],int n)
{
int min,i,j;
for (i = 0; i < n - 1; ++i)
{
min = i;
for ( j = i+1; j < n; ++j)
{
if(a[j] < s[min]) min = j;
}
if(min != i) swap(s[i],s[min]);
}
}
性能分析:
仅使用常数个辅助单元,故空间复杂度为O(n)。
时间复杂度为O(n^2),因为元素间比较的次数与序列的初始状态无关,始终是n(n-1)/2次。
并且是一个不稳定的排序算法。
堆排序
堆排序的特点是:将待排序列s[1…n]看作一颗完全二叉树,并且这颗二叉树是大顶堆(即s[ i ] > s[ 2i ] && s[ i ] > s[ 2i+1 ],父结点的键值大于左右孩子结点的键值)。每趟排序前,首先将序列调整为小顶堆,然后取出根结点,进行下一趟…
代码分析:大根堆适用于升序排序,小根堆适用于降序排序,按要求选择调整方式即可。
代码如下:
/*调整成最大堆*/
void HeapAdjust(ElementType s[],int k,int n)
{
ElementType tmp = s[k];
int i;
for(i = k*2+1; i < n;i = 2*I+1)
{
if(i+1 < n && s[i] < s[i+1])//若右孩子大于左孩子,i+1
i++
if(tmp > s[i])//若s[k]已经是最大值,不做操作
break;
s[k] = s[i];
k = i;
}
s[k] = tmp;
}
void Heap_Sort(ElementType s[],int n)
{
int i;
for (i = n/2; i > 0; --i)
HeapAdjust(s,i,n);
for (int i = n; i > 1; --i)
{
swap(s[1],s[i]);
HeapAdjust(s,1,i-1);
}
}
2-路归并排序
这是本文的最后一个排序算法,归并排序与上述基于交换、选择等排序的思想不一样,归并的含义是将两个或两个以上的有序表组合成一个新的有序表。
原理:假定待排序列含有n个记录,则可以看作n个有序的子表,每个子表的长度为1,然后两两归并,得到n/2(向上取整)个长度为2或1的子表;再两两归并,…一直重复,直到合并为一个长度为n的有序表为止。
示例图:
代码分析:
Merge()函数的功能是将前后相邻的两个有序表归并为一个有序表的算法。设两段有序表s[low…mid]、s[mid+1…high]存放在同一顺序表中的相邻位置上,先将它们复制到辅助数组B中。每次从对应B中的两个段取出一个记录进行关键字的比较,将较小者放入A,当数组B中有一段的下标超过其对应的表长时(即该段的所有元素已经完全复制到A中),将另段的剩余部分直接复制到A中。代码如下:
#define MAXSIZE 100
void Merge(int *list1,int list1_size,int *list2,int list2_size)
{
int tmp[MAXSIZE];
int i,j,k;
i = j = k = 0;
while(i < list1_size && i < list2_size)//表1,2中都有元素
tmp[k++] = (list1[i] < list2[i]) ? list1[i++] : list2[i++];
while(i < list1_size) tmp[k++] = list1[i++];
while(i < list2_size) tmp[k++] = list2[i++];
for (j = 0; j < (list1_size + list2_size); ++j)
list1[m] = tmp[m];
}
函数Merge_Sort()采用递归,代码如下:
void Merge_Sort(int s[],int n)
{
if(n > 1)//递归跳出条件
{
int *list1 = s;
int list1_size = n/2;
int *list2 = s + n/2;
int list2_size = n - list1_size;
Merge_Sort(list1,list1_size);
Merge_Sort(list2,list2_size);
Merge(list1,list1_size,list2,list2_size);
}
}
最后对各个排序算法进行总结:
- 快速、堆、归并排序的平均时间复杂度为O(nlogn),但是快速排序在最坏情况下为O(n^2)<不如后二者。对于n较大的情况,归并排序速度比堆排序快,但是所需辅助空间更大。
- 除希尔排序外的所有插入、起泡、简单选择平均时间复杂度都为O(n^2),直接插入排序最简单,当序列中记录基本有序或n值比较小时,它是最佳算法,因此经常和快速、归并、希尔排序等结合使用。
文章到这里就结束了,如果有写错的地方,欢迎指正!
祝自己复试顺利~~