目录
1、二分查找
需求:在一个有序数组中,快速查询某一个值。时间复杂度O(logN),空间复杂度O(1)。
举个例子:
int[] arr = {1, 2, 2, 2, 4, 5};
int target = 2;
以下二分查找的写法就是返回 >=2 的最左位置的下标。也就是返回 1下标。
切记:在数组中查询不到target时,返回的是无效的下标,上层调用时记得加判断。
新的需求:请返回 >=target 中的最右的下标。就只需调用 >= target + 1的函数,然后返回的下标再-1即可。当然上层调用时,还是需要判断返回的下标值是否合法。
二分查找常见的三种写法,注意区分各自的不同之处。
// 1、闭区间写法。返回 >=target 的最左位置的下标
private static int lowerBound1(int[] arr, int target) {
int l = 0;
int r = arr.length - 1;
while (l <= r) { // 闭区间
int mid = (l + r) >> 1;
if (arr[mid] < target) { // [mid + 1, right]
l = mid + 1;
} else { // [left, mid - 1]
r = mid - 1;
}
}
// 循环停止条件:l = r + 1。返回其一即可
return l;
}
// 2、开区间写法。返回 >=target 的最左位置的下标
private static int lowerBound2(int[] arr, int target) {
int l = -1;
int r = arr.length;
while (l + 1 < r) { // 开区间
int mid = (l + r) >> 1;
if (arr[mid] < target) { // (mid, right)
l = mid;
} else { // (left, mid)
r = mid;
}
}
return r; // 循环停止条件:left+1=right,返回其一即可
}
// 3、左闭右开区间写法。返回 >=target 的最左位置的下标
private static int lowerBound3(int[] arr, int target) {
int l = 0;
int r = arr.length;
while (l < r) { // 左闭右开
int mid = (l + r) >> 1;
if (arr[mid] < target) { // [mid + 1, right)
l = mid + 1;
} else { // [left, mid)
r = mid;
}
}
// 循环停止条件:l == r。返回其一即可
return r;
}
2、线段树
需求:为了快速的对数组某一段连续的区间进行增删改查操作。时间复杂度O(logN),空间复杂度O(N)。
关键字:范围更新。
写法并不统一,这里的写法是 5个数组搭配。(有的写法是4个数组,省去update数组。在change数组上使用Integer类型,若某个位置的元素 == null,说明是没有修改的情况,这里就不多赘述)。
切记:为了方便计算,线段树中的tree数组,是用于存储原数组的数据,但这里的tree数组0下标的空间省去不用,从1下标位置开始存储的。并且为了出现一些例外的情况,导致在后续递归调用时,会出现数组越界异常,所以change、lazy、sum、update这四个数组的存储空间要 开辟 (tree.length * 4)倍的长度。
下文代码的查询操作(query)写的是 某个区间的累加和。也可根据题目意思更改query的代码,将sum数组改成其他含义的数组表示,例如如下题目:
LeetCode练习题。这道题就是计算 某个区间的最大高度,将sum数组改写成hight数组即可。
private static class SegmentTree {
private int[] tree; // 从下标1位置开始填入
private int[] change; // 存储修改的值
private int[] lazy; // 懒更新数组
private int[] sum; // 存储某个范围内的数据总和,根据需求而定。
private boolean[] update; // 记录相应下标位置是否需要进行更新
private int length;
public SegmentTree(int[] arr) {
this.length = arr.length + 1;
tree = new int[length];
for (int i = 1; i < length; i++) { // 将数据填充到tree中
this.tree[i] = arr[i - 1];
}
change = new int[length << 2];
lazy = new int[length << 2];
sum = new int[length << 2];
update = new boolean[length << 2];
}
// 对sum数组进行初始化,也就是计算出相应区间的总和
public void build(int l, int r, int rt) {
if (l == r) {
sum[rt] = tree[l];
return;
}
int mid = (r + l) / 2;
build(l, mid, rt << 1); // 递归左子树
build(mid + 1, r, rt << 1 | 1); // 递归右子树
pushUp(rt); // 两边汇总
}
/**
* 在L和R范围内,添加某个数
* @param L 需要修改数据的范围的左边界
* @param R 需要修改数据的范围的右边界
* @param l 当前递归的左边界
* @param r 当前递归的右边界
* @param num 添加的值
* @param rt lazy数组的下标(树的根节点)
*/
public void add(int L, int R, int num, int l, int r, int rt) {
if (L <= l && R >= r) { // 当前递归范围,超出了修改数据的范围,可以懒
sum[rt] += (r - l + 1) * num; // 总和
lazy[rt] += num;
return;
}
// 不能懒的情况,取中位数进行递归
int mid = (r + l) / 2;
// 先将上次lazy数组留下的数据向下分发之后,再进行调用
// mid - l + 1是左子树的节点数
// r - mid 是右子树的节点数
pushDown(rt, mid - l + 1, r - mid);
if (L <= mid) { // 递归左子树
add(L, R, num, l, mid, rt << 1);
}
if (R > mid) { // 递归右子树
add(L, R, num, mid + 1, r, rt << 1 | 1);
}
pushUp(rt); // 等左右子树递归完,再做汇总
}
/**
* L、R范围内更新值
* @param L 待更新范围左边界(固定值)
* @param R 待更新范围右边界(固定值)
* @param num 更新值
* @param l 当前递归的左边界
* @param r 当前递归的右边界
* @param rt change数组的下标(树的根节点)
*/
public void update(int L, int R, int num, int l, int r, int rt) {
if (L <= l && R >= r) { // 当前递归范围超过了待更新的范围
update[rt] = true;
change[rt] = num;
sum[rt] = (r - l + 1) * num; // 重新计算sum
lazy[rt] = 0; // lazy数组对应的位置要归0
return;
}
// 没有懒到,取中位数,往下递归
int mid = l + ((r - l) >> 1);
// 先往下分发数据,然后才是递归调用
pushDown(rt, mid - l + 1, r - mid);
if (L <= mid) {
update(L, R, num, l, mid, rt << 1);
}
if (R > mid) {
update(L, R, num, mid + 1, r, rt << 1 | 1);
}
pushUp(rt); // 汇总数据
}
// 查询L和R范围内的sum总和
public long query(int L, int R, int l, int r, int rt) {
if (L <= l && R >= r) {
return sum[rt];
}
int mid = l + ((r - l) >> 1);
pushDown(rt, mid - l + 1, r - mid); // 往下分发
long ans = 0;
if (L <= mid) {
ans += query(L, R, l, mid, rt << 1);
}
if (R > mid) {
ans += query(L, R, mid + 1, r, rt << 1 | 1);
}
return ans;
}
// lazy数组向下分发数据
private void pushDown(int rt, int leftChildSum, int rightChildSum) {
// 用于add方法
if (lazy[rt] != 0) { // 懒数组的数据不为0,说明要往下分发
sum[rt << 1] += lazy[rt] * leftChildSum; //左子树的总和
sum[rt << 1 | 1] += lazy[rt] * rightChildSum; // 右子树的总和
// 更新左右子树的lazy数组
lazy[rt << 1] += lazy[rt];
lazy[rt << 1 | 1] += lazy[rt];
lazy[rt] = 0; // 当然位置的lazy值归0
}
// 用于update方法
if (update[rt]) { // 是否需要更新的情况
// 标志update数组,表示需要更新
update[rt << 1] = true;
update[rt << 1 | 1] = true;
// 更新左右子树的change值
change[rt << 1] = change[rt];
change[rt << 1 | 1] = change[rt];
// 更新左右子树的sum总和
sum[rt << 1] = change[rt] * leftChildSum;
sum[rt << 1 | 1] = change[rt] * rightChildSum;
// 左右子树的lazy数组都需要归0
lazy[rt << 1] = 0;
lazy[rt << 1 | 1] = 0;
update[rt] = false; // 当前位置的数据分发完了,就改回false
}
}
// 汇总数据
private void pushUp(int rt) {
sum[rt] = sum[rt << 1] + sum[rt << 1 | 1]; // 将左右子树的数据进行汇总
}
}
3、树状数组
需求:会频繁的更新数组中某一个位置的数据,但又需要快速的计算某个区间的累加和问题。时间复杂度O(logN),空间复杂度O(N)。
树状数组,也称为IndexTree,算是线段树的另一种形式。也是实现数组区间内的快速增删改查。与线段树的区别是 能够实现单点更新,比线段树更轻量化
。还有一个好处就是,可以很轻易的改写成二维的形式。
关键词:单点更新,快速计算某一个段区间的累加和。
IndexTree有三个函数,add、update、query。
query查询的是 0 ~ index位置的累加和。
比如要查询 3 ~ 5位置的累加和问题,就能转换为 求 0 ~ 5的累加和 减去 0~2的累计和。
// 一维。上层调用时的下标,还是从0开始。只是进入IndexTree后,自己手动+1
public class IndexTree {
public int[] nums; // 原数组
public int[] tree; // 累加和数组
public int length; // 0下标的空间省去不用
public IndexTree(int N) {
this.length = N + 1;
tree = new int[this.length];
nums = new int[this.length];
}
/**
* 在index位置插入val值。index从1开始
* @param val 待插入的值
* @param index 数组下标
*/
public void add(int val, int index) {
index += 1;
nums[index] += val;
for (int i = index; i < length; i += (i & -i)) { // index位置插入值,会影响后面位置的计算
tree[i] += val;
}
}
/**
* 更新index位置的值
* @param val 更新的值
* @param index 下标
*/
public void update(int val, int index) {
index += 1;
int num = val - nums[index]; // 差值
nums[index] = val;
for (int i = index; i < length; i += (i & -i)) {
tree[i] += num; // 累加上 差值
}
}
/**
* 返回1下标~index下标的累加和
* @param index 下标
* @return 返回累加和
*/
public int query(int index) {
index += 1;
int ans = 0;
for (int i = index; i > 0; i -= (i & -i)) {
ans += tree[i];
}
return ans;
}
}
// 二维。上层调用时,还是从下标0开始,进入IndexTree后,下标自动+1
public class Code02_IndexTree2D {
private int[][] nums;
private int[][] tree;
private int N; // 行数
private int M; // 列数
public Code02_IndexTree2D(int[][] matrix) {
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
return;
}
N = matrix.length + 1;
M = matrix[0].length + 1;
nums = new int[N][M];
tree = new int[N][M];
for (int i = 0; i < N - 1; i++) {
for (int j = 0; j < M - 1; j++) {
update(matrix[i][j], i, j);
}
}
}
// row,col位置 更新值 val。 row,col的范围在 0~N-1,或者0~M-1
public void update(int val, int row, int col) {
if (N == 0 || M == 0 || row < 0 || col < 0 || row > N - 1 || col > M - 1) {
return;
}
row += 1;
col += 1;
int num = val - nums[row][col]; // 差值
nums[row][col] = val;
for (int i = row; i < N; i++) {
for (int j = col; j < M; j++) {
tree[i][j] += num;
}
}
}
// 返回 row,col 到左上角的矩形的累加和
private int sum(int row, int col) {
if (row < 0 || col < 0 || row > N - 1 || col > M - 1) {
return 0;
}
row += 1;
col += 1;
int ans = 0;
for (int i = row; i > 0; i -= (i & -i)) {
for (int j = col; j > 0; j -= (j & -j)) {
ans += tree[i][j];
}
}
return ans;
}
/**
* @param row1 左上角
* @param col1 左上角
* @param row2 右下角
* @param col2 右下角
* @return 返回左上角 到 右下角 围成的矩形的累加和
*/
public int sumRegion(int row1, int col1, int row2, int col2) {
if (N == 0 || M == 0) {
return 0;
}
return sum(row2, col2) - sum(row2, col1 - 1) - sum(row1 - 1, col2) + sum(row1 - 1, col1 - 1);
}
}
4、差分数组
需求:快速对数组的某一段连续区间进行加减法操作。时间复杂度O(N),空间复杂度O(N)。
/**
* 差分数组
* @param arr 原数组
* @param option 操作数组,有3个参数。
*/
private static void fastUpdateOfArray(int[] arr, int[][] option) {
/*
option数组有三个参数:
option[i][0] = 带更新范围的左边界
option[i][1] = 带更新范围的右边界
option[i][2] = 新值
*/
int N = arr.length;
// 1、由原数组 反推 差分数组
int[] diff = new int[N + 1]; // 多开一个位置的空间
diff[0] = arr[0];
for (int i = 1; i < N; i++) {
diff[i] = arr[i] - arr[i - 1];
}
// 2、将新值 更新到差分数组
for (int[] pos : option) {
int left = pos[0]; // 左边界
int right = pos[1]; // 右边界
int val = pos[2]; // 新值
diff[left] += val; // 左边界 + val
diff[right + 1] -= val; // 右边界的后一个位置 -val
}
// 3、再对diff数组求前缀和数组,就是更新过后的arr数组的值
arr[0] = diff[0];
for (int i = 1; i < N; i++) {
arr[i] = diff[i] + arr[i - 1];
}
}
5、前缀树
需求:给定一组字符串,将这些字符串插入前缀树中,后续可以查询某个子串,在前缀数中有多少个字符串是以这个子串开头的。
应用:后续的AC自动机,就是前缀树 + KMP写的。
public class TrieTree {
private final TrieNode root;
public TrieTree() {
// 根节点不指向任何字符,root节点的pass值就是整颗前缀数有多少字符串
root = new TrieNode();
}
private static class TrieNode {
public int pass; //途径的数量
public int end; // 某个单词的总数量
// 这里的HashMap中的键值,也可以是其他的。这里只是以大小写字母的情况写的
public HashMap<Character, TrieNode> map; //保存下一节点的地址
public TrieNode() {
map = new HashMap<>();
}
}
public void add(String word) {
if (word == null) {
return;
}
char[] array = word.toCharArray();
TrieNode node = root;
node.pass++;
for (char ch : array) {
if (!node.map.containsKey(ch)) {
node.map.put(ch, new TrieNode());
}
node = node.map.get(ch);
node.pass++;
}
node.end++;
}
public int search(String word) {
if (word == null) {
return 0;
}
char[] array = word.toCharArray();
TrieNode node = root;
for (char ch : array) {
if (!node.map.containsKey(ch)) {
return 0;
}
node = node.map.get(ch); //拿到下一节点
}
return node.end; //返回最终的end值
}
/**
* @param word 以word为前缀的字符串
* @return 返回以word为前缀的字符串的数量
*/
public int prefixNumber(String word) {
if (word == null) {
return 0;
}
char[] array = word.toCharArray();
TrieNode node = root;
for (char ch : array) {
if (!node.map.containsKey(ch)) {
return 0;
}
node = node.map.get(ch); //拿到下一节点
}
return node.pass;
}
public boolean delete(String word) {
if (word != null && search(word) != 0) {
char[] array = word.toCharArray();
TrieNode node = root;
node.pass--;
for (char ch : array) {
if (--node.map.get(ch).pass == 0) {
//pass值为0,所以从该节点一下的所有子树,都将不存在,所以直接全部回收即可
//C++ 的,需要遍历所有子树,调用析构函数
node.map.remove(ch);
return true;
}
node = node.map.get(ch);
}
node.end--;
return true;
}
return false;
}
}
6、并查集
需求:快速的判断某两个节点是否属于同一集合。时间复杂度O(1),空间复杂度O(N)。
并查集的写法有很多种,理解其思想,方可改写。还有的是使用数组来写的并查集,比如使用Integer[] 数组,若 某个位置的元素 == null,说明这个位置的元素还没有进来过。
1、初始化时,每个节点的父节点都是指向自己本身的
2、find时,要进行路径压缩。这也是时间复杂度O(1)的来源
3、sizeMap,是在union时,让“小的集合 挂在 大的集合下面”,有一定的优化效果。但大多数OJ时,这个可以不用写,一般都是能过的。
// 包装Node节点的 + sizeMap优化版本。
public class UnionSet {
private HashMap<Node, Node> fatherMap; //key表示当前这个数据,value表示这个数据的代表(父亲)是谁
private HashMap<Node, Integer> sizeMap; //表示当前这个组(集合)的大小
public UnionSet() { //构造方法
fatherMap = new HashMap<>();
sizeMap = new HashMap<>();
}
private static class Node {
public int val;
public Node next;
public Node(int val) {
this.val = val;
}
}
//初始化并查集
public void makeSet(List<Node> list) {
if (list == null) {
return;
}
fatherMap.clear();
sizeMap.clear(); //先将表清空
//遍历list,把每一个节点,都放入哈希表中
for (Node node : list) {
fatherMap.put(node, node); //第一个参数是节点本身,第二个参数就是这个组的代表
sizeMap.put(node, 1); //第一个参数是这个组的代表,第二个参数是大小
}
}
//判断是不是同一个组
public boolean isSameSet(Node node1, Node node2) {
if (node1 == null || node2 == null) {
return false;
}
return findFather(node1) == findFather(node2); //查找各自的代表节点,看是不是同一个。
}
//查找代表节点,并做路径压缩
private Node findFather(Node node) {
if (node == null) {
return null;
}
//查找代表节点
Stack<Node> path = new Stack<>(); //存储沿途的节点
while (node != fatherMap.get(node)) { //代表节点不是自己本身,就继续查找
path.push(node);
node = fatherMap.get(node);
}
//路径压缩
while (!path.isEmpty()) {
Node tmp = path.pop();
fatherMap.put(tmp, node); //此时的node,就是这个组的代表节点
}
return node;
}
//合并操作
public void union(Node node1, Node node2) {
if (node1 == null || node2 == null) {
return;
}
int node1Size = sizeMap.get(node1);
int node2Size = sizeMap.get(node2); //分别得到两个节点所在组的大小
Node node1Father = fatherMap.get(node1);
Node node2Father = fatherMap.get(node2); //分别拿到两个节点的代表节点
if (node1Father != node2Father) { //两个节点,不在同一个组,就合并
if (node1Size < node2Size) { //node1 挂在 node2
fatherMap.put(node1Father, node2Father);
sizeMap.put(node2Father, node1Size + node2Size); //新的组,大小是原来两个组的和
sizeMap.remove(node1Father); //小组的数据,就不需要了,删除
} else { //node2 挂在 node1
//跟上面操作类似
fatherMap.put(node2Father, node1Father);
sizeMap.put(node1Father, node1Size + node2Size);
sizeMap.remove(node1Father);
}
}
}
}
// 稍微简单一点的并查集写法。初始化操作就写在find函数里
// 值得注意的是,这里并没有包装Node节点,只是单纯的使用Integer
// 有的题目,有可能出现两个相同的数字,导致并查集里的索引出现错乱的情况
private class UnionSet {
private HashMap<Integer, Integer> father; // <index, fatherIndex>
public UnionSet() {
father = new HashMap<>();
}
// find要做三件事:
// 查找父亲节点、初始化第一次进来的节点、路径压缩
public int find(int index) {
Integer fa = father.get(index);
if(fa == null) { // 表示index是第一次进来,然后就初始化
father.put(index, index); // 初始化,父亲节点就是自己
return index;
}
if(fa == index) { // 如果查找出来的父亲节点就是自己,所以到头了,直接返回
return fa;
}
// 还没走到最根部的父亲节点,递归继续
fa = find(fa);
// 路径压缩
father.put(index, fa);
return fa;
}
// 合并
public void union(int index1, int index2) {
int fa1 = find(index1);
int fa2 = find(index2);
if (fa1 != fa2) {
// 在左神的讲解中,有个“小挂大”的优化,这里就没有优化
// 直接这样写,也是能过的,只是可能常数项时间有点高
father.put(fa1, fa2);
}
}
}
7、AC自动机
需求:给定一篇文章,和一组敏感词汇,问 这一篇文章中有哪些敏感词汇。
思路:先对这些敏感词汇建立一颗前缀树,然后在前缀树上写KMP。
核心要点:fail指针,其实就是对于到KMP中的那个next数组,将那边的思想搬运过来,如下的build()函数,可能就更好理解了。根节点的fail=null,根节点的下一级子节点的fail指向根节点,这一句话就对于了KMP中的next数组的初始化状态:next[0] = -1, next[1] = 0。都是为了在匹配失败的时候,往前跳转。
public class Code03_AC1 {
public static void main(String[] args) {
ACAutomation ac = new ACAutomation();
ac.insert("dhe");
ac.insert("he");
ac.insert("abcdheks");
// 设置fail指针
ac.build();
List<String> contains = ac.containWords("abcdhekskdjfafhasldkflskdjhwqaeruv");
for (String word : contains) {
System.out.println(word);
}
}
// 前缀树节点
private static class Node {
public String end; // 以当前节点结尾,这条线路的字符串
public boolean endUse; // 标记是否已经找到过这个敏感词
public Node fail; // fail指针,匹配失败时,往上找最佳的前缀字符串的开始节点
public Node[] nexts; // 下级节点,可以是数组,也可以是哈希表的形式,根据数据类型来定
public Node() {
this.end = null;
this.endUse = false;
this.fail = null;
this.nexts = new Node[26]; // 假设是26个小写字母
}
}
private static class ACAutomation {
private Node root;
public ACAutomation() {
this.root = new Node();
}
public void insert(String str) {
char[] chars = str.toCharArray();
Node cur = root;
for (int i = 0; i < chars.length; i++) {
int num = chars[i] - 'a';
if (cur.nexts[num] == null) {
cur.nexts[num] = new Node();
}
cur = cur.nexts[num];
}
cur.end = str; // 尾结点,记录这条线路的字符串
}
// 连接所有节点的fail指针 ----使用BFS
// 根节点的fail是null,根节点的直接下级节点的fail都是 指向 根节点
// fail=null和fail指向根节点,也就直接对应了KMP中next数组的前两个位置就是-1、0的情况
public void build() {
Queue<Node> queue = new LinkedList<>();
queue.add(root);
while (!queue.isEmpty()) { // BFS
Node cur = queue.poll();
// 遍历nexts数组
for (int i = 0; i < 26; i++) { // 处理他的孩子节点
if (cur.nexts[i] != null) { // 有孩子节点的情况
cur.nexts[i].fail = root; // 先指向root。后续如果有其他情况,再修改
Node curFail = cur.fail;
while (curFail != null) {
if (curFail.nexts[i] != null) { // 父节点的fail指向的节点 也有走向i位置的路,就连接
cur.nexts[i].fail = curFail.nexts[i];
break; // 连上之后,直接跳出了
}
curFail = curFail.fail; // 再往下一个fail节点跳转
}
queue.add(cur.nexts[i]); // 当前节点入队列
}
}
}
}
// 查询content文章中的敏感词
public List<String> containWords(String content) {
if (content == null || content.length() == 0) {
return new ArrayList<>();
}
Node cur = root;
int length = content.length();
List<String> ans = new ArrayList<>();
for (int i = 0; i < length; i++) {
int index = content.charAt(i) - 'a';
// 没有走向index的路,但是cur又不是根节点的情况,继续沿着fail走
while (cur.nexts[index] == null && cur != root) {
cur = cur.fail;
}
cur = cur.nexts[index] != null? cur.nexts[index] : root;
// 现在cur要么是走到了下级节点,要么就还是在root位置
// 以当前cur节点跑一遍fail指针,尝试搜集沿途的敏感词
Node follow = cur;
while (follow != root) {
if (follow.endUse) { // 说明当前节点已经搜集过敏感词了,无需再次搜集
break;
}
// 不同的需求,可修改一下代码
if(follow.end != null) {
ans.add(follow.end);
follow.endUse = true;
}
follow = follow.fail;
}
}
return ans;
}
}
}
8、Morris遍历
需求:以时间复杂度O(N), 空间复杂度O(1) 的要求,遍历二叉树。
核心要点:将某一颗左子树中,最右侧的节点的right指针,指向根节点。如下图所示:
//可以在Morris的基础之上,改写前序、中序、后序遍历
// 前序是第一来到的节点就打印
// 中序是第二次来到的节点就打印
// 后序是在第二次来到mostRight时,往上逆序打印。需要反转right指针的走向,才能做到空间O(1)
private static class TreeNode {
public int val;
public TreeNode left;
public TreeNode right;
public TreeNode(int val) {
this.val = val;
}
}
private static void morris(TreeNode node) {
if (node == null) {
return;
}
TreeNode cur = node;
while(cur != null) {
TreeNode mostRight = cur.left; // 左子树
if(mostRight != null) { // 左子树不为空
// 尽可能的往右子树走
while(mostRight.right != null && mostRight.right != cur) {
mostRight = mostRight.right;
}
// 循环停下来,就是走到了最右侧的节点
if (mostRight.right == null) {
// 第一次来到这个节点,right指针连上cur
// 1、做你想做的操作,比如打印节点
// 2、继续往左子树走
cur = cur.left;
continue;
} else {
// 第二次来到这个节点,断开right指针连 cur
mostRight.right = null;
}
} else { // 左子树为空
}
cur = cur.right;
}
}
9、二叉树非递归遍历
两种实现方式:1、Morris遍历;2、使用栈模拟
// TreeNode节点
private static class TreeNode {
public int val;
public TreeNode left;
public TreeNode right;
public TreeNode(int val) {
this.val = val;
}
}
// 1、Morris 前序遍历
// 前序遍历二叉树
public static void morrisPreOrder(TreeNode root) {
if (root == null) {
return;
}
// 找到当前节点的左子树上 最右的节点,并将该节点的右指针指向当前cur节点
TreeNode mostRight = null;
TreeNode cur = root;
while (cur != null) { // 只要cur没有遍历完,循环就继续
mostRight = cur.left;
if (mostRight != null) {
while (mostRight.right != null && mostRight.right != cur) { // 往最右节点靠拢
mostRight = mostRight.right;
}
// 停下来时,有两种情况
// 1是右指针为null,说明是第一次遍历到当前节点
// 2是右指针指向cur,说明是第二次遍历到当前节点
if (mostRight.right == null) {
mostRight.right = cur; // 指向cur节点
System.out.print(cur.val + " ");
cur = cur.left;
continue; // 继续往左子树走
} else {
mostRight.right = null;
}
} else { // 往右子树走之前,先打印当前cur的值
System.out.print(cur.val + " ");
}
cur = cur.right; // 转向右子树
}
System.out.println();
}
// 1、Morris 中序遍历
// 中序遍历二叉树
public static void morrisInOrder(TreeNode root) {
if (root == null) {
return;
}
TreeNode cur = root;
TreeNode mostRight = null;
while (cur != null) {
mostRight = cur.left;
if (mostRight != null) {
// 往最右节点靠拢
while(mostRight.right != null && mostRight.right != cur) {
mostRight = mostRight.right;
}
if (mostRight.right == null) { // 第1次来到cur节点
mostRight.right = cur;
cur = cur.left; // 继续往左子树走
continue;
} else { // 第2次来到cur节点
mostRight.right = null;
System.out.print(cur.val + " ");
}
} else {
System.out.print(cur.val + " ");
}
cur = cur.right; // 往右子树转
}
System.out.println();
}
// 1、Morris 后序遍历
// 后序遍历二叉树
public static void morrisPostOrder(TreeNode root) {
if (root == null) {
return;
}
TreeNode cur = root;
TreeNode mostRight = null;
while (cur != null) {
mostRight = cur.left;
if (mostRight != null) {
while (mostRight.right != null && mostRight.right != cur) {
mostRight = mostRight.right;
}
if (mostRight.right == null) {
mostRight.right = cur;
cur = cur.left;
continue;
} else { // 第2次来到cur节点,此时就打印cur.left节点,最靠右这一列的节点
mostRight.right = null;
printList(cur.left);
}
}
cur = cur.right; // 往右子树转
}
printList(root); // 最后打印根节点最靠右的一列
System.out.println();
}
private static void printList(TreeNode left) {
// 首先反转最靠右的一列,从下面往上打印
TreeNode node = reverseList(left);
TreeNode cur = node;
while (cur != null) {
System.out.print(cur.val + " ");
cur = cur.right;
}
reverseList(node); // 再反转回来
}
// 反转TreeNode最右这一列节点
public static TreeNode reverseList(TreeNode node) {
TreeNode pre = null;
TreeNode next = null;
while (node != null) {
next = node.right;
node.right = pre;
pre = node;
node = next;
}
return pre;
}
// 2、用栈模拟
// 非递归前序遍历
public static void preOrderNoRecursion(TreeNode root) {
if (root == null) {
return;
}
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()) {
root = stack.pop();
System.out.print(root.val + " ");
if (root.right != null) {
stack.push(root.right);
}
if (root.left != null) {
stack.push(root.left);
}
}
System.out.println();
}
// 非递归中序遍历
public static void inOrderNoRecursion(TreeNode root) {
if (root == null) {
return;
}
Stack<TreeNode> stack = new Stack<>();
while (!stack.isEmpty() || root != null) {
if (root != null) {
stack.push(root);
root = root.left;
} else { // 此时root = null.就打印当前栈顶元素
root = stack.pop();
System.out.print(root.val + " ");
root = root.right; // 转向右子树
}
}
System.out.println();
}
// 非递归后序遍历1
public static void postOrderNoRecursion(TreeNode root) {
if (root == null) {
return;
}
Stack<TreeNode> stack = new Stack<>();
Stack<TreeNode> helpStack = new Stack<>(); // 将遍历的结果存储在栈中,最后打印
stack.push(root);
while (!stack.isEmpty()) {
root = stack.pop();
helpStack.push(root);
if (root.left != null) {
stack.push(root.left);
}
if (root.right != null) {
stack.push(root.right);
}
}
// 打印helpStack中的数据
while (!helpStack.isEmpty()) {
root = helpStack.pop();
System.out.print(root.val + " ");
}
System.out.println();
}
// 非递归后序遍历2,省一个辅助栈的空间
public static void postOrderNoRecursion2(TreeNode root) {
if (root == null) {
return;
}
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
TreeNode pre = root; // 上一次打印的节点
while (!stack.isEmpty()) {
root = stack.peek();
if (root.left != null && root.left != pre && root.right != pre) {
stack.push(root.left);
} else if (root.right != null && root.right != pre) {
stack.push(root.right);
} else {
pre = root;
stack.pop(); // 弹出栈顶元素
System.out.print(root.val + " ");
}
}
System.out.println();
}
10、KMP
需求:给定两个字符串,s1和s2,请问在s1中是否包含子串s2?
这算是大学课程里面,数据结构书上比较难的一个算法了。主要思想还是暴力解,在暴力解的基础之上,引入了next数组的。
// s2模式串,s1是主串,在s1里面找s2
public static int indexOf(String s1, String s2) {
if (s1 == null || s2 == null || s2.length() == 0) {
return -1;
}
char[] ch1 = s1.toCharArray();
char[] ch2 = s2.toCharArray();
int[] next = getNextArray(ch2);
int index1 = 0; // 指向ch1
int index2 = 0; // 指向 ch2
while (index1 < ch1.length && index2 < ch2.length) {
if (ch1[index1] == ch2[index2]) {
index1++;
index2++;
} else if (index2 > 0) { // index2还能往前跳转的时候
index2 = next[index2]; // index2往前跳
} else { // 当前位置,既不相等,index2也不能往前跳了,说明index1位置出发行不通,index1后移
index1++;
}
}
return index2 == ch2.length ? index1 - index2 : -1;
}
private static int[] getNextArray(char[] s2) {
if (s2.length == 1) {
return new int[]{-1};
}
// 找的其实就是前后缀字符串
int[] res = new int[s2.length];
res[0] = -1;
res[1] = 0;
int i = 2; // 从第3个字符开始判断
int cn = 0;
while (i < s2.length) {
if (s2[cn] == s2[i - 1]) { // 切记这里其实是i的前一个位置
res[i++] = ++cn;
} else if (cn > 0) {
cn = res[cn];
} else {
res[i++] = 0;
}
}
return res;
}
11、Manacher
需求:在一个字符串中,问其中的最长回文子串的长度。也就是最长回文子串问题。时间复杂度O(N), 空间复杂度O(1)
暴力解,遍历每一个字符,在每一个字符的时候,往左右两边进行扩展,暴力解时间复杂度O(N^2)。
manacher也是在暴力解的基础之上,进行优化,引入了 回文半径数组。
public static int manacher(String str) {
if (str == null || str.length() == 0) {
return 0;
}
// 加工字符串,避免偶数长度的字符串遗漏一些情况
char[] chars = processStr(str);
int N = chars.length;
int R = -1; // 右边界,左闭右开区间
int C = -1; // 中心点
int max = 0;
int[] pArr = new int[N]; // 回文半径数组
for (int i = 0; i < N; i++) {
// 首先根据对称,拿到C点左侧相应的回文半径
pArr[i] = i < R? Math.min(pArr[2 * C - i], R - i) : 1;
// 根据已经计算出来的初始半径,此时再向两边扩展
while (i - pArr[i] >= 0 && i + pArr[i] < N) {
if (chars[i - pArr[i]] == chars[i + pArr[i]]) {
pArr[i]++; // 回文半径增加
} else { // 不相等,直接跳出循环
break;
}
}
// 更新R,C和max
if (i + pArr[i] > R) {
R = i + pArr[i]; // 右边界
C = i; // 以当前点作为新的中心点
}
max = Math.max(max, pArr[i]);
}
return max - 1;
}
// 每个字符之间添加#,用于间隔
private static char[] processStr(String str) {
int N = str.length();
char[] res = new char[2 * N + 1];
for (int i = 0; i < res.length; i += 2) {
res[i] = '#';
}
int index = 0;
for (int i = 1; i < res.length; i+= 2) {
res[i] = str.charAt(index++);
}
return res;
}
12、快速选择 bfprt
需求:在一个无序数组中,返回 第 K 小的数字。时间复杂度O(N),空间复杂度O(1)。
// 0、排序之后,再找第k小的数,时间复杂度O(N*logN),这样写,面试直接挂。
// 1、常规解法,就是TopK问题,使用一个大根堆,将所有数过一遍大根堆就行,这里就不追溯了。时间O(N * logK),空间O(K)
// 2、归并排序中的merge函数,(荷兰国旗问题优化)。
// 因为这里的pivot是随机选取的,在数学证明上,时间复杂度是收敛于O(N)的
public static int fatsSelect(int[] nums, int k) {
fastSelect(nums, 0, nums.length - 1, k - 1);
return nums[k - 1];
}
private static void fastSelect(int[] nums, int l, int r, int k) {
if (l >= r) {
return;
}
int index = l + (int) (Math.random() * (r - l)) + 1; // 随机值
int pivot = nums[index];
int[] mid = partition(nums, pivot);
if (mid[0] <= k && k <= mid[1]) {
return;
} else if (k < mid[0]) { // 往左侧走
fastSelect(nums, l, mid[0] - 1, k);
} else { // 往右侧走
fastSelect(nums, mid[1] + 1, r, k);
}
}
// 荷兰国旗问题优化
private static int[] partition(int[] nums, int l, int r, int pivot) {
int less = l - 1; // 小于区
int more = r + 1; // 大于区
int index = l;
while (less < more) {
if (nums[index] == pivot) {
index++;
} else if (nums[index] < pivot) {
swap(nums, index++, ++less); // 两数交换
} else {
swap(nums, index, --more);
}
}
return new int[]{less + 1, more - 1};
}
// 2、bfprt算法,时间复杂度严格控制在O(N)
// 严格解析,请看https://blog.csdn.net/x0919/article/details/122246065
public static int bfprt(int[] arr, int l, int r, int k) {
if (l == r) {
return arr[l];
}
int pivot = medianOfMedians(arr, l, r);
// 根据基准值进行荷兰国旗问题优化
int[] mid = netherlands(arr, l, r, pivot);
if (mid[0] <= k && k <= mid[1]) {
return arr[k];
} else if (mid[0] > k) {
return bfprt(arr, l, mid[0] - 1, k);
} else {
return bfprt(arr, mid[1] + 1, r, k);
}
}
private static int[] netherlands(int[] arr, int l, int r, int pivot) {
int less = l - 1;
int more = r + 1;
int index = l;
while (index < more) {
if (arr[index] < pivot) {
swap(arr, index++, ++less);
} else if (arr[index] > pivot) {
swap(arr, index, --more);
} else {
index++;
}
}
return new int[]{less + 1, more - 1};
}
private static void swap(int[] arr, int l, int r) {
int tmp = arr[l];
arr[l] = arr[r];
arr[r] = tmp;
}
// 获取基准值
private static int medianOfMedians(int[] arr, int l, int r) {
// 5个数 5个数一组,取5个数的中间值
int len = r - l + 1;
int size = len / 5;
int offset = len % 5 == 0 ? 0 : 1;
int[] tmp = new int[size + offset];
for (int i = 0; i < tmp.length; i++) {
int left = l + 5 * i; // 5个数的左边界
int right = Math.min(r, l + 4); // 5个数的右边界
tmp[i] = sortAndGetMidNum(arr, left, right);
}
// 再返回tmp数组中的中间值
return bfprt(tmp, 0, tmp.length - 1, tmp.length / 2);
}
// 排序这5个数,并返回中间值
private static int sortAndGetMidNum(int[] arr, int left, int right) {
// 直接插入排序
for (int i = left; i < right; i++) {
int cur = arr[i];
int j = i - 1;
for (; j >= left; j--) {
if (arr[j] > arr[j + 1]) {
arr[j + 1] = arr[j];
} else {
break;
}
}
arr[j + 1] = cur;
}
return arr[left + (right - left) / 2]; // 返回中间值
}
13、滑动窗口
需求:常常用于子数组求解之类问题。
维持L、R边界,一般来说都是左闭右开区间的。
/**
* Created by Terry
* User: Administrator
* Date: 2022-06-26
* Time: 15:40
* Description: 窗口最大值。
* 假设一个固定大小为W的窗口,依次划过arr,
* 返回每一次滑出状况的最大值
* 例如,arr = [4,3,5,4,3,3,6,7], W = 3
* 返回:[5,5,5,4,6,7]
*/
public class Code01_WindowMaxNumber {
public static void main(String[] args) {
int[] arr = {4, 3, 5, 4, 3, 3, 6, 7};
int w = 3;
System.out.println(Arrays.toString(windowMaxNumber(arr, w)));
}
public static int[] windowMaxNumber(int[] arr, int w) {
if (arr == null || arr.length == 0 || arr.length < w) {
return new int[]{};
}
int N = arr.length;
int[] ans = new int[N - w + 1];
int index = 0;
LinkedList<Integer> queue = new LinkedList<>(); // 双端队列
for (int i = 0; i < arr.length; i++) {
while (!queue.isEmpty() && arr[queue.peekLast()] <= arr[i]) { // 维持头部大,尾部小的结构
queue.pollLast();
}
queue.addLast(i);
if (i - queue.peekFirst() == w) {
queue.pollFirst();
}
if (i >= w - 1) {
ans[index++] = arr[queue.peekFirst()];
}
}
return ans;
}
}
14、加强堆
需求:系统提供的堆,在压入元素进去之后,若此时需要修改堆中某个元素的数据,然后修改之后,堆的结构应该发生改变,系统提供的堆不能实现这个事。需要自己改堆结构。
举个例子:现在有一个大根堆 heap,类型为Node节点,比较方式是 Node里的val值。
假设现在修改堆中某一个节点node的val值,修改之后,需要手动调用 函数(向堆顶走、向堆下面走)两种情况,才能维持堆的结构。
// 手改堆。核心点就在 反向索引表indexMap,能够获取对象在数组中的下标值
public class HeapGenerate<T> {
private ArrayList<T> arr; // 存储节点的数组
private HashMap<T, Integer> indexOfMap; // 存储每个节点在堆上的下标
private int size; // 堆的大小
private Comparator<? super T> comp; // 比较器
public HeapGenerate(Comparator<? super T> comp) {
this.arr = new ArrayList<>();
this.indexOfMap = new HashMap<>();
this.size = 0;
this.comp = comp;
}
public boolean isEmpty() {
return this.size == 0;
}
public int size() {
return size;
}
public List<T> getAllElements() {
List<T> list = new ArrayList<>();
for (T v : arr) {
list.add(v);
}
return list;
}
public boolean contains(T obj) {
return indexOfMap.containsKey(obj); // 查看当前堆中是否有该对象
}
public T peek() {
return arr.get(0);
}
public void add(T value) {
this.arr.add(value);
this.indexOfMap.put(value, size); // 存储下标值
heapInsert(size++); // 往上调整
}
// 弹出堆顶结果
public T poll() {
T res = arr.get(0);
swap(0, size - 1); // 第一个数据和最后一个数据进行交换
indexOfMap.remove(res); // 删除res对应的下标
arr.remove(--size); // 删除在数组上的数据
heapify(0); // 向下调整
return res;
}
// 手改堆的核心,能删除非堆顶元素
public void remove(T obj) {
T replace = arr.get(size - 1); // 拿到最后一个元素
int index = indexOfMap.get(obj);
indexOfMap.remove(obj); // 删除在表中的下标
arr.remove(--size); // 删除数组中的最后一个元素
if (replace != obj) { // 被删除的元素并不是数组中的最后一个元素
arr.set(index, replace);
indexOfMap.put(replace, index); // 新的下标
resign(replace);
}
}
// 手改堆的核心方法
public void resign(T value) { // 根据对象,获取对象在数组中的下下标,从而进行调整
heapInsert(indexOfMap.get(value));
heapify(indexOfMap.get(value)); // 二者,只可能有一个会调用,只可能向上或向下
}
private void heapify(int i) {
int left = (i << 1) + 1;
while (left < size) {
int maxChild = left + 1 < size && comp.compare(arr.get(left + 1), arr.get(left)) < 0?
left + 1 : left;
maxChild = comp.compare(arr.get(maxChild), arr.get(i)) < 0? maxChild : i; // 跟父节点做判断
if (maxChild == i) {
break;
}
swap(i, maxChild);
i = maxChild;
left = (i << 1) + 1; // 再次刷新左孩子
}
}
// 往上走,调整堆结构
private void heapInsert(int i) {
// 根据自定义的比较器进行比较
// 此处除以2,用位运算代替,要判断i是大于0才行
while (i > 0 && comp.compare(arr.get(i), arr.get((i - 1) >> 1)) < 0) {
swap(i, (i - 1) >> 1);
i = (i - 1) >> 1;
}
}
// 不仅要更新在数组上的值,还要更新indexOfMap中的值
private void swap(int up, int down) {
T o1 = arr.get(up);
T o2 = arr.get(down);
indexOfMap.put(o1, down);
indexOfMap.put(o2, up);
arr.set(up, o2); // 更新
arr.set(down, o1); // 更新
}
}
15、有序表
也就是能够排序的一些结构,比如AVL树、SB树、跳表、红黑树等。比较好写的可能就是SB树。
// Size Balance Tree,通过节点的数量来调整平衡的
private static class SBTNode<K extends Comparable<K>, V> {
public K key;
public V value;
public int size;
public SBTNode<K, V> left;
public SBTNode<K, V> right;
public SBTNode(K key, V value) {
this.key = key;
this.value = value;
size = 1;
}
}
private static class SizeBalanceTree<K extends Comparable<K>, V> {
private SBTNode<K, V> root;
private SBTNode<K, V> getIndex( SBTNode<K, V> cur, int kth) {
if (kth == (cur.left != null ? cur.left.size : 0) + 1) {
return cur;
} else if (kth <= (cur.left != null ? cur.left.size : 0)) {
return getIndex(cur.left, kth);
} else {
return getIndex(cur.right, kth - (cur.left != null ? cur.left.size : 0) - 1);
}
}
public K getIndexKey(int index) {
if (index < 0 || index >= this.size()) {
throw new RuntimeException("invalid parameter.");
}
return getIndex(root, index + 1).key;
}
public V getIndexValue(int index) {
if (index < 0 || index >= this.size()) {
throw new RuntimeException("invalid parameter.");
}
return getIndex(root, index + 1).value;
}
public int size() {
return root == null ? 0 : root.size;
}
public void put(K key, V val) {
if (key == null) {
throw new RuntimeException("key is invalid.");
}
SBTNode<K, V> lastNode = findLastIndex(key);
if (lastNode != null && key.compareTo(lastNode.key) == 0) { // 更新值的情况
lastNode.value = val;
} else { // 新插入值
root = add(root, key, val);
}
}
// 返回等于key的,或者key的父节点。
private SBTNode<K, V> findLastIndex(K key) {
SBTNode<K, V> pre = root;
SBTNode<K, V> cur = root;
while (cur != null) {
pre = cur;
if (key.compareTo(cur.key) == 0) {
break;
} else if (key.compareTo(cur.key) < 0) {
cur = cur.left;
} else {
cur = cur.right;
}
}
return pre;
}
private SBTNode<K, V> add(SBTNode<K, V> node, K key, V val) {
if (node == null) {
return new SBTNode<K, V>(key, val);
} else {
node.size++;
if (key.compareTo(node.key) < 0) {
node.left = add(node.left, key, val);
} else {
node.right = add(node.right, key, val);
}
// 维持平衡
return maintain(node);
}
}
public void remove(K key) {
if (key == null) {
throw new RuntimeException("key is invalid.");
}
if (containsKey(key)) {
root = delete(root, key);
}
}
private SBTNode<K, V> delete(SBTNode<K, V> node, K key) {
node.size--;
int compare = key.compareTo(node.key);
if (compare < 0) {
node.left = delete(node.left, key);
} else if (compare > 0) {
node.right = delete(node.right, key);
} else { // ==0的情况
if (node.right == null) { // 有左孩子的情况,或者左右孩子都没有
node = node.left;
} else if (node.left == null) { // 有右孩子的情况
node = node.right;
} else { // 左右孩子都有的情况
SBTNode<K, V> pre = null;
SBTNode<K, V> cur = node.right;
cur.size--;
while (cur.left != null) {
pre = cur;
cur = cur.left;
cur.size--;
}
if (pre != null) {
pre.left = cur.right;
cur.right = node.right;
}
cur.left = node.left;
cur.size = cur.left.size + (cur.right != null ? cur.right.size : 0) + 1;
node = cur;
}
}
// 维持平衡---可以不用维持平衡,在add的时候再维护
// node = maintain(node);
return node;
}
public boolean containsKey(K key) {
if (key == null) {
throw new RuntimeException("key is invalid.");
}
SBTNode<K, V> lastNode = findLastIndex(key);
return lastNode != null && key.compareTo(lastNode.key) == 0;
}
private SBTNode<K, V> maintain(SBTNode<K, V> node) {
if (node == null) {
return null;
}
// 计算node的下一级节点数和 下下一级节点数
int leftSize = node.left != null ? node.left.size : 0;
int rightSize = node.right != null ? node.right.size : 0;
int leftLeftSize = node.left != null && node.left.left != null ? node.left.left.size : 0; // LL
int leftRightSize = node.left != null && node.left.right != null ? node.left.right.size : 0; // LR
int rightLeftSize = node.right != null && node.right.left != null ? node.right.left.size : 0; // RL
int rightRightSize = node.right != null && node.right.right != null ? node.right.right.size : 0; // RR
if (leftLeftSize > rightSize) { // RR型旋转
node = rightRotate(node);
node.right = maintain(node.right); // 先调整node下级节点的平衡
node = maintain(node);
} else if (leftRightSize > rightSize) { // LR型旋转
node.left = leftRotate(node.left); // 先左旋转
node = rightRotate(node); // 再右旋转
node.left = maintain(node.left);
node.right = maintain(node.right); // 先维持node的下级节点
node = maintain(node);
} else if (rightLeftSize > leftSize) { // RL型旋转
node.right = rightRotate(node.right);
node = leftRotate(node);
node.left = maintain(node.left);
node.right = maintain(node.right);
node = maintain(node);
} else if (rightRightSize > leftSize) { // LL型旋转
node = leftRotate(node);
node.left = maintain(node.left);
node = maintain(node);
}
return node;
}
private SBTNode<K, V> leftRotate(SBTNode<K, V> node) {
SBTNode<K, V> newHead = node.right;
node.right = newHead.left;
newHead.left = node;
newHead.size = node.size;
node.size = (node.left != null ? node.left.size : 0) + (node.right != null ? node.right.size : 0) + 1;
return newHead;
}
private SBTNode<K, V> rightRotate(SBTNode<K, V> node) {
SBTNode<K, V> newHead = node.left;
node.left = newHead.right;
newHead.right = node;
newHead.size = node.size;
node.size = (node.left != null ? node.left.size : 0) + (node.right != null ? node.right.size : 0) + 1;
return newHead;
}
public V get(K key) {
if (key == null) {
throw new RuntimeException("key is invalid.");
}
SBTNode<K, V> lastNode = findLastIndex(key);
if (lastNode != null && key.compareTo(lastNode.key) == 0) {
return lastNode.value;
}
return null;
}
public K firstKey() {
if (root == null) {
return null;
}
SBTNode<K, V> node = root;
while (node.left != null) {
node = node.left;
}
return node.key;
}
public K lastKey() {
if (root == null) {
return null;
}
SBTNode<K, V> node = root;
while (node.right != null) {
node = node.right;
}
return node.key;
}
// <= key的,最接近key的
public K floorKey(K key) {
if (key == null) {
throw new RuntimeException("key is invalid.");
}
SBTNode<K, V> ans = null;
SBTNode<K, V> cur = root;
while (cur != null) {
if (key.compareTo(cur.key) == 0) {
ans = cur;
break;
} else if (key.compareTo(cur.key) < 0) { // key < cur.key
cur = cur.left;
} else { // key >= cur.key
ans = cur;
cur = cur.right;
}
}
return ans == null ? null : ans.key;
}
// >= key的,最接近key的
public K ceilingKey(K key) {
if (key == null) {
throw new RuntimeException("key is invalid.");
}
SBTNode<K, V> ans = null;
SBTNode<K, V> cur = root;
while (cur != null) {
if (key.compareTo(cur.key) == 0) {
ans = cur;
break;
} else if (key.compareTo(cur.key) < 0) { // key < cur.key
ans = cur;
cur = cur.left;
} else { // key >= cur.key
cur = cur.right;
}
}
return ans == null? null : ans.key;
}
}
16、单调栈
需求:在一个无序数组中,找一个元素的左右两侧第一个比它大(小)的数。时间复杂度O(N),空间复杂度O(N)。
细分为单调递增栈、单调递减栈。LeetCode练习题。
- 单调递增栈:从栈顶往下,元素越来越大。用于找元素的两侧第一个比它大的数。
- 单调递减栈:从栈顶往下,元素越来越小。用于找元素的两侧第一个比它小的数。
// 单调递减栈。 反之单调递增栈,只需修改第6行while循环中的 <=符号即可
public static void monotonousStack(int[] nums) {
Stack<Integer> stack = new Stack<>(); // 存入的数组的下标值
for (int i = 0; i < nums.length; i++) {
// 下方的具体是 < 还是<=,也是看题意来定的
while (!stack.isEmpty() && nums[i] <= nums[stack.peek()]) {
// 此时弹出的pos下标,有以下性质:(假设弹出栈顶元素pos后,此时栈顶元素 = k)
// 1、[i]的元素 <= [pos]
// 2、[k] < [pos]的元素
// 所有就有 [k] < [pos] && pos >= [i],
// 所有对于pos来说,左右两侧的比它小的数就出来了
int pos = stack.pop();
int k = stack.isEmpty() ? -1 : stack.peek();
// 后续操作根据题意来定
}
stack.push(i);
}
}
17、数位DP
最简单的数位DP,请看 leetcode233题 数字1的个数。
思路:把n这个数字,转换成字符串类型,然后从左往右开始枚举每一个位置的数值,类似于全排列的那种递归展开。
from functools import cache
# leetcode 1012题 至少有1位重复数字
class Solution:
def numDupDigitsAtMostN(self, n: int) -> int:
# 现在的问题是,如何将cnt数组,进行状态转移
# 换个思路:题目要计算有重复字符的数字,那么【全部数字组合】-【不重复数字组合】
# 剩下的就是 【有重复字符的组合数】
s = str(n)
@cache
# i指向字符串,
# status的二进制位信息,表示每个位置的数字是否出现过 (左神讲的 状态压缩)
# is_limit 表示是否到达n的上限了
# pre_num 表示 0~i-1位置,是否生成了数值
def f(i: int, status: int, is_limit: bool, pre_num: bool) -> int:
if i == len(s):
return int(pre_num)
ans = 0
if not pre_num:
ans = f(i + 1, status, False, False)
# 枚举
# 0~i-1位置还没有生成数值的话,这个位置开始生成,比如从1开始,避开前导0
low = 0 if pre_num else 1
up = int(s[i]) if is_limit else 9
for d in range(low, up + 1):
if (status >> d) & 1 == 0: # 说明d这个数字还没出现过
status += (1 << d) # 将第 d位的二进制位 标记
ans += f(i + 1, status, is_limit and d == up, True)
status -= (1 << d) # 恢复现场
return ans
return n - f(0, 0, True, False)
# 正向思考,枚举每一个位置
def numDupDigitsAtMostN1(self, n: int) -> int:
s = str(n)
cnt = [0] * 10
def f(i: int, is_limit: bool, pre_num: bool) -> int:
if i == len(s):
for x in cnt:
if x >= 2: return True
return False
ans = 0
if not pre_num:
ans = f(i + 1, False, False)
# 枚举
# 0~i-1位置还没有生成数值的话,这个位置开始生成,比如从1开始,避开前导0
low = 0 if pre_num else 1
up = int(s[i]) if is_limit else 9
for d in range(low, up + 1):
cnt[d] += 1
ans += f(i + 1, is_limit and d == up, True)
cnt[d] -= 1
return ans
return f(0, True, False)
18、快速幂
核心思想:将指数折半拆分进行计算,例如指数8 等价于 4 + 4,那么只需要计算 a^4之后,再对其平方,就能得到a ^8。
// 快速幂, a^b次方
// 公式:(n1 * n2) % MOD = [(n1 % MOD) * (n2 % MOD)] % MOD
private int MOD = 1337;
private long quickMul(int a, int b) {
if (b == 0) return 1;
if (b == 1) return a;
long sum = quickMul(a, b / 2) % MOD;
// b是奇数,就拆分为偶数+偶数+1
// b是偶数,就拆分为 偶数+偶数
return b % 2 == 0? sum * sum % MOD : sum * sum * a % MOD;
}
19、图论
1).建图
常见的邻接矩阵、邻接表这里就不细说了,各大数据结构中的教材都有,这里主要说一下一个比赛常用的建图:链式前向星。他的优点在于 能省空间, 假设 一共N个点,M条边。空间复杂度能做到 O(N + M).
思路:对每一条边进行编号,存储边的信息,边与边进行串联。
// 5000个点,2 * 10^5条边。这里乘2,是为了处理有向边
static int[] head = new int[5001]; // head[i] =j,表示i这个点从j号边开始遍历
static int[] next = new int[200001 * 2]; // next[j] = k,表示j号边的后面是k号边
static int[] to = new int[200001 * 2]; // to[j] = d,表示 j号边去往的是d
static int[] weight = new int[200001 * 2]; // weight[j] = w, 表示j号边的权重是w
static int cnt = 1; // 加边的边号。 从1开始,边号=0,说明就是没有边
private static void addEdge(int x, int y, int w) {
next[cnt] = head[x];
to[cnt] = y;
weight[cnt] = w;
head[x] = cnt++;
}
2).最小生成树
a). Kruskal
思路:以边为主体,每次挑选权重最小的边,判断起点、终点是否连通,最后凑齐 N-1条边即可。(N指的是 点数)
public class Kruskal {
static int[][] edges = new int[200001][3];
static int[] father = new int[5001];
private static int find(int x) {
if (father[x] == x) return x;
int next = find(father[x]);
father[x] = next;
return next;
}
// 如果x、y不在一个集合,就合并,并返回true
// 如果在一个集合,就返回false
private static boolean union(int x, int y) {
int fa1 = find(x), fa2 = find(y);
if (fa1 != fa2) {
father[fa1] = fa2;
return true;
}
return false;
}
private static String kruskal(int n, int m ) {
for (int i = 0; i <= n; i++) father[i] = i;
Arrays.sort(edges, 0, m,(o1, o2) -> o1[2] - o2[2]);
int ans = 0, edgeCnt = 0;
for (int i = 0; i < m ; i++) {
if (union(edges[i][0], edges[i][1])) {
ans += edges[i][2];
edgeCnt++;
}
}
return edgeCnt == n - 1? ans + "" : "orz";
}
}
b). Prim
思路:随机从一个点出发,以点去解锁边,将边放入小根堆中,弹出最小权重边,凑齐 N个点就行。时间复杂度O(N*logM)。Prim算法还有优化版本,就是堆中只需要存储N个点以及到该点的权重,时间复杂度能做到 O(N*logN)。
// 普通小根堆版本
public class PrimOfDynamic {
static boolean[] vis = new boolean[5001];
// 邻接表的形式
private static String prim(ArrayList<ArrayList<int[]>> g, int n, int m) {
PriorityQueue<int[]> heap = new PriorityQueue<>((o1, o2) -> {
return o1[1] - o2[1];
});
Arrays.fill(vis, 0, n + 1, false);
heap.add(new int[]{1, 0});
int ans = 0, nodeCnt = 0;
while (!heap.isEmpty()) {
int[] pos = heap.poll();
if (!vis[pos[0]]) {
ans += pos[1];
nodeCnt++;
vis[pos[0]] = true;
for (int[] next : g.get(pos[0])) {
heap.add(next);
}
}
}
return nodeCnt == n ? ans + "" : "orz";
}
}
3).最短路径
a. Dijkstra
思路:以起点解锁边,弹出最小权重边,再去解锁点,直到跑完整个图。
public class Dijkstra {
// s是 起点
private static int[] dijkstra(int n, int m, int s, int[][] edges) {
ArrayList<int[]>[] g = new ArrayList[n + 1]; // 建邻接表
for (int i = 1; i <= n; i++) g[i] = new ArrayList<>();
for (int[] arr : edges) {
int x = arr[0], y = arr[1], w = arr[2];
g[x].add(new int[]{y, w});
}
int[] dis = new int[n + 1];
boolean[] vis = new boolean[n + 1];
Arrays.fill(dis, Integer.MAX_VALUE);
dis[s] = 0;
PriorityQueue<int[]> pq = new PriorityQueue<>((o1, o2) -> {
return o1[1] - o2[1];
});
pq.add(new int[]{s, 0});
while (!pq.isEmpty()) {
int x = pq.poll()[0];
if (vis[x]) continue;
vis[x] = true;
for (int[] next : g[x]) {
int y = next[0], w = next[1];
if(!vis[y] && dis[x] + w < dis[y]) {
dis[y] = dis[x] + w;
pq.add(new int[]{y, dis[x] + w});
}
}
}
return dis;
}
}
b. 分层图最短路
所谓分层图最短路,就是在 题目所给的点上,再添加状态,用来表示 “逻辑上的点”,然后在这种方式下,再去跑 Dijkstra 或者BFS这些。
class Solution {
// 分层图最短路
// 以 景点+剩余电量,这个整体看做 一个虚拟的点
// 问 这个虚拟点 -> end点的最短路径问题
public int electricCarPlan(int[][] paths, int cnt, int start, int end, int[] charge) {
int n = charge.length;
ArrayList<ArrayList<int[]>> g = new ArrayList<>();
for (int i = 0; i < n; i++) g.add(new ArrayList<>());
for (int[] arr: paths) { // 建邻接表
int x = arr[0], y = arr[1], w = arr[2];
g.get(x).add(new int[]{y, w});
g.get(y).add(new int[]{x, w});
}
// 0:表示景点;1:表示电动车的剩余电量
int[][] dis = new int[n][cnt + 1];
boolean[][] vis = new boolean[n][cnt + 1];
for(int i = 0; i < n; i++) Arrays.fill(dis[i], Integer.MAX_VALUE);
dis[start][0] = 0;
// 0: 景点
// 1: 剩余电量
// 2: 从start -> 该点的 最少时间
PriorityQueue<int[]> pq = new PriorityQueue<>((o1, o2) -> o1[2] - o2[2]);
pq.add(new int[]{start, 0, 0});
while (!pq.isEmpty()) { // 跑 Dijkstra算法
int[] pos = pq.poll();
int x = pos[0], rest = pos[1], cost = pos[2];
if (vis[x][rest]) continue;
if (x == end) return cost;
vis[x][rest] = true;
// 跑后续节点
for (int[] next : g.get(x)) {
int y = next[0], w = next[1];
// 1、继续充电
if (rest < cnt) {
dis[x][rest + 1] = cost + charge[x];
pq.add(new int[]{x, rest + 1, cost + charge[x]});
}
// 2、骑车 去下一站点
if (rest >= w && cost + w < dis[y][rest - w]) {
dis[y][rest - w] = cost + w;
pq.add(new int[]{y, rest - w, cost + w});
}
}
}
return -1;
}
}
c. Floyd
需求:得到 每一个点 到其他点的最短路径。
思路:枚举每一个点,作为跳板,看看能不能拉近 A、B两点的距离。时间复杂度O(N^3)。
public class Floyd {
static int[][] grid = new int[101][101]; // 邻接矩阵,初始值是 int最大值
static int n, m;
private static void floyd() {
// 枚举 中间跳板 k
// a -> k ->b,看看能不能缩短 a->b的距离
for (int k = 1; k <= n; k++) { // 跳板
for (int i = 1; i <= n; i++) { // 起点
for (int j = 1; j <= n; j++) { // 终点
if (grid[i][k] != Integer.MAX_VALUE && grid[k][j] != Integer.MAX_VALUE &&
grid[i][k] + grid[k][j] < grid[i][j]) {
grid[i][j] = grid[i][k] + grid[k][j];
}
}
}
}
}
}
d. A星
需求:在二维矩阵中,计算某一点 ->另一点的最小代价(距离)。本身可以跑Dijkstra,但是不够快。
A星算法就是在Dijkstra的基础之上,对 堆上的“权重”进行调整。换句话说,就是对 压入堆中的数据,额外的添加一些“吸引力”。而具体如何添加,常见的有三种方式:
- 曼哈顿距离。|x1 - x2| + |y1 - y2|
- 对角线距离。|x1 - x2| , |y1 - y2|,二者取最大值。
- 欧式距离。就是勾股定理,求斜边的长度。
本质上就是 在Dijkstra的基础上,在将边压入堆的时候,调整一下权重,加了预估值。其他的都和Dijkstra一样
// 左神代码仓库所写的 A*与Dijkstra的测试
import java.util.PriorityQueue;
// A*算法模版(对数器验证)
public class Code01_AStarAlgorithm {
// 0:上,1:右,2:下,3:左
public static int[] move = new int[] { -1, 0, 1, 0, -1 };
// A*算法
// grid[i][j] == 0 代表障碍
// grid[i][j] == 1 代表道路
// 只能走上、下、左、右,不包括斜线方向
// 返回从(startX, startY)到(targetX, targetY)的最短距离
public static int minDistance2(int[][] grid, int startX, int startY, int targetX, int targetY) {
if (grid[startX][startY] == 0 || grid[targetX][targetY] == 0) {
return -1;
}
int n = grid.length;
int m = grid[0].length;
int[][] distance = new int[n][m];
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
distance[i][j] = Integer.MAX_VALUE;
}
}
distance[startX][startY] = 1;
boolean[][] visited = new boolean[n][m];
// 0 : 行
// 1 : 列
// 2 : 从源点出发到达当前点的距离 + 当前点到终点的预估距离
PriorityQueue<int[]> heap = new PriorityQueue<>((a, b) -> a[2] - b[2]);
heap.add(new int[] { startX, startY, 1 + f1(startX, startY, targetX, targetY) });
while (!heap.isEmpty()) {
int[] cur = heap.poll();
int x = cur[0];
int y = cur[1];
if (visited[x][y]) {
continue;
}
visited[x][y] = true;
if (x == targetX && y == targetY) {
return distance[x][y];
}
for (int i = 0, nx, ny; i < 4; i++) {
nx = x + move[i];
ny = y + move[i + 1];
if (nx >= 0 && nx < n && ny >= 0 && ny < m && grid[nx][ny] == 1 && !visited[nx][ny]
&& distance[x][y] + 1 < distance[nx][ny]) {
distance[nx][ny] = distance[x][y] + 1;
heap.add(new int[] { nx, ny, distance[x][y] + 1 + f1(nx, ny, targetX, targetY) });
}
}
}
return -1;
}
// 曼哈顿距离
public static int f1(int x, int y, int targetX, int targetY) {
return (Math.abs(targetX - x) + Math.abs(targetY - y));
}
// 对角线距离
public static int f2(int x, int y, int targetX, int targetY) {
return Math.max(Math.abs(targetX - x), Math.abs(targetY - y));
}
// 欧式距离
public static double f3(int x, int y, int targetX, int targetY) {
return Math.sqrt(Math.pow(targetX - x, 2) + Math.pow(targetY - y, 2));
}
}
e. bellman-ford + SPFA
Bellman-Ford算法,解决可以有负权边但是不能有负环(保证最短路存在)的图,单源最短路算法。
松弛操作:假设源点为A,从A到任意点F的最短距离为distance[F],假设从点P出发某条边,去往点S,边权为W如果发现,distance[P] + W < distance[S],也就是通过该边可以让distance[S]变小,那么就说,P出发的这条边对点S进行了松弛操作。
Bellman-Ford过程:1,每一轮考察每条边,每条边都尝试进行松弛操作,那么若干点的distance会变小。2,当某一轮发现不再有松弛操作出现时,算法停止。
Bellman-Ford算法时间复杂度假设点的数量为N,边的数量为M,每一轮时间复杂度O(M)最短路存在的情况下,因为1次松弛操作会使1个点的最短路的边数+1,而从源点出发到任何点的最短路最多走过全部的n个点,所以松弛的轮数必然 <= n - 1,所以Bellman-Ford算法时间复杂度O(M*N)。
重要推广:判断从某个点出发能不能到达负环上面已经说了,如果从A出发存在最短路(没有负环),那么松弛的轮数必然 <= n - 1而如果从A点出发到达一个负环,那么松弛操作显然会无休止地进行下去所以,如果发现从A点出发,在第n轮时松弛操作依然存在,说明从A点出发能够到达一个负环。
这道题也是可以用上述所讲的 分层图最短路进行求解,也可以用 bellman-ford解。
// Bellman-Ford算法
public static int findCheapestPrice(int n, int[][] flights, int start, int target, int k) {
int[] cur = new int[n];
Arrays.fill(cur, Integer.MAX_VALUE);
cur[start] = 0;
for (int i = 0; i <= k; i++) {
int[] next = Arrays.copyOf(cur, n);
for (int[] edge : flights) {
// a -> b , w
if (cur[edge[0]] != Integer.MAX_VALUE) {
next[edge[1]] = Math.min(next[edge[1]], cur[edge[0]] + edge[2]);
}
}
cur = next;
}
return cur[target] == Integer.MAX_VALUE ? -1 : cur[target];
}
Bellman-Ford + SPFA的优化。
思路:因为bellman-Ford是对每条边都进行遍历,其实并不然,如果上一轮 只有A、B两个点的距离被缩短了,那么来到当前这一轮就只需要考察 A、B两点向外的边,看看能不能缩短。
只是优化了常数项时间,整体的时间复杂度还是在 O(N *M)级别。这个算法作用有两个:1、可以跑负权边的图;2、可以判断图中是否存在 负环。但是只能跑小规模的图,不然容易超时。
// Bellman-Ford + SPFA优化模版(洛谷)
// 给定一个 n个点的有向图,请求出图中是否存在从顶点 1 出发能到达的负环
// 负环的定义是:一条边权之和为负数的回路。
// 测试链接 : https://www.luogu.com.cn/problem/P3385
// 请同学们务必参考如下代码中关于输入、输出的处理
// 这是输入输出处理效率很高的写法
// 提交以下所有代码,把主类名改成Main,可以直接通过
import java.io.*;
import java.util.*;
public class SPFA {
public static int MAXN = 2001;
public static int MAXM = 6001;
// 链式前向星建图需要
public static int[] head = new int[MAXN];
public static int[] next = new int[MAXM];
public static int[] to = new int[MAXM];
public static int[] weight = new int[MAXM];
public static int cnt;
// SPFA需要
public static int MAXQ = 4000001;
// 源点出发到每个节点的距离表
public static int[] distance = new int[MAXN];
// 节点被松弛的次数
public static int[] updateCnt = new int[MAXN];
// 节点是否已经在队列中
public static boolean[] enter = new boolean[MAXN];
public static void build(int n) {
cnt = 1;
Arrays.fill(head, 1, n + 1, 0);
Arrays.fill(enter, 1, n + 1, false);
Arrays.fill(distance, 1, n + 1, Integer.MAX_VALUE);
Arrays.fill(updateCnt, 1, n + 1, 0);
}
public static void addEdge(int u, int v, int w) {
next[cnt] = head[u];
to[cnt] = v;
weight[cnt] = w;
head[u] = cnt++;
}
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StreamTokenizer in = new StreamTokenizer(br);
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
in.nextToken();
int cases = (int) in.nval;
for (int i = 0, n, m; i < cases; i++) {
in.nextToken(); n = (int) in.nval;
in.nextToken(); m = (int) in.nval;
build(n);
for (int j = 0, u, v, w; j < m; j++) {
in.nextToken(); u = (int) in.nval;
in.nextToken(); v = (int) in.nval;
in.nextToken(); w = (int) in.nval;
if (w >= 0) {
addEdge(u, v, w);
addEdge(v, u, w);
} else {
addEdge(u, v, w);
}
}
out.println(spfa(n) ? "YES" : "NO");
}
out.flush();
out.close();
br.close();
}
// Bellman-Ford + SPFA优化的模版
public static boolean spfa(int n) {
distance[1] = 0;
updateCnt[1]++;
Queue<Integer> queue = new LinkedList<>();
queue.add(1);
enter[1] = true;
while (!queue.isEmpty()) {
int u = queue.poll();
enter[u] = false;
for (int ei = head[u], v, w; ei > 0; ei = next[ei]) {
v = to[ei];
w = weight[ei];
if (distance[u] + w < distance[v]) {
distance[v] = distance[u] + w;
if (!enter[v]) {
if (updateCnt[v]++ == n) {
return true;
}
queue.add(v);
enter[v] = true;
}
}
}
}
return false;
}
}
4).拓扑排序
思路:入度为0的点,进入队列,然后BFS扫描即可。
public void f(int[][] edges, int n) {
int[] indegree = new int[n + 1]; // 入度表
ArrayList<ArrayList<Integer>> g = new ArrayList<>();
for (int i =0; i <= n; i++) g.add(new ArrayList<>());
for (int[] arr: edges) { // 建图
int x = arr[0], y = arr[1], w = arr[2];
g.get(x).add(new int[]{y, w});
indegree[y]++;
}
Queue<Integer> q = new LinkedList<>();
for (int i = 1; i <= n; i++) {
if (indegree[i] == 0) {
q.add(i);
}
}
while (!q.isEmpty()) {
int x = q.poll();
for (int[] next: g.get(x)) {
int y = next[0], w = next[1];
if (--indegree[y] == 0) {
q.add(y);
}
}
}
// 队列跑完之后,如果indegree表中,还有非0元素,说明就是存在 环的
}