二分排序与二分查找

1、规定排序的实现

      分治法的三步法:

          划分问题:(把序列二分),

          递归求解:(分别对左右子序列递归排序)

          合并结果:(根据左右两个有序的子序列,依次取两个子序列的最小元素中的最小者加到结果中去)

      实现如下:

#include 
using namespace std;

void mergeSort(int * a, int x, int y, int * t)//归并排序,t为辅助空间
{
	if(y-x > 1)//由于是区间[x,y),非空则有y-x>=1,而1个元素的子序列已经有序
	{
		int m = x + (y-x)/2;//中点
		mergeSort(a,x,m,t);
		mergeSort(a,m,y,t);
		
		//合并
		int p = x, q = m, k = x;
		while(1)
		{		
			if(p < m){//左非空
				if(q < y){//右非空
					if(a[p] < a[q]) t[k++] = a[p++];
					else t[k++] = a[q++];
				}
				else t[k++] = a[p++];
			}
			else{//左空
				if(q < y){//右非空
					t[k++] = a[q++];
				}//右空
				else break;
			}
		}
		for(int i = x; i < y; i ++) a[i] = t[i];
	}
}

int main()
{
	int n;
	int a[100], t[100];
	cin >> n;
	for(int i = 1; i <= n; i ++) scanf("%d",&a[i]);	//输入数据

	mergeSort(a,1,n+1,t);

	for(int i = 1; i <= n; i ++) printf("%d ",a[i]);
	printf("\n");
	
	return 0;
}

    归并排序最主要的就是合并的时候的选择最小的元素添加到结果中的适当位置去。

 

2、归并排序的一个应用——逆序对数

      问题的提法:给出一列数A1,A2,...,An,求它的逆序对数,即有多少个逆序对(i Aj)

      主要的思想:在右边的A[j]复制到T中时,左边还没来得及复制到T中的那些数就是左边左右比A[j]大的数,因为此时A[j]前面复制到T中的数肯定比它小,而尚未复制的数肯定比它大。

      实现如下:     

#include 
using namespace std;

int reverseCount(int * a, int x, int y, int * t)//归并排序,t为辅助空间
{
	int res = 0;
	if(y-x > 1)//由于是区间[x,y),非空则有y-x>=1,而1个元素的子序列已经有序
	{
		int m = x + (y-x)/2;//中点
		res += reverseCount(a,x,m,t);
		res += reverseCount(a,m,y,t);
		
		//合并
		int p = x, q = m, k = x;
		while(1)
		{		
			if(p < m){//左非空
				if(q < y){//右非空
					if(a[p] < a[q]) t[k++] = a[p++];
					else{
						t[k++] = a[q++];
						res += m-p;
					}
				}
				else t[k++] = a[p++];
			}
			else{//左空
				if(q < y){//右非空
					t[k++] = a[q++];
					res += m-p;
				}//右空
				else break;
			}
		}
		for(int i = x; i < y; i ++) a[i] = t[i];
	}
	return res;
}

