《啊哈算法》笔记(一)

《《啊哈算法》笔记(一)》

1 桶排序
2 冒泡排序
3 快速排序
4 队列,栈,链表
5 弗洛伊德算法 -最短路径:求两个城市之间的最短路径
6 迪杰斯特拉算法 -单源最短路径:指定一个点到其余各个顶点的最短路径
7 贝尔曼福特算法(优化) -单源最短路径:解决了负权边的问题
8 堆
9 堆排序
10 并查集

1 桶排序

//根据最大值设立桶数,每出现一个数,就在对应编号的桶中放旗子做标记,最后根据旗子数打印桶序数
//时间复杂度为O(m+n),但是浪费空间

void tong(){
    printf("开始桶排序\n");
    int tong[1001],i,j,t,n;
    for (i = 0; i <= 1000; i++)
    {
        tong[i] = 0;
    }
    //输入一个数n,表示接下来有n个数
    printf("输入数据个数\n");
    scanf("%d",&n);
    
    
    //循环读入n个数,并进行桶排序
    printf("输入数据\n");
    for (i = 1; i <= n; i++)
    {
        scanf("%d",&t);
        //计数,对编号为t的桶插一个旗子
        tong[t]++;
    }
    printf("桶排序完成\n");
    //依次判断带编号的桶
    for (i = 1000; i >= 0; i--)
    {
        //出现几次就将桶编号打印几次
        for (j = 1; j <= tong[i]; j++)
        {
            printf("%d\n",i);
        }
    }
}
2 冒泡排序

//每次都比较两个相邻的元素,如果它们的顺序错误就把它们交换过来,顺序正确位置不变,继续比较
//时间复杂度是O(n²)

void maoPao(){
    printf("开始冒泡排序\n");
    int a[100],i,j,n;
    printf("输入数据个数\n");
    scanf("%d",&n);
    printf("输入数据\n");
    //循环读入数据
    for (i = 1; i <= n; i++)
    {
        scanf("%d",&a[i]);
    }
    
    
    //开始排序
    for (i = 1; i <= n-1; i++)
    {
        for (j = 1; j <= n-i; j++)
        {
            if (a[j] < a[j+1])
            {
                int temp = a[j];
                a[j] = a[j+1];
                a[j+1] = temp;
            }
        }
    }
    
    
    printf("冒泡排序完成\n");
    //输出
    for (i = 1; i <= n; i++)
    {
        printf("%d\n",a[i]);
    }
}

3 快速排序

//也是交换排序,每次交换是跳跃式的.
//每次排序的时候设置一个基准点(通常是第一个数),然后从两头开始比较交换,将小于等于基准点的数全部放在基准点的左边,将大于等于基准点的数全部放在基准点的右边
//平均时间复杂度O(nlogn)

//快速排序
int a[101];
//a[0,3,5,1,4,2]
//left 1 right 5
void realKuaiSu(int left,int right){
    int i,j,t,temp;
    if (left > right)
    {
        return;
    }
    //temp为基准数
    temp = a[left];//temp=3
    i = left;      //i=1
    j = right;      //j=5
    //i不等于j,就一直循环
    while (i != j)
    {
        //从右往左找,当条件成立时,执行j--,再判断表达式,直到跳出
        //2>=3 j=5 跳出
        //5>=3 j=4
        //4>=3 j=3
        //1>=3 j=3 跳出
        while (a[j] >= temp && i < j)
        {
            j--;
        }
        //j=5
        //j=3
        
        //从左往右找
        //3<=3 i=2
        //5<=3 i=2 跳出
        //2<=3 i=3
        //1<=3 i=j 跳出
        while (a[i] <= temp && i < j)
        {
            i++;
        }
        //i=2
        //i=3
        
        //当两个哨兵没有相遇,交换两个数在数组中的位置
        if (i < j)
        {
            t = a[i];
            a[i] = a[j];
            a[j] = t;
        }
    }
    //a[0,3,2,1,4,5]  2!=5 循环 此时j=5 i=2 temp=3
    //i=3 j=3 跳出 此时a[0,3,2,1,4,5] temp=3
    
    //将基准数归位
    a[left] = a[i]; //a[1]=1
    a[i] = temp;    //a[3]=3
    //a[0,1,2,3,4,5]
    //然后一直处理左半边的
    realKuaiSu(left, i-1); //1,2
    //处理右边的
    realKuaiSu(i+1, right); //4,5
}
//调用快速排序
void kuaiSu(){
    printf("开始快速排序\n");
    int i,n;
    printf("输入数据个数\n");
    scanf("%d",&n);
    printf("输入数据\n");
    //循环读入数据
    for (i = 1; i <= n; i++)
    {
        scanf("%d",&a[i]);
    }
    
    realKuaiSu(1, n);
    
    printf("快速排序完成\n");
    //输出
    for (i = 1; i <= n; i++)
    {
        printf("%d\n",a[i]);
    }
}
4 队列,栈,链表
//队列
//一种特殊的线性结构,只允许在队列首部进行删除操作(出列),在队列尾部进行插入操作(入列),先进先出(FIFO)
struct queue {
    int data[100];//数据
    int head;     //队首
    int tail;     //队尾
};
//栈
//只能在一端进行插入和删除操作,后进后出
struct stack {
    int data[10];
    int top;
};
//链表
//在 C 语言中*号有三个用途,分别是:
//1. 乘号,用做乘法运算,例如 5*6。
//2. 申明一个指针,在定义指针变量时使用,例如 int *p;。
//3. 间接运算符,取得指针所指向的内存中的值,例如 printf("%d",*p);。
//->叫做结构体指针运算符,用来访问结构体内部成员的
struct node {
    int data;
    struct node *next;
};

