常用算法的知识与模板

位运算

& 位操作运算

  • X & 1 == 1 :判断是否是奇数(偶数)
  • X & = (X - 1) :将最低位(LSB)的 1 清零
  • X & -X :得到最低位(LSB)的
  • 1 X & ~X = 0

异或运算

  • x ^ 0 = x
  • x ^ x = 0
  • x ^ 11111……1111 = ~x 取反
  • x ^ (~x) = 11111……1111
  • a ^ b = c => a ^ c = b => b ^ c = a (交换律)
  • a ^ b ^ c = a ^ (b ^ c) = (a ^ b)^ c (结合律)

将特殊位置放 0 或 1

  • x & ( ~0 << n ) : 将 x 最右边的 n 位清零
  • (x >> n) & 1 :获取 x 的第 n 位值(0 或者 1),
  • x & (1 << (n - 1)) :获取 x 的第 n 位的幂值,
  • x | (1 << n) :仅将第 n 位置为 1,
  • x & (~(1 << n)):仅将第 n 位置为 0,
  • x & ((1 << n) - 1) : 将 x 最⾼位⾄第 n 位(含)清零,
  • x & (~((1 << (n + 1)) - 1)):将第 n 位⾄第 0 位(含)清零。

bitmap位图

位图大多用于节省大量的内存空间,java里一个int占的是32个字节,但一个byte只需要1个字节,在做一些记忆化存储的时候特别香。最经典的例子莫过于在10亿个数字里找出不重复的数字。

一些备用的知识:

  • ASCII值只有256个
  • char可以直接等于数值来存储到bitmap里:int index = s.charAt(1)

通用模板

public class BitClass {
 
    public static void main(String[] args) {
        // 参考系都会有一个稳定的数量
        int len = 1000000;
        // 初始化bitmap的大小,一般len+1
        byte[] bitmap = new byte[len + 1];
        
        
        for(int i = 0;i<len;i++) {
            // 如果该下标为1, 则直接处理最终的逻辑
            if(byte[i] == 1) {
                // return xx;   
            }
            // 如果符合逻辑条件,则该下标为1
            if(true) {
                byte[i] = 1;
            }
        }
    }
   
}

二分搜索

需要注意的三点:

  1. 循环退出条件,注意是 left <= right,⽽不是 left < right。
  2. mid 的取值,mid = left + (right-left)>>1 ,注意如果是 (left+right)>>1会有越界的可能。
  3. left 和 right 的更新。left = mid + 1,right = mid - 1。

通用模板

int search(int[] nums, int target, int left, int right) {
    // 1、制定循环规则
	while (left <= right) {
        // 2、求左右区间的中间值
		int mid = left + (right-left)>>1;
		
        // 3、中间值是否为目标
		if (target == nums[mid]){
            return mid;
        }
        
        // 4、判断目标在哪一区间
		if (target < nums[mid]) {
			right = mid - 1;
		} else {
			left = mid + 1;
			
		}
	}
	// 没有符合的值
	return -1;
}

递归

「递」的意思是将问题拆解成子问题来解决, 子问题再拆解成子子问题,…,直到被拆解的子问题可以立即求出解便无需再次拆解。
「归」是说从最小的子问题开始解决,一直往上一层子问题开始倒退解决,…,直到最开始的问题解决。
下图以阶乘 f(6) 为例来看下它的「递」和「归」。
在这里插入图片描述

递归的注意点:

  1. 明确递归函数的功能,确定合不合适递归。
  2. 递归结束的条件,就是能直接得到“递”到最小问题的解的结束条件。
  3. 找出函数的等价关系式,或者说是除了最小问题的节点的与它下个节点的等价关系式,比如 f(n)=n * f(n-1)。

通用模板

public int recursionSearch(int n) {
    // 最小问题的结束条件(可能有多个)
    if (n == 1) {
        return 1;
    }
    if (n == 2) {
        return 2;
    }

    // 等价关系式
    return n * recursionSearch(n-1);
}

动态规划

动态规划 = 递归 + 已算出解的记忆化存储,以最经典的斐波那契数列作为例子,它的公式为:

  • f(0) = 0
  • f(1) = 1
  • f(n) = f(n-1) + f(n-2) n>1