int main()
{
	int n;
	int a[100], t[100];
	cin >> n;
	for(int i = 1; i <= n; i ++) scanf("%d",&a[i]);	//输入数据

	cout << "逆序对数:" << reverseCount(a,1,n+1,t) <

       这个问题在O(nlogn)的时间复杂度下解决,当然了也可以通过O(n^2)的枚举直接解决,只不过这里利用了归并排序的一个特性而已。

 

3、快速排序的实现

       还是分治法的三个步骤,(这里的partion函数还有问题,还没实现)

#include 
using namespace std;

int partion(int * a, int x, int y)
{
	int p = x;//以x为基准元素
	int i = x+1, j = y-1;
	while(i < j)
	{
		while(i < j && a[i] <= a[p]) i ++;
		while(j > i && a[j] >= a[p]) j --;
		if(i < j){int t = a[i]; a[j] = a[j]; a[j] = t;}
	}
	int t = a[p]; a[p] = a[i]; a[i] = t;
	return p;
}

void quickSort(int * a, int x, int y)//快速排序,区间为[x,y)
{
	if(y-x > 1)//如果只有一个元素,不用排序
	{
		//划分成左右两个部分(尽量等长),m返回的是划分基准元素所在的位置
		int m = partion(a,x,y);
		//递归解决左右两边的排序
		quickSort(a,x,m);
		quickSort(a,m+1,y);
		//不用合并,此时已经有序
	}
}

int main()
{
	int n;
	int a[100];
	cin >> n;
	for(int i = 1; i <= n; i ++) scanf("%d",&a[i]);	//输入数据

	quickSort(a,1,n+1);

	for(int i = 1; i <= n; i ++) printf("%d ",a[i]);
	printf("\n");

	return 0;
}


 

4、快速排序的一个应用——第k小的数

5、二分查找的实现

      二分查找是在元素有序的前提下的,否则结果是错的。

      问题划分:比较待查找元素v和中间位置的元素a[m]的大小,如果v小,则在左边查找,如果相等,则返回下表m,否者在右边查找。

      递归求解:在判断了v和a[m]大小的情况下,选择左边或者右边进行递归求解。

      合并结果:由于v要么在左边,要么在右边,直接返回就可以了。

       另外,此处的实现是非递归的:

        非递归实现如下:       

#include 
using namespace std;

int binarySearch(int * a, int x, int y, int v)//半开区间[x,y)
{
	int m;
	while(x < y)
	{
		m = x + (y-x)/2;
		if(v == a[m]) return  m;//找到了
		else if(v < a[m]) y = m;//在左边
		else x = m+1;//在右边
	}
	return -1;
}

int main()
{
	int n;
	int a[100];
	cin >> n;
	for(int i = 1; i <= n; i ++) scanf("%d",&a[i]);	//输入数据

	cout << binarySearch(a,1,n+1,3) <

递归实现如下:

#include 
using namespace std;

int binarySearch(int * a, int x, int y, int v)//半开区间[x,y)
{
	if(y-x == 1){//递归边界处理
		if(v == a[x]) return x;
		else return -1;//所找元素不存在
	}
	else//尚未达到边界,继续划分
	{
		int m = x + (y-x)/2;
		if(v == a[m]) return m;
		else if(v < a[m]) //在左边查找
			return binarySearch(a,x,m,v);
		else //在右边查找
			return binarySearch(a,m+1,y,v);
	}
}

int main()
{
	int n;
	int a[100];
	cin >> n;
	for(int i = 1; i <= n; i ++) scanf("%d",&a[i]);	//输入数据

	cout << binarySearch(a,1,n+1,3) <

二分查找在每次子问题划分的时候,只选择子问题的一个分支进行递归查找,从而T(n) = log(n),当然了,前提条件是元素有序。

6、二分查找的lower_bound和upper_bound

      lower_bound和二分查找类似,不过在查找到了元素v == a[m]之后,由于可能在m左边还可能存在元素v,因此,此时不应该返回m,而是继续向左边查找(即使查找不到,最后返回的也是左边的右边界后一个位置,即m),而upper_bound也类似,在找到了v == a[m]之后,由于右边还可能出现v,因此不应该返回m,而是继续向右边查找(即使查找不到,返回的是m+1,即v在数组a中最后一次出现位置的下一个位置)

      程序实现如下:

#include 
using namespace std;

int lower_bound(int * a, int x, int y, int v)//半开区间[x,y)
{
	int m;
	while(x < y)
	{
		m = x + (y-x)/2;
		if(v == a[m]){//找到了,但是可能在左边还存在v
			//return  m;
			y = m;
		}
		else if(v < a[m]) y = m;//在左边
		else x = m+1;//在右边
	}
	return x;
}

int upper_bound(int * a, int x, int y, int v)//开区间[x,y)
{	//返回的是v在数组a中最后一次出现的下一个位置
	int m;
	while(x < y)
	{
		m = x + (y-x)/2;
		if(v == a[m]){//找到了,但是可能在右边还存在v,应该继续向右边搜,如果右边没有,则返回的事m+1,即v在数组a中最后一次出现位置的下一个位置。
			//return  m;
			x = m+1;
		}
		else if(v < a[m]) y = m;//在左边
		else x = m+1;//在右边
	}
	return x;
}
int main()
{
	int n;
	int a[100];
	cin >> n;
	for(int i = 1; i <= n; i ++) scanf("%d",&a[i]);	//输入数据

	int low = lower_bound(a,1,n+1,2);
	int up = upper_bound(a,1,n+1,2);
	cout << "2的下界:" << low <


7、总结

     上面的一些问题,主要是利用了分治法(分解问题、递归求解、合并结果)的思想,具体问题有具体问题的实际技巧。

你可能感兴趣的:(算法与数据结构)