Back

算法

推荐看👉 OI Wiki

算法部分

位图(bitmap)

通过一个bit位来表示某个元素对应的值或者状态,其中的key就是对应元素本身,value对应0或1,我们知道8个bit可以组成一个Byte,所以bitmap本身会极大的节省储存空间。

位图BitMap

字典(map)
  • 字典有什么特点呢?

    • 字典的主要特点是一一对应的关系.
    • 比如保存一个人的信息, 在合适的情况下取出这些信息.
    • 使用数组的方式: [18, “Coderwhy”, 1.88]. 可以通过下标值取出信息.
    • 使用字典的方式: {“age” : 18, “name” : “Coderwhy”, “height”: 1.88}. 可以通过key取出value

    字典(map)的详细解释

kmp算法

kmp算法也就是字符串匹配算法。

比如: 在string str = “abcababcba"中匹配 string str2 = “ababc“字符串

最长公共前后缀:要匹配的字符串生成的数组

从一个字符开始到整个字符串,默认每行的公共前后缀最大值为全部字母数,然后判断是否符合其数字,如果不符合减一,一直判断到0结束。一行元素为1的数字为0,最后一行不统计

0 a

0 a b

1 a b a

2 a b a b

0 a b a b c

得出的公共前后缀为

0 0 1 2 0

然后整体向后移动一位,原最后一位被覆盖,下标为0的位置设为-1,最后的公共前后缀为

-1 0 0 1 2

kmp匹配过程

abcababcba

ababc

图片失效

如果匹配到不相等元素,则通过next数组(P)决定next数组移动的位置,例如上图next数组(P)下标为2的元素与T字符串不相等,则看一下P数组下标2的next数组的值为0,则把next数组下标为0的位置移动到不匹配的地方

图片失效

如果next数组的值为-1则next数组整体右移,或找到T字符串与子字符串第一个元素(如果找到的话),然后把P移动到此位置(T的剩余的元素大于等于子字符串元素个数);

代码

#include <cstring>
#include <iostream>
#include <string>

// 计算最长公共前后缀
void prefix_table(char pattern[], int prefix[], int n) {
     prefix[0] = 0;
     int len = 0;
     int i = 1;
     while (i < n) {
         if (pattern[i] == pattern[len]) {
             len++;
             prefix[i] = len;
             i++;
         } else {
             if (len > 0) {
                 len = prefix[len - 1];
             } else {
                 prefix[i] = len;
                 i++;
             }
         }
     }
}

// 计算next数组
void move_prefix_table(int table[], int n) {
    for (int i = n - 1; i > 0; --i) {
        table[i] = table[i - 1];
    }
    table[0] = -1;
}

// kmp算法
void kmp_search(char text[], char pattern[]) {
    int n = strlen(pattern);
    int m = strlen(text);
    int *prefix = new int[n];
 
    prefix_table(pattern, prefix, n);
    move_prefix_table(prefix, n);
  
    int i = 0;
    int j = 0;
    while (i < m) {
        if (j == n - 1 && text[i] == pattern[j]) {
            std::cout << "Found pattern at " << i - j << '\n';
            j = prefix[j];
        }
        if (text[i] == pattern[j]) {
            i++;
            j++;
        } else {
            j = prefix[j];
            if (j == -1) {
                i++;
                j++;
            }
        }
    }
 
    delete[] prefix;
}

int main(void) {

    char pattern[] = "ABABCABAA";
 
    char text[] = "ABABABCABAABABABAAB";
    kmp_search(text, pattern);
    return 0;
}
双指针
同向双指针

判断一个链表是否有

创建两个指针ptr1,ptr2,两个指针同时指向链表头结点。ptr1每次向后移动一个结点,ptr2每次移动2个结点,如果链表有环他们会指向同一个结点

class Solution {
public:
    /**
     * @param circles: The value of 6 points on n rings
     * @return: Whether there are two same circles
     */
    bool samecircle(Node head) {
        // write your code here
        Node ptr2 = head;
        Node ptr2 = head;
        while (p1 != nullptr && p2 != nullptr)
        {
            ptr1 = p1.next;
            ptr2 = p2.next.next;
            if (ptr1 == ptr2) // 结点相遇
                return true;
        }
        return false;
    }
};

判断环长

第一次相遇代表有环,第二次相遇代表两个指针发生了套环,所以:

环长 = 速度差 * 移动次数

判断入环点

慢指针ptr1每次走一步,所以走的距离是:

d1 = D + X * (S1 + S2) + S1

