算法设计 分治, 归并排序, 快速排序

算法思想:

分治方法与软件设计的模块化方法非常的相似。要解决一个大实例问题,可以(1)将它分解成多个更小的实例,(2)分别解决每个小实例,(3)将小实例的解组合成大实例的解。

例子:

寻找假硬币。一个袋子有16枚硬币,其中可能有一个假币,并且假币比真币要轻,现在要找出这个假币。

现在要找出假硬币,我们把两个或者三个硬币视为不可再分的小实例,只有一个硬币时,不能确定它的真假。

2. 金块问题:

从袋子中寻找出最重的和最轻的金块:希望用最少的比较次数找出最重和最轻的金块;

方法1: 使用max()函数经过n-1次比较,得到重量最大的,再通过n-2次比较得到重量最小的。共需要2n-3次比较

方法2:采用分治的方法:当金块的个数n较大的时候

第一步: 把金块先分成两部分A,B

第二步: 分别找出A,B中的最轻的和最重的金块, L_A,H_A, L_B,H_B

第三步: 比较L_A,L_B, 比较H_A,H_B

现在假设金块的数量n=8,整个比较的过程可以用下面的二叉树来描述:

图中的每个大写字母代表的节点表示一个实例,实例中的金块的个数等于以该节点为根的子树的所有叶子。

1. 沿着根至叶子的路径,把一个大实例划分成若干个大小为1或者2的实例

2. 在每一个大小为2的实例中,比较确定哪一个较轻,哪一个较重(节点DEFG中),大小为1的实例既是最轻的,也是最重的

3. 对较轻的金块比较哪一块是最轻的,对较重的金块比较哪一块是最重的(节点ABC)

找出最大值和最小值的分治非递归算法:

template<typename T>
bool minmax(T a[], int n, int& minIndex, int& maxIndex)
{
	// 元素个数
	if(n<1)
	{
		return false;
	} 
	if(n==1)
	{
		minIndex = 1;
		maxIndex = 1;
		return true;
	}
	
	// n>1的情况
	int s = 1;  // 比较的起点 
	if(n%2==1)   // n奇数
	{
		minIndex = 0;      // 这样是为了使得剩余的节点个数为偶数个 
		maxIndex = 0; 
	} 
	else
	{
		if(a[0]>a[1])
		{
			maxIndex = 0;
			minIndex = 1;
		}
		else
		{
			maxIndex = 1;
			minIndex = 0;
		}
		s = 2;    // 比较的起点 
	}
	
	// 比较剩余的元素    偶数和元素,每两个元素作为一个实例 
	for(int i=s; i<n; i+=2)
	{
		if(a[i]>a[i+1])
		{
			if(a[i]>a[maxIndex])
			{
				maxIndex = i;
			} 
			if(a[i+1]<minIndex)
			{
				minIndex = i+1;
			}
		}
		else
		{
			if(a[i]<a[minIndex])
			{
				minIndex = i;
			}
			if(a[i+1]>a[maxIndex])
			{
				maxIndex = i+1;
			}
		}
	}
	return true;
}

在算法开始的时候首先处理n=1和n<1的情况:

1. 当n为偶数的时候,先比较数组的前两个元素,作为最大最小元素的候选者,剩余的元素个数为偶数,再进行比较。

2. 当n为奇数的时候,将数组的第0个元素作为候选的元素,剩余的元素个数也为偶数

3. 接下来比较剩余的偶数个元素

复杂度分析:

当n为偶数的时候,在for循环的外部,有一次比较,内部有(n/2-1)*3此比较

当n为奇数的时候,在for循环的外部没有比较,内部有(n-1)/2*3次比较

所以总的比较次数为: 【3n/2】-2次比较,这是在寻找最大最小值的算法中比较次数最小的算法

归并排序:

可以使用分治的方法设计排序算法,这种方法的思路是将序列划分为k个子序列(k>=2), 先对每一个子序列排序,然后将有序子序列归并为一个序列。

假设A组包含n/k个元素,B组包含其余的元素,递归的应用分治对A,B两个序列进行排序,然后采用归并的过程,将有序子序列归并为一个序列。

在k=2时的排序称为归并排序,更准确的说,是二路归并排序。

例如: 对于序列:【10, 4,6, 3, 8,2,5,7】,选定k=2, 则子序列[10,4,6,3]和[8.2.5.7]需要分别独立排序,得到两个有序的子序列,在将这两个有序的子序列进行合并。

伪代码如下所示:

void sort(E, n)   // 对E中k个元素进行排序
{
    if(n>=k)
    {
       i=n/k;    // A的元素个数
       j = n-i;  // B的元素个数
       sort(A, i);
       sort(B, j);
       merge(A,B,E,i,j);   // 把AB合并到E中
    }
    else
    {
        对E进行插入排序
    }
}

归并n个元素所需的时间为O(n),假设t(n)为分治排序算法在最坏情况下的用时,则其递推公式为:

当两个小的实例所包含的元素个数近似相等时,分治算法通常具有最佳的性能。

在上面的递推公式中,取k=2, 且假设n为2的幂:

则递推公式可以简化为:

在n位其他数的时候,这个结论也适用:可以得到

这也是归并排序在最好和最坏情况下的时间复杂度;

归并排序的C++实现:

1. 一种方法可以将排序的元素存储到一个链表中,在n/2处将链表断开,分成两个大致相等的子链表。然后再将两个排序后的子链表归并到一起。

2. 使用数组存储元素

归并排序的非递归写法:

#include <iostream>

using namespace std;