如果我们以递归的方式去解决,举个节点为6的等价关系式f(6) = f(5) + f(4),那这时候我们就得分别计算 f(5)跟 f(4)的结果了,很明显计算下个节点的时候f(5) = f(4) + f(3),相当于计算了2次f(4),可想而知数量多了起来的话,这些重复计算的数量是成几何倍的速率增加的。
那如果我们在递归的过程中,对已经算过的解进行存储的话,那岂不是可以用少量的空间去替换大量的计算量,那怎么去计算跟存储呢?
反推即可,也就是我们从最小问题开始反推,比如f(2) = f(0) + f(1) ——> f(3) = f(1) + f(2) …,这样的话我们就可以把每个节点计算的结果都进行存储,直到计算到最开始的问题。
所以又可以简单地总结一下:动态规划 = 递归 + 已算出解的记忆化存储 = 反推 + 记忆化 ,他有三个关键点:

  1. 记忆化的存储方式。
  2. 最小问题的解的直接存储。
  3. 反推公式:dp[n] = func(dp[n-1],dp[n-2]…)

通用模板

public int dpSearch(int n) {
    // 最小问题的可直接返回
    if (n == 0) {
        return 0;   
    }
    if (n == 1) {
        return 1;   
    }
    
    // 记忆化的存储方式。
    int[] dpArr = new int[n+1];
    
    // 最小问题的解的直接存储。
    dpArr[0] = 0;
    dpArr[1] = 1;
    
    // 开始反推,代入反推公式
    for(int i=2;i<=n;i++) {
        dpArr[i] = dpArr[i - 1] + dpArr[i - 2];
    }
    
    return dpArr[n];
    
}

BFS

广度优先搜索,理解它的搜索方式最重要。他是从第一层,一层层查到最后一层,如下图。
在这里插入图片描述

那么很明显,在遍历过程中,利用先入先出的队列性质,按照左到右的规则入队出队即可达到这种一层层的查找了。
拿上面的图示写个例子,假设我要寻找等于F的节点,则步骤为:

  • 建立一个队列,把根节点加到队列里,队列用【】代表,红色代表出列,所以现在的状态为:【A】
  • 把队列首元素A弹出,因为A不等于F,则把A的子节点BCD加到队列里:【A、B、C、D】
  • 把队列首元素B弹出,因为B不等于F,则把B的子节点E加到队列里:【B、C、D、E】
  • 把队列首元素C弹出,因为C不等于F,则把C的子节点FG加到队列里:【C、D、E、F、G】
  • 把队列首元素D弹出,因为D不等于F,则把D的子节点(无就不加)加到队列里:【D、E、F、G】
  • 把队列首元素E弹出,因为E不等于F,则把E的子节点H加到队列里:【E、F、G、H】
  • 把队列首元素F弹出,因为F等于F,所以返回此节点。

通用模板

class Node {
    public int value;
    public List<Node> children;	
}
public Node bfs(Node root, int target) {
    // 1、根节点的判空
    if (root == null) {
        return null;
    }
    
    // 2、建立队列,并把根节点加进去
    Queue<Node> queue = new LinkedList<>();
    queue.add(root);

    // 3、只要队列不为空便一直遍历
    while (!queue.isEmpty()) {
        
        // 4、弹出队列的首元素
        Node node = queue.poll();
        // 5、判断弹出的元素,也就是遍历到的元素是否是寻找目标,可直接结束寻找
        if (node.value == target) {
            return node;   
        }
        
        // 6、如果不是,则把自己的子节点按顺序加到队列里
        if (node.children != null) {
            for(Node childNode: node.children){
                 if (childNode != null) {
                    queue.add(childNode);
                 }
            }
        }
    }
    return null;
}

DFS

深度优先搜索,理解它的搜索方式最重要,从树的根节点 开始,沿着一个分支一直查到底,然后从这个分支的叶节点回退到上一个节点,再从另一个分支开始走到底…,不断重复此过程,直到查到目标元素或者所有的节点都遍历完成。它的特点是不撞南墙不回头,先笔直查完一个分支,再换一个分支继续查,如下图:
在这里插入图片描述