快指针ptr2走的距离是:

d2 = D + N * (S1 + S2) + S1

快指针速度是慢指针2倍。 即:

2 * d1 = d2

整理后的公式为

D = (n - 2X - 1)(S1 + S2) + S2

假设n - 2X -1的值为0,则D = S2;,那么我们就可以在首次相遇点的时候,定义一个指针指向链表的起点,一个指针指向首次相遇点,然后两个指针每次前进1步,当两个指针相遇的时候就是链表的入环点。

例子:

class Solution {
public:
    /**
     * @param circles: The value of 6 points on n rings
     * @return: Whether there are two same circles
     */
    Node find_node(Node head) {
        // write your code here
        Node ptr1 = head;
        Node ptr2 = head;
        Node ptr3 = nullptr;
        Node ptr4 = nullptr;
        while (p1 != nullptr && p2 != nullptr) {
            ptr1 = p1.next;
            ptr2 = p2.next.next;
            if (ptr1 == ptr2) // 结点相遇
                ptr3 = head;
            	ptr4 = ptr1;
            	break;
        }
        if (ptr3 != nullptr && != ptr4 != nullptr) {
            while(ptr3 != ptr4) {
                ptr3 = ptr3.next;
                ptr4 = ptr4.next;
            }
            return ptr4;
        }
        return nullptr;
    }
};
相向双指针
二分法
  • 又称折半搜索,期望时间复杂度为O(log2n),最差为O(log2(n + 1))

二分法前提是数据已经有序

在A[0] … A[n]中搜索K。

步骤: \1. 令low = 0, high = n - 1,初始的查找区域为[low, high]. \2. 取low和high的中间值mid = (low+high)/2。 \3. 如果A[mid] = K,则返回mid, 如果不等,则重新确定查找区间。 \4. 当low > high 时,则表示区间已经失效,如果还未找到,则表示数组中不包含K的值,返回-1。

template<class T>
int binary_search(vector<T> &A, T K) {
    int low = 0;
    int high = A.size() - 1;
    
    while( low < high ) {
        int mid = (low + high)/2;

        if( A[mid]  < K )
            low = mid + 1;
        else if( A[mid] > k )
            high = mid - 1;
        else
            return mid;
    }  

    return -1;      /*返回-1表示数组不存在K的值*/
 }
二分答案

二分答案二分查找 其实是不一样的

二分答案: 即对你要求的答案进行二分 二分查找: 对一个已知的有序数据集上进行二分的查找

基础算法·二分答案 - Potassium - 博客园 (cnblogs.com)

分治法

分治分治,即分而治之。分治,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。这个技巧是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变换)……直接说就是将一个难以直接解决的大问题,分割成一些规模比较小的相同的小问题,以便各个击破,分而治之。

分治法所能解决的问题一般具有以下几个特征:

\1) 该问题的规模缩小到一定的程度就可以容易地解决

\2) 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。

\3) 利用该问题分解出的子问题的解可以合并为该问题的解;

\4) 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题。

第一条: 特征是绝大多数问题都可以满足的,因为问题的计算复杂性一般是随着问题规模的增加而增加;

第二条: 特征是应用分治法的前提它也是大多数问题可以满足的,此特征反映了递归思想的应用;、

第三条: 特征是关键,能否利用分治法完全取决于问题是否具有第三条特征,如果具备了第一条和第二条特征,而不具备第三条特征,则可以考虑用贪心法或动态规划法。

第四条: 特征涉及到分治法的效率,如果各子问题是不独立的则分治法要做许多不必要的工作,重复地解公共的子问题,此时虽然可用分治法,但一般用动态规划法较好

经典问题:

二分查找

棋盘覆盖

汉诺塔问题

归并排序/合并排序

快速排序

宽度优先搜索(BFS)

它并不考虑结果的可能位置,彻底地搜索整张图,直到找到结果为止。

基本过程,BFS 是从根节点开始,沿着树(图)的宽度遍历树(图)的节点。如果所有节点均被访问,则算法中止。一般用队列数据结构来辅助实现 BFS 算法。

广度优先搜索原理与实践 - huansky - 博客园 (cnblogs.com)

