算法复习重点

算法复习重点

一、概论

什么是算法?

算法是求解问题的一系列计算步骤,用来将输入数据转换成输出结果。

算法设计应满足以下几个目标

  • 正确性
  • 可使用性
  • 可读性
  • 健壮性
  • 高效率与低存储量需求

算法的特性

  • 有限性
  • 确定性
  • 可行性
  • 输入性
  • 输出性

渐进符号(O、Ω、θ)

大O符号:上界

大Ω符号:下界

大θ符号:同阶

STL概述

STL由container(容器)、algorithm(算法)和iterator(迭代器)三大部分构成

  • 向量(vector)

  • 字符串(string)

  • 链表(list)

  • 栈(stack)

  • 队列(queue)

  • 优先队列(priority_queue)

顺序容器

vector(向量容器)

  • push_back():在当前向量容器尾部添加一个元素
  • insert(pos,elem):在pos位置插入元素elem,即将元素elem插入到迭代器pos指定的元素之前
  • begin():容器的第一个元素
  • end():容器的最后一个元素的后面的位置
  • rbegin():容器的最后一个元素
  • rend():容器的第一个元素的前面的位置
  • erase():删除当前向量容器中某个迭代器或迭代器区间指定的元素
#include<vector>
using namespace std;
void main(){
    vector<int> myv;
    vector<int>::iterator it;
    myv.push_back(1);
    it=myv.begin();
    myv.insert(it,2);
    myv.push_back(3);
    myv.push_back(4);
    it=myv.end();
    it--;
    myv.erase(it);
    for(it=myv.begin();it!=myv.end();++it)
        printf("%d",*it);
    printf("\n");
}
}

输出:
2 1 3

string(字符串容器)

char cstr[]="China! Greate Wall";
string s1(cstr);								//s1:China! Greate Wall
string s2(s1);									//s2:China! Greate Wall
string s3(cstr,7,11);							//s3:Greate Wall
string s4(cstr,6);								//s4:China!
string s5(5,'A');								//s5:AAAAA

常用函数:

  • empty():判断当前字符串是否为空

  • size():返回当前字符串的实际字符个数

  • length():返回当前字符串的实际字符个数

  • [idx]:返回当前字符串位于idx位置的元素,idx从0开始

  • at(idx):返回当前字符串位于idx位置的元素

  • append(cstr):在当前字符串末尾添加一个字符串str

  • insert(size_type idx, string str):在当前字符串的idx处插入一个字符串str

  • find(string s, size_type pos):从前字符串的pos位置开始查找s的第一个位置,找到返回位置,未找到返回-1

  • replace(size_type idx, size_type len, string str):将当前字符串中起始于idx的len个字符用str替换

  • substr(size_type idx):返回当前字符串起始于idx的子串

  • substr(size_type idx, size_type len):返回当前字符串起始于idx的长度为len的子串

  • erase():删除字符串的所有字符

  • clear():删除字符串的所有字符

  • erase(size_type idx):删除字符串从idx开始的所有字符

  • erase(size_type idx, size_type len):删除当前字符串从idx开始的len个字符

#include<iostream>
#include<string>
using namespace std;
void maia(){
    string s1="",s2,s3="Bye";
    s1.append("Good morning");			//s1:Good morning
    s2=s1;								//s2:Good morning
    int i = s2.find("morning");			//i:5
    s2.replace(i,s2.length()-i,s3);		//s2:Good Bye		(5,12-5,3)
}

输出:
s1:Good morning
s2:Good Bye

二、递归算法设计技术

什么是递归?

递归(recursion)是指在函数的定义中又调用函数自身的方法

求n!——递归算法

int fun(int n){
    if(n==1)
        return 1;
    else
        return fun(n-1)*n;
}
//完整的程序
#include <stdio.h>
int fun(int n) {
	if (n<0) 
		return -1;
	if (n == 1)
		return 1;
	else
		return n * fun(n - 1);
}

int main() {
	int num;
	printf("请输入数字:\n");
	scanf_s("%d", &num);
	int res = fun(num);
	printf("%d的阶乘为:%d",num,res);
	return 0;
}

求n!——非递归算法

int fun(int n)
{
	int res = 1;
	int i = 1;
	for (i = 1; i <= n; i++)
	{
		res *= i;
	}
	return res;
}
//完整的程序
#include <stdio.h>
int fun1(int n) {
	int res=1, i;
	for (i = 2; i <= n;i++) {
		res = res * i;
	}
	return res;
}

int main() {
	int num;
	printf("输入数字:");
	scanf_s("%d", &num);
	printf("结果是:%d", fun1(num));
	return 0;
}

