写在前头:
刚刚考完研究生初试,正在紧张的准备复试当中…
怎奈实在是难以静下心好好的看书,于是想到了写博客这个办法,希望奏效……=-=
话不多说,开始吧~
提醒:本文排序均为升序,即是第一个元素为最小值。
就从最为大家熟知的冒泡排序开始,
原理:
假设有一个无序数组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 性能分析: 原理: 然后就是重点的Partition()函数,如果我们能够选择出一个pivot值,使得其最终位置处于待排序列的中间,那么两个子表的长度相近,则递归的次数就会少很多,代码也相对高效(在这里,我选择第一个元素作为pivot,在性能分析中会提出更优的选择方案。),接下来的操作很简单,只需要将比pivot大的向右边移动,将比pivot小的向左边移动即可。代码如下: 性能分析: 插入排序有多种:直接插入排序,折半插入排序,希尔排序等。 首先来看直接插入排序: 原理:将一个已知元素s[ i ]插入到已有序的子序列s[1…i-1]当中,过程分三步:1.查找出s[ i ]在子序列s[1…i-1]中的位置k。2.将k开始往后的元素后移。3插入元素s[ i ]。 代码分析: 折半插入排序 在直接插入排序中,我们可以将过程简化为查找到元素位置,并插入。在此基础上可以对查找过程进行优化,折半插入,顾名思义就是先通过折半查找,找到元素的位置,再插入即可。 代码如下: 希尔排序 希尔排序又成缩小增量排序,基本思想是:先将待排序列分割为形如s[i],s[i+d],s[i+2d],s[i+3d]…的特殊间隔子表,再对每个子表进行直接插入排序。最关键的环节就是如何确定增量序列,一个简单的选择就是d[1] = n/2,d[i+1] = d[i] / 2,并且最后一个增量为1。代码如下: 下面我们对这三个常见的直接插入排序进行总结: 原理:假设待排序列为s[1…n],第 i 趟排序就是从s[i…n]中选择出最小的那个与s[ i ]交换。一次确定一个最小元素的最终位置。 性能分析: 堆排序的特点是:将待排序列s[1…n]看作一颗完全二叉树,并且这颗二叉树是大顶堆(即s[ i ] > s[ 2i ] && s[ i ] > s[ 2i+1 ],父结点的键值大于左右孩子结点的键值)。每趟排序前,首先将序列调整为小顶堆,然后取出根结点,进行下一趟… 代码分析:大根堆适用于升序排序,小根堆适用于降序排序,按要求选择调整方式即可。 代码如下: 这是本文的最后一个排序算法,归并排序与上述基于交换、选择等排序的思想不一样,归并的含义是将两个或两个以上的有序表组合成一个新的有序表。 代码分析: 函数Merge_Sort()采用递归,代码如下: 最后对各个排序算法进行总结: 文章到这里就结束了,如果有写错的地方,欢迎指正!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);//对两个子表递归
}
}
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即可,当然还有其他更好方案。
快速排序不是一个稳定的排序算法,值得一提的是,快速排序算法是所有内部排序算法中平均性能最优的。(当然我们要明确一个概念,这世界上没有最好的算法,每个算法都有最适合自己的应用场景,这里讨论的是平均性能~)插入排序
其实我们可以将这个过程看做打牌时抽牌的过程,一般情况下,先抽牌,再落位。代码如下: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];
}
}
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)
否
简单选择排序
代码很简单,如下: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次。
并且是一个不稳定的排序算法。堆排序
/*调整成最大堆*/
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];
}
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);
}
}
祝自己复试顺利~~