1 介绍
1.1 概念
01.算法
操作数据、解决程序问题的一组方法
02.衡量
a.时间复杂度
程序所消耗的时间,一个算法中的语句执行次数称为语句频度或时间频度
b.空间复杂度
程序所占用的内存空间,在规模为n的情况下额外消耗的储存空间
c.平均时间复杂度
略
1.2 线性结构
01.一维数组
内存中连续分配、数组元素通过下标为0进行分配
连续内存空间,长度固定,一般不可动态扩容,随机访问
常见的Uitil有:String [],int [],ArrayList,Vector,CopyOnWriteArrayLis
ArrayList和Vector的区别是:Vector是线程安全的,方法同步
CopyOnWriteArrayList也是线程安全的但效率要比Vector高很多
02.栈
线性表,只允许栈顶操作,栈底不允许操作,
先进后出,放入元素-出栈/取出元素入栈
常见的Uitil有:LinkedList,LinkedMap等
03.链表
通过链表的指针地址实现,有单链表,双向链表,循环链表
非连续内存空间,长度可以动态变化,顺序访问
常见的Uitil有:java.util.Stack
04.队列
线性表,
先进先出,放入元素-入队/取出元素-出队
如线程池,mq,连接池等。
05.串
字符串,是由N个字符组成的优先序列。
在Java里面就是指String,而String里面是由chat[]来进行储存。
1.3 非线性结构
01.多维数组
略
02.集合
由一个或多个确定的元素所构成的整体叫做集合。在Java里面可以去广义理解为实现了Collection接口的类叫集合。
03.树
最少有一个根节点组成,可以有多个子节点。
二叉树、 一般二叉树、完全二叉树
04.图
由结点的有穷集合V和边的集合E组成
05.散列表
哈希表,根据关键码和值 (key和value) 直接进行访问的数据结构
Java中的hashCode:class都是Object的子类,既所有的class都会有默认Object.java里面的hashCode的方法,
如果自己没有重写,默认情况就是native方法通过对象的内存的+对象的值然后通过hash散列算法计算出来个int的数字。
1.4 附:8种数据结构
01.顺序表(数组)
a.介绍
顺序表是以数组为基础的数据结构,具有随机访问的特点,支持增删改查等基本操作。
b.算法技巧
线性枚举:逐一列举数组中的元素,常用于简单的查找或统计问题。
前缀和:用来快速计算某段连续子数组的和。
双指针:在数组上使用两个指针以不同的步伐进行移动,常用于滑动窗口、区间查找等。
二分枚举:在有序数组中查找元素,时间复杂度为O(log n)。
三分枚举:用于查找单峰函数的极值点。
离散化:将数据映射到较小范围的整数,用于降低复杂度。
c.排序算法
冒泡排序:每次比较相邻元素并交换,时间复杂度O(n^2)。
选择排序:每次选择最小值并放置于前端,时间复杂度O(n^2)。
插入排序:每次将一个新元素插入已排序的序列,时间复杂度O(n^2)。
希尔排序:插入排序的改进版,使用间隔排序,时间复杂度O(n log n)。
归并排序:分治法排序,时间复杂度O(n log n)。
快速排序:分区交换排序,时间复杂度O(n log n)。
堆排序:利用堆这种数据结构排序,时间复杂度O(n log n)。
计数排序:适合元素范围小的整数排序,时间复杂度O(n+k)。
基数排序:对数字按位数分配排序,时间复杂度O(n*k)。
d.常用思想
模拟:直接按照题意逐步实现。
贪心:每一步选择局部最优解,求全局最优。
02.链表
a.介绍
链表是一种动态数据结构,适合频繁插入和删除操作。
b.类型
单向链表:只有一个方向的指针。
双向链表:每个节点有前后两个指针,便于双向遍历。
03.栈
a.介绍
栈是一种LIFO(后进先出)的数据结构。
b.类型
普通栈:用于函数调用、括号匹配等。
单调栈:栈内元素单调递增或递减,用于解决区间最值问题。
04.队列
a.介绍
队列是一种FIFO(先进先出)的数据结构。
b.类型
普通队列:常用于广度优先搜索。
双端队列:可从两端插入和删除元素。
单调队列:队列中的元素保持单调性,适用于滑动窗口问题。
05.字符串
a.介绍
字符串问题常见的有模式匹配、前缀树等。
b.算法
KMP:用于字符串模式匹配,时间复杂度O(n)。
字典树(Trie):用于词频统计、单词查找。
马拉车算法:用于求最长回文子串,时间复杂度O(n)。
AC自动机:多模式串匹配,结合字典树和KMP。
后缀数组:用于字符串后缀排序,求最长重复子串。
BM算法:用于长文本中快速定位模式串。
06.树
a.介绍
树是分层结构的数据结构,广泛用于存储有层次关系的数据。
b.类型
二叉树:每个节点最多有两个子节点。
二叉搜索树:左子树小于根,右子树大于根。
AVL树:自平衡的二叉搜索树。
线段树:动态维护区间和、区间最值等。
霍夫曼树:用于数据压缩。
堆:一种完全二叉树,常用于优先队列。
红黑树:平衡二叉树,插入和删除操作较快。
伸展树:用于动态维护节点的访问频率。
左偏树:用于优先队列合并。
Treap:结合二叉搜索树和堆的性质。
B+树:多路搜索树,常用于数据库索引。
树链剖分:将树拆分成链以便于查询。
07.图
a.介绍
图是一种复杂的结构,表示节点间的关系。
b.常用算法
二分图:图的节点可以分成两部分,且没有同一部分内部的边。
最短路算法:如Dijkstra、Bellman-Ford、Floyd等。
最小生成树:如Prim、Kruskal算法。
最近公共祖先:用于求解两个节点的最近公共父节点。
深度优先搜索(DFS):图的遍历算法。
强连通分量:求图中的强连通子图。
双连通分量:图中没有割点的最大子图。
2-SAT问题:布尔表达式可满足性问题。
欧拉回路和哈密尔顿回路:图的特殊路径问题。
广度优先搜索(BFS):图的遍历,常用于最短路径。
拓扑排序:用于有向无环图的线性排序。
A*算法:用于最短路径的启发式搜索。
稳定婚姻问题:配对问题。
双向广搜:双端搜索,减小搜索空间。
差分约束:用于约束问题建模。
并查集:用于动态连通性问题。
哈希表和跳跃表:快速查找数据结构。
树状数组:用于动态维护前缀和等。
最大流:如Edmonds-Karp、Dinic算法。
08.动态规划
a.介绍
动态规划是分治思想的延伸,常用于求解最优子结构问题。
b.类型
递推:通过状态转移方程不断递推求解。
线性DP:如最长子序列问题。
记忆化搜索:将递归结果存储以避免重复计算。
背包问题:多种背包类型的动态规划求解。
树形DP:树结构上的动态规划。
区间DP:如矩阵连乘问题。
数位DP:处理数值的动态规划。
状态压缩DP:将状态以二进制压缩表示。
2 常见算法
2.1 排序算法
00.八大排序算法
1.直接插入排序(Insertion Sort)
2.希尔排序(Shell Sort)
3.选择排序(Selection Sort)
4.堆排序(Heap Sort)
5.冒泡排序(Bubble Sort)
6.快速排序(Quick Sort)
7.归并排序(Merging Sort)
8.基数排序(Radix Sort)
01.冒泡排序
a.说明
冒泡排序是最容易实现的排序, 最坏的情况是每次都需要交换, 共需遍历并交换将近n²/2次, 时间复杂度为O(n²).
最佳的情况是内循环遍历一次后发现排序是对的, 因此退出循环, 时间复杂度为O(n). 平均来讲, 时间复杂度为O(n²).
b.示例
/**
* 冒泡排序
*
* ①. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
* ②. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
* ③. 针对所有的元素重复以上的步骤,除了最后一个。
* ④. 持续每次对越来越少的元素重复上面的步骤①~③,直到没有任何一对数字需要比较。
* @param arr 待排序数组
*/
public static void bubbleSort(int[] arr){
for (int i = arr.length; i > 0; i--) { //外层循环移动游标
for(int j = 0; j < i && (j+1) < i; j++){ //内层循环遍历游标及之后(或之前)的元素
if(arr[j] > arr[j+1]){
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
System.out.println("Sorting: " + Arrays.toString(arr));
}
}
}
}
02.选择排序
a.说明
每一趟从待排序的数据元素中选出最小(或最大)的一个元素,顺序放在已排好序的数列的最后,直到全部待排序的数据元素排完。
选择排序是不稳定的排序方法。
b.示例
int[] arr = { 1, 5, 6, 12, 4, 9, 3, 23, 39, 403, 596, 87 };
System.out.println("排序前:" + Arrays.toString(arr));
for (int i = 0; i < arr.length; i++) {
int lowerIndex = i;
for (int j = i + 1; j < arr.length; j++) {
// 循环找出最小的一个索引
if (arr[j] < arr[lowerIndex]) {
lowerIndex = j;
}
}
// 交换
int temp = arr[i];
arr[i] = arr[lowerIndex];
arr[lowerIndex] = temp;
}
System.out.println("排序后:" + Arrays.toString(arr));
03.插入排序
a.说明
把要排序的数组分成两部分:第一部分包含了这个数组的所有元素,但将最后一个元素除外,而第二部分就只包含这一个元素。
在第一部分排序后,再把这个最后元素插入到此刻已是有序的第一部分里的正确位置中。
b.示例
int[] arr = { 2, 1, 5, 6, 12, 4, 9, 3, 23, 39, 403, 596, 87 };
System.out.println("排序前:" + Arrays.toString(arr));
// i从一开始,因为第一个数已经是排好序的
for (int i = 1; i < arr.length; i++) {
for (int j = i; j > 0; j--) {
if (arr[j] < arr[j - 1]) {
// 元素交换
int temp = arr[j];
arr[j] = arr[j - 1];
arr[j - 1] = temp;
}
}
}
System.out.println("排序后:" + Arrays.toString(arr));
04.希尔排序
a.说明
简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。
b.示例
/**
* 希尔排序
*
* @param array
* @return
*/
public static int[] ShellSort(int[] array) {
int len = array.length;
int temp, gap = len / 2;
while (gap > 0) {
for (int i = gap; i < len; i++) {
temp = array[i];
int preIndex = i - gap;
while (preIndex >= 0 && array[preIndex] > temp) {
array[preIndex + gap] = array[preIndex];
preIndex -= gap;
}
array[preIndex + gap] = temp;
}
gap /= 2;
}
return array;
}
2.2 冒泡排序
01.魔术步骤的算法化拆解
a.初始状态
将筷子、杯子和勺子随机排列在左、中、右三个位置
b.筷子与左边交换
如果筷子不在最左边,则与左边物品交换位置,交换后的摆放情况如下
c.杯子与右边交换
如果杯子不在最右边,则与右边物品交换位置,交换后的摆放情况如下
d.勺子与左边交换
如果勺子不在最左边,则与左边物品交换位置,交换后的摆放情况如下
e.最终结果
无论初始排列如何,杯子最终都会出现在最右侧
02.冒泡排序是一种通过相邻元素交换逐步将最大值“冒泡”到序列末端的算法
a.核心特征
交换操作:通过多次遍历,每次比较相邻元素并交换位置。
确定性结果:无论初始排列如何,最终总能使元素有序。
循环结构:依赖循环次数与条件判断实现目标。
b.在刘谦的魔术中,某些操作步骤与冒泡排序的交换逻辑不谋而合
交换操作的相似性:魔术中的每一步交换都类似于冒泡排序中的相邻元素交换。例如,筷子与左边交换、杯子与右边交换,都是通过局部调整逐步将目标物品移动到指定位置。
确定性结果的映射:无论初始排列如何,魔术最终总能将杯子移动到最右侧,这与冒泡排序的确定性结果一致。
循环结构的体现:魔术中的三步交换操作可以看作是一个循环过程,类似于冒泡排序的多轮遍历。
c.数学共性:置换群与位置控制
从数学角度看,魔术中的操作可抽象为**置换群(Permutation Group)**的应用。例如:
1.冒泡排序的交换步骤:每次交换相当于对排列施加一次置换操作。
2.魔术中的牌序调整:插入、丢弃和循环移动均可视为对物品排列的置换。
-----------------------------------------------------------------------------------------------------
以2025年刘谦魔术中涉及杯、勺、筷的操作为例:
1.交换规则:筷子与左侧交换、杯子与右侧交换、勺子与左侧交换。
2.冒泡排序映射:通过三次交换操作,杯子被强制移动到最右侧,类似于冒泡排序中将最大值“冒泡”到末尾。
d.魔术与算法的本质联系
刘谦的魔术设计巧妙融合了数学与表演艺术,其底层逻辑与计算机算法的共性体现在:
确定性流程:通过固定规则保证结果的唯一性。
位置操作:利用循环、置换等操作控制元素位置。
抽象映射:将复杂问题简化为数学模型(如置换群或冒泡排序)。
-----------------------------------------------------------------------------------------------------
尽管2025年魔术的核心更贴近排列组合问题,但其步骤中隐含的交换与循环逻辑与冒泡排序的哲学不谋而合。
这种跨界关联不仅展现了数学的普适性,也为我们理解魔术的“奇迹时刻”提供了新的视角。
e.原理
冒泡排序是一种简单的比较排序算法,其核心思想是通过多次比较相邻元素并交换位置,将最大(或最小)的元素逐步 “冒泡” 到数组末尾(或开头)。
-----------------------------------------------------------------------------------------------------
以下是用 JavaScript 实现冒泡排序的代码
function bubbleSort(arr) {
let len = arr.length;
for (let i = 0; i < len - 1; i++) {
for (let j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
// 交换arr[j] 与 arr[j + 1]
[arr[j],arr[j+1]] = [arr[j + 1],arr[j]];
}
}
}
return arr;
}
// 测试示例
let numbers = [64, 34, 25, 12, 22, 11, 90];
console.log(bubbleSort(numbers));
3 字符串
3.1 每个字符出现的次数
01.说明
a.HashMap
使用 HashMap 来统计字符出现的次数,适用于任何字符集。首先将字符串转换为字符数组,遍历每个字符,
并在 HashMap 中更新每个字符的出现次数。最后输出每个字符的计数。
b.Array
使用一个大小为 256 的 int[] 数组来存储所有字符的计数,适用于 ASCII 字符集。遍历字符串中的字符,
将其 ASCII 值作为数组索引,直接增加计数。最后输出出现次数大于 0 的字符及其次数。
c.Stream
使用 Java 8 引入的 Stream API,通过 Collectors.groupingBy 和 Collectors.counting 来统计字符出现次数。
mapToObj 将每个字符的 int 值转换为 Character,然后按字符进行分组并计数。
d.TreeMap
使用 TreeMap 来统计字符出现的次数,并按字典顺序输出结果。TreeMap 会自动按字符排序,适合需要有序输出的场景。
e.ApacheCommons
使用 Apache Commons Lang 库中的 StringUtils.countMatches 方法来统计每个字符的出现次数。
此方法非常简洁,但需要额外引入 Apache Commons Lang 库。
02.代码
import java.util.*; // 导入集合框架
import org.apache.commons.lang3.StringUtils; // 导入Apache Commons Lang库
public class CharacterCount {
// 方法一:使用 HashMap 来统计字符出现的次数
public static void countUsingHashMap(String str) {
// 将字符串转化为字符数组
char[] chars = str.toCharArray();
// 创建一个 HashMap 来存储字符及其出现次数
HashMap<Character, Integer> hm = new HashMap<>();
// 遍历字符数组
for (char c : chars) {
// 使用 containsKey 判断字符是否已经出现过
if (!hm.containsKey(c)) {
// 如果没有出现过,初始化次数为 1
hm.put(c, 1);
} else {
// 否则,获取当前次数并加 1
hm.put(c, hm.get(c) + 1);
}
}
// 输出每个字符及其出现次数
System.out.println("方法一:使用 HashMap");
for (Character key : hm.keySet()) {
System.out.println(key + " === " + hm.get(key));
}
}
// 方法二:使用 int[] 数组(仅适用于 ASCII 字符)
public static void countUsingArray(String str) {
// 创建一个大小为 256 的数组,用于存储所有 ASCII 字符的出现次数
int[] count = new int[256];
// 遍历字符串中的每个字符
for (char c : str.toCharArray()) {
// 将字符的 ASCII 值作为数组的索引,增加对应位置的计数
count[c]++;
}
// 输出每个字符及其出现次数
System.out.println("方法二:使用 int[] 数组");
for (int i = 0; i < 256; i++) {
if (count[i] > 0) {
System.out.println((char) i + " === " + count[i]);
}
}
}
// 方法三:使用 Java 8 的 Stream API 来统计字符出现的次数
public static void countUsingStream(String str) {
// 使用 Stream API 统计每个字符出现的次数
Map<Character, Long> charCountMap = str.chars()
.mapToObj(c -> (char) c) // 将 int 转换为 Character
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting())); // 按字符分组并计数
// 输出每个字符及其出现次数
System.out.println("方法三:使用 Stream API");
charCountMap.forEach((key, value) -> System.out.println(key + " === " + value));
}
// 方法四:使用 TreeMap 来统计字符出现的次数,并按字典顺序输出
public static void countUsingTreeMap(String str) {
// 创建一个 TreeMap,按字符的自然顺序排序
Map<Character, Integer> charCountMap = new TreeMap<>();
// 遍历字符串中的每个字符
for (char c : str.toCharArray()) {
// 使用 getOrDefault 方法来统计每个字符的次数
charCountMap.put(c, charCountMap.getOrDefault(c, 0) + 1);
}
// 输出每个字符及其出现次数
System.out.println("方法四:使用 TreeMap");
charCountMap.forEach((key, value) -> System.out.println(key + " === " + value));
}
// 方法五:使用 Apache Commons Lang 库中的 StringUtils 来统计字符出现的次数
public static void countUsingApacheCommons(String str) {
// 使用 StringUtils.countMatches 来统计每个字符出现的次数
System.out.println("方法五:使用 Apache Commons Lang");
for (char c : str.toCharArray()) {
System.out.println(c + " === " + StringUtils.countMatches(str, String.valueOf(c)));
}
}
public static void main(String[] args) {
// 示例字符串
String input = "aabccddeea";
// 调用每种方法进行字符计数
countUsingHashMap(input); // 使用 HashMap
System.out.println();
countUsingArray(input); // 使用 int[] 数组
System.out.println();
countUsingStream(input); // 使用 Stream API
System.out.println();
countUsingTreeMap(input); // 使用 TreeMap
System.out.println();
countUsingApacheCommons(input); // 使用 Apache Commons Lang
}
}
3.2 判断是否是回文字符串
01.说明
a.通过反转字符串判断
通过 StringBuilder 的 reverse() 方法反转字符串,然后判断原字符串与反转后的字符串是否相同。
如果相同,则是回文,否则不是回文。
b.通过双指针法判断
使用双指针法,从字符串的两端开始比较字符,逐步向中间逼近。
如果有任何一对字符不相等,返回 false,否则返回 true。
c.忽略大小写和非字母数字字符判断回文
先将字符串转换为小写,并删除非字母和数字的字符(使用正则表达式)。
然后通过双指针法判断回文。
e.通过递归法判断
使用递归的方式检查字符串的首尾字符是否相等,然后递归地处理剩余部分。
02.代码
public class Palindrome {
// 方法一:通过反转字符串判断
public static boolean isPalindromeByReverse(String str) {
// 如果字符串为空或仅包含一个字符,直接返回 true
if (str == null || str.length() <= 1) {
return true;
}
// 反转字符串
String reversed = new StringBuilder(str).reverse().toString();
// 判断原字符串与反转字符串是否相同
return str.equals(reversed);
}
// 方法二:通过双指针法判断
public static boolean isPalindromeByTwoPointers(String str) {
// 如果字符串为空或仅包含一个字符,直接返回 true
if (str == null || str.length() <= 1) {
return true;
}
int left = 0;
int right = str.length() - 1;
// 双指针从两端向中间移动,逐个字符进行比较
while (left < right) {
if (str.charAt(left) != str.charAt(right)) {
return false; // 一旦发现不相等,直接返回 false
}
left++;
right--;
}
return true; // 全部字符相等,返回 true
}
// 方法三:忽略大小写和非字母数字字符判断回文
public static boolean isPalindromeIgnoreCaseAndSymbols(String str) {
// 如果字符串为空或仅包含一个字符,直接返回 true
if (str == null || str.length() <= 1) {
return true;
}
// 将字符串转为小写并去除非字母数字字符
String cleanedStr = str.replaceAll("[^a-zA-Z0-9]", "").toLowerCase();
// 使用双指针法判断回文
int left = 0;
int right = cleanedStr.length() - 1;
while (left < right) {
if (cleanedStr.charAt(left) != cleanedStr.charAt(right)) {
return false;
}
left++;
right--;
}
return true;
}
// 方法四:通过递归法判断回文
public static boolean isPalindromeByRecursion(String str) {
// 基本情况:如果字符串长度为 0 或 1,返回 true
if (str == null || str.length() <= 1) {
return true;
}
// 递归:检查首尾字符是否相等,并递归处理中间部分
if (str.charAt(0) != str.charAt(str.length() - 1)) {
return false;
}
// 递归检查去掉首尾字符的子字符串
return isPalindromeByRecursion(str.substring(1, str.length() - 1));
}
// 主函数,执行所有方法并展示结果
public static void main(String[] args) {
String str1 = "madam";
String str2 = "hello";
String str3 = "A man, a plan, a canal: Panama";
String str4 = "race a car";
// 方法一:通过反转字符串判断
System.out.println("方法一:通过反转字符串判断");
System.out.println("字符串 \"" + str1 + "\" 是否是回文: " + isPalindromeByReverse(str1)); // 输出: true
System.out.println("字符串 \"" + str2 + "\" 是否是回文: " + isPalindromeByReverse(str2)); // 输出: false
System.out.println();
// 方法二:通过双指针法判断
System.out.println("方法二:通过双指针法判断");
System.out.println("字符串 \"" + str1 + "\" 是否是回文: " + isPalindromeByTwoPointers(str1)); // 输出: true
System.out.println("字符串 \"" + str2 + "\" 是否是回文: " + isPalindromeByTwoPointers(str2)); // 输出: false
System.out.println();
// 方法三:忽略大小写和非字母数字字符判断回文
System.out.println("方法三:忽略大小写和非字母数字字符判断回文");
System.out.println("字符串 \"" + str3 + "\" 是否是回文: " + isPalindromeIgnoreCaseAndSymbols(str3)); // 输出: true
System.out.println("字符串 \"" + str4 + "\" 是否是回文: " + isPalindromeIgnoreCaseAndSymbols(str4)); // 输出: false
System.out.println();
// 方法四:通过递归法判断回文
System.out.println("方法四:通过递归法判断");
System.out.println("字符串 \"" + str1 + "\" 是否是回文: " + isPalindromeByRecursion(str1)); // 输出: true
System.out.println("字符串 \"" + str2 + "\" 是否是回文: " + isPalindromeByRecursion(str2)); // 输出: false
}
}