求不带头节点的单链表L中的所有节点值之和

int sum(LinkNode * L){
    if(L==NULL)
        return 0;
    else
        return(L->data + sum(L->next));
}
#include <stdio.h>
#include <stdlib.h>

struct LinkNode {
	int data;
	struct LinkNode *next;
};

int sum(struct LinkNode *L) {
	if (L == NULL) {
		return 0;
	}
	else {
		return (L->data + sum(L->next));
	}
}

int main() {
	// 构造单链表
	struct LinkNode *head = NULL;
	struct LinkNode *tail = NULL;
	int val;
	printf("请输入链接列表(用空格分隔每个节点,以-1结尾):\n");
	while (scanf_s("%d", &val) == 1 && val != -1) {
		struct LinkNode *node = (struct LinkNode*)malloc(sizeof(struct LinkNode));
		node->data = val;
		node->next = NULL;
		if (head == NULL) {
			head = node;
			tail = node;
		}
		else {
			tail->next = node;
			tail = node;
		}
	}

	// 计算节点值之和
	int sum_val = sum(head);
	printf("单链表中节点值的总和为: %d\n", sum_val);

	// 释放链表内存
	struct LinkNode *curr = head;
	struct LinkNode *next;
	while (curr) {
		next = curr->next;
		free(curr);
		curr = next;
	}

	return 0;
}

斐波那契数列——递归算法

int Fib(int n){
    if(n==1||n==2)
        return 1;
    else
        return Fib(n-1)+Fib(n-2);
}
//完整程序
#include <stdio.h>
int fib(int n) {
	if (n == 1 || n == 2)
		return 1;
	else;
		return fib(n-1)+fib(n-2);
}
int main() {
	int num;
	printf("请输入斐波那契数列第几项");
	scanf_s("%d", &num);
	printf("你好,第%d项结果是:%d", num, fib(num));
	return 0;
}

斐波那契数列——非递归

int fib1(int n) {
	int i, f1=1, f2=1, f3;
	if (n == 1 || n == 2)
		return 1;
	for (i = 3; i <= n; i++) {
		f3 = f1 + f2;
		f1 = f2;
		f2 = f3;
	}
	return f3;
}
//完整程序
#include <stdio.h>
int fib1(int n) {
	int i, f1=1, f2=1, f3;
	if (n == 1 || n == 2)
		return 1;
	for (i = 3; i <= n; i++) {
		f3 = f1 + f2;
		f1 = f2;
		f2 = f3;
	}
	return f3;
}

int main() {
	int num;
	printf("输入:");
	scanf_s("%d",&num);
	int res = fib1(num);
	printf("结果是:%d",res);
}

求解n皇后问题的——测试(i,j)位置能否放皇后

bool place(int i,int j){
    if(i==1)
        return true;
    int k = 1;
    while(k<i){//q[]存放各皇后所在的列号
        if((q[k]==j)||(abs(q[k]-j)==abs(i-k)))		//同列:q[k]==j	对角线abs(q[k]-j)==abs(i-k)
            return false;
        k++;
    }
    return true;
}

三、分治法

快速排序 时间复杂度O(nlog2n)

#include<stdio.h>
void disp(int a[],int n){						//输出a中的所有元素
    int i;
    for(i==0;i<n;i++)
        printf("%d",a[i]);
    printf("\n");
}
int Partition(int a[],int s,int t){				//划分算法
    int i=s,j=t;
    int tmp=a[s];
    while(i!=j)
    {
        while(j>i&&a[j]>=tmp)
            j--;
        a[i]=a[j];
        while(i<j&&a[i]<=tmp)
            i++;
        a[j]=a[i];
    }
    a[i]=tmp;
    return i;
}
void QuickSort(int a[],int s,int t){			//对a[s...t]元素序列进行递增排序
    if(s<t)										//序列内至少存在两个元素的情况
    {											
        int i = Partition(a,s,t);				
        QuickSort(a,s,i-1);						//对左子序列递归排序
        QuickSort(a,i+1,t);						//对右子序列递归排序
    }
}
void main()
{
    int n=10;
    int a[]={2,5,1,7,10,6,9,4,3,8};
    printf("排序前:");
    disp(a,n);
    QuickSort(a,0,n-1);
    printf("排序后:");
    disp(a,n);
}

自顶向下的二路归并排序算法 时间复杂度O(nlog2n)

