基础算法 + 数据结构
一、基础算法
1. BufferReader改善scanner读取慢的问题
😋 比较麻烦的一点就是readLine() 的返回值是 String ,读取整型的时候需要强转
😋 write 不能直接输出 int 类型,得加一个字符或者转换成字符串
public static void main(String[] args) throws IOException { //抛出异常
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(System,out));
while (true) {
String readLine = bufferedReader.readLine(); // 读取一行数据
if (readLine.equals("END")) { //(可选) 判断输入是否结束(END为结束标志,可自定义)
break;
}
out.write("test"+"\n");
out.flush();
}
bufferedReader.close();// 释放资源
}
2. 快速排序
🤮自己照着写的一个版本,能跑,但是判断是否等于标准值那段代码显得重复改到交换上边判断又跑不了,嗯…
/**
* @param arr 待排序数组
* @param left 数组的左边界下标
* @param right 数组的右边界下标
*/
public static void quickSort2(int[] arr, int left, int right) {
// 进来就判断左右边界是否合法(递归的出口)
if (left >= right) {
return;
}
// 标准值 左指针 右指针
int stand = arr[left], i = left, j = right;
while (i < j) {
while (i < j && arr[i] < stand)
i++;
while (i < j && arr[j] > stand)
j--;
if (i < j) {
int t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
// 处理一下 arr[i] 和 arr[j] 都等于标准值的情况(即i j 都不会动了造成死循环)
if (arr[i] == stand) {
i++;
}
if (arr[j] == stand) {
j--;
}
}
// while 出来时,i >= j , 即
// 左递归
quickSort2(arr, left, j);
// 右递归
quickSort2(arr, i, right);
}
😈大佬的版本,更简洁更高效,妙不可言
public static void quickSort1(int[] arr, int start, int end) {
// 进来就判断 start end 有没有越界
if (start < end) {
int stand = arr[start];
int l = start;
int r = end;
while (l < r) {
while (l < r && stand <= arr[r]) {
r--;
}
// 这里把 arr[r] 直接放在了正确的位置上,覆盖了标准值,后边得把标准值放回来
arr[l] = arr[r];
while (l < r && stand >= arr[l]) {
l++;
}
// arr[l] 也放在正确的位置
arr[r] = arr[l];
}
// 这里while循环出来,肯定是 l == r ,因为当出现 l == r 时,所有调整 l 和 r 的语句都不可能执行
// 选取 l 或 r 的位置存放之前丢失的标准值
arr[l] = stand;
quickSort1(arr, start, r);
quickSort1(arr, r+1, end);
}
}
3.归并排序(分治)
🤑main函数里边是测试案例,八万数据一秒内跑完且无误,效率嘎嘎高。
public class MergeSort1 {
static int[] tmp; //定义一个全局变量
public static void main(String[] args) {
// int arr[] = { 8, 4, 5, 7, 1, 3, 6, 2 };
int[] arr = new int[80000];
for (int i = 0; i < arr.length; i++) {
arr[i] = (int) (Math.random() * 800); // 生成一个[0, 8000000) 数
}
tmp = new int[arr.length];
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
String format = sdf.format(new Date());
System.out.println(format);
mergeSort(arr, 0, arr.length - 1);
format = sdf.format(new Date());
System.out.println(format);
// System.out.println(Arrays.toString(arr));
}
public static void mergeSort(int[] arr, int left, int right) {
// 第一步写递归结束的条件
if (left >= right)
return;
int mid = left + right >> 1; // 右移一位相当于除以2
// 一直递归 分,指到分成 left==right 即分到最小单位了,每个组只有一个元素了
mergeSort(arr, left, mid);
mergeSort(arr, mid + 1, right);
int k = 0; // 临时数组的指针
int i = left; // 左边分组的起始指针
int j = mid + 1;// 右边分组的起始指针
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) // 比较左右,<= 小于等于的 先放入临时数组(等于的放入是为了排序的稳定性)
{
tmp[k++] = arr[i++];
} else {
tmp[k++] = arr[j++];
}
}
// 到这,两分组中有一组的数据可能没完全放进去
while (i <= mid)
tmp[k++] = arr[i++];
while (j <= right)
tmp[k++] = arr[j++];
// 数组的复制,把临时数组的数据更新到源数组中
for (i = left, j = 0; i <= right; i++, j++)
arr[i] = tmp[j];
}
}
4. 二分查找
①浮点数的二分查找
😋题目地址:X的平方根
public static double binarySearch(Double x) { // 案例:查找 x 的平方根
double left = 0;
double right = x;
while (right - left > Math.pow(10, -8)) { // 控制循环次数控制精度
double mid = (left + right) / 2;
if (x <= mid * mid) { // x 比区间中点小,在左,右边界缩到中点
right = mid;
} else {
left = mid;
}
}
//到这,left和right相同,返回哪个都一样
return left;
}
②整数的二叉查找(边界处理)
😋参考题目需求地址:(下边代码有主要的思路,但不是该题题解,其实也差不多了)
在排序数组中查找元素的第一个和最后一个位置
public static void main(String[] args) {
// 要查询的值 x
int x = 2; // 3,4,5
// 要查询的源数组
int[] arr = { 1, 2, 2, 2, 2, 2, 3, 3, 4 };
int l = 0, r = arr.length - 1;
// 明确需求,找x的左边界
// 从右往左查,等于 就一直滑,直到第一个小于 x 的值的时候就退出
while (l < r) {
int mid = l + r >> 1;
// 只要中值点的值 >= x,就一直把右边界移到中值点处
if (arr[mid] >= x)
r = mid;
else
l = mid + 1;
}
if (arr[l] != x)
System.out.println("-1 -1");
else {
System.out.print(l + " ");
l = 0;
r = arr.length - 1;
// 明确需求,找x的右边界
// 从左向右查,等于 就一直向右滑,直到碰见第一个大于 x 的值
while (l < r) {
// 例 : l + 1 = R
// mid = (l + R) / 2 = l , 而 更新边界的方式是 l = mid ,避免死循环必须 +1
// 这里多了 + 1 就是为了 mid 能走到 l+1 的地方,因为整除是向下取整的
int mid = l + r + 1 >> 1;
// 只要arr[mid] <= x 成立,直到arr[mid] 大于x
if (arr[mid] <= x)
l = mid;
else
r = mid - 1;
}
System.out.println(l);
}
}
5. 高精度(java)
因为java库里就有大整型类 BigInteger ,建议直接使用库里的,但它不能直接进行常规的运算操作, 得调用它的方法,常用方法如下:
名称 | 方法 |
---|---|
加 | BigInteger add(BigInteger val) |
减 | BigInteger subtract(BigInteger val) |
乘 | BigInteger multiply(BigInteger val) |
除 | BigInteger divide(BigInteger val) |
取模 | BigInteger mod(BigInteger m) |
取相反数 | BigInteger negate() |
😙示例:
public static void main(String[] args) {
// 传一个十进制数的 字符串,转换为 BigInteger 类型
BigInteger b = new BigInteger("-12345678998765432100");
// 求绝对值
BigInteger abs = b.abs();
// 传一个 BigInteger,返回两者之和
BigInteger sum = b.add(abs);
// 比较大小, 返回值 :
// 1 b > abs
// 0 b = abs
// -1 b < abs
int compare = b.compareTo(abs);
}
6. 前缀和(前n项和,可用于求区间和)
① 一维前缀和(数组)
🤗 注意: 求前缀和的数组下标应该从 1 开始,即0不放元素
public static int[] qianZhuiHe(int[] arr) {
int[] res = new int[arr.length];
res[0] = 0;
// 前缀和的初始化
for (int i = 1; i < arr.length; i++) {
res[i] = res[i - 1] + arr[i];
}
return res;
}
② 二维前缀和(矩阵)
😥例题(教程)
😐注意: 源数组 和 前缀和数组 下标都是从 1 开始存
public class QianZhuiHe2 {
// 直接开一个足够大的数组
static int[][] arr = new int[1010][1010];
static int N;
static int M;
public static void main(String[] args) {
// int[][] arr = { { 0, 0, 0, 0, 0 }, { 0, 1, 2, 4, 3 }, { 0, 5, 1, 2, 4 }, { 0, 6, 3, 5, 9 } };
// int[][] res = qianZhuiHe2(arr);
Scanner sc = new Scanner(System.in);
N = sc.nextInt(); // 行
M = sc.nextInt(); // 列
// 切记,下标从 1 开始
for (int i = 1; i <= N; i++) {
for (int j = 1; j <= M; j++) {
arr[i][j] = sc.nextInt();
}
}
int[][] res = qianZhuiHe2(arr);
for (int i = 0; i <= N; i++) {
for (int j = 0; j <= M; j++)
System.out.print(res[i][j] + " ");
System.out.println();
}
}
// 初始化一个二维前缀和的数组
public static int[][] qianZhuiHe2(int[][] arr) {
int[][] res = new int[arr.length + 1][arr[0].length + 1];
// 首先得初始化 0 位置的值
// res[0][0] = 0;
// res[1][0] = 0;
// res[0][1] = 0;
for (int i = 1; i <= N; i++) {
for (int j = 1; j <= M; j++) {
res[i][j] = res[i - 1][j] + res[i][j - 1] - res[i - 1][j - 1] + arr[i][j];
}
}
return res;
}
}
③差分(逆前缀和)(前n项)(一维)
😍注意: 遍历的时候 i 从1 开始,到 <= 边界结束
public class ChaFen1 {
// 直接把固定要用到的数组变量定义成全局变量
static int[] b = new int[100010]; // 差分的数组 (前n项)
// 差分常用操作
public static void main(String[] args) {
int[] arr = { 0, 1, 2, 2, 1, 2, 1 }; // 前缀和数组 (前n项和)
int n = 6; // 这里是元素的数量(保证不越界) (测试用偷懒直接给,oj应该输入)
// 不用arr.length是因为数组可以开始就开到题目要求的最大值
// 初始化 b[] ,逆向思维
for (int i = 1; i <= n; i++) {
insert(i, i, arr[i]);
}
// 模拟输入操作
insert(1, 3, 1);
insert(3, 5, 1);
insert(1, 6, 1);
// 把 差分数组转化成前缀和数组
for (int i = 1; i <= n; i++) {
b[i] += b[i - 1]; // b[1] = b[1] + b[0] b[1] 就变成前 1 项和a[1]了
}
// 输出修改后的数组 3 4 5 3 4 2
for (int i = 1; i <= n; i++) {
System.out.print(b[i] + " ");
}
}
/**
* @param l 左边界
* @param r 右边界
* @param c 添加的数
*/
// 给固定区间(l - r)的值加上 c 的方法
public static void insert(int l, int r, int c) {
b[l] += c;
b[r + 1] -= c;
}
}
④ 差分(二维)
🥰注意:空间一定要开多两个以上
🤣个人理解:
差分: 点(带权值)
前缀和:就是某区域内所有点值的和
public class ChaFen2 {
// 数组行跟列都要开多两个,因为0位置不存储数据,还有插入操作时要用到多一位的空间
static int[][] a = { { 0, 0, 0, 0, 0, 0 }, { 0, 1, 2, 2, 1, 0 }, { 0, 3, 2, 2, 1, 0 }, { 0, 1, 1, 1, 1, 0 },
{ 0, 0, 0, 0, 0, 0 } };
static int[][] b = new int[6][6];
static int n = 3, m = 4;
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
// 初始化差分数组
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
insert(i, j, i, j, a[i][j]);
}
}
// 通过修改差分数组间接修改源数组(前缀和数组)
insert(1, 1, 2, 2, 1);
insert(1, 3, 2, 3, 2);
insert(3, 1, 3, 4, 1);
// 把差分数组再转成前缀和数组
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
b[i][j] = b[i - 1][j] + b[i][j - 1] - b[i - 1][j - 1] + b[i][j];
}
}
System.out.println();
}
// 给部分矩阵加上 c 值的方法
public static void insert(int x1, int y1, int x2, int y2, int c) {
b[x1][y1] += c; // 左上角
b[x2 + 1][y1] -= c; // 左下角
b[x1][y2 + 1] -= c; // 右上角再向右一个点
b[x2 + 1][y2 + 1] += c; // 右下角再向右一个点
}
}
7. 双指针
模板:
public static int lengthOfLongestSubstring(String s) {
int res =0;
// 开一个足够大的数组 记录所有字符 是否存在的信息
boolean[] map = new boolean[256];
// 转成数组方便操作
char[] chars = s.toCharArray();
// 典型的双指针模板 i 为右边界, j 为左边界
for (int i=0,j=0,len = s.length(); i<len; i++){
char c = chars[i];
if(map[c]){ //当map[c]为true,说明子串中已有此 字符
while(map[c]){
map[chars[j]]= false; //把左边界的字符去掉
j++; //左边界向右移
}
}
map[c] = true;
res = Math.max(res,i-j+1);
}
return res;
}
8. 位运算(常用操作)
① 求某数二进制第k位上的值
😎二进制位次
- 数值 :1 0 1 0
- 位次 :3 2 1 0 (从右边第0位开始算起)
public static int valueOfk(int x, int k) {
// 右移 k位就把第 k位上的数值放在第 0 位了 ,再与上 1,返回的结果只剩第 0 位的值
return x >> k & 1;
}
② 求最低位的1(lowbit)
应用:二进制中1的个数
😀正负数的进制转换
负数,例: -1
原码:1000 0000 … 0000 0001
反码:1111 1111 … 1111 1110 (符号不变按位取反)
补码:1111 1111 … 1111 1111
正数,例:1
原码 :0000 0000 … 0000 0001
反码 :0000 0000 … 0000 0001 (正数的反码与原码相同)
补码 :0000 0000 … 0000 0001 (正数的补码、反码、原码都相同) 😂
~1+1:1111 1111 … 1111 1110 + 1 == 补码
得:-x = ~x + 1
😀lowbit:求某数二进制中最低位的 1 (值)
// x = 10 = 1010(二进制) , res = 10(二进制)
public static int lowbit(int x) {
int res = x & -x;
//在二进制中, -x = ~x+1
return res;
}
😋推导(假设只有16位,其实int有32位)
9.离散化 1.0
(1)开个数组存下标
(2)List 排序+去重
(3)坐标的映射
/*假定有一个无限长的数轴,数轴上每个坐标上的数都是0。现在,我们首先进行n次操作,每次操作将某一位置上的数加C接下来,进行 m次询问,每个询问包含两个整数和,你需要求出在区间,之间的所有数的和
输入格式
第一行包含两个整数n和m。
接下来n 行,每行包含两个整数 a 和 c
再接下来m行,每行包含两个整数和r。
输出格式
共m行,每行输出一个询问中所求的区间内数字和。
数据范围
-10'< 2 < 109
* */
public class LiSanHua {
static int n, m;
// a 是 前n项 s 是前 n 项和
static int a[] = new int[1010], s[] = new int[1010];
// 真实下标和映射的下标的关系(核心)
static List<Integer> alls = new ArrayList<>();
// 保存操作输入的值(不重要)
static HashMap<Integer, Integer> adds = new HashMap<>();
static HashMap<Integer, Integer> query = new HashMap<>();
public static void main(String[] args) {
// 读取输入(不重要)
n = 3;
m = 3;
// 参数的下标和对应的值
adds.put(1, 2);
adds.put(3, 6);
adds.put(7, 5);
// 把下标放进alls统一离散化
alls.add(1);
alls.add(3);
alls.add(7);
// 要查询区间的左边界和右边界
query.put(1, 3);
query.put(4, 6);
query.put(7, 8);
// 得把用到的所有下标都传进 alls 里边去
alls.add(1);
alls.add(3);
alls.add(4);
alls.add(6);
alls.add(7);
alls.add(8);
// 离散本 散
// 对alls进行排序和去重
alls.sort(Comparator.naturalOrder()); // 排序,直接调用库里的方法,传一个比较器
// 由于java里边没有unique函数,所以手写一个 去重的方法
int right = unique(alls);
alls = alls.subList(0, right); // List.subList(左边界(包含),右边界(不包含))
// 生成 前n项(差分)
for (int k : adds.keySet()) {
int x = find(k); //通过 find(原始下标) 在 alls 数组中找到存储 '原始下标' 的位置 x (映射值)
a[x] += adds.get(k); // 给差分数组的 x 位加上 adds[原始下标]的值
}
// 生成前n项和(前缀和)
for (int i = 1; i <= alls.size(); i++) {
s[i] = s[i - 1] + a[i];
}
// 执行查询操作
for (int key : query.keySet()) {
int l = find(key);
int r = find(query.get(key));
System.out.println(s[r] - s[l - 1]);
}
}
// 二分查找
static int find(int k) {
int l = 0, r = alls.size() - 1;
while (l < r) {
int mid = l + r >> 1;
if (alls.get(mid) >= k)
r = mid;
else
l = mid + 1;
}
return r + 1; // 因为差分数组的下标是从1开始的,所以要 +1
}
// 返回值是不重复数组的最后一位数的下标 +1
static int unique(List<Integer> alls) {
int j = 0;
for (int i = 0; i < alls.size(); i++) {// 因为是有序的数组,所以只要不等于前一位的元素那就是不重复
if (i == 0 || alls.get(i) != alls.get(i - 1)) {
alls.set(j++, alls.get(i));
}
}
return j;
}
}
10.区间合并
😁原题地址:合并区间
public int[][] merge(int[][] intervals) {
// 先按左边界进行升序排序
Arrays.sort(intervals,new Comparator<int[]>() {
public int compare(int[] a,int[] b) {
return a[0]-b[0];
}
});
int[][] res =new int[intervals.length][2];
int index = -1;
for(int[] interval : intervals) {
// 数组为空 当前左边界 > 前一个区间的右边界
if(index == -1 || interval[0]>res[index][1]) {
res[++index] = interval;
}else {
// 比较前区间右边界和新区间右区间的大小(取大值)
res[index][1] = Math.max(interval[1], res[index][1]);
}
}
// res数组经过合并区间,用不到 intervals.length 长的空间
Arrays.copyOf(res, index+1); // inmdex是下标,从 0 开始,copyOf(源数组,拷贝的长度):index+1
return res;
}
二、数据结构
1. 链表(数组实现)(静态链表)
😤用数组比较快,可以节省 new Node()的时间
😂数组实现单链表
// 使用数组实现单链表
static int N = 1010; // 题目要求空间+10
static int head; // 头节点的下标
static int e[] = new int[N];// 结点的值
static int ne[] = new int[N];// 结点的下一个结点的地址
static int idx;// 数组的末尾下标,方便添加结点
// 初始化
static void init() {
head = -1;
idx = 0;
}
// 头插法
static void addTop(int x) {
e[idx] = x;
ne[idx] = head;
head = idx++;
}
// 将x插入到下标为k的结点后边
static void add(int x,int k) {
e[idx] = x;
ne[idx] = ne[k];
ne[k] = idx++;
}
// 删除下标k后一位的结点
static void del(int k) {
ne[k] = ne[ne[k]];
}
😋数组实现双链表
// 数组实现双指针链表
static int N = 1010;
// 值、左指针、右指针数组,数组的末尾索引
static int e[] = new int[N],l[] = new int[N],r[] = new int[N],idx;
// 初始化
static void init() {
// 偷懒版
// 0 表示左端点,1 表示右端点
r[0] = 1; //左端点的右指针
l[1] = 0;
idx = 2;
}
// 在下标为k的后边加上x
static void add(int k,int x) {
e[idx] = x;
r[idx] = r[k];
l[idx] = k;
//给下一个结点(k+1)的左指针赋上新结点的下标一定要在上一个结点(k)右指针的更改之前
l[r[k]] = idx;
r[k] = idx;
}
// 删除第k(下标)个点
static void remove(int k) {
r[l[k]] = r[k];
l[r[k]] = l[k];
}
2. 栈和队列
👼数组模拟栈
// 数组模拟栈
int[] statck;
int tt = 0; // 栈顶
// 入栈
void add(int x) {
statck[tt++] = x;
}
// 出栈
int pop() {
return statck[tt--];
}
// 判空
boolean idEmpty() {
return tt<=0;
🤤数组模拟队列
// 数组模拟队列
int[] q;
int hh, tt; // 表示队头和队尾
// 插入
void add(int x) {
q[tt++]=x; // 补充:变量赋值语句必须写在方法体中
}
// 出队
int get() {
return q[hh--];
}
// 判空
boolean idEmpty() {
return hh>=tt;
}
🤔单调栈(去除单调性不同的元素)
public static void main(String[] args) {
// 求比当前元素更小的前一个元素
int[] inp = { 3, 4, 2, 7, 5 }; // 测试数据 参考输出:[-1,3,-1,2,2]
int[] stack = new int[5];
int tt = 0;
for (int i = 0; i < inp.length; i++) {
int x = inp[i];
while (tt > 0 && x <= stack[tt]) {// 非空的情况下
tt--; // 只要当前元素不比栈顶元素大,栈顶元素出栈
}
if (tt == 0) {// 空栈 即前边没有比x小的元素了
System.out.println("-1 ");
} else {
System.out.println(stack[tt]); // 栈不为空,栈顶元素即是最近一个小于x的元素,因为比x大的都出栈了
}
stack[++tt] = x;
}
}
😚单调队列
/*
* 求滑动窗口的最小值和最大值 给定一个大小为n<10的数组。 有一个大小为k的滑动窗口,它从数组的最左边移动到最右边您只能在窗口中看到k个数字
* 每次滑动窗口向右移动一个位置
*
* 输入包含两行。 第一行包含两个整数n和k,分别代表数组长度和滑动窗口的长度第二行有n个整数,代表数组的具体数值。 同行数据之间用空格隔开。 输出格式
* 输出包含两个。 第一行输出,从左至右,每个位置滑动窗口中的最小值。第二行输出,从左至右,每个位置滑动窗口中的最大值
*/
public static void main(String[] args) {
int n = 8, k = 3;
int a[] = { -11, -10, -9, -3, 5, 3, 6, 7 };// 测试数据
//单调递增的队列,队头元素是最小值
int[] q = new int[a.length + 5]; // 队列维护滑动窗口 里边存的是 下标
int hh = 0, tt = -1; // 队头(先入的为头,即滑动窗口的左端) 队尾
for (int i = 0; i < n; i++) {//用 i 来枚举每一个窗口
// 判断队头(左端) 是否已经滑出窗口(i-k+1即是从下标i开始往前走3位)
if (hh <= tt && i - k + 1 > q[hh])
hh++;//队头出队
// 核心
// 如果新来的元素比队列队尾值小,那就把队列的最小元素拿出,把当前元素入队
while (hh <= tt && a[q[tt]] >= a[i]) {
tt--; // 保证队列头的元素最小
}
q[++tt] = i;
// 只要i下标大于2,每移动一位输出一个前面的最小值
if (i >= k - 1) // 控制输出,当滑动窗口中元素大于三个才输出
System.out.print(a[q[hh]] + " ");
}
}
3. KMP
😭
// kmp匹配过程
/**
* @param s1 源串
* @param s2 子串
* @param next 部分匹配表
* @return
*/
public static int kmpSearch(String s1, String s2, int[] next) {
for (int i = 0, j = 0; i < s1.length(); i++) {
// ++ 后的j和 ++ 后 i继续匹配,若不成功,找最大回文子串的长度
while (j > 0 && s1.charAt(i) != s2.charAt(j)) {
j = next[j - 1];
}
if (s1.charAt(i) == s2.charAt(j)) // i , j 位匹配成功,j++
j++;
if (j == s2.length())// 找到返回起始下标
return i - j + 1;
}
return -1;
}
// 求next数组(部分匹配表)
/**
* @param dest 子串
* @return
*/
public static int[] kmpNext(String dest) {
int[] next = new int[dest.length()];
next[0] = 0;
for (int i = 1, j = 0; i < next.length; i++) {
while (j > 0 && dest.charAt(i) != dest.charAt(j))// 更新已经匹配上的子串长度
j = next[j - 1];
if (dest.charAt(i) == dest.charAt(j))
j++;// 每一次循环 i只 +1,所以j在基础上顶多 +1
next[i] = j;
}
return next;
}
4. trie字典树
😑 每个结点有26个子结点,也有对应的26个特殊的next指针 son[ idx ] [ 字母 ]
🤠 son[x] [y] :存 idx,非 0 即有值,idx是(x,y)的特殊标记,用于 cnt[idx] 查看是否有以此为终点的字符串
// 注意 10 是题目给的字符串长度
static int idx; // 当前 son二维数组中的 一维下标(即第一个[ ](方便开辟空间)
static int[][] son = new int[10][26]; // 结点数组 一维:树的层数 二维:字符的对应数值(特殊的next指针)
static int cnt[] = new int[10]; // 记录以该点结束的字符串有多少个
// trie树的插入操作
static void insert(char[] str) {
//相当于根节点
int p = 0;
for (int i = 0; i < str.length; i++) {
int u = str[i] - 'a'; // 把字符转换为0到25的数字表示
if (son[p][u] == 0)
//以前没有开辟字符,直接新开辟一个一维数组空间,即二维数组的一个下标
son[p][u] = ++idx;
p = son[p][u];
}
cnt[p]++;
}
// 查询操作
static int query(char str[]) {
int p = 0;
for (int i = 0; i < str.length; i++) {
int u = str[i] - 'a';
if (son[p][u] == 0) //一个字符不符合,直接pass
return 0;
p = son[p][u];
}
return cnt[p];
}
5. 并查集
😀合并2个集合
😁判断两个元素是否在一个集合当中
😏还可以统计一下集合元素数量
👨🏫基本原理:
每个集合用一棵树来表示。树根的编号就是整个集合的编号。每个节点存储它的父节点,p[x]表示x的父节点。
static int N = 1010;
static int p[] = new int[N]; //记录父节点的集合
static int size[] = new int[N]; //统计当前集合的元素个数
int m, n;
// 查找x的祖宗(根)结点,顺便压缩路径
static int find(int x) {
if (p[x] != x)
p[x] = find(p[x]);
return p[x];
}
// 并查集操作
public static void main(String[] args) {
// 初始化操作
for (int i = 0; i < N; i++) {
p[i] = i; //先初始化 每一个集合都是一个祖宗集合
size[i]=1;
}
int a = 0, b = 1; // 假设
// 合并并查集
p[find(a)] = find(b);// a的父结点的父结点 = b的父节点
size[find(b)] += size[find(a)]; //更新元素数量,只维护父节点的元素数量
// 判断两元素是否在同一集合
String s = (find(a) == find(b)) ? "same" : "dif";
}
6. 堆
👼 完全二叉树(一维数组存储)
👵 基本操作
1.插入一个数: heap[ ++ size] = x; up(size);2.求集合当中的最小值heap[1];
3.删除最小值: heap[1] = heap[size]; szie – ; down(1);
4.删除任意一个元素: heap[k] = heap[size]; size – ; down(k); up(k);
5.修改任意一个元素: heap[k] = x; down(k); up(k);
static int[] h = new int[1010];// 存储根的一维数组(从下标1开始放)
static int size; // 记录堆的元素个数
static void up(int x) {
while (x / 2 > 0 && h[x / 2] > h[x]) {
// 交换值
int tmp = h[x];
h[x] = h[x / 2];
h[x / 2] = tmp;
x /= 2;
}
}
static void down(int x) {
// 找寻最小值的下标
int t = x;
if (2 * x <= size && h[2 * x] < h[x])
t = 2 * x;
if (2 * x + 1 <= size && h[2 * x + 1] < h[x])
t = 2 * x + 1;
if (x != t) {
int tmp = h[x];
h[x] = h[t];
h[t] = tmp;
down(t);
}
}
public static void main(String[] args) {
int n = 100;// 设
// 建堆(小根堆)
// 1. 初始化数据
for (int i = 0; i < n; i++) {
h[i] = (int) (Math.random() * n);
}
size = n;
// 2. 转换为小根堆
for (int i = n / 2; i >= 0; i--) { // 对倒数第二层以上的元素进行down(i),就可以实现 O(n)生成小根堆
down(i);
}
}
7. 哈希表
👼 拉链法
// 拉链法
static int N = 103; // 取大于数据长度的第一个质数
static int[] h = new int[N];// 哈希表
static int[] e = new int[N];// 单链表结点的值
static int[] ne = new int[N]; // 单链表的next指针数组
static int idx = 0; // 当前地址,便于元素的插入
// 插入
static void insert(int x) {
int k = (x % N + N) % N; // 哈希函数,将x映射到 0 - N 的范围
// 头插法
e[idx] = x; // 拿一个结点存放该值
ne[idx] = h[k]; // next指针指向 k 位置的头结点
h[k] = idx;// 给k位置的头结点更新地址
idx++;
}
// 查找
static boolean find(int x) {
// 以相同的哈希函数映射地址
int k = (x % N + N) % N;
// 遍历该地址的链表
for (int i = 0; i != -1; i = ne[i]) {
if (e[i] == x)
return true;
}
return false;
}
public static void main(String[] args) {
// 初始化hash表,全部置为 -1,-1代表尾结点
Arrays.fill(h, -1);
// 操作......
}
👩🦰 开放寻址法
static int N = 200003; // 开放寻址法,数组长度得开数据个数的两到三倍 取模数(模) 一般取大于 数据个数 的 最小质数
static int[] h = new int[N]; // 哈希表
static int nul = 0x3f3f3f3f; // 空值判定
// 开放寻址法
static int find(int x) {
int k = (x % N + N) % N; // k取正,在数组中找到对应的坑位
while (h[k] != nul && h[k] != x) {
k++; // 对应的坑位被占,顺延找坑位
if (k == N)
k = 0; // 找到尾就从头开始
}
return k; // 返回x存放的空间所在数组的下标地址
}
public static void main(String[] args) {
// TODO Auto-generated method stub
// System.out.println(-10 % 3); //java负数取模得负数 -10 % 3 = -1
Arrays.fill(h, 0x3f3f3f3f); //哈希表初始化成无数据状态
Scanner sc = new Scanner(System.in);
int x = sc.nextInt();
int k = find(x);
// 插入
h[k] = x;
// 查询
if (h[k] != x) {
System.out.println("查询失败");
} else {
System.out.println("查询成功");
}
}
8. 字串哈希
👨🦰 字符串转为数值再哈希
static int N = 1000010;
static int P = 131; // 模
static long h[] = new long[N];// 前n个字串的哈希值
static long p[] = new long[N];// p的n次方,n对应下标
static long get(int l, int r) {
//求区间字串对应的数值 + 哈希函数映射
return (long) ((h[r] - h[l - 1] * p[r - l + 1]) % Math.pow(2, 63));
}
// 字串哈希
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();// 字串的长度
String s = sc.next();// 要哈希的字符串
char[] arr = s.toCharArray();
// 初始化
p[0] = 1; // p 的 0 次方为 1
for (int i = 0; i < n; i++) {
p[i] = p[i - 1] * P;
h[i] = h[i - 1] * P + arr[i]; //此处的arr[i] 是字符,+ 的时候用的是它的ASC码值
}
}
杂记🐷
-
String.toCharArray() : 字符串转字符数组
-
Arrays.copyOf( T[ ], int length:数组拷贝,返回一个新数组 T[ ] 是原数组; length是截取的长度(即要拷贝的长度)
-
HashSet) A.retainAll(B) : 取A B的并集重新赋给 A
-
Hashset : 常用于去重
常用apl: add(), remove(), contains(), size(), iterator() 元素随机返回 -
Arrays.sort(T[] a, Comparator<? super T> c) :java库里边的排序方法
Arrays.sort(intervals,new Comparator<int[]>() {//比较器的定义
public int compare(int[] a,int[] b) {
return a[0]-b[0]; // 升序
}
});
-
ArrayList.sort(Comparator.naturalOrder()) : 按照自然升序排序
ArrayList.sort(Comparator.reverseOrder()) : 按照降序排序
两者都是动态改变数组列表的元素顺序,并不会有1返回值 -
List subList(int fromIndex, int toIndex)
返回此列表中指定的 fromIndex (包括)和 toIndex之间的独占视图。 -
List.subList(左边界(包含),右边界(不包含))
-
Arrays.fill(数组,某值) :将数组所有元素全部初始化成某值
🤗 注意:不能是二维数组 -
0x3f3f3f3f: 无穷大,相当于 10^9 级,一般用不到这么大
-
HashMap.getOrDefault(Object key, V defaultValue)
有值则返回该键的映射值,未赋值则返回默认值
👨🏫参考:Acwing
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器