void lian(){
    struct node *p;
    //动态申请一个空间,用来存放一个结点,并用临时指针p指向这个结点
    p = (struct node *)malloc(sizeof(struct node));
}

//模拟链表
//两个数组,第一个数组存储数据,第二个数组存放每个元素右边的元素在第一个数组中位置的

//模拟链表
//两个数组,第一个数组存储数据,第二个数组存放每个元素右边的元素在第一个数组中位置的

//枚举
//又叫穷举算法,有序地尝试每一种可能
//奥数题
//炸弹人

//火柴棍等式 m根(m<=24)火柴棍,可以拼出多少个不同形状如A+B=C的等式
//枚举A,B,C,范围都是0~11111(20根火柴棍),A+B+C=m-4

//数的全排列 输入一个指定点的数n,输出1~n的全排列

//深度优先搜索
//关键在于当下该如何做
void DFS(int step){
    int i,n = 0;
    //判断边界
    //尝试每一种可能
    for (i = 1; i <= n; i++)
    {
        //继续下一步
        DFS(step +1);
    }

}
//图的遍历:以一个未被访问过的顶点作为起始顶点,沿当前顶点的边走到未访问过的顶点,当没有未访问过的顶点时,回到上一个顶点,继续访问别的顶点...一条路走到黑,然后回头
//城市地图

//广度优先搜索
//图的遍历:以一个未被访问过的顶点作为起始顶点,访问其所有相邻的顶点,然后对每个相邻的顶点再访问相邻的顶点...
//最小转机
//适用于所有边的权值相同的情况

5 弗洛伊德算法

//最短路径 有些城市之间有公路,有些城市之间没有,求两个城市之间的最短路径
//时间复杂度为O(n³),不能解决带有负权环,负权回路的图
//用二维数组存储所有点之间关系,刚开始只允许经过1顶点进行中转,慢慢增加允许中转的点数量

void Floyd(){
    int n = 100,e[10][10];
    for (int k=1; k<=n; k++){
        for (int i=1; i<=n; i++){
            for (int j=1; j<=n; j++) {
                if (e[i][j] > e[i][k]+e[k][j]) {
                    e[i][j] = e[i][k]+e[k][j];
                }
            }
        }
    }
}

6 迪杰斯特拉算法

//单源最短路径:指定一个点到其余各个顶点的最短路径
//时间复杂度为O(n²)
//用一个二维数组存储所有点之间关系,用一个一维数组存储一个点到其他点的初始路程,称为估计值,找到最小的,称为确定值
//每次找到离源点最近的一个顶点,通过比较各路径与估计值大小,更新估计值(松弛),直到都成为确定值,即最短路线

void Dijkstra(){
    //n为顶点数,e为关系数组,book为已知最短路径顶点,dis为初始路程
    int n = 100,e[10][10],book[10],dis[10],u=0,v=0;
    for (int i=1; i<=n-1; i++)
    {
        //找到离1号顶点最近的顶点
        int min = 9999999;
        for (int j=1; j<=n; j++)
        {
            if (book[j]==0 && dis[j]<min)
            {
                min = dis[j];
                u = j;
            }
        }
        book[u] = 1;
        for (v=1; v<=n; v++)
        {
            if (e[u][v] < 9999999)
            {
                if (dis[v] > dis[u]+e[u][v])
                {
                    dis[v] = dis[u]+e[u][v];
                }
            }
        }
    }
}
//优化
//时间复杂度为O(m+n)logn
//用邻接表存储图的时间复杂度是O(m)
//但不能有负权边

7 贝尔曼福特算法