private Map<String, Boolean> status = new HashMap<String, Boolean>();
    private Queue<String> queue = new LinkedList<String>();
    public void BFSSearch(String startPoint) {
        //1.把起始点放入queue;
        queue.add(startPoint);
        status.put(startPoint, false);
        bfsLoop();
    }
    
    private void bfsLoop() {
        while(!queue.isEmpty()) {
            //  1) 从queue中取出队列头的点;更新状态为已经遍历。
            String currentQueueHeader = queue.poll(); //出队
            status.put(currentQueueHeader, true);
            System.out.println(currentQueueHeader);
            //  2) 找出与此点邻接的且尚未遍历的点,进行标记,然后全部放入queue中。
            List<String> neighborPoints = graph.get(currentQueueHeader);
            for (String poinit : neighborPoints) {
                if (!status.getOrDefault(poinit, false)) { //未被遍历
                    if (queue.contains(poinit)) continue;
                    queue.add(poinit);
                    status.put(poinit, false);
                }
            }
        }
    }
拓扑排序法
  • 拓扑排序指的是将有向无环(又称“DAG”图)中的顶点按照图中指定的先后顺序进行排序。

算法:拓扑排序 - 子烁爱学习 - 博客园 (cnblogs.com)

拓扑排序算法及C语言实现 (biancheng.net)

深度优先搜索/回溯法 (DFS)

深度优先搜索(缩写DFS)有点类似广度优先搜索,也是对一个连通图进行遍历的算法。它的思想是从一个顶点V0开始,沿着一条路一直走到底,如果发现不能到达目标解,那就返回到上一个节点,然后从另一条路开始走到底,这种尽量往深处走的概念即是深度优先的概念。

/**
 * DFS核心伪代码
 * 前置条件是visit数组全部设置成false
 * @param n 当前开始搜索的节点
 * @param d 当前到达的深度,也即是路径长度
 * @return 是否有解
 */
bool DFS(Node n, int d){
    if (isEnd(n, d)){//路径长度为返回true,表示此次搜索有解
        return true;
    }
 
    for (Node nextNode in n){//遍历跟节点n相邻的节点nextNode,
        if (!visit[nextNode]){//未访问过的节点才能继续搜索
 
            //例如搜索到V1了,那么V1要设置成已访问
            visit[nextNode] = true;
 
            //接下来要从V1开始继续访问了,路径长度当然要加
 
            if (DFS(nextNode, d+1)){//如果搜索出有解
                //例如到了V6,找到解了,你必须一层一层递归的告诉上层已经找到解
                return true;
            }
 
            //重新设置成未访问,因为它有可能出现在下一次搜索的别的路径中
            visit[nextNode] = false;
 
        }
        //到这里,发现本次搜索还没找到解,那就要从当前节点的下一个节点开始搜索。
    }
    return false;//本次搜索无解
}
动态规划

它将问题分成小问题,并先着手解决这些小问题

背包型DP

假设有三种商品,分别是小汽车1吨,卡车3吨,起重机4吨.价格分别是:3000,4000,6000。

现在只能卖总共4吨的商品,怎么卖商品售价最高?

所有的组合为:

组合 价值
0
小汽车 3000
卡车 4000
起重机 6000
小汽车和起重机 重量过大
小汽车和卡车 7000
卡车和起重机 重量过大
小汽车,卡车,起重机 重量过大

可以看到随着商品种类增加组合也在飞速增长,时间复杂度为O(2^n),太慢了

动态规划算法可以用网格描述

图片失效

填充这些表格最后就是背包问题的解法。

当在小汽车一行时,其他种类的商品选不了,所以都是3000

图片失效

当加入卡车时1-2吨放不下3吨的卡车,3吨可以放下卡车,所以为4000,4吨可以放下一个卡车和一个小汽车,共7000

图片失效

加入起重机时,1-2背包只能装小汽车,3吨可以装一辆卡车,4吨可以装起重机,但是价格低于7000所以不选起重机的价格,定位卡车加小汽车,价格为7000.

图片失效

坐标型DP

又被称为网格型动态规划

一个网格有m行n列,一个小动物从(0, 0)出发,每一步可以向下或向右走一步,最总到达(m - 1, n - 1)处

  • 最简单的动态规划类型
  • 给定一个序列或者网格
  • 需要找到序列中某个/些子序列或网格中的某条路径
    • 某种性质最大/最小
    • 计数
    • 存在性
  • 动态规划方程 f[i] 中的下标i表示以ai为结尾的满足条件的子序列的性质,f[i][j] 中下标 i , j 表示以格子( i , j )为结尾的满足条件的路径的性质
    • 最大值/最小值
    • 个数
    • 存在性
  • 坐标型动态规划的初始条件f[0]就是指以a0为结尾的子序列的性质

D2-坐标型动态规划

