《算法导论》学习总结——第一部分

时间:2021-11-09 03:28:48

      第二章

      首先讲了最基础的插入排序(虽然浩强哥的书基础的是冒泡和选择),然后讲到了分治:将原问题划分成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