那么很明显,在遍历过程中,利用先入后出的栈性质,按照制定的规则入栈出栈即可达到这种深层式的查找了。
拿上面的图示写个例子,假设我要寻找等于F的节点,则步骤为:

  • 建立一个栈,把根节点加到栈里,栈用【】代表,红色代表出栈,所以现在的状态为:【A】
  • 把栈顶元素A弹出,因为A不等于F,则把A的子节点BCD加到栈里:【A、B、C、D】
  • 把栈顶元素B弹出,因为B不等于F,则把B的子节点E加到栈里:【B、E、C、D】
  • 把栈顶元素E弹出,因为E不等于F,则把E的子节点H加到栈里:【E、H、C、D】
  • 把栈顶元素H弹出,因为H不等于F,则把H的子节点(无就不加)加到栈里:【H、C、D】
  • 把栈顶元素C弹出,因为C不等于F,则把C的子节点FG加到栈里:【C、F、G、D】
  • 把栈顶元素F弹出,因为F等于F,所以返回此节点。

注意的是,dfs也可以使用递归去实现的!递归的例子就不举了,在通用模板里写出。

通用模板

栈形式:

class Node {
    public int value;
    public List<Node> children;	
}
public Node bfs(Node root, int target) {
    // 1、根节点的判空
    if (root == null) {
        return null;
    }
    
    // 2、建立栈,并把根节点加进去
    Stack<Node> stack = new Stack<>();
    stack.push(root);

    // 3、只要栈不为空便一直遍历
    while (!stack.isEmpty()) {
        
        // 4、弹出栈的首元素
        Node node = stack.pop();
        // 5、判断弹出的元素,也就是遍历到的元素是否是寻找目标,可直接结束寻找
        if (node.value == target) {
            return node;   
        }
        
        // 6、如果不是,则把自己的子节点按倒序加到栈里
        if (node.children != null) {
            for(int i = node.children.size()-1; i>=0; i--){
                 Node childNode = node.children[i];
                 if (childNode != null) {
                    stack.push(node);
                 }
            }
        }
    }
    return null;
}

递归形式:

class Node {
    public int value;
    public List<Node> children;	
}
public Node dfs(Node node, int target) {
    // 1、当前节点的判空
    if (root == null) {
        return null;
    }
    
    // 2、判断当前节点是否与目标一致,直接返回
    if (node.value == target) {
         return node;   
    }
    
     // 3、如果不是,则按顺序递归自己的子节点
     if (node.children != null) {
         for(Node childNode: node.children){
             if (dfs(childNode, target) != null) {
                 return childNode;
             }
         }
     }
    return null;
}

滑动窗口

滑动窗口使用双指针解决问题,所以一般也叫双指针算法,因为两个指针间形成一个窗口,一般步骤为:

  1. 使用双指针中的左右指针技巧,初始化 left = right = 0,把索引闭区间 [left, right] 称为一个「窗口」。
  2. 先不断地增加 right 指针扩大到窗口初始化的大小。
  3. 根据需求,不断对left或者right去递增达到窗口移动的效果,进行逻辑处理。
  4. 直到 right 到达尽头。

滑动窗口本质是双指针的玩法,不同题目有不同的套路,从最简单的按照规律包夹,到快慢指针,再到无固定套路的因题而异的特殊算法。
其实按照规律包夹的套路属于碰撞指针范畴,一般对于排序好的数组,可以一步一步判断,或者用二分法判断,总之不用根据整体遍历来判断,效率自然高。

通用模板

class Solution {
    public void windows(String s) {
        // 初始化滑动窗口的左右指针
        int left = 0;
        int right = 0;
        
        // 目标的长度
        int len = s.length();
        
        // 左右指针都要小于长度
        while (left<len && right<len) {
           
            // 如果当前的值没在窗口内,则处理相关逻辑,并且扩大窗口,右指针右移一位
            if(true) {
                // 处理相关逻辑
                right++;
            } else {
               // 否则就缩小窗口,左指针右移一位
                left++;
            }
        }
    }
}

指针