//贝尔曼福特算法
//时间复杂度为O(mn)
//解决了负权边的问题
void  BellmanFord(){
    //n为顶点个数,m为边数,uvw记录边的信息
    int n=10,m=10,dis[10],v[10],u[10],w[10];
    for (int k=1; k<=n-1; k++)//进行n-1轮松弛
    {
        for (int i=1; i<=m; i++)//枚举每一条边
        {
            if (dis[v[i]] > dis[u[i]] + w[i])//尝试松弛
            {
                dis[v[i]] = dis[u[i]] + w[i];
            }
        }
    }
}

//贝尔曼福特算法优化
//1.可以用一个一维数组备份数组dis,如果在新一轮的松弛中数量dis没有发生变化,则可以提前跳出循环
void  BellmanFord1(){
    //n为顶点个数,m为边数,uvw记录边的信息
    int n=10,m=10,dis[10],bak[10],v[10],u[10],w[10],check,flag;
    //进行n-1轮松弛
    for (int k=1; k<=n-1; k++)
    {
        //将dis数组备份至bak数组中
        for (int i=1; i<=n; i++)
        {
            bak[i] = dis[i];
        }
        //枚举每一条边
        for (int i=1; i<=m; i++)
        {
            //尝试松弛
            if (dis[v[i]] > dis[u[i]] + w[i])
            {
                dis[v[i]] = dis[u[i]] + w[i];
            }
        }
        //松弛完毕后检测dis数组是否有更新
        check=0;
        for (int i=1; i<=n; i++)
        {
            if (bak[i] != dis[i])
            {
                check = 1;
                break;
            }
        }
        //如果dis数组没有更新,提前退出循环
        if (check==0)
        {
            break;
        }
    }
    //检测负权回路
    flag = 0;
    for (int i=1; i<=m; i++)
    {
        if (dis[v[i]] > dis[u[i]] + w[i])
        {
            flag = 1;
        }
    }
    if (flag==1)
    {
        //此图含有负权回路
    }
}
//贝尔曼福特算法优化
//2.每次仅对最短路程发生变化了的顶点的相邻边执行松弛操作
//用一个队列que维护这些最短路程发生变化了的点
void BellmanFord2(){
    //n顶点个数,m边数,dis最短路径数组,que队列,head队列头,tail队列尾
    int n,m,i,j,k;
    //u,v,w大小一般要比m的最大值大1 边的数组
    int u[8],v[8],w[8];
    //first要比n最大值大1,next比m的最大值大1 建立邻接表用
    int first[6],next[8];
    //book数组记录已在队列中的顶点
    int dis[6] = {0},book[6] = {0};
    int que[101] = {0},head = 1,tail = 1;
    
    //假设图为5点,7边结构
    m=5,n=7;
    //把边建立邻接表
    for (i =1; i<=m; i++)
    {
        scanf("%d,%d,%d",&u[i],&v[i],&w[i]);
        next[i] = first[u[i]];
        first[u[i]] = i;
    }
    
    //开始算法
    //顶点入列
    que[tail] = 1;
    tail++;
    //1号已经入列
    book[1] = 1;
    //队列不为空循环
    while (head < tail)
    {
        //当前需要处理的队列首顶点
        k = first[que[head]];
        //扫描当前顶点所有的边
        while (k!=-1)
        {
            ///尝试松弛
            if (dis[v[k]] > dis[u[k]] + w[k])
            {
                //松弛
                dis[v[k]] = dis[u[k]] + w[k];
                //优化的部分,book数组判断顶点v[k]是否在队列中,节省了时间
                //0表示不在
                if (book[v[k]]==0)
                {
                    //入队操作
                    que[tail] = v[k];
                    tail++;
                    //标记已经入队
                    book[v[k]] = i;
                }
            }
            k = next[k];
        }
        //出队
        book[que[head]] = 0;
        head++;
    }
    //输出dis即可,这个优化在不用遍历判断,队列为空时算法结束,book数组判断是否在队列中
}

《《啊哈算法》笔记(一)》 三种算法的总结

8 堆

//满二叉树
//每个结点都有两个儿子
//完全二叉树
//一棵二叉树除了最右边位置上有一个或者几个叶结点缺少外,其他是丰满的,为完全二叉树

//堆
//堆是一种特殊的完全二叉树
//所有父结点比子结点大的为最大堆,所有父结点比子结点小的为最小堆
//创建堆:把n个元素先从左到右从1到n编码,转换成一棵完全二叉树,然后从最后一个非叶结点到根结点,逐个挪动结点,直到成为堆

//最小堆应用
//删除一个数组中最小的数,并增加一个新的数,再次求这个数组中最小的一个数
//相比遍历而言,时间复杂度大大降低,为O(logn)
//插入一个新的元素同理,插在末尾,然后上移,时间复杂度也为O(logn)

