需求:在一个有序数组中,快速查询某一个值。时间复杂度O(logN),空间复杂度O(1)。
举个例子:
int[] arr = {1, 2, 2, 2, 4, 5};
int target = 2;
以下二分查找的写法就是返回 >=2 的最左位置的下标。也就是返回 1下标。
切记:在数组中查询不到target时,返回的是无效的下标,上层调用时记得加判断。
新的需求:请返回 >=target 中的最右的下标。就只需调用 >= target + 1的函数,然后返回的下标再-1即可。当然上层调用时,还是需要判断返回的下标值是否合法。
LeetCode练习题
二分查找常见的三种写法,注意区分各自的不同之处。
// 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;
}
需求:为了快速的对数组某一段连续的区间进行增删改查操作。时间复杂度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]; // 将左右子树的数据进行汇总
}
}
需求:会频繁的更新数组中某一个位置的数据,但又需要快速的计算某个区间的累加和问题。时间复杂度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 + 1; 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, col2 - 1) + sum(row1 - 1, col1 - 1);
}
}
需求:快速对数组的某一段连续区间进行加减法操作。时间复杂度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];
}
}
需求:给定一组字符串,将这些字符串插入前缀树中,后续可以查询某个子串,在前缀数中有多少个字符串是以这个子串开头的。
应用:后续的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;
}
}
需求:快速的判断某两个节点是否属于同一集合。时间复杂度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; //
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);
}
}
}
需求:给定一篇文章,和一组敏感词汇,问 这一篇文章中有哪些敏感词汇。
思路:先对这些敏感词汇建立一颗前缀树,然后在前缀树上写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;
}
}
}
需求:以时间复杂度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;
}
}
两种实现方式: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();
}
需求:给定两个字符串,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;
}
需求:在一个字符串中,问其中的最长回文子串的长度。也就是最长回文子串问题。时间复杂度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;
}
需求:在一个无序数组中,返回 第 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]; // 返回中间值
}
需求:常常用于子数组求解之类问题。
维持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;
}
}
需求:系统提供的堆,在压入元素进去之后,若此时需要修改堆中某个元素的数据,然后修改之后,堆的结构应该发生改变,系统提供的堆不能实现这个事。需要自己改堆结构。
举个例子:现在有一个大根堆 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); // 更新
}
}
也就是能够排序的一些结构,比如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;
}
}
需求:在一个无序数组中,找一个元素的左右两侧第一个比它大(小)的数。时间复杂度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);
}
}
} 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 ans = null;
SBTNode 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;
}
}
### 15、单调栈
**需求:**在一个无序数组中,找一个元素的左右两侧第一个比它大(小)的数。时间复杂度O(N),空间复杂度O(N)。
细分为单调递增栈、单调递减栈。[LeetCode练习题](https://leetcode.cn/problems/trapping-rain-water/description/)。
- 单调递增栈:从栈顶往下,元素越来越大。用于找元素的两侧第一个比它大的数。
- 单调递减栈:从栈顶往下,元素越来越小。用于找元素的两侧第一个比它小的数。
~~~java
// 单调递减栈。 反之单调递增栈,只需修改第6行while循环中的 <=符号即可
public static void monotonousStack(int[] nums) {
Stack 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);
}
}
最简单的数位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)
核心思想:将指数折半拆分进行计算,例如指数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;
}