分治策略
1. Divide:将大问题分解成若干个小问题;
2. Conquer:递归解决所有子问题;
3. Conbine:合并
T(n) = 2T(n/2) + theta(n); T(n) = θ(nlgn);
二分搜索
1. Divide x与中间值比较
2. Conquer 递归找下一个子数组
3. Conbine 不做动作
T(n) = T(n/2) + θ(1); T(n) = θ(lgn);
x^n
Sol. 1
x*x*x*x...x
T(n) = θ(n);
Sol. 2
x^n = x^(n/2) * x^(n/2)
T(n) = θ(n);
Fibonacci数列
F(n) = F(n-1) + F(n-2); F(1) = 1, F(0) = 0;
Sol. 1
递归:从n求到1,时间复杂度指数级增长
Sol. 2
Bottom-up求解:从1到n求解,F(1), F(2), ... F(n)
时间复杂度:θ(n)
Sol. 3
利用下面的矩阵我就可以实现θ(lgn)的时间复杂度
集成电路完全二叉树布局
正常情况二叉树面积θ(nlogn)
改进后面积θ(n)
矩阵相乘
Sol. 1
用三重循环计算,时间复杂度θ(n^3)
Sol. 2
用分治,包含8个递归,时间复杂度仍然为θ(n^3)
T(n) = 8T(n/2) + θ(n^2)
Sol. 3
Strassen算法,通过数学转换将8个递归转换成7个递归,时间复杂度降为θ(n^2.81)
理论上矩阵相乘的时间复杂度可以达到θ(n^2.376)
快速排序
原理与实现
xi j
5 4 8 9 6 3 2 5 7 1
x i j
5
4 8 9 6 3 2 5 7 1
x i j
5
4 8
9 6 3 2 5 7 1
x i j
5
4 8 9
6 3 2 5 7 1
x i j
5
4 8 9 6
3 2 5 7 1
x i j
5 4
3 9 6
8 2 5 7 1
x i j
5 4
3 9 6 8
2 5 7 1
x i j
5 4 3
2 6 8
9 5 7 1
x i j
5 4 3
2 6 8 9
5 7 1
x i j
5 4 3
2 6 8 9 5 7
1
x i j
5 4 3
2 6 8 9 5 7
1
x i j
5 4 3 2
1 8 9 5 7
6
i x j
1 4 3 2
5 8 9 5 7
6
排序代码
Partition(A, p ,q)
r = p; i = p; x = A[p]; // i 指向最后一个小于x的值的序号,j指向当前循环位置
r = p; i = p; x = A[p]; // i 指向最后一个小于x的值的序号,j指向当前循环位置
// 对于随机算法 r = random(p,q);
for j = p+1 to q
if x > A[j]
i++;
exchange(A[i],A[j]);
exchange(A[i],A[p]);
return i;
for j = p+1 to q
if x > A[j]
i++;
exchange(A[i],A[j]);
exchange(A[i],A[p]);
return i;
QickSort(A,p,q)
if p < q
r = Partition(A,p,q)
QickSort(A,p,r);
QickSort(A,r+1,q);
if p < q
r = Partition(A,p,q)
QickSort(A,p,r);
QickSort(A,r+1,q);
// 代码
#include <iostream>
#include <string>
using namespace std;
void exchange(int& a, int& b)
{
int m = a;
a = b;
b = m;
}
int Partition(int* A,int& p ,int& q)
{
int r = p; int i = p; int x = A[p]; // i 指向最后一个小于x的值的序号,j指向当前循环位置
for(int j = p+1; j <= q; ++j)
{
if( x > A[j])
{
i++;
exchange(A[i],A[j]);
}
}
exchange(A[i],A[p]);
return i;
}
void QickSort(int* A,int p ,int q)
{
if(p < q)
{
int r;
r = Partition(A,p,q);
QickSort(A,p,r-1);
QickSort(A,r+1,q);
}
}
int main()
{
int A[] = {4,5,6,8,3,4,9,7,3,5,8,5,10,15,0,2,36,5,8,12};
int len = sizeof(A)/sizeof(int);
cout<<"A排序前:";
for (int i = 0; i < len; ++i)
{
cout<<A[i]<<" ";
}
cout<<endl;
QickSort(A,0,len-1);
cout<<"A排序后:";
for (int i = 0; i < len; ++i)
{
cout<<A[i]<<" ";
}
cout<<endl;
}
算法时间复杂度分析
快排相对于归并排序来说时间复杂度最好时是一样的,归并排序是稳定的排序算法,但要求窗外的内存空间,而快排是原址排序,时间复杂度上是不稳定的,除非加入随机算法过程;
最坏的情况:
就是每次递归得到的值都在一边,则此时时间复杂度有 T(n) = T(0) + T(n-1) + θ(n); T(n) = θ(n^2);
一般情况:
取正中间 T(n) = 2T(n/2) + θ(n); T(n) = θ(nlgn);
取其它值:时间复杂T(n) = θ(nlgn);
非随机化版本:
好坏交替出现的情况,其时间复杂度与最好的情况一样,T(n) = θ(nlgn); // 即平均情况
随机化版本:
平均时间复杂度为T(n) = θ(nlgn);
快速排序在应用中是归并排序的3倍快
非比较排序
前面的插入、选择、堆、归并、冒泡、快速排序版本,最好的情况都只能达到T(n) = θ(nlgn);
原因是它们都是通过比较大小的模型 来排序,因此为了达到更快的排序效果,我们可以使用非比较大小的方式来实现;
决策树模型:
树的每一个内节点有下标:i:j
对于a1,a2,...an;比较ai与aj,如果ai<=aj,左子树;否则右子树
最终每个叶节点代表一种排序
计数排序
对于n个输入元素中每个数在0~k之间,如果k=O(n),则排序时间为T(n) = θ(n); // 即一般如果数的个数多但范围有限且相对n较小,则可以用计数排序。
// 伪代码 n为数组长度,k为最大数,则里面的数组C长度应该为k+1(0~k)
CountSort(A,n,k)
for i = 0 to k
C[i] = 0;
for j = 0 to n-1
C[A[j]]++; // C[i]表示了值为i的个数
for i = 1 to k
C[i] += C[i-1]; // 对于当前位i的值C[i]表示了至少有C[i]个数小于等于i
// 排序
for i = n-1 to 0
B[C[A[i]]] = A[i];
C[A[i]]--;
// 代码
int* CountSort(int* A,const int n,const int k)
{
int *C = new int[k+1](); // k+1长度,初始化为0
int *B = new int[n]();
for(int j = 0; j < n; ++j)
C[A[j]]++; // C[i]表示了值为i的个数
for(int t = 1; t <= k+1; ++t)
C[t] += C[t-1]; // 对于当前位i的值C[i]表示了至少有C[i]个数小于等于i
// 排序
for(int m = 0; m < n; ++m)
{
B[C[A[m]]] = A[m];
C[A[m]]--;
}
// delete[] C; // delete会崩溃
return B;
}