1. 简单选择排序
简单选择排序的思想:将数组看作两段,左边已经排好序,右边待排序,左边元素都小于等于右边元素,用sorted_index 指向排序元素的下一个位置,用unsorted_index指向sorted_index的下一个元素,向后移动unsorted_index找到待排序中最小的一个元素,与sorted_index的元素交换。直到sorted_index==n-1.
void simple_select_sort(Item a[],int n)
{
for(int sorted_index = 0; sorted_index < n - 1; sorted_index++){
int min_index = sorted_index;
for(int unsorted_index = sorted_index + 1; unsorted_index < n; unsorted_index++) {
if(a[unsorted_index] < a[min_index]) {
min_index = unsorted_index;
}
}
if(min_index != sorted_index) {
Item tmp = a[sorted_index];
a[sorted_index] = a[min_index];
a[min_index] = tmp;
}
}
}
这个代码内层循环中为了找最小的元素,做了n-sorted_index-1 次比较,交换次数为1或0。外层循环遍历了n-1次。排序的复杂度最坏情况,平均情况,最好情况都是O(n^2)。
并且,可以看出简单选择排序是稳定的。
2. 堆排序
为什么叫堆排序,堆是用来实现优先队列的,优先队列具有这样的性质:当你从优先队列中取一个元素,总是取到具有最高优先级的。通常用最大堆来实现堆排序,堆数组中,第一个元素总是最大的。
堆排序不是一个稳定的排序,因为每次调整的时候,总是要将第一个元素和最后一个元素交换。
我们先来看看最大堆的重要性质,假设有n个元素。
1) 堆对应的二叉树是完全二叉树。
2) 第i个(i>=1)个结点的如果有左右子结点,则左右子结点为a[i*2]和a[i*2+1],并且a[i]>=a[i*2] && a[i] >= a[i*2]。
堆排序的步骤如下:
1 .对于一个数组,先调整为一个最大堆,第一个元素就是最大元素。
2. 我们交换堆顶(最大元素)与堆中最后一个元素,并且将交换后的最后一个元素从堆中剔除。
3. 剩下的元素重新调整为堆结构。
重复2&3,直至堆中元素个数为0。
其中第1步是从第floor(n/2)个元素起一直调整至根元素。已经证明这一步是线性时间。第三步复杂度为O(logn),因此总体复杂度为O(nlogn)。
现在开始解决每一步。首先根据一个数组构造堆,假如我们的数组为{5,1,5,4,2,3,0},起初:
显然不满足堆结构,我们从第floor(7/2)=3个元素开始调整,由于{5,3,0}满足堆结构,再到第3-1=2个元素,由于{1,4,2}不满组堆结构,调整成{4,1,2},再到第2-1=1个元素,由于{5,4,5}满足堆结构,不需要调整,至此,将原始数组调整成堆结束:
/
这一步的代码:
void build_heap(int a[],int n) //数组a index是从1开始的
{
for(int i = n/2; i >= 1; i--) {
int child_bigger_index = 2*i;
if(2*i +1 <= n && a[2*i + 1] < a[2*i]) {
child_bigger_index = 2*i+1;
}
if(a[i] < a[child_bigger_index]) {
fixDown(a,i,n);
}
}
}
有了最大堆之后,我们进行第2步,和第3步。
先将第一个元素和第7个元素(最后一个元素)交换,并将交换后的最后一个元素从堆中剔除。现在剩下的元素不是堆了,我们需要用第3步进行调整成堆:
重复上面两步:
直到最终数组排好序。
这两步代码如下:
void fixDown(int a[],int k,int n){
while(k <= n/2) {
int child_bigger_index = 2*k;
if(2*k+1<=n && a[2*k+1]>a[2*k]) {
child_bigger_index = 2*k+1;
}
if(a[child_bigger_index] > a[k]) {
int tmp = a[k];
a[k] = a[child_bigger_index];
a[child_bigger_index] = tmp;
k = child_bigger_index;
}
else {
break;
}
}
}
void heapSort(int a[],int n) {
buildHeap(a,n);
for(int sorted_index = n; sorted_index > 1; sorted_index--) {
int tmp = a[1];
a[1] = a[sorted_index];
a[sorted_index] = tmp;
fixDown(a,1,sorted_index-1);
}
}
link:
3. 直接插入排序
插入排序背后的思想很简单:
* 调整一下前2个元素,让他们处在相对排好序的位置
* 将第3个元素插入到前2个元素中,使他们相对排好序
* 将第4个元素插入到前3个元素中,使他们相对排好序
* ......
插入排序和选择排序一样,排序的时候都是分为两段,第一段为已经排好序的,第二段为没有排好序的。但是和选择排序不同的是,第一段中已经排好序的不是最终排序结果,只是相对排好序。在内循环中,通常要挪动已经排好序的元素位置。代码如下:
void insert_sort(int a[],int n) // index from 1
{
if(n<=1) return;
for(int unsorted_index = 2; unsorted_index <= n; unsorted_index++) {
int tmp = a[unsorted_index];
int sorted_index = unsorted_index - 1;
while(sorted_index >= 1 && a[sorted_index]>tmp) {
a[sorted_index+1] = a[sorted_index];
sorted_index--;
}
a[sorted_index+1] = tmp;
}
}
插入排序是稳定的,并且插入排序适合几乎排好序的数组,因为几乎排好序的数组移动次数较少,最好的情况是O(n),最坏的情况是O(n^2)
4. 希尔排序
我们直到插入排序在数组几乎排序的情况下效率很高,如果一个小元素在很后面,需要挪动前面很多元素来空出位置来让希尔这个元素插入,这样效率不高。希尔排序是在插入排序的基础上做了改进,其基本思想是:
将数组元素分成多个子序列,每个子序列index以某一个增量增加,假如当前增量为gap,则每个子序列元素个数为ceil(n/gap)。对于每个子序列,进行插入排序。然后递减gap,直到gap减为1,排序结果即为最终结果。
void shell_sort(int a[],int n) // index from 1
{
int gap;
for(gap = n/2; gap>=1; gap/=2)
{
for(int i = 1; i <= gap; i++)
{
for(j = i + gap; j <= n; j += gap) { //子序列,进行插入排序
int tmp = a[j];
int k = j - gap;
while(k>=0 && a[k] > tmp){
a[k+gap] = a[k];
k-=gap;
}
a[k+gap] = tmp;
}
}
}
}
希尔排序不是稳定的,因为是跳跃的。
5. 冒泡排序
插入排序是基于“逐个记录插入”的,选择排序是基于“选择”的,冒泡排序是基于"交换"的,冒泡排序也是将数组看作两段,左边是待排序的,右边是已经排好序的,并且右边已经排好序的比左边元素都大,并且递增,冒泡排序是将大元素逐渐沉到数组底部。
void bubule_sort(int a[],int n) //index from 1
{
for(int bottom = n; bottom > 1; bottom--)
{
//swap the max of the unsorted to bottom
for(int i = 1; i < bottom; i++)
{
if(a[i] > a[i+1])
{
int tmp = a[i+1];
a[i+1] = a[i];
a[i] = tmp;
}
}
}
}
相对于简单选择排序,冒泡排序交换次数明显更多。它是通过不断地交换把最大的数冒出来。冒泡排序平均时间和最坏情况下(逆序)时间为o(n^2)。最佳情况下虽然不用交换,但比较的次数没有减少,时间复杂度仍为o(n^2)。此外冒泡排序是稳定的。
6. 快速排序
快速排序用的是归并的思想,为了排序一个数组,我们先将它分成两部分,分别排序这两部分,这样整个数组就排好序了,不过与归并排序不同的是,快排用元素来划分数组成两部分,将小于划分点的元素放在左边,大于等于划分点的元素放到右边,然后递归地对两部分进行排序。
比如一个数组有8个元素:
我们用第一个元素55来进行划分:
如果我们递归的对1到3的元素和5到8的元素进行排序,最终整个数组就排好序了。
假如我们以第一个元素作为划分点,划分的某一个过程为:
一趟划分完成。
代码如下:
int partition(int a[],int l,int u)
{
int povit = a[l];
int m = l; // less than povit index
for(int i = l+1; i <= u; i++) { //i is the index of undecided elements.
if(a[i] < povit) {
int tmp = a[i];
a[i] = a[++m];
a[m] = tmp;
}
}
a[l] = a[m];
a[m] = povit;
return m;
}
void quick_sort(int a[],int l, int u)
{
if(l<u){
int p = partition(a,l,u);
quick_sort(a,l,p-1);
quick_sort(a,p+1,u);
}
}
算法平均复杂度为O(nlogn),但是当数组已经有序,算法的比较次数O(n^2),也就是最坏情况下为O(n^2),可以用随机选取划分点的方法来解决这个问题:
int randomize_partition(int a[],int l,int u)
{
int rand_index = l+rand()%(u-l);
swap(a[rand_index],a[l]);
return partition(a,l,u);
}
还可以对快排进行调优,当l与u之差很小,也就是递归排序的元素很少的时候,可以用插入排序进行排序
void quick_sort(int a[],int l, int u)
{
if(l<u){
if(u-l < cutoff) {
insertion_sort(a,l,u);
}
int p = partition(a,l,u);
quick_sort(a,l,p-1);
quick_sort(a,p+1,u);
}
}
7. 归并排序
归并排序思想很简单,将一个数组平均分成两个部分,分别对这两个部分进行排序,然后将排好序的两部分合并成一个数组,但是合并过程需要开辟新的空间,将排好序两部分元素按顺序加到新的空间中,然后再将新的空间直接复制到原来的数组中。
void insert_sort(Item a[],int l,int r)
{
int i,j;
for(i = l+1; i <= r; i++)
{
j = i;
while(j >= 1 && a[j] < a[j-1]) {exch(a[j],a[j-1]);j--;}
}
}
void merge(Item a[],Item t[],int l,int m,int r)
{
int i,j,k;
for(i = l,j = m+1,k=l;i <= m && j <= r;k++)
if(less(a[i],a[j])) t[k] = a[i++];
else t[k] = a[j++];
while(i<=m) t[k++] = a[i++];
while(j<=r) t[k++] = a[j++];
}
void mergesort(Item a[],Item t[],int l,int r)
{
int m;
if(r-l<=10) insert_sort(a,l,r);
else{
m = (l+r)/2;
mergesort(t,a,l,m);
mergesort(t,a,m+1,r);
merge(t,a,l,m,r);
}
}