一、实现思想
1.将一个问题划分为同一类型的若干子问题,子问题最好规模相同
2.对子问题求解(一般使用递归方法)
3.有必要的话,合并子问题的解,得到原始问题的答案
下图描述的是将一个问题划分为两个较小子问题的例子,也是最常见的情况:
二、分治算法之 合并排序
1.主要思想
对于一个需要排序的数组A[0…n-1],将其一分为二:A[0…n/2-1]和A[n/2…n-1] (n/2向下取整),对每个子数组递归排序,然后把这两个排好序的子数组合并为一个有序数组
Mergesort(A[0...n-1])
//递归调用Mergesort对数组A[0...n-1]进行排序
//输入:一个可排序数组A[0...n-1]
//输出:非降序排列数组A[0...n-1]
if n>1
copy A[0....n/2-1] to B[0....n/2-1]
copy A[n/2....n-1] to C[0....n/2-1]
Mergesort(B[0....n/2-1])
Mergesort(C[0....n/2-1])
Merge(B,C,A) //合并
Merge(B[0....p-1],C[0....q-1],A[0....p+q-1])
//将两个有序数组合并为一个有序数组
//输入:两个有序数组B[0....p-1]和C[0....q-1]
//输出:有序数组A[0....p+q-1]
i=0;j=0;k=0
while i<p and j<q do
if B[i]<=C[j]
A[k]=B[i]
i++
else
A[k]=C[j]
j++
k++
if i=p //这里不会出现i=p,j=q的情况
copy C[j...q-1] to A[k....p+q-1]
else
copy B[i...p-1] to A[k...p+q-1]
2.分析
算法 | 是否原地排序 | 是否稳定 | 最好、最坏、平均时间复杂度 | 空间复杂度 | 是否基于比较 |
---|---|---|---|---|---|
归并排序 | 否 | 是 | O(nlogn)、O(nlogn)、O(nlogn) | O(n) | 是 |
优点:稳定
缺点:需要线性的额外空间
3.两类主要变化形式
1.算法自底向上合并数组的一个个元素对,然后再合并这些有序对,以此类推:分治法-合并排序
2.把数组划分为待排序的多个部分,再对他们递归排序,最后将其合并在一起:如上所述。
三、分治算法之 快速排序
1.主要思想
快速排序就是给基准找正确索引位置的过程.
如下图所示,假设最开始的基准数据为数组第一个元素23,则首先用一个临时变量去存储基准数据,即tmp=23;然后分别从数组的两端扫描数组,设两个指示标志:low指向起始位置,high指向末尾.
首先从后半部分开始,如果扫描到的值大于基准数据就让high减1,如果发现有元素比该基准数据的值小或等于基准数据(如上图中18<=tmp),就将high位置的值赋值给low位置 ,结果如下:
然后开始从前往后扫描,如果扫描到的值小于基准数据就让low加1,如果发现有元素大于基准数据的值或者等于基准数据(如上图46=>tmp),就再将low位置的值赋值给high位置的值,指针移动并且数据交换后的结果如下:
然后再开始从后向前扫描,原理同上,发现上图11<=tmp,则将high位置的值赋值给low位置的值,结果如下:
然后再开始从前往后遍历,直到low=high结束循环,此时low或high的下标就是基准数据23在该数组中的正确索引位置.如下图所示.
此时基准已经位于它在有序数组中的最终位置,比基准数大的都放在基准数的右边,比基准数小的放在基准数的左边
接下来采用递归的方式分别对基准数前半部分和后半部分用同样的方法排序,当前半部分和后半部分均有序时该数组就自然有序了。
参考:快速排序—(面试碰到过好几次)
Quicksort(A[a...b])
//用Quicksort对子数组排序
//输入:数组A[0...n-1]的子数组A[a...b]
//输出:非降序排列的子数组A[a...b]
if a<b
s=Partition(A[a...b]) //求基准数位置
Quicksort(A[a...s-1])
Quicksort(A[s+1...b])
Partition(A[a...b])
//以第一个元素为基准,对子数组进行划分
//输入:数组A[0...n-1]的子数组A[a...b]
//输出:基准最终位置的下标
p=A[a]
i=a;j=b+1
repeat
repeat i++ until A[i]>=p
repeat j-- until A[j]<=p
swap(A[i],A[j])
until i>=j
swap(A[i],A[j]) //撤销最后一次交换
swap(p,A[j]) //交换基准和A[j]
return j
2.为什么当遇到与基准元素相等的元素时值得停止扫描?
因为当遇到有很多相同元素的数组时,这个方法可以将数组分的更加平均(左右),从而使算法运行得更快。如果我们遇到相同元素时继续扫描,对于一个具有n个相同元素的数组来说,最后得到的两个子数组的长度可能分别是n-1和0,在扫描了整个数组后只将问题的规模减1。
3.分析
算法 | 是否原地排序 | 是否稳定 | 最好、最坏、平均时间复杂度 | 空间复杂度 | 是否基于比较 |
---|---|---|---|---|---|
快速排序 | 是 | 否 | O(nlogn)、O(n^2)、O(nlogn) | O(logn)~O(n) | 是 |
4.优化
1.更好的基准选择方法:例如随机快速排序,它使用随机元素作为基准;三平均划分法以数组最左边、最右边和最中间的元素的中位数作为基准。
2.当子数组足够小时(5~15个),改用插入排序方法,或者不再对小数组进行排序,而是在快速排序快结束后再使用插入排序的方法对整个近似有序的数组进行排序。
3.一些划分方法的改进。例如三路划分,将数组分成三段,每段的元素分别小于、等于、大于基准元素。
四、分治算法之 折半查找
1.主要思想
设有序数组a[low…high]是当前的查找区间,首先确定该区间的中点位置mid=(low+ high)/2 , 然后将待查的值与a[mid].key比较:
(1)若k= =a[mid],则查找成功并返回该元素的下标;
(2)若k< a[mid],新的查找区间是左子表a[low…mid-1];
(3)若k> a[mid],新的查找区间是右子表a[mid+ 1…high]
五、分治算法之 大整数乘法
1.主要思想
计算两个n位整数a和b的积,其中n是一个正的偶数。
把a的前半部分记为a1,a的后半部分记为a2;对于b,分别记为b1和b2。
a=a1*10^(n/2)+a2,b=b1*10^(n/2)+b2
。
c=a*b
=[a1*10^(n/2)+a2]*[b1*10^(n/2)+b2]
=(a1*b1)*10^n+(a1*b2+a2*b1)*10^(n/2)+(a2*b2)
=c2*10^n+c1*10^(n/2)+c0
c2=前半部分之积
c1=(a1+a2)*(b1+b2)-(c2+c0) 两部分和的积减去c2和c0
c0=后半部分之积
如果n/2也是偶数,我们可以用相同的方法来计算c2,c1和c0。当n=1时,递归就停止了。或者当我们认为n已经足够小了,小到可以直接对这样大小的数相乘时,递归也可以停止了。
六、分治算法之 Strassen矩阵乘法
C11=M4+M5-M1+M6
C12=M1+M2
C21=M3+M4
C22=M1+M5-M3+M7