虽然久闻大名,但第一次接触算法导论,是看了网易公开课MIT的《算法导论》课,记得 第一集就讲到了插入排序和归并排序。
几个星期前也买了算法导论这本书,准备慢慢啃~
这星期主要在看前两部分,除了对于讲渐进时间、递归式分析这些东西感到云里雾里的,其它的都就
感觉越看越有觉得入迷,果然不愧是一本经典之作
好吧,开始。本文主要是用C++把书中的算法实现,以及一些笔记。
一、插入排序。
插入算法的设计使用的是增量(incremental)方法:在排好子数组A[1..j-1]后,将
元素A[ j]查入,形成排好序的子数组A[1..j]
插入排序的效率为O(n^2),在数据规模较小的时候效率较高。
最佳的情况是一个数组已经是排好序的,只需O(n)。最坏情况是输入数据是逆序的, O(n^2)。 对于一个算法,一般是考察它的最坏情况运行时间
.
伪代码:这里需要注意的是由于大部分编程语言的数组都是从0开始算起,而伪代码是从1开始的
C++实现:
template <typename T> void insert_sort(T *begin, T *end){ // 采用了更接近C++ STL sort的调用方式 T *p,*t,key; for(p=begin+1; p!=end; ++p){ // 把A[j]插入排好序的A[1...j-1]中 key = *p; t=p-1; while(t>=begin && *t>key){ *(t+1) = *t; --t; } *(t+1) = key; } }
记得曾在刘汝佳的《算法竞赛入门经典》中看到一段代码,书上说是冒泡排序,但是却和冒泡排序的过程有点不一样。一直到最近才知道,
原来那个应该算作是交换排序。
选择排序伪代码:
冒泡排序伪代码:
// 选择排序 void select_sort(int *start,int *end){ int *p,*q,*min; for(p=start; p<end; ++p){ min=p; for(q=start+1; q<end; ++q){ if(*q<*min) min=q; } Swap(min,p); } } // 冒泡排序 void bubble_sort(int *start,int *end){ int *p,*q; for(p=start; p<end; ++p){ for(q=end-1; q>start; --q){ if(*q<*(q-1)) Swap(q,q-1); } } }
这三个排序的效率都为O(n^2)
从中可以看出,选择排序和冒泡排序都是从当前位置i的后面所有数中最小的一个数,交换到i位置中,但是选择排序只需要交换一次,冒泡排序可能会交换多次,速度应该是选择排序更快。。
除了选择排序和冒泡排序,还有一个叫”交换排序“的,思想也几乎一样,它是分别将第 i 个数 和后面的数一一比较,一旦后面的一个数比它小,就交换。
三、归并排序(合并排序)
归并排序是一种高效的排序算法,按照分治三步法:
划分问题:把序列分成元素个数尽量相等的两半
递归求解:把两半元素分别排序
合并问题:把两个有序表合并成一个
MERGE-SORT伪代码:
MERGE是关键:
这里给的代码实现是在每一堆的底部放一张“哨兵卡”,可以用于简化代码。
但是我用C++实现的方式采用另外一种形式,放“哨兵卡”虽然可以简化代码,但是这个“哨兵值”却有点局限性,在不同平台和机器下可能会不同。
// 算法导论实现版本 template<typename T> void merge(T array[],int p,int q,int r){ int n1,n2,i,j,k; n1=q-p+1; n2=r-q; T *left=new T[n1], *right=new T[n2]; for(i=0; i<n1; ++i) // 对左数组赋值 left[i] = array[p+i]; for(i=0; i<n2; ++i) // 对又数组赋值 right[i] = array[q+i+1]; i=j=0; k=p; while(i<n1 && j<n2){ if(left[i] <= right[j]) array[k++] = left[i++]; else array[k++] = right[j++]; } for( ; i<n1; ++i) //如果左数组有元素剩余,则将剩余元素合并到array数组 array[k++] = left[i]; for( ; j<n2; ++j) //如果右数组有元素剩余,则将剩余元素合并到array数组 array[k++] = right[j]; delete [] left; // 记住要释放空间 delete [] right; } template<typename T> void merge_sort(T array[],int p,int r){ if(p<r){ int q = (p+r)/2; merge_sort(array,p,q); merge_sort(array,q+1,r); merge(array,p,q,r); } }
并归排序在ACM中有个用处,可以用来求逆序数对(《算法竞赛入门经典》P144),因为如果直接枚举的效率为O(n^2)会超时。
受并归排序启发,由于合并操作是从小到大进行的,当右边的A【j】复制到T中时(这里的T是刘汝佳版的并归排序实现,后面给出),左边还没来得及复制到T得那些数就是左边所有比A【j】大的数,即A【j】的逆序数
刘汝佳神牛的并归排序+顺便求出逆序数
void merge_sort(int *A,int x,int y,int *T){ // T是专门开得一个临时数组。 这样可以大大节约了很多时间(上面的方式要多次开辟数组空间) if(y-x > 1){ int m=x+(y-x)/2; // 划分 int p=x, q=m, i=x; merge_sort(A,x,m,T); merge_sort(A,m,y,T); while(p<m || q<y){ // 非递归调用的方式还可以节省栈调用时间 if(q>=y || (p<m && A[p] <= A[q])) T[i++] = A[p++]; else { T[i++] = A[q++]; cnt += m-p; } // cnt用来计算逆序数对,用全局变量,调用前清零 } for(i=x; i<y; ++i) A[i]=T[i]; } }