void downSmall(){
    //i代表顶点,h数组与n数组长度
    int i=1,h[10],n=10;
    //flag标记是否需要继续向下调整
    int t,flag=0;
    //当i结点有儿子并且需要继续调整的时候,执行循环
    while (i*2<=n && flag==0)
    {
        //首先判断它和左儿子的关系,并用t记录值较小的结点编号
        if (h[i] > h[i*2])
        {
            t = i*2;
        }
        else
        {
            t = i;
        }
        //如果有右儿子,对右儿子进行操作
        if (i*2+1 <= n)
        {
            //如果右儿子值更小,更新小的结点编号
            if (h[t] > h[i*2+1])
            {
                t = i*2+1;
            }
        }
        //如果发现最小的结点编号不是自己,说明子结点中有比父结点更小的
        if (t!=i)
        {
            int temp = h[t];
            h[t] = h[i];
            h[i] = temp;
            //更新i为刚才与它交换的儿子结点的编号,便于向下调整
            i = t;
        }
        //否则说明当前的父结点已经比两个子结点都要小,不需要再进行调整
        else
        {
            flag = 1;
        }
    }
}

//删除最大的元素
int deletemax(){
    //h一个堆,n长度
    int t,h[10],n=10;
    //将堆最后一个点赋值到堆顶
    t = h[1];
    h[1] = h[n];
    //堆的元素减少1
    n--;
    //从顶点向下调整排序
    downSmall();
    return t;
}
9 堆排序
//先建立最大堆
void downBig(){
    //i代表顶点,h数组与n数组长度
    int i=1,h[10],n=10;
    //flag标记是否需要继续向下调整
    int t,flag=0;
    //当i结点有儿子并且需要继续调整的时候,执行循环
    while (i*2<=n && flag==0)
    {
        //首先判断它和左儿子的关系,并用t记录值较大的结点编号   //此处不同
        if (h[i] < h[i*2])
        {
            t = i*2;
        }
        else
        {
            t = i;
        }
        //如果有右儿子,对右儿子进行操作
        if (i*2+1 <= n)
        {
            //如果右儿子值更大,更新小的结点编号     此处不同
            if (h[t] < h[i*2+1])
            {
                t = i*2+1;
            }
        }
        //如果发现最大的结点编号不是自己,说明子结点中有比父结点更大的
        if (t!=i)
        {
            int temp = h[t];
            h[t] = h[i];
            h[i] = temp;
            //更新i为刚才与它交换的儿子结点的编号,便于向下调整
            i = t;
        }
        //否则说明当前的父结点已经比两个子结点都要大,不需要再进行调整
        else
        {
            flag = 1;
        }
    }
}
//堆排序
//每次都把堆顶最大值换到末尾,然后排除掉再排序,保持堆顶是最大值
//时间复杂度为O(nlogn)
void duiPaiXu(){
    int h[10],n=10;
    while (n > 1)
    {
        int temp = h[1];
        h[1] = h[n];
        h[n] = temp;
        n--;
        downBig();
    }
}

//像这样支持插入元素和寻找最值的数据结构称为优先队列,堆就是一种优先队列
//普通队列寻找最值需要穷举,而已排序好的数组插入元素需要移动很多
//堆还可以用来求一个数列中第k大的数,要建立一个大小为k的最小堆,从k+1个数开始与堆顶比较,如果比堆顶的数要大,则舍弃当前堆顶将这个数作为堆顶,然后重新排序

10 并查集

并查集通过一个一维数组来实现,本质是维护一个森林,逐渐将树合并成一棵大树,遵循靠左原则和擒贼先擒王原则

//擒贼先擒王原则  递归函数,不停地找根树
int get(int v){
    int f[10]={1,2,3,4,5,6,7,8,9,10};
    if (f[v] == v)
    {
        return v;
    }
    else
    {
        //路径压缩  每次在函数返回的时候,把路上遇到的树的根改为最后找到的根树,可以提高其他树找到根树的速度
        f[v] = get(f[v]);
        return f[v];
    }
}
//合并两子集合的函数
void merge(int v,int u){
    int f[10]={1,2,3,4,5,6,7,8,9,10};
    int t1,t2;
    t1 = get(v);
    t2 = get(u);
    //判断两个结点是否在同一个集合中,即是否为同一个祖先
    if (t1 != t2)
    {
        //靠左原则,左边变成右边的根数,把右边集合作为左边集合的子集合
        f[t2] = t1;
    }
}
void bingChaJi(){
    int i,x,y,m,n,sum=0;
    int f[10]={1,2,3,4,5,6,7,8,9,10};
    scanf("%d %d",&n,&m);
    for (i=1; i<=m; i++)
    {
        //合并树
        scanf("%d %d",&x,&y);
        merge(x,y);
    }
    //最后看有几个树
    for (i=1; i<=n; i++)
    {
        if (f[i]==i)
        {
            sum++;
        }
    }
}
    原文作者:oldSix_Zhu
    原文地址: https://www.jianshu.com/p/82b95695ad47
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