void MergeSort(int a[],int low,int high)
{
    int mid;
    if(low<high)						//子序列有两个或两个以上的元素
    {
        mid = (low+high)/2;				//取中间的位置
        MergeSort(a,low,mid);			//对a[low,mid]子序列排序
        MergeSort(a,mid+1,high);		//对a[mid+1,high]子序列排序
        Merge(a,low,mid,high);			//合并两个子序列
    }
}

查找最大和次大 时间复杂度O(n)

void solve(int a[],int low,int high,int &max1,int &max2)
{
    if(low==high)
    {
        max1=a[low];
        max2=-INF;
    }
    else if(low==high-1)
    {
        max1=max(a[low],a[high]);
        max2=min(a[low],a[high])
    }
    else
    {
        int mid = (low+high)/2;
        int lmax1,lmax2;
        slove(a,low,mid,lmax1,lmax2);
        int rmax1,rmax2;
        slove(a,mid+1,high,rmax2,rmax2);
        if(lmax1>rmax1)
        {
            max1 = lmax1;
            max2 = max(lmax2,rmax1);
        }
        else
        {
            max1 = rmax1;
            max2 = max(lmax1,rmax2);
        }
    }
}

折半查找 O(log2n)

#include<stdio.h>
int BinSearch(int a[],int low,int high,int k)
{
    int mid;
    if(low<=high)
    {
        mid = (low+high)/2;
        if(a[mid]==k)
            return mid;
        if(a[mid]>k)
            return BinSearch(a,low,mid-1,k);
        else
            return BinSearch(a,mid+1,high,k);
    }
    else return -1;
}
void main()
{
    int n=10,i;
    int k=6;
    int a[]={1,2,3,4,5,6,7,8,9,10};
    i = BinSearch(a,0,n-1,k);
    if(i>=0)
        printf("a[%d]=%d\n",i,k);
    else
        printf("未找到%d元素\n",k);
}

折半查找 ——非递归 O(log2n)

int BinSearch(int a[],int n,int k)
{
    int low=0,high=n-1,mid;					
    while(low<=high)						//当前区间存在元素时循环
    {										
        mid = (low+high)/2;					//求中间位置
        if(a[mid]==k)							//中间位置刚好是查找的
            return mid;
        if(a[mid]>k)							//查找的k在左半边区间
            high = mid - 1;					//改变high的值为中间位置的前一位
        else								
            low = mid + 1;					//改变low 的值为中间位置的后一位
    }
    return -1;								//没有找到
}

四、蛮力法

蛮力法的优点如下:

  • 逻辑清晰,编写程序简洁。
  • 可以用来解决广阔领域的问题。
  • 对于一些重要的问题,它可以产生一些合理的算法
  • 可以解决一些小规模的问题。
  • 可以作为其他高效算法的行亮标准

使用蛮力法通常有以下几种情况

  • 搜索所有的解空间
  • 搜索所有的路径
  • 直接计算
  • 模拟和仿真
字符串匹配

BF算法

int BF(string s,string t)
{
    int i=0,j=0;
    while(i<s.length()&&j<t.length())
    {
        if(s[i]==t[j])
        {
            i++;
            j++;
        }
        else
        {
            i=i-j+1;
            j=0;
        }
    }
    if(j==t.length())
        return i-j;
    else
        return -1;
}

求t在s中出现的次数

int Count(string s,string t)
{
    int num=0;
    int i=0,j=0;
    while(i<s.length()&&j<t.length())
    {
        if(s[i]==t[j])						//两个字符相同时
        {
            i++;
            j++;
        }
        else								//两个字符不相同
        {
            i=i-j+1;
            j=0;
        }
        if(j==t.length())
        {
            num++;							//出现次数加1
            j=0;							//j从0开始继续
        }
    }
    return num;
}

求解全排列问题P137

五、回溯法

回溯法概述

  • 一般情况下,问题的解仅是问题解空间的一个子集,解空间中满足约束条件的解空间称为可行解

  • 解空间中使用目标函数取最大或最小值的可行解称为最优解


一个问题的求解过程就是在对应的解空间中搜索以寻找满足目标函数的的解,所以算法设计的关键点有3个:

  1. 结点是如何扩展的。
  2. 解空间树中按什么方式搜索。
  3. 如何高效的找到问题的解。

  • 解空间树通常有两种类型:子集树排列树

  • 约束函数和限界函数统称为剪枝函数


