一、蛮力法
1.顺序查找
蛮力法一个一个对比,直至找到想要内容,返回当前位置。
int SeqSearch1(int r[ ], int n, int k) //数组r[1] ~ r[n]存放查找集合
{
i=n;
while (i>0 && r[i]!=k)
i--;
return i;
}
改进后增加哨兵,无需判断越界问题。
int SeqSearch2(int r[ ], int n, int k) //数组r[1] ~ r[n]存放查找集合
{
r[0]=k; i=n;
while (r[i]!=k)
i --;
return i;
}
2.串匹配问题
给定两个串S=“s1s2…sn” 和T=“t1t2…tm”,在主串S中查找子串T的过程称为串匹配,也称模式匹配。
BF算法
int BF(char S[ ], char T[ ])
{
int i=0,j=0,index=0
while (i<S.length && j<T.length )
{
if (S[i]==T[j]) {i++; j++;}
else {i=++; j=0; }
}
if (j=T.length) return index+1;
else return 0;
}
3.选择排序
首先在未排序序列中找到【最小元素】,存放到排序序列的【起始位置】,然后,再从剩余未排序元素中继续寻找最小元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
void SelectSort(int r[ ], int n)
{
for (i=0; i<n-1; i++) //对n个记录进行n-1趟简单选择排序
{
index=i;
for (j=i+1; j<n; j++) //在无序区中找最小记录
if (r[j]<r[index]) index=j;
if (index!=i) r[i]←→r[index]; //若最小记录不在最终位置则交换
}
}
4.冒泡排序
空间复杂性为O(1)
依次遍历数组,将相邻的两个数字进行比较,将反序的两个数字进行交换。直至排序完毕。
void BubbleSort(int r[ ], int n)
{
for (i=0; i<n-1; i++)
for (j=0; j<n-i; j++)
if (r[j]>r[j+1]) r[j]←→r[j+1];//如果反序,则交换元素
}
二、分治法
分治法是将原问题划分为子问题,通过求解子问题的解,合并子问题的解。从而得到原问题的解。
1.归并排序:时间复杂度(O(nlog2n))
将无序表划分为k个字表(k>=2),对字表分别排序,再两两合并,直至将原表恢复成有序表。
void MergeSort(int r[ ], int r1[ ], int s, int t)
//r[ ]—原序列数组,r1[ ]—归并后序列数组
{
if (s= =t) r1[s]=r[s];
else {
m=(s+t)/2;
Mergesort(r, r1, s, m); //归并排序前半个子序列
Mergesort(r, r1, m+1, t); //归并排序后半个子序列
Merge(r1, r, s, m, t); //合并两个已排序的子序列
}
void Merge(int r[ ], int r1[ ], int s, int m, int t)
{ // i,j分别指向两个待合并的有序子序列,k指向最终有序序列的当前记录
i=s; j=m+1; k=s;
while (i<=m && j<=t)
{
if (r[i]<=r[j]) r1[k++]=r[i++]; //取r[i]和r[j]中较小者放入r1[k]
else r1[k++]=r[j++]; }
if (i<=m) while (i<=m)
//若第一个子序列没处理完,则进行收尾处理
r1[k++]=r[i++];
else while (j<=t)
//若第二个子序列没处理完,则进行收尾处理
r1[k++]=r[j++]; }
2.快速排序:时间复杂度为O(n2)
任意选取一个数(一般是第一个数)作为【轴值】,以他为基准,比他小的值,放在他的左边,比他大的值,放在右边。分别对两边的子序列,进行【递归】处理。这样子序列是有序的,原表不需要合并自然是有序的。
//划分
int Partition(int r[ ], int first, int end)
{
i=first; j=end; //初始化
while (i<j)
{
while (i<j && r[i]<= r[j]) j--; //右侧扫描
if (i<j) {
r[i]←→r[j]; //将较小记录交换到前面
i++; }
while (i<j && r[i]<= r[j]) i++; //左侧扫描
if (i<j) {
r[j]←→r[i]; //将较大记录交换到后面
j--;
} }
retutn i; // i为轴值记录的最终位置 }
//快速排序
void QuickSort(int r[ ], int first, int end)
{
if (first<end) {
pivot=Partition(r, first, end);
//问题分解,pivot是轴值在序列中的位置
QuickSort(r, first, pivot-1);
//递归地对左侧子序列进行快速排序
QuickSort(r, pivot+1, end);
//递归地对右侧子序列进行快速排序
}
}
3.最大子段和:时间复杂度(O(nlog2n))
给定一个数组,找出其中可以构成最大数的子段,字段必须是连续的。
大致有三种情况:
- 当最大子段和在所选的界定值左边的时候
- 当最大字段和在所选的界定值右边的时候
- 当最大子段和包含所选的界定值,也就是在界定值的两侧的时候
在第一种情况下,通过不断地递归,保留最大的子段和,最后相加便可得到。
同样,在第二种情况下,也就是说最大子段和在右边的时候,类似与上面的,通过不断地递归,相加可以求得
第三种情况相对来说比较复杂,当界定值包含在最大子段和中的时候,看上去就是类似于与问题了,但是,根据上述的想法,取得的界定值往左右两边分别寻找,相加便可以求得。
int MaxSum (int a[ ], int left, int right)
{
sum=0;
if (left= =right) { //如果序列长度为1,直接求解
if (a[left]>0) sum=a[left];
else sum=0;
}
else {
center=(left+right)/2; //划分
leftsum=MaxSum (a, left, center); //对应情况①,递归求解
rightsum=MaxSum (a, center+1, right); //对应情况②,递归求解
s1=0; lefts=0; //以下对应情况③,先求解s1
for (i=center; i>=left; i--)
{
lefts+=a[i];
if (lefts>s1) s1=lefts; }
s2=0; rights=0; //再求解s2
for (j=center+1; j<=right; j++)
{
rights+=a[j];
if (rights>s2) s2=rights; }
sum=s1+s2; //计算情况③的最大子段和
if (sum<leftsum) sum=leftsum;
//合并,在sum、leftsum和rightsum中取较大者
if (sum<rightsum) sum=rightsum; }
return sum; }
三、减治法:O (log2n)
把一个大问题划分为多个子问题,求出其中一个【子问题】的解即是原问题的解。
1.折半查找
取【中间记录】作为比较对象,(1)若给定值与中间记录的关键码相等,则查找成功;(2)若给定值小于中间记录的关键码,则在中间记录的左半区继续查找;(3)若给定值大于中间记录的关键码,则在中间记录的右半区继续查找。
int BinSearch(int r[],int n,int k)
{
int low=0,high=n-1; //low1指向第一个数,high指向最后一个数
int mid;
while(low<=high) //当区间存在时
{
mid=(low+high)/2; //划分区间
if(k<r[mid]) //当查找数比中间值小
high=mid-1; //查找左区间
else if(k>r[mid]) //当查找数比中间值大
low=mid+1; //查找右区间
else return mid; //查找成功,返回元素序号
}
return 0; //查找失败,返回0
}
2.二叉查找树
在二叉排序树root中查找给定值k的过程是:
⑴ 若root是空树,则查找失败;
⑵ 若k=根结点的值,则查找成功;
⑶ 否则,若k<根结点的值,则在root左子树上查找;
⑷ 否则,在root的右子树上查找;
上述过程一直持续到k被找到或者待查找的子树为空,如果待查找的子树为空,则查找失败。
//二叉排序树的查找
BiNode * SearchBST(BiNode *root, int k)
{
if (root= =NULL) return NULL;
else if (root->data==k) return root;
else if (k<root->data) return SearchBST(root->lchild, k);
else return SearchBST(root->rchild, k); }
//建立二叉查找树算法
BiNode* InsertBST(BiNode* root, int data){
if(root==NULL){
root=new BiNode; root->data=data;//申请一个新结点
root->lchild=root->rchild=NULL;//叶子结点
return root;
}
if(data<=root->data)
root->lchild=InsertBST(root->lchild, data);
else root->rchild=InsertBST(root->rchild, data);
return root;
}
//建立二叉查找树
BiNode* createBST(int a[], int n)
{
BiNode* T=NULL;
for(int i=0; i<n; i++)
T=InsertBST(T, a[i]); //在二叉查找树 T中插入a[i]
return T;
}
3.选择问题
在【无序序列】T中寻找【升序】排列后【第k小】的元素称为选择问题。寻找第n/2小元素的问题称为中值问题。
思想:随机选取一个数作为【轴值】,假设轴值的位置为s,将比轴值小的数,放在轴值的前面,将比轴值大的数,放在轴值的后面。对比k和s。
(1)如果k=s,那么恰好轴值就是【第k小】的元素。
(2)如果k<s,那么【第k小】的元素在轴值前面这个子序列里。
(3)如果k>s,那么【第k小】的元素在轴值后面这个子序列里。
#include <iostream>
using namespace std;
#define NUM 1001
int a[NUM];
//在a[left:right]中选择第k小的元素
int select(int left, int right, int k)
{
//找到了第k小的元素
if (left >= right) return a[left];
int i = left; //左侧
int j = right+1; //右侧
int pivot = a[left]; //把最左侧的元素作为分界数据
while (true) //快速排列交换数据循环
{
do { //左侧
i = i+1;
} while (a[i] < pivot);
do { //右侧
j = j-1;
} while (a[j] > pivot);
if (i >= j) break;
swap(a[i], a[j]);
}
if (j-left+1 == k) return pivot;
a[left] = a[j];
a[j] = pivot;
if (j-left+1 < k)
//对一个段进行递归调用
return select(j+1, right, k-j+left-1); //找右边
else return select(left, j-1, k); //找左边
}
int main()
{
int n, k;
while (cin>>n>>k)
{
for (int i=0; i<n; i++)
cin>>a[i];
cout<<select(0, n-1, k)<<endl;
}
return 0;
}
最好情况:O(n);
最坏情况:O(n2);
平均情况:O(n);
四、动态规划法
1.多段图的最短路径
时间复杂度为O(m+k):m为边的数目,k为段的数目。
设图G=(V, E)是一个带权有向连通图,如果把顶点集合V 划分成k个互不相交的子集Vi(2≤k, 1≤i≤k),使得E中的任何一条边(u, v),必有u∈Vi,v∈Vi+m(1≤i<k, 1<m),则称图G为多段图,称s∈V1为源点,t∈Vk为终点。
解决方案:从起点开始,要走过j段通路,走第一步时,要走向加上j-1段是到达终点最短的路径。而j-1段最短的路径,需要寻找加上j-2段是到达终点最短的路径。如此一次进行,直到能够直接找到到达终点最短的路径。
const int n=10;
void floyd(int arc[n][n], int dist[n][n]){
int i, j, k;
for(i=0;i<n;i++) //初始化矩阵dist
for(j=0;j<n;j++)
dist[i][j]=arc[i][j];
for(k=0;k<n;k++) //进行n次迭代
for(i=0;i<n;i++)
for(j=0;j<n;j++)
if(dist[i][k]+dist[k][j]<dist[i][j])
dist[i][j]=dist[i][k]+dist[k][j];
}
五、贪心算法
1.最小生成树问题
设G=(V,E)是一个无向连通网,生成树上各边的权值之和称为该生成树的代价,在G 的所有生成树中,代价最小的生成树称为最小生成树。可以理解为,要用最小的代价,将这几个连接起来,使每两个点之间都有通路。
(1)最近顶点:时间复杂度:O(n2)
解决方案:任意选定一点s,放在集合S{}中,在不在S集合的点中寻找一点j,使j为到距离S集合中某点i最短的点。并将j加入到集合S{}中,这两点之间形成生成树上的一条边(i,j)。重复此操作,直到所有的点都加入到集合当中。
(2)最短边:时间复杂度:O(elog2e)
解决方案:先将图中所有边按照权值大小进行排序,权值小的排在前面。按权值大小,选中权值较小的边,如果该边与集合S中的边没有构成回路,则将边放入集合S,否则丢弃。以此类推,直至在有n个顶点的图G中将n-1条边放入集合S,则算法结束
2.背包问题
时间复杂度:O(nlog2n)
给定n种物品和一个容量为C的背包,物品i 的重量是 Wi,其价值为 Vi,背包问题是: 如何选择装入背包的物品,使得装入背包中物品的总价值最大?
(1)选取价值最大的装入
(2)选取质量最小的装入,保证装入的东西多
(3)选取性价比高的装入
float knapsack_greedy (float M,OBJECT instance[],float x[],int n)
{
int i;
float m,p = 0;
for (i=0;i<n;i++)
{ /* 计算物体的价值重量比 */
instance[i].v = instance[i].p / instance[i].w;
x[i] = 0; /* 解向量赋初值 */
}
merge_sort (instance,n); /* 按关键值v的递减顺序排序物体 */
m = M; /* 背包的剩余载重量 */
for (i=0;i<n;i++)
{
if (instance[i].w<=m)
{ /* 优先装入价值重量比大的物体 */
x[i] = 1; m -= instance[i].w;
p += instance[i].p;
}
else
{ /* 最后一个物体的装入份量 */
x[i] = m / instance[i].w;
p += x[i] * instance[i].p;
break;
}
}
return p;
}
六、回溯法
思想:回溯法按深度优先策略搜索问题的解空间树。首先从根节点出发搜索解空间树,当算法搜索至解空间树的某一节点时,先利用剪枝函数判断该节点是否可行(即能得到问题的解)。如果不可行,则跳过对该节点为根的子树的搜索,逐层向其祖先节点回溯;否则,进入该子树,继续按深度优先策略搜索。
回溯法求可行解、往深度延伸
1.图着色问题
①初始化颜色总数为无穷多种。
②每次从点集中选择一个顶点并从第一种颜色开始尝试对其进行着色;
③如果着色不冲突,则继续通过相同的方式处理点集中的下一个顶点;如果着色冲突,则说明该种着色方法行不通,退回到上一个结点,将上一个结点的着色改为当前着色的下一种颜色。
④重复上述过程,直到所有的顶点都着色为止,此时确定了一个可行解。如果可行解的颜色数少于当前颜色总数,则将颜色总数更新为可行解的颜色数。
⑤得到一个可行解后进行回溯,退回到上一个着色颜色序号小于当前颜色总数的结点上,将其着色改为下一种,并进行如上所示的推理过程。
⑥当存在一个结点没有颜色可以着色时,算法停止。当前最优解即全局最优解。
七、分支限界法
分支限界法按广度优先策略遍历问题的解空间树,在遍历过程中对已经处理的每一个节点根据衔接函数估算目标函数的可能取值,从中选取使目标函数取得极值(极大或极小)的节点优先进行广度优先搜索,从而不断调整搜索方向,尽快找到问题的解。
1.TSP问题
TSP问题是指旅行家要旅行n个城市,要求各个城市经历且仅经历一次然后回到出发城市,并要求所走的路程最短。
采用贪心法求得近似解为1→3→5→4→2→1,其路径长度为1+2+3+7+3=16,这可以作为TSP问题的上界。
把矩阵中每一行最小的两个元素相加再除以2,如果图中所有的代价都是整数,再对这个结果向上取整,就得到了一个合理的下界。
lb=((1+3)+(3+6)+(1+2)+(3+4)+(2+3))/2=14