class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        if(obstacleGrid==null||obstacleGrid.length==0){
            return 0;
        }
        int [] [] dp = new int[obstacleGrid.length][obstacleGrid[0].length];
        for(int i=0;i<obstacleGrid.length;i++){
            for(int j=0;j<obstacleGrid[i].length;j++){
                if(obstacleGrid[i][j]==1){//有障碍
                    dp[i][j]=0;
                    continue;
                }else{
                    if(i == 0 && j == 0) dp[i][j]=1;//没有障碍起点
                    else if(i==0) dp[i][j]=dp[i][j-1];//在第一行,上一题在没有障碍情况下默认是1,但是有障碍就取决于该行前面是否有障碍了
                    else if(j==0) dp[i][j]=dp[i-1][j];//第一列,没有障碍默认是1,有障碍就取决于该列前面是否有障碍了
                    else{
                         dp[i][j]=dp[i-1][j]+dp[i][j-1];
                    }
                }
                
            }
        }
        return dp[obstacleGrid.length-1][obstacleGrid[0].length-1];
    }
}
序列型DP

D3-序列型动态规划

双序列型DP

D7-双序列型动态规划

划分型DP

常见类型:

1. 给定长度为N的序列或字符串要求划分成若干段
   - 段数不限或指定K段
   - 每一段满足一定的性质
2. 做法
   - 类似于序列型动态规划但是通常要加上段数信息
   - 一般用`f[i][j]`记录前i个元素元素0~i-1分成 j 段的性质如最小代价

D4 划分型动态规划

记忆化搜索

记忆化搜索 - OI Wiki (oi-wiki.org)

区间型DP

D6-区间型动态规划

状态压缩DP

状压 DP - OI Wiki (oi-wiki.org)

博弈型DP

D4-博弈型动态规划

匹配型DP
数位 DP

数位 DP - OI Wiki (oi-wiki.org)

树形DP

树形 DP - OI Wiki (oi-wiki.org)

插头 DP

插头 DP - OI Wiki (oi-wiki.org)

概率 DP

概率 DP - OI Wiki (oi-wiki.org)

动态 DP

动态 DP - OI Wiki (oi-wiki.org)

排序算法

排序简介 - OI Wiki (oi-wiki.org)

外排序算法

排序之外部排序 - Judy518 - 博客园 (cnblogs.com)

快速排序算法

快速排序 - OI Wiki (oi-wiki.org)

欧拉路径

什么是欧拉路径?欧拉路径就是一条能够不重不漏地经过图上的每一条边的路径,即小学奥数中的一笔画问题。而若这条路径的起点和终点相同,则将这条路径称为欧拉回路。

欧拉路径详解 - TEoS - 博客园 (cnblogs.com)

模拟法

模拟算法(C++)_1只小弛的博客-CSDN博客_c++模拟

C++算法:模拟 - 无咕 - 博客园 (cnblogs.com)

扫描线算法

扫描线是一种用于求矩阵面积并或者周长并的算法,可以使用 线段树 来优化。假设给定了平面上若干个可能相交的矩阵,需要求出它们的面积并(面积之和减去相交部分)或者周长并(外轮廓的长度)。我们可以虚拟出一条按顺序扫描整个平面的线段,通过对平行或垂直于 [Math Processing Error]x 轴的线段进行处理得到答案。

扫描线 - Ling_Lover - 博客园 (cnblogs.com)

枚举法

枚举法 - Huise.J - 博客园 (cnblogs.com)

最短路径

看完就懂了!一篇搞定图论最短路径问题 - thousfeet - 博客园 (cnblogs.com)

贪心法

贪心算法原理及其应用 - vcjmhg - 博客园 (cnblogs.com)

最小生成树

最小生成树 - SeanOcean - 博客园 (cnblogs.com)

狄克斯特拉算法

Dijkstra算法(一)之 C语言详解 - 如果天空不死 - 博客园 (cnblogs.com)

近似算法

算法课堂笔记6—近似算法 - f91og - 博客园 (cnblogs.com)

高精度计算

高精度计算

LRU算法

缓存淘汰算法

长期不被使用的数据,在未来被用到的几率也不大。如果缓存到达了预设值就要删除一些内容,给新的内容腾位置

如何实现LRU算法? - murphy_gb - 博客园 (cnblogs.com)

A星寻路算法

A星寻路算法 - szmtjs10 - 博客园 (cnblogs.com)

A星寻路算法介绍 - 莫水千流 - 博客园 (cnblogs.com)

A-Star(A*)寻路算法原理与实现 - 知乎 (zhihu.com)