// 归并排序:
/*
原理: 
初始段: [8] [4] [5] [6] [2] [1] [7] [3]
归并到b  [4,  8] [5,  6] [1,  2] [3,  7] 
复制到a  [4,  8] [5,  6] [1,  2] [3,  7]
归并到b  [4   5   5   8] [1   2   3   7]
复制带a  [4   5   5   8] [1   2   3   7]
归并到b  [1   2   4   5   6   7   7   8]
复制到a  [1   2   4   5   6   7   7   8]
*/ 
template<typename T>
void merge(T c[], T d[], int startOfFirst, int endOfFirst, int endOfSecond)
{
	// 数据段a,b
	int first = startOfFirst;
	int second = endOfFirst+1;
	int res = startOfFirst; 
	// 开始归并数据段
	// 反复将c归并到d中,再将d归并到a中 
	while(first<=endOfFirst && second<=endOfSecond)
	{
		if(c[first]<=c[second])
		{
			d[res++] = c[first++];
		}
		else
		{
			d[res++] = c[second++];
		}	
	}
	
	// 归并剩余的字段
	if(first>endOfFirst)     // 此时first段已经完全归并了,则考虑归并second,如果second有剩余的话
	{
		for(int i=second; i<=endOfSecond; i++)
		{
			d[res++] = c[i];
		}
	}
	else     // 相反
	{
		for(int i=first; i<=endOfFirst; i++)
		{
			d[res++] = c[i];
		}
	}
	 
}




template<typename T>
void mergePass(T x[], T y[], int n, int segment)
{
	// 从x到y归并相邻的数据段
	int i=0;   // 数据段起始位置
	while(i<=n-2*segment)
	{
		merge(x, y, i, i+segment-1, i+2*segment-1);    // x, y 数据段1 :i起始位置, i+segment截至位置; 数据段2;结束位置: i+2*segment-1 
		i = i+2*segment;
	} 
	
	// 少于两个满数据段 
	if(i+segment<n)   // 少于两个数据段没有合并
	{
		merge(x, y, i, i+segment-1, n-1);    // n-1是最后一个元素的位置 
	} 
	else  // 只剩一个数据段 
	{
		for(int j=i; j<n; j++)
		{
			y[j] = x[j];
		}
	} 
}

template<typename T>
void mergeSort(T a[], int n)
{
	T* b = new T[n];
	int segmentSize = 1;
	while(segmentSize<n)
	{
		mergePass(a, b, n, segmentSize);   
		segmentSize += segmentSize;
		mergePass(b, a, n, segmentSize);
		segmentSize += segmentSize;
	}
	delete []b;
} 



int main(int argc, char *argv[])
{
	int array[] = {8,4,5,6,2,1,7,3};
	mergeSort(array, 8);
	for(int i=0; i<8; i++)
	{
		cout << array[i] << " ";
	}
	cout << endl;
	return 0;
}

快速排序:

用分治可以实现另一种排序方法,快速排序。把n个元素分为三段,left, middle, right,其中middle段仅有一个元素,left段中的元素都不大于中间段的元素,right段的元素都不小于middle段的元素。middle的元素称为支点。

算法思路:

1,从a[0:n-1]之间选择一个元素作为middle段

2. 把生于元素分成left和right两部分,是左段的元素都不大于middle,右段的元素都不小于middle。

3. 对left段递归快速排序

4. 对right段递归快速排序

C++实现:

#include <iostream>

using namespace std;

template<typename T>
void quickSort(T a[], int n)
{
	// a[]排序数组   n 元素个数
	if(n<=1)
	{
		return;
	} 
	// 找到最大的元素, 将其放到数组最右端
	int index = 0;
	for(int i=1; i<n; i++)
	{
		if(a[i]>a[index])
		{
			index = i;
		}
	} 
	T temp = a[index];
	a[index] = a[n-1];
	a[n-1] = temp;
	quickSort(a, 0, n-2);   // 调用递归函数quickSort 
}

template<typename T>
void quickSort(T a[], int leftend, int rightend)
{
	// a[leftend:rightend]排序
	if(leftend>=rightend)
	{
		return;
	} 
	int leftCursor = leftend;   // 从左到右移动搜索 
	int rightCursor = rightend+1;    // 从右到左移动搜索
	T pivot = a[leftend];     // 选择支点 
	while(1)
	{
		do     // 寻找左侧不小于支点的元素 
		{
			leftCursor++;
		} while(a[leftCursor]<pivot);
		
		do    // 寻找优策不大于支点的元素 
		{
			rightCursor--;
		} while(a[rightCursor]>pivot);
		
		if(leftCursor>=rightCursor)
		{
			break;
		}
		T temp = a[leftCursor];
		a[leftCursor] = a[rightCursor];
		a[rightCursor] = temp; 
	}
	
	// 放置支点
	a[leftend] = a[rightCursor];
	a[rightCursor] = pivot; 
	// 递归调用quickSort() 
	quickSort(a, leftend, rightCursor-1);
	quickSort(a, rightCursor+1, rightend);
} 


int main(int argc, char *argv[])
{
	int array[] = {8,4,5,6,2,1,7,3};
	quickSort(array, 8);
	for(int i=0; i<8; i++)
	{
		cout << array[i] << " ";
	}
	cout << endl;
	return 0;
}

复杂度分析:
快速排序的平均复杂度是,其中n为元素的个数

各种排序算法的时间复杂度对比:

排序算法最坏情况下性能最好情况下性能
冒泡排序
计数排序
插入排序
选择排序
堆排序
归并排序
快速排序

性能测量:

上图生成的曲线:

 

 

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值