通过设置多个指针进行移动来求出解,一般情况下都要求数组是有序的。
双指针的话,就是在left<right, right<arr.length的情况下,不断对left或right指针进行递增来处理逻辑,时间复杂度为O(n)。
三指针的话,相当于遍历第一个指针+双指针,也就是执行n次双指针来处理逻辑,时间复杂度为O(nn)。
以此类推,n指针的话,相当于执行n-2次双指针来处理逻辑,时间复杂度为O(n
n(n-2次方))。

回溯与剪枝

● 回溯:一旦遇到不符合规则的节点,或者是已经到达最深处,则跳回上个节点继续遍历下种可能,比较拗口。
● 剪枝:跳过不符合规则的节点,不对它进行深度查询。
回溯与剪枝是相辅相成的,回溯的遍历方式就是DFS。下面举个例子来理解,给出三个数字123,然后求出所有组成2位数的个位十位不重复的结果。我们根据DFS的方式去遍历下图:
在这里插入图片描述

● 从1开始,记录下当前结果 [1],发现不是叶节点,则继续遍历子节点,遍历完毕,然后把当前结果删掉尾数变成[]
○ 遍历到1,发现当前结果存在1,则跳过。
○ 遍历到2,发现当前结果不存在2,记录下当前结果 [1,2],发现是叶节点,把当前结果加到全局结果中,然后把当前结果删掉尾数变成[1]
○ 遍历到3,发现当前结果不存在3,记录下当前结果 [1,3],发现是叶节点,把当前结果加到全局结果中,然后把当前结果删掉尾数变成[1]
● 从2开始,记录下当前结果 [2],发现不是叶节点,则继续遍历子节点,遍历完毕,然后把当前结果删掉尾数变成[]
○ 遍历到1,发现当前结果不存在1,记录下当前结果 [2,1],发现是叶节点,把当前结果加到全局结果中,然后把当前结果删掉尾数变成[2]
○ 遍历到2,发现当前结果存在2,则跳过。
○ 遍历到3,发现当前结果不存在3,记录下当前结果 [2,3],发现是叶节点,把当前结果加到全局结果中,然后把当前结果删掉尾数变成[2]
● 从3开始,记录下当前结果 [3],发现不是叶节点,则继续遍历子节点,遍历完毕,然后把当前结果删掉尾数变成[]
○ 遍历到1,发现当前结果不存在1,记录下当前结果 [3,1],发现是叶节点,把当前结果加到全局结果中,然后把当前结果删掉尾数变成[3]
○ 遍历到1,发现当前结果不存在2,记录下当前结果 [3,2],发现是叶节点,把当前结果加到全局结果中,然后把当前结果删掉尾数变成[3]
○ 遍历到2,发现当前结果存在3,则跳过。

堆(heap)本质上就是一棵完全二叉树,它的每一个结点都必须大于等于或者小于等于其子节点。 通常,我们将结点都大于或者等于子树所有结点的堆称为最大堆 ;将结点都小于或者等于子树所有结点的堆称为最小堆 。
一般都是以数组来存储堆的数据的,如下图:
在这里插入图片描述

父节点与子节点的关系:
● 左子节点 (leftIndex-1) / 2= fatherIndex
● 左子节点 (leftIndex-2) / 2= fatherIndex
根据上面公式自然也能从父节点推算出2个子节点。

向堆中插入元素,也就是向数组尾部加入新元素,然后再调整新元素的位置,需要向上查找,找到它的父节点,经过对比满足交换条件则交换两个节点,重复该过程直到每个节点都满足堆的性质为止,这个过程我们称之为上浮操作

而堆中取出堆顶节点,如果我们直接将下标为0的节点删除,然后后面的节点都往前挪一位,成本是很高的。所以这里采用了头节点与尾节点进行交换,这样堆顶的节点就到了尾部,我们再删除尾部即可取出,然后再把换到头部的节点,尾结点放入堆顶后,然后与它的子节点比较,经过对比满足交换条件则交换两个节点,交换后继续向下与子节点比较,直到当前结点的值与子节点对比不需要进行交换时,堆化就完成了,这个过程我们称之为下沉操作

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我叫小八

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值