回溯法解题的一般步骤

  1. 针对给定的问题确定问题的解空间树,问题的解空间树应至少包含问题的一个解或者最优解。
  2. 确定结点的扩展搜索规则
  3. 以深度优先方式搜索解空间树,并在搜索过程中可以采用剪枝函数来避免无效搜索。其中,深度优先方式可以选择递归回溯或者迭代(非递归)回溯。

  • 解空间树为子集树时对应算法的时间复杂度为O(2n
  • 解空间树为排列树时对应算法的时间复杂度为O(n!)

采用的剪枝方式如下:

  1. 左分枝剪枝:仅仅扩展满足tw+ω[i]≤W条件的左孩子结点。
  2. 右分枝剪枝:仅仅扩展满足tw+rw-ω[i]>maxw条件的右孩子结点,即不选择第i个集装箱也可能找到更优解。
void dfs(int i,int tw,int rw,int op[])
{
    if(i>n)
    {
        if(tw>maxw)
        {
            maxw=tw;
            for(int j=1;j<=n;j++)
                x[j]=op[j];
        }
    }
    else
    {
        if(tw+w[i]<=w)
        {
            op[i]=1;
            dfs(i+1,tw+w[i],rw-w[i],op);
        }
        if(tw+rw-w[i]>maxw)
        {
            op[i]=0;
            dfs(i+1,tw,rw-w[i],op);
        }
    }
}

六、分枝限界法

什么是分支限界法

分支限界法类似于回溯法,也是一种在问题的解空间树上搜索问题解的算法,但在一般情况下分支限界法和回溯法的求解目标不同。

分支限界法的设计思想

  1. 设计合适的限界函数
    1. 求最大值,则设计上界限界函数ub
    2. 求最小值,则设计下界限界函数lb
  2. 组织活结点表
    1. 队列式分支限界法
      • 队列式分枝限界法将活结点表组织成一个队列,并按照队列先进先出原则选取下一个结点为扩展结点
    2. 优先队列式分枝限界法
      • 优先队列式分枝限界法的主要特点是将活结点表组成一个优先队列,并选取优先级最高的活结点作为当前扩展结点。
  3. 确定最优解的解向量
    • 采用分枝限界法求解的3个关键问题如下:
      1. 如何确定合适的限界函数。
      2. 如何组织待处理结点的活结点表
      3. 如何确定解向量的各个分量。

求结点e的上界e.ub的算法如下:

void bound(NodeType &e)
{
    int i=e.i+1;
    int sumw=e.w;
    double sumv=e.v;
    while((sumw+w[i]<=W)&&i<=n)
    {
        sumw+=w[i];
        sumv+=v[i];
        i++;
    }
    if(i<=n)
        e.ub=sumv+(W-sumw)*v[i]/w[i];
    else
        e.ub=sumv;
}

求给定非负整数序列中的数字排列成的最大数字,如{50,2,1,9}---->95021

重载关系函数

struct Cmp
{
    bool operator()(const string &s,const string &t) const
    {
        return s+t>t+s;
    }
}

求解背包问题并返回总价值

void Knap(){
    V=0;
    double weight = W;
    memset(x,0,sizeof(x));
    int i = 1;
    while(A[i].w<weight){
        x[i]=1;
        weight-=A[i].w;
        V+=A[i].v;
        i++;
    }
    if(weight>0){
        x[i]=weight/A[i].w;
        V+=x[i]*A[i].v;
    }
}

七、贪心法

什么是贪心法

贪心算法(greedy algorith m ,又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择 。

在求解问题时,通常求解问题直接给出或者可以分析出某些约束条件,满足约束条件的问题解称为可行解

另外,求解问题直接给出或者可以分析出衡量可行解好坏的目标函数,使目标函数取最大(或最小)值的可行解称为最优解

用贪心法求解的问题应具有的性质

  1. 贪心选择性质
  2. 最优子结构性质

贪心法的一般求解过程

用贪心法求解问题的基本思路如下:

  1. 建立数学模型来描述问题。

  2. 把求解的问题分成若干个子问题。

  3. 对每一个子问题求解,得到子问题的局部最优解。

  4. 把子问题的局部最优解合成原来解问题的一个解。

    求解活动安排问题 时间复杂度O(nlog2n)

void solve()								//求解最大兼容活动子集
{											
    memset(flag,0,sizeof(flag));			//初始化为false
    sort(A+1,A+n+1);						//A[1...n]按活动结束时间递增排序
    int preend=0;							//前一个兼容活动的结束时间
    for(int i=1;i<=n;i++)					//扫描所有活动
    {		
        if(A[i].b>=preend)					//找到一个兼容活动
        {
            flag[i]=true;					//选择A[i]活动
            preend=A[i].e;					//更新preend值
        }
    }
}
posted @ 2023-06-04 21:16  苏弋吖  阅读(112)  评论(0)    收藏  举报