第二章
首先讲了最基础的插入排序(虽然浩强哥的书基础的是冒泡和选择),然后讲到了分治:将原问题划分成n个规模较小而结构与原问题相似的子问题;递归的解决这些子问题,然后再合并其结果,就得到原问题的解。
分治策略的三步骤(P17):分解(Divide),解决(Conquer),合并(Combine)。
对算法,如果在最坏情况下运行时间比另一个算法的低,我们就常常认为此算法效率更高。
插入算法实现如下:
void InsertSort(vector<int> &v) { int temp; for (int i=1 ; i < v.size() ; i++) { if (v[i] < v[i-1])//需要插入了 { temp = v[i]; for (int j=i-1 ; v[j] > temp ; j--) v[j+1] = v[j];//后移 v[j+1] = temp; } } } void BInsertSort(vector<int> &v) {//折半插入 int temp; for (int i=1 ; i<v.size() ; i++) { int low=0,high=i-1; temp = v[i] ; while (low <= high) { int mid = (low+high)/2 ; if (temp < v[mid] ) //插入位置在低半段 high = mid-1; else low =mid+1; } for (int j=i-1 ; j>=high+1 ; j--)//high+1到i-1,后移 v[j+1] = v[j]; v[high+1] = temp; } } void reverseInsert(vector<int> &v , int n) {//递归插入,到1w会stack overflow if (n==0) v.push_back(v[0]); else { reverseInsert(v,n-1); //插入n到有序的n-1大的v中 int temp = v[n] ; int low=0,high=n-1; while (low <= high) { int mid = (low+high)/2 ; if (temp < v[mid] ) //插入位置在低半段 high = mid-1; else low =mid+1; } for (int j=n-1 ; j>=high+1 ; j--)//high+1到i-1,后移 v[j+1] = v[j]; v[high+1] = temp; } }
第一个为直接插入排序,缺点是比较次数可能过多,因此,第二个折半插入,首先折半查找插入位置,再插入,可明显减少比较次数;第三个为插入排序的递归版,是练习题2.3-4,存在的问题是,当元素超过1w时会stack overflow。 递归插入,思路是,要排序A[1....n],先插入排序A[1...n-1],然后再将A[n]插入到已排序数组A[1.....n-1]中。
经过测试,折半插入稍稍快于递归插入,不过不明显,不过他们都明显快于直接插入。
对测试文件的说明:由于用数组方式,到较大数字会stack overflow,所以用了vector替代数组;又因为数组方式,数组名相当于指针,地址传参方式可以进行正确排序修改,而vector则必须添加引用。
归并排序实现如下:
void Merge(vector<int> &v ,int low ,int mid , int high) {//归并过程,用哨兵 int n1=mid-low+1; //前半段 int n2=high-mid; //后半段 vector<int> L,R; int i,j , k; for (i=0 ; i<n1 ; i++) //分别复制上下半段 L.push_back(v[low+i]); for (i=0 ; i<n2 ; i++) R.push_back(v[mid+i+1]); L.push_back(INT_MAX);//设置个无穷大哨兵元素,方便后面的比较 R.push_back(INT_MAX); for ( k=low ,i=0,j=0 ; k <= high ; k++) { if (L[i] <= R[j]) v[k] = L[i++]; else if (R[j] < L[i]) v[k] = R[j++]; } } void MergeNo(vector<int> &v ,int low ,int mid , int high) {//不使用哨兵 int n1=mid-low+1; //前半段 int n2=high-mid; //后半段 vector<int> L,R; int i,j , k; for (i=0 ; i<n1 ; i++) //分别复制上下半段 L.push_back(v[low+i]); for (i=0 ; i<n2 ; i++) R.push_back(v[mid+i+1]); for ( k=low ,i=0,j=0 ; i<n1 && j< n2; k++) { if (L[i] <= R[j]) v[k] = L[i++]; else if (R[j] < L[i]) v[k] = R[j++]; }//for if (i<n1) for (int m=0 ; m<n1-i ; m++) //把i到mid这段的复制过去 v[k+m] = L[i+m]; if (j<n2) for (int m=0 ; m<n2-j ; m++) v[k+m]=R[j+m]; } void MergeSort(vector<int> &v,int low ,int high) {//递归排序 if (low < high) { int mid = (low+high)/2; MergeSort(v,low,mid); MergeSort(v,mid+1,high); // Merge(v , low , mid ,high) ; MergeNo(v,low,mid,high); } }
其中第一个采用哨兵元素,伪代码见p17,如图表示中设置不可达为无穷大一样,循环条件就只需要让k从low到high;第二个为练习2.3-2(p22)修改为不带哨兵的merge,循环条件改为i和j都小于各自长度,不过存在某个数组没有归并完成的可能,所以需要增加2个判断,将剩下的继续归并到v。
测试结果蛮符合理论的
当然,还有个,是可以在merge过程中,使用插入排序。尽管归并排序最坏情况运行时间仍为O(nlgn),插入排序最坏情况下运行时间为O(n2),但插入排序中的常数因子可以让它在n较小时,运行得更快一些。因为,在归并中,当子问题足够小时,差用插入排序就比较适合了。对n/k个长度为k的子数组进行排序,再标准合并,不过k的值怎么取是个问题。
最后还提到个shell排序,其实也是一种插入排序,只是对序列的周期子序列采用插入排序。实现如下:
void ShellInsert(vector<int> &v , int dk) {//对v做一趟shell插入排序,增量为dk,不是1;j<=0时,找到插入位置 int i,j; for ( i=dk ; i < v.size() ; i++) { if (v[i] < v[i-dk]) { int temp = v[i];//暂存 for (j=i-dk ; j>0 && temp < v[j] ; j -= dk) v[j+dk] = v[j] ;//记录后移,查找插入位置 v[j+dk] = temp; //插入 } } } void ShellSort(vector<int> &v ,const int dlta[] ,int t) {//希尔排序 for (int k=0 ; k < t ; k++) ShellInsert(v,dlta[k]) ;//一趟增量为dlta[k]的插入排序 }另外,最蛋疼的冒泡排序,也可以改为双向,分别向大和小端冒泡,虽然实际上速度并没有提升,实现如下:
for (i=0 ; i< MAX ; i++) { int k = MAX - i - 1; for (j=i+1 ; j< k ; j++) { if (v1[j] < v1[i]) { swap(v1[i],v1[j]); } if (v1[MAX-j-1] < v1[k]) { swap(v1[MAX-j-1],v1[k]); } } }在MAX=9000时,以上所有算法运行时间(win7+vc6)
insert cost : 1828
binary insert cost : 1210
reverse cost : 1193
merge cost : 91 (以前没觉得分治哥这么给力)
bubble cost : 4366
two way bubble cost : 4121
gcc下为:
insert cost : 283
binary insert cost : 224
reverse cost : 192
merge cost : 29
bubble cost : 686
two way bubble cost : 631
第三章
主要是几个渐进上下界的定义,如图
第四章
主要讲递归式,以及三种求解方式:代换法,递归树方法,主方法。感觉数学的东西居多,没有细看。不过递归的写法,简洁性还是可以的,效率未知,不过参考插入的递归,还是很可观的。最快的快排不也是递归呢么?
递归,果然是高级货啊,突然想到递归插入会溢出,而递归的快排则不会。莫非是收敛速率的问题?快排以一半的速度减少,而插入,则以相同数量级减少。
第五章
主要围绕一个雇佣问题展开,问题:有一批参与面试的人,你要一个个面试(面试每个人都要花费c1),如果当前面试者比自己的助理能力强,则辞掉当前助理的,并把当前面试者提拔为助理(雇佣一个人要花费c2),一直面试完所有人。
另一个很有意思的是“生日悖论”P62。问题:一个房间里的人达到多少,才能使有2个人生日相同的机会达到50%。答案居然是23,确实蛮震撼的。
很重要的2个东东,概率分析,随机算法。
随机算法,很重要的一点,是随机性发生在算法上,而不是发生在输入分布上。大多数编程环境提供伪随机数生成器。比如C/C++库函数rand()不过它并不是真正意义上的随机函数,所以称之为伪随机函数。因为当srand(seed)设置的seed一样时,则rand()产生的随机数也一样。 所以一般用 srand((unsigned)time(NULL)),采用当前时间作为种子来产生随机数。
另外,球与盒子,在线招聘等问题,都是概率论上的问题了。
P68还提到个概率计数法,一般用b位计数器,只能到2的b次方-1,不过用概率计数法,可以到很大的值,不过代价是精度有所损失,哎,高级货啊。
继续前进ing